Add new plugins, refactor exp, introduce stat forge to replace GAS
This commit is contained in:
@@ -171,7 +171,6 @@ bMoviesAreSkippable=False
|
||||
[/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"))
|
||||
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=500.000000
|
||||
+ProfessionExpArray=1500.000000
|
||||
@@ -192,4 +191,5 @@ ExpIncreaseEffect=/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/Gamepl
|
||||
+ProfessionExpArray=180000.000000
|
||||
+ProfessionExpArray=208000.000000
|
||||
+ProfessionExpArray=240000.000000
|
||||
ExpIncreaseEffectClass=/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/GameEffects/GSE_AddExp_Template.GSE_AddExp_Template_C'
|
||||
|
||||
|
||||
BIN
Content/Blueprints/GameEffects/GSE_AddExp_Template.uasset
LFS
Normal file
BIN
Content/Blueprints/GameEffects/GSE_AddExp_Template.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Blueprints/GameEffects/GSE_ModifyEnergyTemplate.uasset
LFS
Normal file
BIN
Content/Blueprints/GameEffects/GSE_ModifyEnergyTemplate.uasset
LFS
Normal file
Binary file not shown.
BIN
Plugins/InteractionSystem/Content/Materials/M_UiBackgroundBlur.uasset
LFS
Normal file
BIN
Plugins/InteractionSystem/Content/Materials/M_UiBackgroundBlur.uasset
LFS
Normal file
Binary file not shown.
BIN
Plugins/InteractionSystem/Content/WBP_Interaction.uasset
LFS
Normal file
BIN
Plugins/InteractionSystem/Content/WBP_Interaction.uasset
LFS
Normal file
Binary file not shown.
24
Plugins/InteractionSystem/InteractionSystem.uplugin
Normal file
24
Plugins/InteractionSystem/InteractionSystem.uplugin
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "InteractionSystem",
|
||||
"Description": "",
|
||||
"Category": "Gameplay",
|
||||
"CreatedBy": "",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "",
|
||||
"MarketplaceURL": "",
|
||||
"SupportURL": "",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"IsExperimentalVersion": false,
|
||||
"Installed": false,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "InteractionSystem",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/InteractionSystem/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/InteractionSystem/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,53 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class InteractionSystem : ModuleRules
|
||||
{
|
||||
public InteractionSystem(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core", "UMG",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"DeveloperSettings"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "Components/InteractionWidgetComponent.h"
|
||||
#include "InteractionSettings.h"
|
||||
#include "Interface/InteractableActorInterface.h"
|
||||
#include "Widgets/BaseInteractionWidget.h"
|
||||
|
||||
|
||||
UInteractionWidgetComponent::UInteractionWidgetComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
|
||||
Space = EWidgetSpace::Screen;
|
||||
bManuallyRedraw = true;
|
||||
RedrawTime = 1.f;
|
||||
bDrawAtDesiredSize = true;
|
||||
Pivot = FVector2D(0.5f, 0.5f);
|
||||
TickMode = ETickMode::Disabled;
|
||||
if (const UInteractionSettings* InteractionSettings = GetDefault<UInteractionSettings>())
|
||||
{
|
||||
WidgetClass = InteractionSettings->DefaultInteractionWidgetClass;
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionWidgetComponent::ToggleWidget(bool bActive)
|
||||
{
|
||||
if (bActive)
|
||||
{
|
||||
if (UBaseInteractionWidget* InteractionWidget = Cast<UBaseInteractionWidget>(GetWidget()))
|
||||
{
|
||||
InteractionWidget->UpdateInfo(IInteractableActorInterface::Execute_GetInteractionText(GetOwner()));
|
||||
}
|
||||
}
|
||||
|
||||
SetVisibility(bActive);
|
||||
RequestRenderUpdate();
|
||||
}
|
||||
|
||||
void UInteractionWidgetComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (UBaseInteractionWidget* InteractionWidget = Cast<UBaseInteractionWidget>(GetWidget()))
|
||||
{
|
||||
InteractionWidget->UpdateInfo(IInteractableActorInterface::Execute_GetInteractionText(GetOwner()));
|
||||
}
|
||||
|
||||
SetPivot(GetPivot() + WidgetOffset);
|
||||
ToggleWidget(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InteractionSystem.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FInteractionSystemModule"
|
||||
|
||||
void FInteractionSystemModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
}
|
||||
|
||||
void FInteractionSystemModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FInteractionSystemModule, InteractionSystem)
|
||||
@@ -0,0 +1,180 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "Subsystem/InteractionSubsystem.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "InteractionSettings.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Interface/InteractableActorInterface.h"
|
||||
|
||||
void UInteractionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
if (GetWorld())
|
||||
{
|
||||
GetWorld()->AddOnActorSpawnedHandler(FOnActorSpawned::FDelegate::CreateUObject(this, &UInteractionSubsystem::RegisterInteractableActor));
|
||||
GetWorld()->AddOnActorDestroyedHandler(FOnActorDestroyed::FDelegate::CreateUObject(this, &UInteractionSubsystem::UnregisterInteractableActor));
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::Deinitialize()
|
||||
{
|
||||
InteractableActorList.Empty();
|
||||
|
||||
if (UInteractionSubsystem* IS = GetWorld()->GetSubsystem<UInteractionSubsystem>())
|
||||
{
|
||||
if (IS->CurrentlyInteractableObject.IsValid())
|
||||
{
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false);
|
||||
}
|
||||
IS->CurrentlyInteractableObject = nullptr;
|
||||
}
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
bool UInteractionSubsystem::ShouldCreateSubsystem(UObject* Outer) const
|
||||
{
|
||||
if (!Super::ShouldCreateSubsystem(Outer)) return false;
|
||||
|
||||
if (!Outer || !Outer->GetWorld()) return false;
|
||||
|
||||
UWorld* World = Outer->GetWorld();
|
||||
return World->WorldType == EWorldType::Type::Game || World->WorldType == EWorldType::Type::PIE;
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
||||
{
|
||||
Super::OnWorldBeginPlay(InWorld);
|
||||
|
||||
RescanWorld();
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::Tick(float DeltaTime)
|
||||
{
|
||||
GetClosestInteractable(this);
|
||||
}
|
||||
|
||||
AActor* UInteractionSubsystem::GetClosestInteractable(const UObject* WorldContext)
|
||||
{
|
||||
AActor* Closest = nullptr;
|
||||
|
||||
if (!IsValid(WorldContext) || !IsValid(WorldContext->GetWorld()))
|
||||
{
|
||||
return Closest;
|
||||
}
|
||||
|
||||
UInteractionSubsystem* IS = WorldContext->GetWorld()->GetSubsystem<UInteractionSubsystem>();
|
||||
ensure(IS);
|
||||
|
||||
const APlayerController* PC = WorldContext->GetWorld()->GetFirstPlayerController();
|
||||
if (!PC) return Closest;
|
||||
|
||||
const ACharacter* PlayerCharacter = Cast<ACharacter>(PC->GetPawn());
|
||||
if (!PlayerCharacter) return Closest;
|
||||
|
||||
const UInteractionSettings* InteractionSettings = GetDefault<UInteractionSettings>();
|
||||
ensure(InteractionSettings);
|
||||
|
||||
float ClosestDist = 9999999.f;
|
||||
for (TWeakObjectPtr<AActor> Actor : IS->InteractableActorList)
|
||||
{
|
||||
if (!Actor.IsValid())
|
||||
continue;
|
||||
|
||||
const float Dist = FVector::DistSquared(Actor->GetActorLocation(), PlayerCharacter->GetActorLocation());
|
||||
const float MaxInteractionDistance = IInteractableActorInterface::Execute_GetInteractionDistance(Actor.Get());
|
||||
|
||||
if (FMath::Pow(MaxInteractionDistance, 2) < Dist || Dist > ClosestDist)
|
||||
continue;
|
||||
|
||||
FVector2D ScreenLoc;
|
||||
if (!PC->ProjectWorldLocationToScreen(Actor->GetActorLocation(), ScreenLoc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IInteractableActorInterface::Execute_IsInteractable(Actor.Get()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ClosestDist = Dist;
|
||||
Closest = Actor.Get();
|
||||
}
|
||||
|
||||
if (IS->CurrentlyInteractableObject != Closest)
|
||||
{
|
||||
if (IS->CurrentlyInteractableObject.IsValid())
|
||||
{
|
||||
// UE_LOG(LogTemp, Warning, TEXT("Turning off last interactable: %s"), *IS->CurrentlyInteractableObject.Get()->GetActorNameOrLabel())
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false);
|
||||
}
|
||||
if (Closest)
|
||||
{
|
||||
// UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *Closest->GetActorNameOrLabel())
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(Closest, true);
|
||||
}
|
||||
IS->CurrentlyInteractableObject = Closest;
|
||||
}
|
||||
|
||||
return Closest;
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::UpdateInteractableText(AActor* Interactable)
|
||||
{
|
||||
if (!Interactable) return;
|
||||
|
||||
UInteractionSubsystem* InteractionSubsystem = Interactable->GetWorld()->GetSubsystem<UInteractionSubsystem>();
|
||||
if (!InteractionSubsystem) return;
|
||||
|
||||
if (InteractionSubsystem->CurrentlyInteractableObject != Interactable) return;
|
||||
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(InteractionSubsystem->CurrentlyInteractableObject.Get(), true);
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::RescanWorld()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
int32 Count = 0;
|
||||
|
||||
if (!World)
|
||||
return;
|
||||
|
||||
// Prune stuff
|
||||
InteractableActorList.RemoveAllSwap(
|
||||
[](TWeakObjectPtr<AActor> Actor)
|
||||
{
|
||||
return !Actor.IsValid();
|
||||
}
|
||||
);
|
||||
|
||||
for (TActorIterator<AActor> It(World); It; ++It)
|
||||
{
|
||||
Count++;
|
||||
AActor* Actor = *It;
|
||||
if (IsValid(Actor) && Actor->Implements<UInteractableActorInterface>())
|
||||
{
|
||||
RegisterInteractableActor(Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::RegisterInteractableActor(AActor* InteractableActor)
|
||||
{
|
||||
if (IsValid(InteractableActor) && InteractableActor->Implements<UInteractableActorInterface>())
|
||||
{
|
||||
InteractableActorList.Add(InteractableActor);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::UnregisterInteractableActor(AActor* InteractableActor)
|
||||
{
|
||||
InteractableActorList.RemoveAllSwap(
|
||||
[InteractableActor](TWeakObjectPtr<AActor> Actor)
|
||||
{
|
||||
return !Actor.IsValid() || Actor.Get() == InteractableActor;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "Widgets/BaseInteractionWidget.h"
|
||||
@@ -0,0 +1,28 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "InteractionWidgetComponent.generated.h"
|
||||
|
||||
|
||||
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
|
||||
class INTERACTIONSYSTEM_API UInteractionWidgetComponent : public UWidgetComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UInteractionWidgetComponent();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction Widget Component")
|
||||
void ToggleWidget(bool bActive);
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction Widget Component")
|
||||
FVector2D WidgetOffset;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "InteractionSettings.generated.h"
|
||||
|
||||
class UBaseInteractionWidget;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS(Config=Game, DefaultConfig, meta=(DisplayName="Interaction System"))
|
||||
class INTERACTIONSYSTEM_API UInteractionSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
// Max distance at which interactable will be considered active
|
||||
UPROPERTY(Config, EditAnywhere)
|
||||
float InteractableMaxDistance = 1000.f;
|
||||
|
||||
// Angle from the camera view towards object to consider "in view"
|
||||
UPROPERTY(Config, EditAnywhere)
|
||||
float InteractableCameraViewAngle = 60.f;
|
||||
|
||||
UPROPERTY(Config, EditAnywhere)
|
||||
TSubclassOf<UBaseInteractionWidget> DefaultInteractionWidgetClass;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FInteractionSystemModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "InteractionSettings.h"
|
||||
#include "InteractableActorInterface.generated.h"
|
||||
|
||||
UINTERFACE(MinimalAPI)
|
||||
class UInteractableActorInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class INTERACTIONSYSTEM_API IInteractableActorInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
FText GetInteractionText() const;
|
||||
virtual FText GetInteractionText_Implementation() const { return FText::GetEmpty(); }
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
bool IsInteractable() const;
|
||||
virtual bool IsInteractable_Implementation() const { return false; }
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
void Interact(AActor* InteractionInstigator);
|
||||
virtual void Interact_Implementation(AActor* InteractionInstigator) {}
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
void ToggleInteractableUI(bool bActive);
|
||||
virtual void ToggleInteractableUI_Implementation(bool bActive) {}
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
float GetInteractionDistance() const;
|
||||
virtual float GetInteractionDistance_Implementation() const
|
||||
{
|
||||
return GetDefault<UInteractionSettings>()->InteractableMaxDistance;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/WorldSubsystem.h"
|
||||
#include "Tickable.h"
|
||||
#include "Stats/Stats.h"
|
||||
|
||||
#include "InteractionSubsystem.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class INTERACTIONSYSTEM_API UInteractionSubsystem : public UWorldSubsystem, public FTickableGameObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
|
||||
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
|
||||
virtual bool IsTickable() const override { return !IsTemplate(); }
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(UInteractionSubsystem, STATGROUP_Tickables); }
|
||||
|
||||
static AActor* GetClosestInteractable(const UObject* WorldContext);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Interactable")
|
||||
static void UpdateInteractableText(AActor* Interactable);
|
||||
|
||||
protected:
|
||||
|
||||
UFUNCTION()
|
||||
void RescanWorld();
|
||||
|
||||
void RegisterInteractableActor(AActor* InteractableActor);
|
||||
void UnregisterInteractableActor(AActor* InteractableActor);
|
||||
|
||||
UPROPERTY()
|
||||
TArray<TWeakObjectPtr<AActor>> InteractableActorList;
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<AActor> CurrentlyInteractableObject;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "BaseInteractionWidget.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class INTERACTIONSYSTEM_API UBaseInteractionWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category = "Interaction System")
|
||||
void UpdateInfo(const FText& InteractionText);
|
||||
|
||||
};
|
||||
27
Plugins/NoiseMapGenerator/NoiseMapGenerator.uplugin
Normal file
27
Plugins/NoiseMapGenerator/NoiseMapGenerator.uplugin
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0.0",
|
||||
"FriendlyName": "Noise Map Generator",
|
||||
"Description": "Bake tiling noise textures with live preview.",
|
||||
"Category": "Editor",
|
||||
"CreatedBy": "UNmisterIZE(Sebastien Durocher)",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "https://drive.google.com/file/d/134xAHp1LABTME2vNDhH0gcCkiqj1sL-o/view?usp=sharing",
|
||||
"MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/2fd402da-2ee0-4532-b219-a0703790151d",
|
||||
"SupportURL": "suroduro7@gmail.com",
|
||||
"CanContainContent": true,
|
||||
"Installed": true,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "NoiseMapGenerator",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Mac",
|
||||
"Linux"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/NoiseMapGenerator/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/NoiseMapGenerator/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
Project: NoiseMapGenerator Plugin */
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class NoiseMapGenerator : ModuleRules
|
||||
{
|
||||
public NoiseMapGenerator(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core", "CoreUObject", "Engine"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Slate", "SlateCore", "EditorStyle", "UnrealEd",
|
||||
"Projects", "InputCore", "ToolMenus", "AssetTools",
|
||||
"AssetRegistry", "RenderCore", "RHI", "DeveloperSettings"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGenerator.cpp
|
||||
*/
|
||||
|
||||
#include "NoiseMapGenerator.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
|
||||
////// ---------------------------------------------------------
|
||||
namespace TileableNoise
|
||||
{
|
||||
static FORCEINLINE float Fade(float t){ return t*t*t*(t*(t*6 - 15) + 10); }
|
||||
static FORCEINLINE float Lerp(float a,float b,float t){ return a + t*(b - a); }
|
||||
|
||||
// Permutation table (512 entries = 256 duplicated)
|
||||
struct Perm
|
||||
{
|
||||
TArray<int32> P;
|
||||
explicit Perm(int32 Seed)
|
||||
{
|
||||
P.SetNumUninitialized(512);
|
||||
TArray<int32> base; base.SetNumUninitialized(256);
|
||||
for (int32 i=0;i<256;++i) base[i]=i;
|
||||
FRandomStream RNG(Seed);
|
||||
for (int32 i=255;i>0;--i){ const int32 j=RNG.RandRange(0,i); Swap(base[i],base[j]); }
|
||||
for (int32 i=0;i<512;++i) P[i]=base[i & 255];
|
||||
}
|
||||
FORCEINLINE int32 H4(int32 x,int32 y,int32 z,int32 w) const
|
||||
{ return P[P[P[P[x & 255] + (y & 255)] + (z & 255)] + (w & 255)]; }
|
||||
};
|
||||
|
||||
// 4D gradient (32 dirs)
|
||||
static FORCEINLINE float Grad4(int32 h, float x,float y,float z,float w)
|
||||
{
|
||||
const int32 b = h & 31;
|
||||
switch (b)
|
||||
{
|
||||
case 0: return x + y + z + w; case 1: return x + y + z - w;
|
||||
case 2: return x + y - z + w; case 3: return x + y - z - w;
|
||||
case 4: return x - y + z + w; case 5: return x - y + z - w;
|
||||
case 6: return x - y - z + w; case 7: return x - y - z - w;
|
||||
case 8: return -x + y + z + w; case 9: return -x + y + z - w;
|
||||
case 10: return -x + y - z + w; case 11: return -x + y - z - w;
|
||||
case 12: return -x - y + z + w; case 13: return -x - y + z - w;
|
||||
case 14: return -x - y - z + w; case 15: return -x - y - z - w;
|
||||
case 16: return x + y + z; case 17: return x + y - z;
|
||||
case 18: return x - y + z; case 19: return x - y - z;
|
||||
case 20: return -x + y + z; case 21: return -x + y - z;
|
||||
case 22: return -x - y + z; case 23: return -x - y - z;
|
||||
case 24: return x + z + w; case 25: return x + z - w;
|
||||
case 26: return x - z + w; case 27: return x - z - w;
|
||||
case 28: return -x + z + w; case 29: return -x + z - w;
|
||||
case 30: return -x - z + w; default: return -x - z - w;
|
||||
}
|
||||
}
|
||||
|
||||
static float Perlin4D(float x,float y,float z,float w,const Perm& perm)
|
||||
{
|
||||
const int32 X0 = FMath::FloorToInt(x), Y0 = FMath::FloorToInt(y), Z0 = FMath::FloorToInt(z), W0 = FMath::FloorToInt(w);
|
||||
const float xf = x - X0, yf = y - Y0, zf = z - Z0, wf = w - W0;
|
||||
const float u = Fade(xf), v = Fade(yf), s = Fade(zf), t = Fade(wf);
|
||||
const int32 X1=X0+1,Y1=Y0+1,Z1=Z0+1,W1=W0+1;
|
||||
|
||||
const float n0000 = Grad4(perm.H4(X0,Y0,Z0,W0), xf, yf, zf, wf);
|
||||
const float n1000 = Grad4(perm.H4(X1,Y0,Z0,W0), xf-1.f, yf, zf, wf);
|
||||
const float n0100 = Grad4(perm.H4(X0,Y1,Z0,W0), xf, yf-1.f, zf, wf);
|
||||
const float n1100 = Grad4(perm.H4(X1,Y1,Z0,W0), xf-1.f, yf-1.f, zf, wf);
|
||||
const float n0010 = Grad4(perm.H4(X0,Y0,Z1,W0), xf, yf, zf-1.f, wf);
|
||||
const float n1010 = Grad4(perm.H4(X1,Y0,Z1,W0), xf-1.f, yf, zf-1.f, wf);
|
||||
const float n0110 = Grad4(perm.H4(X0,Y1,Z1,W0), xf, yf-1.f, zf-1.f, wf);
|
||||
const float n1110 = Grad4(perm.H4(X1,Y1,Z1,W0), xf-1.f, yf-1.f, zf-1.f, wf);
|
||||
|
||||
const float n0001 = Grad4(perm.H4(X0,Y0,Z0,W1), xf, yf, zf, wf-1.f);
|
||||
const float n1001 = Grad4(perm.H4(X1,Y0,Z0,W1), xf-1.f, yf, zf, wf-1.f);
|
||||
const float n0101 = Grad4(perm.H4(X0,Y1,Z0,W1), xf, yf-1.f, zf, wf-1.f);
|
||||
const float n1101 = Grad4(perm.H4(X1,Y1,Z0,W1), xf-1.f, yf-1.f, zf, wf-1.f);
|
||||
const float n0011 = Grad4(perm.H4(X0,Y0,Z1,W1), xf, yf, zf-1.f, wf-1.f);
|
||||
const float n1011 = Grad4(perm.H4(X1,Y0,Z1,W1), xf-1.f, yf, zf-1.f, wf-1.f);
|
||||
const float n0111 = Grad4(perm.H4(X0,Y1,Z1,W1), xf, yf-1.f, zf-1.f, wf-1.f);
|
||||
const float n1111 = Grad4(perm.H4(X1,Y1,Z1,W1), xf-1.f, yf-1.f, zf-1.f, wf-1.f);
|
||||
|
||||
const float nx00 = Lerp(n0000, n1000, u);
|
||||
const float nx10 = Lerp(n0100, n1100, u);
|
||||
const float nx01 = Lerp(n0010, n1010, u);
|
||||
const float nx11 = Lerp(n0110, n1110, u);
|
||||
const float nxy0 = Lerp(nx00, nx10, v);
|
||||
const float nxy1 = Lerp(nx01, nx11, v);
|
||||
const float nxyz0 = Lerp(nxy0, nxy1, s);
|
||||
|
||||
const float nx00b = Lerp(n0001, n1001, u);
|
||||
const float nx10b = Lerp(n0101, n1101, u);
|
||||
const float nx01b = Lerp(n0011, n1011, u);
|
||||
const float nx11b = Lerp(n0111, n1111, u);
|
||||
const float nxy0b = Lerp(nx00b, nx10b, v);
|
||||
const float nxy1b = Lerp(nx01b, nx11b, v);
|
||||
const float nxyz1 = Lerp(nxy0b, nxy1b, s);
|
||||
|
||||
return Lerp(nxyz0, nxyz1, t); // [-1,1]
|
||||
}
|
||||
|
||||
static float FBM4D(float x,float y,float z,float w,const Perm& perm,int32 Octaves,float Lac,float Gain)
|
||||
{
|
||||
float amp=1.f,sum=0.f,range=0.f;
|
||||
float X=x,Y=y,Z=z,W=w;
|
||||
for (int32 o=0;o<Octaves;++o)
|
||||
{
|
||||
sum += amp * Perlin4D(X,Y,Z,W,perm);
|
||||
range += amp;
|
||||
X*=Lac; Y*=Lac; Z*=Lac; W*=Lac;
|
||||
amp*=Gain;
|
||||
}
|
||||
return (range>0.f)? sum/range : 0.f;
|
||||
}
|
||||
|
||||
// Hash utilities for Worley
|
||||
static FORCEINLINE uint32 Hash2D(int32 x,int32 y,uint32 seed)
|
||||
{
|
||||
uint32 h = (uint32)x * 0x27d4eb2d ^ (uint32)y * 0x165667b1u ^ seed*0x9e3779b9u;
|
||||
h ^= (h >> 16); h *= 0x7feb352d; h ^= (h >> 15); h *= 0x846ca68b; h ^= (h >> 16);
|
||||
return h;
|
||||
}
|
||||
static FORCEINLINE float Frand01(uint32& state)
|
||||
{
|
||||
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
|
||||
return (state & 0x00FFFFFF) / 16777216.0f;
|
||||
}
|
||||
|
||||
// Torus-safe distance between (u,v) and feature in neighbor cell (iu,iv) with wrap
|
||||
static float WorleyF1_Torus(float u,float v,int32 CellsU,int32 CellsV,uint32 seed)
|
||||
{
|
||||
// Base integer cell
|
||||
const float fu = u * CellsU;
|
||||
const float fv = v * CellsV;
|
||||
const int32 iu0 = FMath::FloorToInt(fu);
|
||||
const int32 iv0 = FMath::FloorToInt(fv);
|
||||
|
||||
float best = 1e9f;
|
||||
|
||||
for (int du=-1; du<=1; ++du)
|
||||
for (int dv=-1; dv<=1; ++dv)
|
||||
{
|
||||
const int32 iu = (iu0 + du + CellsU) % CellsU;
|
||||
const int32 iv = (iv0 + dv + CellsV) % CellsV;
|
||||
|
||||
uint32 st = Hash2D(iu, iv, seed);
|
||||
const float jitterX = Frand01(st);
|
||||
const float jitterY = Frand01(st);
|
||||
|
||||
const float fx = (float(iu) + jitterX) / float(CellsU);
|
||||
const float fy = (float(iv) + jitterY) / float(CellsV);
|
||||
|
||||
// torus wrap distance in [0,1]
|
||||
float dx = FMath::Abs(u - fx); dx = FMath::Min(dx, 1.0f - dx);
|
||||
float dy = FMath::Abs(v - fy); dy = FMath::Min(dy, 1.0f - dy);
|
||||
|
||||
best = FMath::Min(best, FMath::Sqrt(dx*dx + dy*dy));
|
||||
}
|
||||
return best; // [0..~0.5]
|
||||
}
|
||||
|
||||
}
|
||||
/////// ---------------------------------------------------------
|
||||
|
||||
static void MakeUniqueAssetPath(const FString& Folder, const FString& BaseName, bool bOverwrite, FString& OutPkg, FString& OutAsset)
|
||||
{
|
||||
FString CleanFolder = Folder;
|
||||
if (!CleanFolder.StartsWith(TEXT("/"))) CleanFolder = TEXT("/") + CleanFolder;
|
||||
if (!CleanFolder.StartsWith(TEXT("/Game"))) CleanFolder = TEXT("/Game") + CleanFolder;
|
||||
|
||||
FString CandidateName = BaseName;
|
||||
FString CandidatePkg = CleanFolder / CandidateName;
|
||||
|
||||
if (bOverwrite)
|
||||
{
|
||||
OutPkg = CandidatePkg;
|
||||
OutAsset = CandidateName;
|
||||
return;
|
||||
}
|
||||
|
||||
int32 Suffix = 1;
|
||||
while (FPackageName::DoesPackageExist(CandidatePkg))
|
||||
{
|
||||
CandidateName = FString::Printf(TEXT("%s_%03d"), *BaseName, Suffix++);
|
||||
CandidatePkg = CleanFolder / CandidateName;
|
||||
}
|
||||
OutPkg = CandidatePkg;
|
||||
OutAsset = CandidateName;
|
||||
}
|
||||
|
||||
|
||||
bool FNoiseMapGenerator::GeneratePixels(const FNoiseBakeSettings& S, int32 RepeatsUV, TArray<FColor>& OutPixels, int32& OutW, int32& OutH)
|
||||
{
|
||||
// --- size ---
|
||||
const int32 W = S.Width;
|
||||
const int32 H = S.Height;
|
||||
if (W <= 0 || H <= 0)
|
||||
{
|
||||
OutW = OutH = 0;
|
||||
OutPixels.Reset();
|
||||
return false;
|
||||
}
|
||||
OutW = W;
|
||||
OutH = H;
|
||||
OutPixels.SetNumUninitialized(W * H);
|
||||
|
||||
// --- common setup ---
|
||||
const float invW = 1.f / float(W);
|
||||
const float invH = 1.f / float(H);
|
||||
const float TwoPi = 6.28318530717958647692f;
|
||||
const float Scale = 1.0f / FMath::Max(S.Frequency, 1e-6f);
|
||||
|
||||
TileableNoise::Perm perm(S.Seed);
|
||||
|
||||
// 4D torus mapping (period-1 in u,v)
|
||||
auto AnglesFromUV = [&](float u, float v)
|
||||
{
|
||||
const float th = TwoPi * u;
|
||||
const float ph = TwoPi * v;
|
||||
struct { float X, Y, Z, W; } R
|
||||
{
|
||||
FMath::Cos(th) * Scale,
|
||||
FMath::Sin(th) * Scale,
|
||||
FMath::Cos(ph) * Scale,
|
||||
FMath::Sin(ph) * Scale
|
||||
};
|
||||
return R;
|
||||
};
|
||||
|
||||
// Base sampler per algorithm, returns [-1,1]
|
||||
auto SampleBase = [&](float u, float v) -> float
|
||||
{
|
||||
switch (S.Algorithm)
|
||||
{
|
||||
case ENoiseAlgorithm::Perlin:
|
||||
{
|
||||
const auto C = AnglesFromUV(u, v);
|
||||
return (S.Mode == ENoiseMode::FBM)
|
||||
? TileableNoise::FBM4D(C.X, C.Y, C.Z, C.W, perm, S.Octaves, S.Lacunarity, S.Gain)
|
||||
: TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm);
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::RidgedFBM:
|
||||
{
|
||||
float amp = 0.5f;
|
||||
float sum = 0.f;
|
||||
float range = 0.f;
|
||||
float freqMul = 1.f;
|
||||
|
||||
for (int32 o = 0; o < S.Octaves; ++o)
|
||||
{
|
||||
const float ou = FMath::Frac(u * freqMul);
|
||||
const float ov = FMath::Frac(v * freqMul);
|
||||
const auto C = AnglesFromUV(ou, ov);
|
||||
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
|
||||
const float r = 1.f - FMath::Abs(p); // [0,1]
|
||||
|
||||
sum += amp * r;
|
||||
range += amp;
|
||||
|
||||
freqMul *= S.Lacunarity;
|
||||
amp *= S.Gain;
|
||||
}
|
||||
|
||||
return (range > 0.f) ? (sum / range) * 2.f - 1.f : 0.f;
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::Worley:
|
||||
{
|
||||
const float cellsF = FMath::Clamp(
|
||||
(1.0f / FMath::Max(S.Frequency, 1e-6f)) * 0.75f,
|
||||
1.f, 512.f
|
||||
);
|
||||
|
||||
const int32 CellsU = FMath::Max(1, int32(FMath::RoundToInt(cellsF)) * RepeatsUV);
|
||||
const int32 CellsV = FMath::Max(1, int32(FMath::RoundToInt(cellsF)) * RepeatsUV);
|
||||
|
||||
const float d = TileableNoise::WorleyF1_Torus(u, v, CellsU, CellsV, (uint32)S.Seed); // [0..~0.5]
|
||||
const float v01 = 1.0f - FMath::Clamp(d * 2.0f, 0.f, 1.f);
|
||||
return v01 * 2.f - 1.f;
|
||||
}
|
||||
default:
|
||||
return 0.f;
|
||||
}
|
||||
};
|
||||
|
||||
auto SampleNthOctave01 = [&](float u, float v, int32 OctIdx) -> float
|
||||
{
|
||||
switch (S.Algorithm)
|
||||
{
|
||||
case ENoiseAlgorithm::Perlin:
|
||||
{
|
||||
// Higher octave = higher frequency
|
||||
const float freqMul = FMath::Pow(S.Lacunarity, OctIdx);
|
||||
const auto C = AnglesFromUV(u * freqMul, v * freqMul);
|
||||
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
|
||||
return 0.5f * (p + 1.f); // → [0,1]
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::RidgedFBM:
|
||||
{
|
||||
const float freqMul = FMath::Pow(S.Lacunarity, OctIdx);
|
||||
const float ou = FMath::Frac(u * freqMul);
|
||||
const float ov = FMath::Frac(v * freqMul);
|
||||
const auto C = AnglesFromUV(ou, ov);
|
||||
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
|
||||
const float r = 1.f - FMath::Abs(p); // [0,1] – "ridge" shape
|
||||
return r;
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::Worley:
|
||||
{
|
||||
// More cells per octave for finer detail
|
||||
const float cellsFBase = FMath::Clamp(
|
||||
(1.0f / FMath::Max(S.Frequency, 1e-6f)) * 0.75f,
|
||||
1.f, 512.f
|
||||
);
|
||||
const float octaveMul = FMath::Pow(S.Lacunarity, OctIdx);
|
||||
const int32 CellsU = FMath::Max(1, int32(FMath::RoundToInt(cellsFBase * octaveMul)) * RepeatsUV);
|
||||
const int32 CellsV = FMath::Max(1, int32(FMath::RoundToInt(cellsFBase * octaveMul)) * RepeatsUV);
|
||||
|
||||
const float d = TileableNoise::WorleyF1_Torus(u, v, CellsU, CellsV, (uint32)S.Seed);
|
||||
const float v01 = 1.0f - FMath::Clamp(d * 2.0f, 0.f, 1.f); // [0,1], "cells" as bright
|
||||
return v01;
|
||||
}
|
||||
|
||||
default:
|
||||
// Fallback mid-gray
|
||||
return 0.5f;
|
||||
}
|
||||
};
|
||||
|
||||
// Write pixels
|
||||
if (S.OutputMode == ENoiseOutputMode::Grayscale)
|
||||
{
|
||||
float minV = 1e9f;
|
||||
float maxV = -1e9f;
|
||||
TArray<float> tmp;
|
||||
tmp.SetNumUninitialized(W * H);
|
||||
|
||||
int32 I = 0;
|
||||
for (int32 y = 0; y < H; ++y)
|
||||
{
|
||||
for (int32 x = 0; x < W; ++x)
|
||||
{
|
||||
const float u = float(RepeatsUV) * float(x) * invW;
|
||||
const float v = float(RepeatsUV) * float(y) * invH;
|
||||
|
||||
const float vraw = SampleBase(u, v); // [-1,1]
|
||||
tmp[I++] = vraw;
|
||||
minV = FMath::Min(minV, vraw);
|
||||
maxV = FMath::Max(maxV, vraw);
|
||||
}
|
||||
}
|
||||
|
||||
const float invRange =
|
||||
(S.bNormalize && maxV > minV) ? 1.f / (maxV - minV) : 0.5f;
|
||||
|
||||
I = 0;
|
||||
for (int32 y = 0; y < H; ++y)
|
||||
{
|
||||
for (int32 x = 0; x < W; ++x)
|
||||
{
|
||||
float v = tmp[I++];
|
||||
v = S.bNormalize
|
||||
? (v - minV) * invRange // [0,1]
|
||||
: 0.5f * (v + 1.f); // [-1,1] → [0,1]
|
||||
|
||||
const uint8 g = (uint8)FMath::Clamp(
|
||||
FMath::RoundToInt(v * 255.f), 0, 255);
|
||||
|
||||
OutPixels[y * W + x] = FColor(g, g, g, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
else // Split First 3 Octaves to RGB channles
|
||||
{
|
||||
for (int32 y = 0; y < H; ++y)
|
||||
{
|
||||
for (int32 x = 0; x < W; ++x)
|
||||
{
|
||||
const float u = float(RepeatsUV) * float(x) * invW;
|
||||
const float v = float(RepeatsUV) * float(y) * invH;
|
||||
|
||||
const float r = SampleNthOctave01(u, v, 0);
|
||||
const float g = SampleNthOctave01(u, v, 1);
|
||||
const float b = SampleNthOctave01(u, v, 2);
|
||||
|
||||
OutPixels[y * W + x] = FColor(
|
||||
(uint8)FMath::Clamp(FMath::RoundToInt(r * 255.f), 0, 255),
|
||||
(uint8)FMath::Clamp(FMath::RoundToInt(g * 255.f), 0, 255),
|
||||
(uint8)FMath::Clamp(FMath::RoundToInt(b * 255.f), 0, 255),
|
||||
255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FNoiseMapGenerator::BakeFromPixels(
|
||||
const FNoiseBakeSettings& S,
|
||||
const TArray<FColor>& Pixels,
|
||||
int32 W,
|
||||
int32 H)
|
||||
{
|
||||
if (W <= 0 || H <= 0 || Pixels.Num() != W * H)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Resolve package path/name ---
|
||||
FString OutPath = S.OutputPath.IsEmpty()
|
||||
? TEXT("/Game/Noise")
|
||||
: S.OutputPath;
|
||||
|
||||
if (!OutPath.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
OutPath = TEXT("/Game/Noise");
|
||||
}
|
||||
|
||||
FString PackageName, AssetName;
|
||||
MakeUniqueAssetPath(OutPath, S.BaseName, S.bOverwrite, PackageName, AssetName);
|
||||
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!Package)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UTexture2D* Tex = NewObject<UTexture2D>(
|
||||
Package,
|
||||
*AssetName,
|
||||
RF_Public | RF_Standalone
|
||||
);
|
||||
if (!Tex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Tex->Modify();
|
||||
|
||||
// Match preview flags
|
||||
Tex->SRGB = false;
|
||||
|
||||
if (S.OutputMode == ENoiseOutputMode::Grayscale)
|
||||
{
|
||||
Tex->CompressionSettings = TC_Grayscale;
|
||||
}
|
||||
else
|
||||
{
|
||||
Tex->CompressionSettings = TC_VectorDisplacementmap; // linear RGB
|
||||
}
|
||||
|
||||
Tex->CompressionNone = true;
|
||||
Tex->DeferCompression = false;
|
||||
Tex->MipGenSettings = TMGS_NoMipmaps;
|
||||
Tex->AddressX = TA_Wrap;
|
||||
Tex->AddressY = TA_Wrap;
|
||||
Tex->LODGroup = TEXTUREGROUP_World;
|
||||
|
||||
// Copy pixels into source mip
|
||||
Tex->Source.Init(W, H, 1, 1, TSF_BGRA8);
|
||||
{
|
||||
uint8* Src = Tex->Source.LockMip(0);
|
||||
const int64 Bytes = int64(W) * H * sizeof(FColor);
|
||||
FMemory::Memcpy(Src, Pixels.GetData(), Bytes);
|
||||
Tex->Source.UnlockMip(0);
|
||||
}
|
||||
|
||||
Tex->PostEditChange();
|
||||
Tex->UpdateResource();
|
||||
|
||||
FAssetRegistryModule::AssetCreated(Tex);
|
||||
Package->MarkPackageDirty();
|
||||
|
||||
const FString PkgFile =
|
||||
FPackageName::LongPackageNameToFilename(
|
||||
PackageName,
|
||||
FPackageName::GetAssetPackageExtension()
|
||||
);
|
||||
|
||||
FSavePackageArgs Args;
|
||||
Args.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
Args.SaveFlags = SAVE_None;
|
||||
Args.Error = GError;
|
||||
Args.bWarnOfLongFilename = true;
|
||||
|
||||
UPackage::SavePackage(Package, Tex, *PkgFile, Args);
|
||||
}
|
||||
|
||||
void FNoiseMapGenerator::BakeNoiseTexture(const FNoiseBakeSettings& S)
|
||||
{
|
||||
TArray<FColor> Pixels;
|
||||
int32 W = 0, H = 0;
|
||||
|
||||
// IMPORTANT: RepeatsUV = 1 for a single seamless period
|
||||
if (!GeneratePixels(S, /*RepeatsUV=*/1, Pixels, W, H))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BakeFromPixels(S, Pixels, W, H);
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorModule.cpp
|
||||
*/
|
||||
|
||||
#include "NoiseMapGeneratorModule.h"
|
||||
#include "NoiseMapGenerator.h"
|
||||
#include "NoiseMapGeneratorSettings.h"
|
||||
#include "Widgets/Docking/SDockTab.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Input/SSpinBox.h"
|
||||
#include "Widgets/Input/SCheckBox.h"
|
||||
#include "Widgets/Input/SEditableTextBox.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "ToolMenus.h"
|
||||
#include "LevelEditor.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "ContentBrowserModule.h"
|
||||
#include "IContentBrowserSingleton.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
static const FName NoiseMapGenTabName("NoiseMapGeneratorTab");
|
||||
|
||||
// Preview helper
|
||||
static UTexture2D* CreateTransientTexture(int32 W, int32 H, const TArray<FColor>& Pixels)
|
||||
{
|
||||
if (Pixels.Num() != W * H) return nullptr;
|
||||
UTexture2D* T = UTexture2D::CreateTransient(W, H, PF_B8G8R8A8);
|
||||
if (!T) return nullptr;
|
||||
T->SRGB = false;
|
||||
T->CompressionSettings = TC_Default;
|
||||
T->MipGenSettings = TMGS_NoMipmaps;
|
||||
|
||||
FTexture2DMipMap& Mip = T->GetPlatformData()->Mips[0];
|
||||
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
|
||||
const int64 Bytes = (int64)W * H * sizeof(FColor);
|
||||
FMemory::Memcpy(Data, Pixels.GetData(), Bytes);
|
||||
Mip.BulkData.Unlock();
|
||||
T->UpdateResource();
|
||||
return T;
|
||||
}
|
||||
|
||||
// Plugin UI setup
|
||||
class SNoiseMapGeneratorWidget : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SNoiseMapGeneratorWidget) {}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments&)
|
||||
{
|
||||
Settings = GetMutableDefault<UNoiseMapGeneratorSettings>();
|
||||
PreviewBrush = MakeShared<FSlateBrush>();
|
||||
|
||||
ChildSlot
|
||||
[
|
||||
SNew(SBorder)
|
||||
[
|
||||
SNew(SSplitter)
|
||||
|
||||
+ SSplitter::Slot().Value(0.33f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(580.f)
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ SNew(STextBlock).Text(FText::FromString("Noise Settings")) ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Width", Settings->Width, 8, 8192, 64) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Height", Settings->Height, 8, 8192, 64) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Preview Tiling", Settings->TileRepeats, 1, 64, 1) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Seed", Settings->Seed, 0, INT_MAX, 1) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Octaves", Settings->Octaves, 1, 12, 1) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Frequency", Settings->Frequency, 0.001f, 2.0f, 0.005f) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Lacunarity", Settings->Lacunarity, 1.f, 8.f, 0.1f) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Gain", Settings->Gain, 0.f, 1.f, 0.05f) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Normalize 0..1", Settings->bNormalize) ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowMode() ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowOutputMode() ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowAlgorithm() ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowText("Save Path (/Game/...)", Settings->OutputPath) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowText("Base Name", Settings->BaseName) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Overwrite Existing", Settings->bOverwrite) ]
|
||||
// + SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Generate Mips", Settings->bGenerateMips) ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0,0,6,0)
|
||||
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnGenerate)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Generate Preview")) ] ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0,0,6,0)
|
||||
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnBake)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Bake & Save")) ] ]
|
||||
+ SHorizontalBox::Slot().AutoWidth()
|
||||
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnQuit)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Quit")) ] ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
+ SSplitter::Slot().Value(0.67f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
[ SAssignNew(PreviewImage, SImage).Image(nullptr) ]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
Regenerate();
|
||||
}
|
||||
|
||||
private:
|
||||
// UI helpers
|
||||
TSharedRef<SWidget> MakeRowInt(const char* Label, int32& V, int32 Min, int32 Max, int32 Step)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[ SNew(SSpinBox<int32>).MinValue(Min).MaxValue(Max).Delta(Step)
|
||||
.Value_Lambda([&V]{ return V; })
|
||||
.OnValueChanged_Lambda([&V](int32 NV){ V = NV; }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowFloat(const char* Label, float& V, float Min, float Max, float Step)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[ SNew(SSpinBox<float>).MinValue(Min).MaxValue(Max).Delta(Step)
|
||||
.Value_Lambda([&V]{ return V; })
|
||||
.OnValueChanged_Lambda([&V](float NV){ V = NV; }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowBool(const char* Label, bool& B)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||
[ SNew(SCheckBox)
|
||||
.IsChecked_Lambda([&B]{ return B ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
||||
.OnCheckStateChanged_Lambda([&B](ECheckBoxState S){ B = (S == ECheckBoxState::Checked); }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowText(const char* Label, FString& T)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[ SNew(SEditableTextBox)
|
||||
.Text_Lambda([&T]{ return FText::FromString(T); })
|
||||
.OnTextCommitted_Lambda([&T](const FText& New, ETextCommit::Type){ T = New.ToString(); }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowMode()
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString("Mode")) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||
[ SNew(SCheckBox)
|
||||
.OnCheckStateChanged_Lambda([this](ECheckBoxState S){ Settings->Mode = (S == ECheckBoxState::Checked) ? ENoiseMode::FBM : ENoiseMode::Perlin; Regenerate(); })
|
||||
.IsChecked_Lambda([this]{ return Settings->Mode == ENoiseMode::FBM ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
||||
.Content()[ SNew(STextBlock).Text(FText::FromString("FBM (unchecked = Perlin)")) ] ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowOutputMode()
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString("Output")) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||
[ SNew(SCheckBox)
|
||||
.OnCheckStateChanged_Lambda([this](ECheckBoxState S){ Settings->OutputMode = (S == ECheckBoxState::Checked) ? ENoiseOutputMode::SplitFirst3Octaves : ENoiseOutputMode::Grayscale; Regenerate(); })
|
||||
.IsChecked_Lambda([this]{ return Settings->OutputMode == ENoiseOutputMode::SplitFirst3Octaves ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
||||
.Content()[ SNew(STextBlock).Text(FText::FromString("Split 3 Octaves to RGB")) ] ];
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
static FString AlgoToString(ENoiseAlgorithm A)
|
||||
{
|
||||
switch (A)
|
||||
{
|
||||
case ENoiseAlgorithm::Perlin: return TEXT("Perlin");
|
||||
case ENoiseAlgorithm::Worley: return TEXT("Worley (Cellular)");
|
||||
case ENoiseAlgorithm::RidgedFBM: return TEXT("Ridged FBM");
|
||||
default: return TEXT("Perlin");
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> OnGenerateAlgoWidget(TSharedPtr<ENoiseAlgorithm> InItem)
|
||||
{
|
||||
return SNew(STextBlock).Text(FText::FromString(AlgoToString(*InItem)));
|
||||
}
|
||||
|
||||
void OnAlgoChanged(TSharedPtr<ENoiseAlgorithm> NewSel, ESelectInfo::Type)
|
||||
{
|
||||
if (!NewSel.IsValid()) return;
|
||||
Settings->Algorithm = *NewSel;
|
||||
SelectedAlgo = NewSel;
|
||||
Regenerate();
|
||||
}
|
||||
|
||||
FText GetAlgoCurrentText() const
|
||||
{
|
||||
return FText::FromString(AlgoToString(Settings->Algorithm));
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowAlgorithm()
|
||||
{
|
||||
// Populate items once if empty
|
||||
if (AlgoItems.Num() == 0)
|
||||
{
|
||||
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::Perlin));
|
||||
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::Worley));
|
||||
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::RidgedFBM));
|
||||
|
||||
// pick current
|
||||
for (const auto& It : AlgoItems)
|
||||
{
|
||||
if (*It == Settings->Algorithm)
|
||||
{
|
||||
SelectedAlgo = It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!SelectedAlgo.IsValid())
|
||||
{
|
||||
SelectedAlgo = AlgoItems[0];
|
||||
}
|
||||
}
|
||||
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Algorithm")) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[
|
||||
SNew(SComboBox<TSharedPtr<ENoiseAlgorithm>>)
|
||||
.OptionsSource(&AlgoItems)
|
||||
.InitiallySelectedItem(SelectedAlgo)
|
||||
.OnGenerateWidget(this, &SNoiseMapGeneratorWidget::OnGenerateAlgoWidget)
|
||||
.OnSelectionChanged(this, &SNoiseMapGeneratorWidget::OnAlgoChanged)
|
||||
[
|
||||
SNew(STextBlock).Text_Lambda([this]{ return GetAlgoCurrentText(); })
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<ENoiseAlgorithm>> AlgoItems;
|
||||
TSharedPtr<ENoiseAlgorithm> SelectedAlgo;
|
||||
|
||||
FReply OnGenerate()
|
||||
{
|
||||
Regenerate();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply OnBake()
|
||||
{
|
||||
FNoiseBakeSettings B;
|
||||
B.Width = Settings->Width;
|
||||
B.Height = Settings->Height;
|
||||
B.TileRepeats = Settings->TileRepeats;
|
||||
B.Seed = Settings->Seed;
|
||||
B.Octaves = Settings->Octaves;
|
||||
B.Frequency = Settings->Frequency;
|
||||
B.Lacunarity = Settings->Lacunarity;
|
||||
B.Gain = Settings->Gain;
|
||||
B.bNormalize = Settings->bNormalize;
|
||||
B.Mode = Settings->Mode;
|
||||
B.OutputMode = Settings->OutputMode;
|
||||
B.OutputPath = Settings->OutputPath;
|
||||
B.BaseName = Settings->BaseName;
|
||||
B.bOverwrite = Settings->bOverwrite;
|
||||
//B.bGenerateMips = Settings->bGenerateMips;
|
||||
B.Algorithm = Settings->Algorithm;
|
||||
|
||||
// Calls GeneratePixels then bakes with the same flags as preview helper.
|
||||
FNoiseMapGenerator::BakeNoiseTexture(B);
|
||||
|
||||
// Open content browser at location
|
||||
{
|
||||
FString FolderPathStr = Settings->OutputPath;
|
||||
if (!FolderPathStr.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
FolderPathStr = TEXT("/Game");
|
||||
}
|
||||
|
||||
TArray<FString> Folders;
|
||||
Folders.Add(FolderPathStr);
|
||||
|
||||
IContentBrowserSingleton& CB =
|
||||
FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser").Get();
|
||||
CB.SyncBrowserToFolders(Folders);
|
||||
}
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply OnQuit()
|
||||
{
|
||||
TSharedPtr<SDockTab> Tab = FGlobalTabmanager::Get()->FindExistingLiveTab(NoiseMapGenTabName);
|
||||
if (Tab.IsValid())
|
||||
{
|
||||
Tab->RequestCloseTab();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
void Regenerate()
|
||||
{
|
||||
FNoiseBakeSettings B;
|
||||
B.Width=Settings->Width; B.Height=Settings->Height; B.TileRepeats=Settings->TileRepeats; B.Seed=Settings->Seed;
|
||||
B.Octaves=Settings->Octaves; B.Frequency=Settings->Frequency; B.Lacunarity=Settings->Lacunarity; B.Gain=Settings->Gain;
|
||||
B.bNormalize=Settings->bNormalize; B.Mode=Settings->Mode; B.OutputMode=Settings->OutputMode;
|
||||
B.OutputPath=Settings->OutputPath; B.BaseName=Settings->BaseName; B.bOverwrite=Settings->bOverwrite; //B.bGenerateMips=Settings->bGenerateMips;
|
||||
B.Algorithm = Settings->Algorithm;
|
||||
|
||||
TArray<FColor> Pixels; int32 W=0, H=0;
|
||||
if (FNoiseMapGenerator::GeneratePixels(B,Settings->TileRepeats, Pixels, W, H))
|
||||
{
|
||||
if (UTexture2D* T = CreateTransientTexture(W, H, Pixels))
|
||||
{
|
||||
if (!PreviewBrush.IsValid()) PreviewBrush = MakeShareable(new FSlateBrush());
|
||||
PreviewBrush->ImageSize = FVector2D(W, H);
|
||||
PreviewBrush->SetResourceObject(T);
|
||||
PreviewImage->SetImage(PreviewBrush.Get());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
UNoiseMapGeneratorSettings* Settings = nullptr;
|
||||
TSharedPtr<SImage> PreviewImage;
|
||||
TSharedPtr<FSlateBrush> PreviewBrush;
|
||||
};
|
||||
|
||||
void FNoiseMapGeneratorModule::StartupModule()
|
||||
{
|
||||
if (FGlobalTabmanager::Get()->HasTabSpawner(NoiseMapGenTabName))
|
||||
{
|
||||
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(NoiseMapGenTabName);
|
||||
}
|
||||
|
||||
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
|
||||
NoiseMapGenTabName,
|
||||
FOnSpawnTab::CreateLambda([](const FSpawnTabArgs&)
|
||||
{
|
||||
return SNew(SDockTab)
|
||||
.TabRole(ETabRole::NomadTab)
|
||||
[
|
||||
SNew(SNoiseMapGeneratorWidget)
|
||||
];
|
||||
})
|
||||
)
|
||||
.SetDisplayName(FText::FromString("Noise Map Generator"))
|
||||
.SetMenuType(ETabSpawnerMenuType::Hidden);
|
||||
|
||||
if (UToolMenus::IsToolMenuUIEnabled())
|
||||
{
|
||||
RegisterMenus();
|
||||
}
|
||||
else
|
||||
{
|
||||
UToolMenus::RegisterStartupCallback(
|
||||
FSimpleMulticastDelegate::FDelegate::CreateRaw(
|
||||
this, &FNoiseMapGeneratorModule::RegisterMenus));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FNoiseMapGeneratorModule::ShutdownModule()
|
||||
{
|
||||
if (FGlobalTabmanager::Get()->HasTabSpawner(NoiseMapGenTabName))
|
||||
{
|
||||
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(NoiseMapGenTabName);
|
||||
}
|
||||
UToolMenus::UnregisterOwner(this);
|
||||
}
|
||||
|
||||
|
||||
void FNoiseMapGeneratorModule::OpenWindow()
|
||||
{
|
||||
FGlobalTabmanager::Get()->TryInvokeTab(NoiseMapGenTabName);
|
||||
}
|
||||
|
||||
void FNoiseMapGeneratorModule::RegisterMenus()
|
||||
{
|
||||
FToolMenuOwnerScoped OwnerScoped(this);
|
||||
|
||||
if (UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window"))
|
||||
{
|
||||
// Insert a custom section at the top.
|
||||
FToolMenuSection& TopSection = Menu->AddSection(
|
||||
"NoiseMapGeneratorSection",
|
||||
FText::GetEmpty(),
|
||||
FToolMenuInsert(NAME_None, EToolMenuInsertType::First)
|
||||
);
|
||||
|
||||
TopSection.AddMenuEntry(
|
||||
"OpenNoiseMapGenerator",
|
||||
FText::FromString("Noise Map Generator"),
|
||||
FText::FromString("Open the Noise Map Generator window"),
|
||||
FSlateIcon(),
|
||||
FUIAction(FExecuteAction::CreateRaw(this, &FNoiseMapGeneratorModule::OpenWindow))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_MODULE(FNoiseMapGeneratorModule, NoiseMapGenerator)
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorSettings.cpp
|
||||
*/
|
||||
|
||||
#include "NoiseMapGeneratorSettings.h"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGenerator.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "NoiseMapGeneratorSettings.h"
|
||||
|
||||
class UTexture2D;
|
||||
|
||||
struct FNoiseBakeSettings
|
||||
{
|
||||
int32 Width = 1024;
|
||||
int32 Height = 1024;
|
||||
int32 TileRepeats = 1;
|
||||
int32 Seed = 1337;
|
||||
int32 Octaves = 5;
|
||||
float Frequency = 0.25f;
|
||||
float Lacunarity = 2.0f;
|
||||
float Gain = 0.5f;
|
||||
bool bNormalize = true;
|
||||
ENoiseMode Mode = ENoiseMode::FBM;
|
||||
ENoiseAlgorithm Algorithm = ENoiseAlgorithm::Perlin;
|
||||
ENoiseOutputMode OutputMode = ENoiseOutputMode::Grayscale;
|
||||
FString OutputPath = TEXT("/Game/Noise");
|
||||
FString BaseName = TEXT("T_Noise");
|
||||
bool bOverwrite = false;
|
||||
bool bGenerateMips = false;
|
||||
};
|
||||
|
||||
class NOISEMAPGENERATOR_API FNoiseMapGenerator
|
||||
|
||||
{
|
||||
public:
|
||||
static bool GeneratePixels(const FNoiseBakeSettings& S, int32 RepeatsUV, TArray<FColor>& OutPixels, int32& OutW, int32& OutH);
|
||||
static void BakeNoiseTexture(const FNoiseBakeSettings& Settings);
|
||||
static void BakeFromPixels(const FNoiseBakeSettings& S, const TArray<FColor>& Pixels, int32 W, int32 H);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorModule.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "ToolMenus.h"
|
||||
|
||||
class FNoiseMapGeneratorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
private:
|
||||
void OpenWindow();
|
||||
void RegisterMenus();
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorSettings.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "NoiseMapGeneratorSettings.generated.h"
|
||||
|
||||
UENUM()
|
||||
enum class ENoiseMode : uint8
|
||||
{
|
||||
Perlin,
|
||||
FBM
|
||||
};
|
||||
|
||||
UENUM()
|
||||
enum class ENoiseOutputMode : uint8
|
||||
{
|
||||
Grayscale,
|
||||
SplitFirst3Octaves
|
||||
};
|
||||
|
||||
UENUM()
|
||||
enum class ENoiseAlgorithm : uint8
|
||||
{
|
||||
Perlin UMETA(DisplayName="Perlin"),
|
||||
Worley UMETA(DisplayName="Worley (Cellular)"),
|
||||
RidgedFBM UMETA(DisplayName="Ridged FBM")
|
||||
};
|
||||
|
||||
|
||||
UCLASS(config=EditorPerProjectUserSettings, defaultconfig)
|
||||
class UNoiseMapGeneratorSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
|
||||
virtual FText GetSectionText() const override { return FText::FromString(TEXT("Noise Map Generator")); }
|
||||
|
||||
|
||||
// Resolution
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="8", ClampMax="16384"))
|
||||
int32 Width = 1024;
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="8", ClampMax="16384"))
|
||||
int32 Height = 1024;
|
||||
|
||||
// Tiling
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1", ClampMax="64"))
|
||||
int32 TileRepeats = 1;
|
||||
|
||||
// Noise parameters
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
int32 Seed = 1337;
|
||||
|
||||
// Used by FBM (ignored for Perlin-only grayscale)
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1", ClampMax="12"))
|
||||
int32 Octaves = 5;
|
||||
|
||||
// features size
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="0.001", ClampMax="2.0"))
|
||||
float Frequency = 0.5f;
|
||||
|
||||
// Frequency multiplier per octave
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1.0", ClampMax="8.0"))
|
||||
float Lacunarity = 2.0f;
|
||||
|
||||
// Amplitude multiplier per octave
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="0.0", ClampMax="1.0"))
|
||||
float Gain = 0.5f;
|
||||
|
||||
// Normalize result to [0..1] (if off, uses 0.5*(v+1))
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
bool bNormalize = true;
|
||||
|
||||
// Perlin or FBM for grayscale output
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
ENoiseMode Mode = ENoiseMode::FBM;
|
||||
|
||||
// Which base algorithm to use
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
ENoiseAlgorithm Algorithm = ENoiseAlgorithm::Perlin;
|
||||
|
||||
// Grayscale or pack first 3 octaves into RGB
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
ENoiseOutputMode OutputMode = ENoiseOutputMode::Grayscale;
|
||||
|
||||
// Save opt
|
||||
UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
FString OutputPath = TEXT("/Game/Noise");
|
||||
|
||||
UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
FString BaseName = TEXT("T_Noise");
|
||||
|
||||
UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
bool bOverwrite = false;
|
||||
|
||||
//UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
// bool bGenerateMips = false;
|
||||
};
|
||||
25
Plugins/PSOFunctionLibrary/PSOFunctions.uplugin
Normal file
25
Plugins/PSOFunctionLibrary/PSOFunctions.uplugin
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "PSOFunctions",
|
||||
"Description": "Esposes ShaderPipelineCache functions to Blueprint project",
|
||||
"Category": "Other",
|
||||
"CreatedBy": "Danielel",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "https://www.dropbox.com/scl/fi/bjhz5wmklikkojlukz92g/PSOFunctions-Plugin-Documentation.pdf?rlkey=hfowk5im2gnnc58xwrsbfxvjr&st=82lih7p5&dl=0",
|
||||
"MarketplaceURL": "",
|
||||
"SupportURL": "",
|
||||
"CanContainContent": true,
|
||||
"Installed": true,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "PSOFunctions",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/PSOFunctionLibrary/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/PSOFunctionLibrary/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class PSOFunctions : ModuleRules
|
||||
{
|
||||
public PSOFunctions(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"RenderCore"
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#include "PSOFunctions.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FPSOFunctionsModule"
|
||||
|
||||
void FPSOFunctionsModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
}
|
||||
|
||||
void FPSOFunctionsModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FPSOFunctionsModule, PSOFunctions)
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#include "ShaderPipeline.h"
|
||||
#include "ShaderPipelineCache.h"
|
||||
#include "RenderResource.h"
|
||||
|
||||
|
||||
void UShaderPipeline::SetBatchModeFast()
|
||||
{
|
||||
FShaderPipelineCache::SetBatchMode(FShaderPipelineCache::BatchMode::Fast);
|
||||
}
|
||||
|
||||
void UShaderPipeline::SetBatchModeBackground()
|
||||
{
|
||||
FShaderPipelineCache::SetBatchMode(FShaderPipelineCache::BatchMode::Background);
|
||||
}
|
||||
|
||||
void UShaderPipeline::SetBatchModePrecompile()
|
||||
{
|
||||
FShaderPipelineCache::SetBatchMode(FShaderPipelineCache::BatchMode::Precompile);
|
||||
}
|
||||
|
||||
void UShaderPipeline::PauseBatching()
|
||||
{
|
||||
FShaderPipelineCache::PauseBatching();
|
||||
}
|
||||
|
||||
void UShaderPipeline::ResumeBatching()
|
||||
{
|
||||
FShaderPipelineCache::ResumeBatching();
|
||||
}
|
||||
|
||||
bool UShaderPipeline::IsPrecompiling()
|
||||
{
|
||||
return FShaderPipelineCache::IsPrecompiling();
|
||||
}
|
||||
|
||||
int UShaderPipeline::NumPrecompilesRemaining()
|
||||
{
|
||||
return FShaderPipelineCache::NumPrecompilesRemaining();
|
||||
}
|
||||
|
||||
bool UShaderPipeline::IsBatchingPaused()
|
||||
{
|
||||
return FShaderPipelineCache::IsBatchingPaused();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FPSOFunctionsModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "ShaderPipelineCache.h"
|
||||
|
||||
#include "ShaderPipeline.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class PSOFUNCTIONS_API UShaderPipeline : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Sets the precompilation batching mode to Fast"))
|
||||
static void SetBatchModeFast();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Sets the precompilation batching mode to Background"))
|
||||
static void SetBatchModeBackground();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Sets the precompilation batching mode to Precompile"))
|
||||
static void SetBatchModePrecompile();
|
||||
|
||||
/** Pauses precompilation batching. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Pauses precompilation batching"))
|
||||
static void PauseBatching();
|
||||
|
||||
/** Resumes precompilation batching. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Resumes precompilation batching"))
|
||||
static void ResumeBatching();
|
||||
|
||||
/** true if pipeline cache(s) are precompiling. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", BlueprintPure, meta = (ToolTip = "True if pipeline cache(s) are precompiling"))
|
||||
static bool IsPrecompiling();
|
||||
|
||||
/** Returns the number of pipelines waiting for precompilation of the current PSOFC task.
|
||||
if there are multiple PSOFCs pending this will return a minimum value of 1. It may increase as subsequent caches are processed.*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", BlueprintPure, meta = (ToolTip = "Returns the number of pipelines waiting for precompilation of the current PSOFC task. If there are multiple PSOFCs pending this will return a minimum value of 1. It may increase as subsequent caches are processed."))
|
||||
static int NumPrecompilesRemaining();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", BlueprintPure, meta = (ToolTip = "Returns if the precompilation batching is paused"))
|
||||
static bool IsBatchingPaused();
|
||||
};
|
||||
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png
LFS
Normal file
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png
LFS
Normal file
Binary file not shown.
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png
LFS
Normal file
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png
LFS
Normal file
Binary file not shown.
6
Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json
vendored
Normal file
6
Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"speakerline",
|
||||
"squarebrackets"
|
||||
]
|
||||
}
|
||||
12
Plugins/SUDS/Extras/PygmentsLexer/setup.py
Normal file
12
Plugins/SUDS/Extras/PygmentsLexer/setup.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup (
|
||||
name='sudslexer',
|
||||
packages=find_packages(),
|
||||
version="0.4.0",
|
||||
entry_points =
|
||||
"""
|
||||
[pygments.lexers]
|
||||
sudslexer = sudslexer.lexer:SudsLexer
|
||||
""",
|
||||
)
|
||||
71
Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py
Normal file
71
Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from pygments.lexer import RegexLexer, bygroups
|
||||
from pygments.token import *
|
||||
|
||||
class SudsLexer(RegexLexer):
|
||||
name = 'SUDS'
|
||||
aliases = ['suds']
|
||||
filenames = ['*.sud']
|
||||
|
||||
tokens = {
|
||||
'root': [
|
||||
(r'===\s*\n', Generic.Heading),
|
||||
# Choices
|
||||
(r'\s*\*\-?\s+[^\@\n]+[\@\n]', Generic.Subheading),
|
||||
(r'\s*#.*\n', Comment),
|
||||
# Speaker lines
|
||||
(r'\s*\S+\:', Name.Class, 'speakerline'),
|
||||
# Open brackets for special functions
|
||||
(r'\[', Operator, 'squarebrackets'),
|
||||
|
||||
# Variables
|
||||
(r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)),
|
||||
(r'(\|)(plural|gender)(\()(.*?)(\))', bygroups(Operator, Keyword, Operator, Keyword, Operator)),
|
||||
(r'\b([tT]rue|[fF]alse|[mM]asculine|[fF]eminine|[nN]euter)\b', Name.Constant),
|
||||
# Line IDs
|
||||
(r'\@[0-9a-fA-F]+\@', Comment.Special),
|
||||
# Embedded markup
|
||||
(r'\<\w+\>', Name.Decorator),
|
||||
(r'\<\/\>', Name.Decorator),
|
||||
# Goto labels
|
||||
(r'\s*:\S+\n', Name.Label),
|
||||
# Comments
|
||||
(r'\s*\#[\=\+\%]?.*\n', Comment.Special),
|
||||
# Fallback for all other text
|
||||
# Needs an optional \n on the end to finish lines correctly
|
||||
(r'\s+[\n]?', Text),
|
||||
(r'\S+?[\n]?', Text), # non-greedy so we don't consume all non-whitespace
|
||||
|
||||
|
||||
],
|
||||
# While in a speaker line, we ignore everything else except variables & markup
|
||||
'speakerline' : [
|
||||
# Variables
|
||||
(r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)),
|
||||
(r'(\|)(plural|gender)(\()(.*?)(\))', bygroups(Operator, Keyword, Operator, Keyword, Operator)),
|
||||
(r'[\@\n]', Text, '#pop'),
|
||||
# Embedded markup
|
||||
(r'\<\w+\>', Name.Decorator),
|
||||
(r'\<\/\>', Name.Decorator),
|
||||
(r'[^\@\n\<\{]+', Text),
|
||||
|
||||
],
|
||||
# While in a [] block, highlight operators etc (don't do it elsewhere)
|
||||
'squarebrackets' : [
|
||||
# Close bracket finishes
|
||||
(r'\]\s*[\n]?', Operator, '#pop'),
|
||||
# Variables
|
||||
(r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)),
|
||||
(r'\+\/\-\*\!\%', Operator),
|
||||
(r'\"[^\"]*\"', String.Double),
|
||||
(r'\`[^\`]*\`', String.Escape),
|
||||
(r'\d+(\.\d+)?', Number),
|
||||
(r'\b([tT]rue|[fF]alse|[mM]asculine|[fF]eminine|[nN]euter)\b', Name.Constant),
|
||||
# Set, event commands so we can highlight variable/event differently
|
||||
(r'\s*(set|event)(\s+)([^\]\s]+)', bygroups(Keyword, Text, Name.Variable)),
|
||||
(r'\s*(if|else|elseif|endif|event|return|goto|gosub|go to|go sub|random|endrandom)\b', Keyword),
|
||||
(r'\b(and|or|&&|\|\||not)\b', Operator),
|
||||
(r'[,]', Punctuation),
|
||||
(r'\s+', Text), # whitespace OK
|
||||
|
||||
]
|
||||
}
|
||||
18
Plugins/SUDS/License.txt
Normal file
18
Plugins/SUDS/License.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
The MIT License (MIT) Copyright © 2022 Steve Streeting
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
BIN
Plugins/SUDS/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/SUDS/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
BIN
Plugins/SUDS/Resources/logo.afdesign
Normal file
BIN
Plugins/SUDS/Resources/logo.afdesign
Normal file
Binary file not shown.
33
Plugins/SUDS/SUDS.uplugin
Normal file
33
Plugins/SUDS/SUDS.uplugin
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "SUDS",
|
||||
"Description": "Steve's Unreal Dialogue System",
|
||||
"Category": "Other",
|
||||
"CreatedBy": "Steve Streeting",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "",
|
||||
"MarketplaceURL": "",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"IsExperimentalVersion": false,
|
||||
"Installed": false,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "SUDS",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
},
|
||||
{
|
||||
"Name": "SUDSEditor",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default"
|
||||
},
|
||||
{
|
||||
"Name": "SUDSTest",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
Plugins/SUDS/Source/SUDS/Private/SUDS.cpp
Normal file
21
Plugins/SUDS/Source/SUDS/Private/SUDS.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDS.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FSUDSModule"
|
||||
|
||||
void FSUDSModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
}
|
||||
|
||||
void FSUDSModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FSUDSModule, SUDS)
|
||||
4
Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp
Normal file
4
Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#include "SUDSCommon.h"
|
||||
|
||||
const FName FSUDSConstants::RandomItemSelectIndexVarName(SUDS_RANDOMITEM_VAR);
|
||||
|
||||
1379
Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp
Normal file
1379
Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp
Normal file
File diff suppressed because it is too large
Load Diff
465
Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp
Normal file
465
Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp
Normal file
@@ -0,0 +1,465 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSExpression.h"
|
||||
|
||||
#include "SUDSLibrary.h"
|
||||
#include "Misc/DefaultValueHelper.h"
|
||||
#include "Internationalization/Regex.h"
|
||||
|
||||
bool FSUDSExpression::ParseFromString(const FString& Expression, FString* OutParseError)
|
||||
{
|
||||
// Assume invalid until we've parsed something
|
||||
bIsValid = false;
|
||||
Queue.Empty();
|
||||
VariableNames.Empty();
|
||||
SourceString = Expression;
|
||||
|
||||
// Shunting-yard algorithm
|
||||
// Thanks to Nathan Reed https://www.reedbeta.com/blog/the-shunting-yard-algorithm/
|
||||
// We take a natural string, parse it and then turn it into a queue of tokens (operators and operands)
|
||||
// expressed in Reverse Polish Notation, which can be easily executed later
|
||||
// Variables are not resolved at this point, only at execution time.
|
||||
|
||||
// Split into individual tokens; sections of the regex are:
|
||||
// - {Variable}
|
||||
// - Literal numbers (with or without decimal point, with or without preceding negation)
|
||||
// - Arithmetic operators & parentheses
|
||||
// - Boolean operators & comparisons
|
||||
// - Predefined constants (Masculine, feminine, true, false etc)
|
||||
// - Quoted strings "string"
|
||||
// - Including ignoring escaped double quotes
|
||||
// - Quoted names `name`
|
||||
const FRegexPattern Pattern(TEXT("(\\{[\\w\\.]+\\}|-?\\d+(?:\\.\\d*)?|[-+*\\/%\\(\\)]|and|&&|\\|\\||or|not|\\<\\>|!=|!|\\<=?|\\>=?|==?|[mM]asculine|[fF]eminine|[nN]euter|[tT]rue|[fF]alse|\"(?:[^\"\\\\]|\\\\.)*\"|`([^`]*)`)"));
|
||||
FRegexMatcher Regex(Pattern, Expression);
|
||||
// Stacks that we use to construct
|
||||
TArray<ESUDSExpressionItemType> OperatorStack;
|
||||
bool bParsedSomething = false;
|
||||
bool bErrors = false;
|
||||
while (Regex.FindNext())
|
||||
{
|
||||
FString Str = Regex.GetCaptureGroup(1);
|
||||
ESUDSExpressionItemType OpType = ParseOperator(Str);
|
||||
if (OpType != ESUDSExpressionItemType::Null)
|
||||
{
|
||||
bParsedSomething = true;
|
||||
|
||||
if (OpType == ESUDSExpressionItemType::LParens)
|
||||
{
|
||||
OperatorStack.Push(OpType);
|
||||
}
|
||||
else if (OpType == ESUDSExpressionItemType::RParens)
|
||||
{
|
||||
if (OperatorStack.IsEmpty())
|
||||
{
|
||||
if (OutParseError)
|
||||
*OutParseError = TEXT("Mismatched parentheses");
|
||||
bErrors = true;
|
||||
break;
|
||||
}
|
||||
|
||||
while (OperatorStack.Num() > 0 && OperatorStack.Top() != ESUDSExpressionItemType::LParens)
|
||||
{
|
||||
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
|
||||
}
|
||||
if (OperatorStack.IsEmpty())
|
||||
{
|
||||
if (OutParseError)
|
||||
*OutParseError = TEXT("Mismatched parentheses");
|
||||
bErrors = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Discard left parens
|
||||
OperatorStack.Pop();
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// All operators are left-associative except not
|
||||
const bool bLeftAssociative = OpType != ESUDSExpressionItemType::Not;
|
||||
// Valid operator
|
||||
// Apply anything on the operator stack which is higher / equal precedence
|
||||
while (OperatorStack.Num() > 0 &&
|
||||
// higher precedence applied now, and equal precedence if left-associative
|
||||
(static_cast<int>(OperatorStack.Top()) < static_cast<int>(OpType) ||
|
||||
(static_cast<int>(OperatorStack.Top()) <= static_cast<int>(OpType) && bLeftAssociative)))
|
||||
{
|
||||
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
|
||||
}
|
||||
|
||||
OperatorStack.Push(OpType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Attempt to parse operand
|
||||
FSUDSValue Operand;
|
||||
if (ParseOperand(Str, Operand))
|
||||
{
|
||||
bParsedSomething = true;
|
||||
Queue.Add(FSUDSExpressionItem(Operand));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OutParseError)
|
||||
*OutParseError = FString::Printf(TEXT("Unrecognised token %s"), *Str);
|
||||
bErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// finish up
|
||||
while (OperatorStack.Num() > 0)
|
||||
{
|
||||
if (OperatorStack.Top() == ESUDSExpressionItemType::LParens ||
|
||||
OperatorStack.Top() == ESUDSExpressionItemType::RParens)
|
||||
{
|
||||
bErrors = true;
|
||||
if (OutParseError)
|
||||
*OutParseError = TEXT("Mismatched parentheses");
|
||||
break;
|
||||
}
|
||||
|
||||
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
|
||||
}
|
||||
|
||||
if (!Validate() ||
|
||||
(Expression.Len() > 0 && Queue.IsEmpty())) // Empty expressions validate correctly, but if there was text incoming that resolved to nothing, this is an error
|
||||
{
|
||||
bErrors = true;
|
||||
if (OutParseError)
|
||||
*OutParseError = FString::Printf(TEXT("Bad expression '%s'"), *Expression);
|
||||
}
|
||||
|
||||
bIsValid = bParsedSomething && !bErrors;
|
||||
|
||||
// Build list of variables
|
||||
if (bIsValid)
|
||||
{
|
||||
for (auto& Item : Queue)
|
||||
{
|
||||
if (Item.IsOperand() && Item.GetOperandValue().IsVariable())
|
||||
{
|
||||
VariableNames.AddUnique(Item.GetOperandValue().GetVariableNameValue());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return bIsValid;
|
||||
}
|
||||
|
||||
void FSUDSExpression::Reset()
|
||||
{
|
||||
bIsValid = true;
|
||||
Queue.Empty();
|
||||
VariableNames.Empty();
|
||||
SourceString = "";
|
||||
}
|
||||
|
||||
bool FSUDSExpression::IsRandomCondition() const
|
||||
{
|
||||
if (Queue.Num() > 0 && Queue[0].GetType() == ESUDSExpressionItemType::Operand)
|
||||
{
|
||||
const auto& Operand = Queue[0].GetOperandValue();
|
||||
return Operand.IsVariable() && Operand.GetVariableNameValue() == FSUDSConstants::RandomItemSelectIndexVarName;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ESUDSExpressionItemType FSUDSExpression::ParseOperator(const FString& OpStr)
|
||||
{
|
||||
if (OpStr == "+")
|
||||
return ESUDSExpressionItemType::Add;
|
||||
if (OpStr == "-")
|
||||
return ESUDSExpressionItemType::Subtract;
|
||||
if (OpStr == "*")
|
||||
return ESUDSExpressionItemType::Multiply;
|
||||
if (OpStr == "/")
|
||||
return ESUDSExpressionItemType::Divide;
|
||||
if (OpStr == "%")
|
||||
return ESUDSExpressionItemType::Modulo;
|
||||
if (OpStr == "and" || OpStr == "&&")
|
||||
return ESUDSExpressionItemType::And;
|
||||
if (OpStr == "or" || OpStr == "||")
|
||||
return ESUDSExpressionItemType::Or;
|
||||
if (OpStr == "not" || OpStr == "!")
|
||||
return ESUDSExpressionItemType::Not;
|
||||
if (OpStr == "==" || OpStr == "=")
|
||||
return ESUDSExpressionItemType::Equal;
|
||||
if (OpStr == ">=")
|
||||
return ESUDSExpressionItemType::GreaterEqual;
|
||||
if (OpStr == ">")
|
||||
return ESUDSExpressionItemType::Greater;
|
||||
if (OpStr == "<=")
|
||||
return ESUDSExpressionItemType::LessEqual;
|
||||
if (OpStr == "<")
|
||||
return ESUDSExpressionItemType::Less;
|
||||
if (OpStr == "<>" || OpStr == "!=")
|
||||
return ESUDSExpressionItemType::NotEqual;
|
||||
if (OpStr == "(")
|
||||
return ESUDSExpressionItemType::LParens;
|
||||
if (OpStr == ")")
|
||||
return ESUDSExpressionItemType::RParens;
|
||||
|
||||
return ESUDSExpressionItemType::Null;
|
||||
}
|
||||
|
||||
bool FSUDSExpression::ParseOperand(const FString& ValueStr, FSUDSValue& OutVal)
|
||||
{
|
||||
// Try Boolean first since only 2 options
|
||||
{
|
||||
if (ValueStr.Compare("true", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(true);
|
||||
return true;
|
||||
}
|
||||
if (ValueStr.Compare("false", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try gender
|
||||
{
|
||||
if (ValueStr.Compare("masculine", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(ETextGender::Masculine);
|
||||
return true;
|
||||
}
|
||||
if (ValueStr.Compare("feminine", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(ETextGender::Feminine);
|
||||
return true;
|
||||
}
|
||||
if (ValueStr.Compare("neuter", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(ETextGender::Neuter);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try quoted text (will be localised later in asset conversion)
|
||||
{
|
||||
const FRegexPattern Pattern(TEXT("^\"((?:[^\"\\\\]|\\\\.)*)\"$"));
|
||||
FRegexMatcher Regex(Pattern, ValueStr);
|
||||
if (Regex.FindNext())
|
||||
{
|
||||
FString Val = Regex.GetCaptureGroup(1);
|
||||
// Consolidate any escaped double quotes into just quotes
|
||||
Val.ReplaceInline(TEXT("\\\""), TEXT("\""));
|
||||
OutVal = FSUDSValue(FText::FromString(Val));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try FName
|
||||
{
|
||||
const FRegexPattern Pattern(TEXT("^`([^`]*)`$"));
|
||||
FRegexMatcher Regex(Pattern, ValueStr);
|
||||
if (Regex.FindNext())
|
||||
{
|
||||
const FString Name = Regex.GetCaptureGroup(1);
|
||||
OutVal = FSUDSValue(FName(Name), false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try variable name
|
||||
{
|
||||
const FRegexPattern Pattern(TEXT("^\\{([^\\}]*)\\}$"));
|
||||
FRegexMatcher Regex(Pattern, ValueStr);
|
||||
if (Regex.FindNext())
|
||||
{
|
||||
const FName VariableName(Regex.GetCaptureGroup(1));
|
||||
OutVal = FSUDSValue(VariableName, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try Numbers
|
||||
{
|
||||
float FloatVal;
|
||||
int IntVal;
|
||||
// look for int first; anything with a decimal point will fail
|
||||
if (FDefaultValueHelper::ParseInt(ValueStr, IntVal))
|
||||
{
|
||||
OutVal = FSUDSValue(IntVal);
|
||||
return true;
|
||||
}
|
||||
if (FDefaultValueHelper::ParseFloat(ValueStr, FloatVal))
|
||||
{
|
||||
OutVal = FSUDSValue(FloatVal);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSExpression::Validate()
|
||||
{
|
||||
// Empty expressions are always valid, mean "true"
|
||||
if (Queue.IsEmpty())
|
||||
return true;
|
||||
|
||||
// Same algorithm as Execute, we just don't execute
|
||||
TArray<FSUDSExpressionItem> EvalStack;
|
||||
const TMap<FName, FSUDSValue> TempVariables;
|
||||
for (auto& Item : Queue)
|
||||
{
|
||||
if (Item.IsOperator())
|
||||
{
|
||||
FSUDSExpressionItem Arg1, Arg2;
|
||||
if (Item.IsBinaryOperator())
|
||||
{
|
||||
if (EvalStack.IsEmpty())
|
||||
return false;
|
||||
Arg2 = EvalStack.Pop();
|
||||
}
|
||||
if (EvalStack.IsEmpty())
|
||||
return false;
|
||||
Arg1 = EvalStack.Pop();
|
||||
|
||||
EvalStack.Push(EvaluateOperator(Item.GetType(), Arg1, Arg2, TempVariables, TempVariables));
|
||||
}
|
||||
else
|
||||
{
|
||||
EvalStack.Push(Item);
|
||||
}
|
||||
}
|
||||
|
||||
// Must be one item left and must be an operand
|
||||
return EvalStack.Num() == 1 && EvalStack[0].IsOperand();
|
||||
|
||||
}
|
||||
|
||||
FSUDSValue FSUDSExpression::Evaluate(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const
|
||||
{
|
||||
checkf(bIsValid, TEXT("Cannot execute an invalid expression tree"));
|
||||
|
||||
// Blanks are mostly used for conditionals, for simplicity always return true
|
||||
if (Queue.IsEmpty())
|
||||
return FSUDSValue(true);
|
||||
|
||||
TArray<FSUDSExpressionItem> EvalStack;
|
||||
// We could pre-optimise all literal expressions, but let's not for now
|
||||
for (auto& Item : Queue)
|
||||
{
|
||||
if (Item.IsOperator())
|
||||
{
|
||||
FSUDSExpressionItem Arg1, Arg2;
|
||||
// Arg2 (RHS) has to be popped first
|
||||
if (Item.IsBinaryOperator())
|
||||
{
|
||||
checkf(!EvalStack.IsEmpty(), TEXT("Args missing before operator, bad expression"));
|
||||
Arg2 = EvalStack.Pop();
|
||||
}
|
||||
checkf(!EvalStack.IsEmpty(), TEXT("Args missing before operator, bad expression"));
|
||||
Arg1 = EvalStack.Pop();
|
||||
EvalStack.Push(EvaluateOperator(Item.GetType(), Arg1, Arg2, Variables, GlobalVariables));
|
||||
}
|
||||
else
|
||||
{
|
||||
EvalStack.Push(Item);
|
||||
}
|
||||
}
|
||||
|
||||
checkf(EvalStack.Num() == 1, TEXT("We should end with a single item in the eval stack and it should be an operand"));
|
||||
|
||||
return EvaluateOperand(EvalStack.Top().GetOperandValue(), Variables, GlobalVariables);
|
||||
}
|
||||
|
||||
bool FSUDSExpression::EvaluateBoolean(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables, const FString& ErrorContext) const
|
||||
{
|
||||
const auto Result = Evaluate(Variables, GlobalVariables);
|
||||
|
||||
if (Result.GetType() != ESUDSValueType::Boolean &&
|
||||
Result.GetType() != ESUDSValueType::Variable) // Allow unresolved variable, will assume false
|
||||
{
|
||||
UE_LOG(LogSUDS, Error, TEXT("%s: Condition '%s' did not return a boolean result"), *ErrorContext, *SourceString)
|
||||
}
|
||||
|
||||
return Result.GetBooleanValue();
|
||||
}
|
||||
|
||||
FSUDSExpressionItem FSUDSExpression::EvaluateOperator(ESUDSExpressionItemType Op,
|
||||
const FSUDSExpressionItem& Arg1,
|
||||
const FSUDSExpressionItem& Arg2,
|
||||
const TMap<FName, FSUDSValue>& Variables,
|
||||
const TMap<FName, FSUDSValue>& GlobalVariables) const
|
||||
{
|
||||
const FSUDSValue Val1 = EvaluateOperand(Arg1.GetOperandValue(), Variables, GlobalVariables);
|
||||
FSUDSValue Val2;
|
||||
if (Arg1.IsBinaryOperator())
|
||||
{
|
||||
Val2 = EvaluateOperand(Arg2.GetOperandValue(), Variables, GlobalVariables);
|
||||
}
|
||||
|
||||
switch (Op)
|
||||
{
|
||||
case ESUDSExpressionItemType::Not:
|
||||
return FSUDSExpressionItem(!Val1);
|
||||
case ESUDSExpressionItemType::Multiply:
|
||||
return FSUDSExpressionItem(Val1 * Val2);
|
||||
case ESUDSExpressionItemType::Divide:
|
||||
return FSUDSExpressionItem(Val1 / Val2);
|
||||
case ESUDSExpressionItemType::Modulo:
|
||||
return FSUDSExpressionItem(Val1 % Val2);
|
||||
case ESUDSExpressionItemType::Add:
|
||||
return FSUDSExpressionItem(Val1 + Val2);
|
||||
case ESUDSExpressionItemType::Subtract:
|
||||
return FSUDSExpressionItem(Val1 - Val2);
|
||||
case ESUDSExpressionItemType::Less:
|
||||
return FSUDSExpressionItem(Val1 < Val2);
|
||||
case ESUDSExpressionItemType::LessEqual:
|
||||
return FSUDSExpressionItem(Val1 <= Val2);
|
||||
case ESUDSExpressionItemType::Greater:
|
||||
return FSUDSExpressionItem(Val1 > Val2);
|
||||
case ESUDSExpressionItemType::GreaterEqual:
|
||||
return FSUDSExpressionItem(Val1 >= Val2);
|
||||
case ESUDSExpressionItemType::Equal:
|
||||
return FSUDSExpressionItem(Val1 == Val2);
|
||||
case ESUDSExpressionItemType::NotEqual:
|
||||
return FSUDSExpressionItem(Val1 != Val2);
|
||||
case ESUDSExpressionItemType::And:
|
||||
return FSUDSExpressionItem(Val1 && Val2);
|
||||
case ESUDSExpressionItemType::Or:
|
||||
return FSUDSExpressionItem(Val1 || Val2);
|
||||
|
||||
|
||||
default: // these won't occur
|
||||
case ESUDSExpressionItemType::Null:
|
||||
case ESUDSExpressionItemType::Operand:
|
||||
case ESUDSExpressionItemType::LParens:
|
||||
case ESUDSExpressionItemType::RParens:
|
||||
return FSUDSExpressionItem();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
FSUDSValue FSUDSExpression::EvaluateOperand(const FSUDSValue& Operand,
|
||||
const TMap<FName, FSUDSValue>& Variables,
|
||||
const TMap<FName, FSUDSValue>& GlobalVariables) const
|
||||
{
|
||||
// Simplify conversion to variable values
|
||||
if (Operand.IsVariable())
|
||||
{
|
||||
FName Name = Operand.GetVariableNameValue();
|
||||
FName GlobalName;
|
||||
if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName))
|
||||
{
|
||||
// This will have stripped the prefix so direct find is OK
|
||||
if (const auto Var = GlobalVariables.Find(GlobalName))
|
||||
{
|
||||
return *Var;
|
||||
}
|
||||
}
|
||||
if (const auto Var = Variables.Find(Operand.GetVariableNameValue()))
|
||||
{
|
||||
return *Var;
|
||||
}
|
||||
// Note: we're NOT warning about unset variables here, and just defaulting to initial values (false, 0 etc)
|
||||
// This is more usable in practice than complaining about it
|
||||
}
|
||||
|
||||
return Operand;
|
||||
}
|
||||
41
Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h
Normal file
41
Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "SUDSSubsystem.h"
|
||||
#include "SUDSValue.h"
|
||||
|
||||
inline const TMap<FName, FSUDSValue>& InternalGetGlobalVariables(UWorld* WorldContext)
|
||||
{
|
||||
if (auto Sub = GetSUDSSubsystem(WorldContext))
|
||||
{
|
||||
return Sub->GetGlobalVariables();
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// In editor mode, return static global vars since we may be unit testing or in editor tester
|
||||
return USUDSSubsystem::Test_DummyGlobalVariables;
|
||||
#else
|
||||
static TMap<FName, FSUDSValue> Blank;
|
||||
return Blank;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// For our code only
|
||||
inline void InternalSetGlobalVariable(UWorld* WorldContext, FName Name, const FSUDSValue& Value, bool bFromScript, const FString& ScriptName, int LineNo)
|
||||
{
|
||||
if (auto Sub = GetSUDSSubsystem(WorldContext))
|
||||
{
|
||||
Sub->InternalSetGlobalVariable(Name, Value, bFromScript, LineNo);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// In editor mode, update static global vars since we may be unit testing or in editor tester
|
||||
USUDSSubsystem::Test_DummyGlobalVariables.Add(Name, Value);
|
||||
#else
|
||||
checkf(false, TEXT("%s: Attempted to set global variable %s with no world context, did you forget to set the dialogue owner?"), *ScriptName, *Name.ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
154
Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp
Normal file
154
Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSLibrary.h"
|
||||
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
USUDSDialogue* USUDSLibrary::CreateDialogue(UObject* Owner, USUDSScript* Script, bool bStartImmediately, FName StartLabel)
|
||||
{
|
||||
if (IsValid(Script))
|
||||
{
|
||||
if (!IsValid(Owner))
|
||||
{
|
||||
Owner = GetTransientPackage();
|
||||
}
|
||||
const FName Name = MakeUniqueObjectName(Owner, USUDSDialogue::StaticClass(), Script->GetFName());
|
||||
USUDSDialogue* Ret = NewObject<USUDSDialogue>(Owner, Name);
|
||||
Ret->Initialise(Script);
|
||||
if (bStartImmediately)
|
||||
{
|
||||
Ret->Start(StartLabel);
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
UE_LOG(LogSUDS, Error, TEXT("Called CreateDialogue with an invalid script"))
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSDialogue* USUDSLibrary::CreateDialogueWithParticipants(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
const TArray<UObject*>& Participants, bool bStartImmediately, FName StartLabel)
|
||||
{
|
||||
if (IsValid(Script))
|
||||
{
|
||||
if (!IsValid(Owner))
|
||||
{
|
||||
Owner = GetTransientPackage();
|
||||
}
|
||||
// Don't use the base CreateDialogue to start, we want to set participants first before init/start
|
||||
USUDSDialogue* Dlg = NewObject<USUDSDialogue>(Owner, Script->GetFName());
|
||||
Dlg->SetParticipants(Participants);
|
||||
Dlg->Initialise(Script);
|
||||
if (bStartImmediately)
|
||||
{
|
||||
Dlg->Start(StartLabel);
|
||||
}
|
||||
return Dlg;
|
||||
}
|
||||
UE_LOG(LogSUDS, Error, TEXT("Called CreateDialogue with an invalid script"))
|
||||
return nullptr;
|
||||
|
||||
|
||||
}
|
||||
|
||||
USUDSDialogue* USUDSLibrary::CreateDialogueWithParticipant(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
UObject* Participant,
|
||||
bool bStartImmediately,
|
||||
FName StartLabel)
|
||||
{
|
||||
TArray<UObject*> Participants;
|
||||
Participants.Add(Participant);
|
||||
return CreateDialogueWithParticipants(Owner, Script, Participants, bStartImmediately, StartLabel);
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsText(const FSUDSValue& Value, FText& TextValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Text)
|
||||
{
|
||||
TextValue = Value.GetTextValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsBoolean(const FSUDSValue& Value, bool& BoolValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Boolean)
|
||||
{
|
||||
BoolValue = Value.GetBooleanValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsInt(const FSUDSValue& Value, int& IntValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Int)
|
||||
{
|
||||
IntValue = Value.GetIntValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsFloat(const FSUDSValue& Value, float& FloatValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Float)
|
||||
{
|
||||
FloatValue = Value.GetFloatValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsGender(const FSUDSValue& Value, ETextGender& GenderValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Gender)
|
||||
{
|
||||
GenderValue = Value.GetGenderValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsName(const FSUDSValue& Value, FName& NameValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Name)
|
||||
{
|
||||
NameValue = Value.GetNameValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
ESUDSValueType USUDSLibrary::GetDialogueValueType(const FSUDSValue& Value)
|
||||
{
|
||||
return Value.GetType();
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueIsEmpty(const FSUDSValue& Value)
|
||||
{
|
||||
return Value.IsEmpty();
|
||||
}
|
||||
|
||||
bool USUDSLibrary::IsDialogueVariableGlobal(const FName& Name, FName& OutName)
|
||||
{
|
||||
static const FString Prefix(TEXT("global."));
|
||||
FString TempStr;
|
||||
Name.ToString(TempStr);
|
||||
if (TempStr.StartsWith(Prefix, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TempStr.RightChopInline(Prefix.Len());
|
||||
OutName = FName(TempStr);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutName = Name;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
5
Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp
Normal file
5
Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSParticipant.h"
|
||||
|
||||
|
||||
276
Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp
Normal file
276
Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScript.h"
|
||||
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
|
||||
void USUDSScript::StartImport(TArray<TObjectPtr<USUDSScriptNode>>** ppNodes,
|
||||
TArray<TObjectPtr<USUDSScriptNode>>** ppHeaderNodes,
|
||||
TMap<FName, int>** ppLabelList,
|
||||
TMap<FName, int>** ppHeaderLabelList,
|
||||
TArray<FString>** ppSpeakerList)
|
||||
{
|
||||
*ppNodes = &Nodes;
|
||||
*ppHeaderNodes = &HeaderNodes;
|
||||
*ppLabelList = &LabelList;
|
||||
*ppHeaderLabelList = &HeaderLabelList;
|
||||
*ppSpeakerList = &Speakers;
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetNextNode(const USUDSScriptNode* Node) const
|
||||
{
|
||||
switch (Node->GetEdgeCount())
|
||||
{
|
||||
case 0:
|
||||
return nullptr;
|
||||
case 1:
|
||||
return Node->GetEdge(0)->GetTargetNode().Get();
|
||||
default:
|
||||
UE_LOG(LogSUDS, Error, TEXT("Called GetNextNode on a node with more than one edge"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#define kChoiceFound 1
|
||||
#define kChoiceNotFoundBeforeText -1
|
||||
#define kChoiceNotFoundBeforeEnd 0
|
||||
|
||||
|
||||
bool USUDSScript::DoesAnyPathAfterLeadToChoice(USUDSScriptNode* FromNode)
|
||||
{
|
||||
// Look for any possible choice following a node (text or gosub)
|
||||
// If it's possible to find a choice in one of the paths ahead, before another text node, the return true
|
||||
// Given that there might be conditionals, not all paths might lead to a choice, but we only care if one of them does
|
||||
// We recurse into conditional paths where they exist until we know.
|
||||
// For a gosub this is looking for the next after a return, not inside the sub
|
||||
USUDSScriptNode* CurrNode = GetNextNode(FromNode);
|
||||
|
||||
return RecurseLookForChoice(CurrNode) == kChoiceFound;
|
||||
}
|
||||
|
||||
|
||||
int USUDSScript::RecurseLookForChoice(USUDSScriptNode* CurrNode)
|
||||
{
|
||||
// Return int so that we can differentiate:
|
||||
// 1 = we found a choice
|
||||
// 0 = we didn't find a choice, but also didn't hit another text node (reached end, or gosub return)
|
||||
// -1 = we hit a text node
|
||||
while (CurrNode)
|
||||
{
|
||||
switch (CurrNode->GetNodeType())
|
||||
{
|
||||
case ESUDSScriptNodeType::Text:
|
||||
// if we hit a text node, there was no choice
|
||||
return kChoiceNotFoundBeforeText;
|
||||
case ESUDSScriptNodeType::Choice:
|
||||
// we found a choice
|
||||
return kChoiceFound;
|
||||
case ESUDSScriptNodeType::Select:
|
||||
{
|
||||
// Explore all possible routes
|
||||
int WorstResult = kChoiceNotFoundBeforeEnd;
|
||||
for (auto& Edge : CurrNode->GetEdges())
|
||||
{
|
||||
auto TargetNode = Edge.GetTargetNode();
|
||||
if (TargetNode.IsValid())
|
||||
{
|
||||
const int ConditionalPath = RecurseLookForChoice(TargetNode.Get());
|
||||
if (ConditionalPath == kChoiceFound)
|
||||
return kChoiceFound;
|
||||
WorstResult = FMath::Min(ConditionalPath, WorstResult);
|
||||
}
|
||||
}
|
||||
return WorstResult;
|
||||
}
|
||||
case ESUDSScriptNodeType::Event:
|
||||
case ESUDSScriptNodeType::SetVariable:
|
||||
CurrNode = GetNextNode(CurrNode);
|
||||
break;
|
||||
case ESUDSScriptNodeType::Gosub:
|
||||
// When we hit a gosub here we go into it, not after it
|
||||
if (auto GosubNode = Cast<USUDSScriptNodeGosub>(CurrNode))
|
||||
{
|
||||
int SubResult = RecurseLookForChoice(GetNodeByLabel(GosubNode->GetLabelName()));
|
||||
if (SubResult != 0)
|
||||
{
|
||||
// Found definitive result (choice or text) inside sub
|
||||
return SubResult;
|
||||
}
|
||||
}
|
||||
// Otherwise, we didn't conclude within the sub, continue following it
|
||||
CurrNode = GetNextNode(CurrNode);
|
||||
break;
|
||||
default: ;
|
||||
case ESUDSScriptNodeType::Return:
|
||||
// this is when we're exploring a sub for the choice
|
||||
return kChoiceNotFoundBeforeEnd;
|
||||
};
|
||||
}
|
||||
|
||||
return kChoiceNotFoundBeforeEnd;
|
||||
}
|
||||
|
||||
void USUDSScript::FinishImport()
|
||||
{
|
||||
// As an optimisation, make all text/gosub nodes pre-scan their follow-on nodes for choice nodes
|
||||
// We can actually have intermediate nodes, for example set nodes which run for all choices that are placed
|
||||
// between the text and the first choice. Resolve whether they exist now
|
||||
for (auto Node : Nodes)
|
||||
{
|
||||
if (Node->GetNodeType() == ESUDSScriptNodeType::Text ||
|
||||
Node->GetNodeType() == ESUDSScriptNodeType::Gosub)
|
||||
{
|
||||
if (DoesAnyPathAfterLeadToChoice(Node))
|
||||
{
|
||||
switch (Node->GetNodeType())
|
||||
{
|
||||
case ESUDSScriptNodeType::Text:
|
||||
{
|
||||
if (auto TextNode = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
TextNode->NotifyMayHaveChoices();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESUDSScriptNodeType::Gosub:
|
||||
{
|
||||
if (auto GosubNode = Cast<USUDSScriptNodeGosub>(Node))
|
||||
{
|
||||
GosubNode->NotifyMayHaveChoices();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetHeaderNode() const
|
||||
{
|
||||
if (HeaderNodes.Num() > 0)
|
||||
return HeaderNodes[0];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetFirstNode() const
|
||||
{
|
||||
if (Nodes.Num() > 0)
|
||||
return Nodes[0];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetNodeByLabel(const FName& Label) const
|
||||
{
|
||||
if (const int* pIdx = LabelList.Find(Label))
|
||||
{
|
||||
return Nodes[*pIdx];
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
USUDSScriptNodeText* USUDSScript::GetNodeByTextID(const FString& TextID) const
|
||||
{
|
||||
for (auto N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(N))
|
||||
{
|
||||
if (TextID.Equals(TN->GetTextID()))
|
||||
{
|
||||
return TN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSScriptNodeGosub* USUDSScript::GetNodeByGosubID(const FString& ID) const
|
||||
{
|
||||
for (auto N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (auto GN = Cast<USUDSScriptNodeGosub>(N))
|
||||
{
|
||||
if (ID.Equals(GN->GetGosubID()))
|
||||
{
|
||||
return GN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UDialogueVoice* USUDSScript::GetSpeakerVoice(const FString& SpeakerID) const
|
||||
{
|
||||
if (auto pVoice = SpeakerVoices.Find(SpeakerID))
|
||||
{
|
||||
return *pVoice;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void USUDSScript::SetSpeakerVoice(const FString& SpeakerID, UDialogueVoice* Voice)
|
||||
{
|
||||
SpeakerVoices.Add(SpeakerID, Voice);
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
|
||||
void USUDSScript::PostInitProperties()
|
||||
{
|
||||
if (!HasAnyFlags(RF_ClassDefaultObject))
|
||||
{
|
||||
AssetImportData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
|
||||
}
|
||||
Super::PostInitProperties();
|
||||
}
|
||||
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
void USUDSScript::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const
|
||||
{
|
||||
if (AssetImportData)
|
||||
{
|
||||
Context.AddTag( FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden) );
|
||||
}
|
||||
|
||||
Super::GetAssetRegistryTags(Context);
|
||||
}
|
||||
#else
|
||||
void USUDSScript::GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const
|
||||
{
|
||||
if (AssetImportData)
|
||||
{
|
||||
OutTags.Add( FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden) );
|
||||
}
|
||||
|
||||
Super::GetAssetRegistryTags(OutTags);
|
||||
}
|
||||
#endif
|
||||
|
||||
void USUDSScript::Serialize(FArchive& Ar)
|
||||
{
|
||||
Super::Serialize(Ar);
|
||||
|
||||
if (Ar.IsLoading() && Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !AssetImportData)
|
||||
{
|
||||
// AssetImportData should always be valid
|
||||
AssetImportData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
79
Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp
Normal file
79
Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptEdge.h"
|
||||
|
||||
#include "SUDSScriptNode.h"
|
||||
|
||||
|
||||
FSUDSScriptEdge::FSUDSScriptEdge(USUDSScriptNode* ToNode, ESUDSEdgeType InType, int LineNo): Type(InType),
|
||||
TargetNode(ToNode),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSScriptEdge::FSUDSScriptEdge(const FText& InText, USUDSScriptNode* ToNode, int LineNo): Text(InText),
|
||||
Type(ESUDSEdgeType::Decision),
|
||||
TargetNode(ToNode),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
void FSUDSScriptEdge::ExtractFormat() const
|
||||
{
|
||||
// Only do this on demand, and only once
|
||||
TextFormat = Text;
|
||||
ParameterNames.Empty();
|
||||
TArray<FString> TextParams;
|
||||
TextFormat.GetFormatArgumentNames(TextParams);
|
||||
for (auto Param : TextParams)
|
||||
{
|
||||
ParameterNames.Add(FName(Param));
|
||||
}
|
||||
bFormatExtracted = true;
|
||||
}
|
||||
|
||||
FString FSUDSScriptEdge::GetTextID() const
|
||||
{
|
||||
return SUDS_GET_TEXT_KEY(Text);
|
||||
}
|
||||
|
||||
void FSUDSScriptEdge::SetText(const FText& InText)
|
||||
{
|
||||
Text = InText;
|
||||
bFormatExtracted = false;
|
||||
}
|
||||
|
||||
void FSUDSScriptEdge::SetTargetNode(const TWeakObjectPtr<USUDSScriptNode>& InTargetNode)
|
||||
{
|
||||
TargetNode = InTargetNode;
|
||||
}
|
||||
|
||||
const FTextFormat& FSUDSScriptEdge::GetTextFormat() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return TextFormat;
|
||||
|
||||
}
|
||||
|
||||
const TArray<FName>& FSUDSScriptEdge::GetParameterNames() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return ParameterNames;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSScriptEdge::HasParameters() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return !ParameterNames.IsEmpty();
|
||||
|
||||
}
|
||||
38
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp
Normal file
38
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNode.h"
|
||||
|
||||
USUDSScriptNode::USUDSScriptNode()
|
||||
{
|
||||
}
|
||||
|
||||
void USUDSScriptNode::InitChoice(int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Choice;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
|
||||
void USUDSScriptNode::InitSelect(int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Select;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
|
||||
void USUDSScriptNode::InitReturn(int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Return;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
|
||||
bool USUDSScriptNode::IsRandomSelect() const
|
||||
{
|
||||
return NodeType == ESUDSScriptNodeType::Select &&
|
||||
GetEdgeCount() > 0 &&
|
||||
GetEdge(0)->GetCondition().IsRandomCondition();
|
||||
}
|
||||
|
||||
void USUDSScriptNode::AddEdge(const FSUDSScriptEdge& NewEdge)
|
||||
{
|
||||
Edges.Add(NewEdge);
|
||||
}
|
||||
|
||||
12
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp
Normal file
12
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNodeEvent.h"
|
||||
|
||||
void USUDSScriptNodeEvent::Init(const FString& EvtName, const TArray<FSUDSExpression>& InArgs, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Event;
|
||||
EventName = FName(EvtName);
|
||||
Args = InArgs;
|
||||
SourceLineNo = LineNo;
|
||||
|
||||
}
|
||||
11
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp
Normal file
11
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNodeSet.h"
|
||||
|
||||
void USUDSScriptNodeSet::Init(const FString& VarName, const FSUDSExpression& InExpression, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::SetVariable;
|
||||
Identifier = FName(VarName);
|
||||
Expression = InExpression;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
62
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp
Normal file
62
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNodeText.h"
|
||||
|
||||
void USUDSScriptNodeText::Init(const FString& InSpeakerID, const FText& InText, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Text;
|
||||
SpeakerID = InSpeakerID;
|
||||
Text = InText;
|
||||
TextFormat = Text;
|
||||
SourceLineNo = LineNo;
|
||||
bFormatExtracted = false;
|
||||
|
||||
}
|
||||
|
||||
FString USUDSScriptNodeText::GetTextID() const
|
||||
{
|
||||
return SUDS_GET_TEXT_KEY(Text);
|
||||
}
|
||||
|
||||
const FTextFormat& USUDSScriptNodeText::GetTextFormat() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return TextFormat;
|
||||
}
|
||||
|
||||
const TArray<FName>& USUDSScriptNodeText::GetParameterNames() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return ParameterNames;
|
||||
}
|
||||
|
||||
bool USUDSScriptNodeText::HasParameters() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return !ParameterNames.IsEmpty();
|
||||
|
||||
}
|
||||
|
||||
void USUDSScriptNodeText::ExtractFormat() const
|
||||
{
|
||||
// Only do this on demand, and only once
|
||||
TextFormat = Text;
|
||||
ParameterNames.Empty();
|
||||
|
||||
TArray<FString> TextParams;
|
||||
TextFormat.GetFormatArgumentNames(TextParams);
|
||||
for (auto Param : TextParams)
|
||||
{
|
||||
ParameterNames.Add(FName(Param));
|
||||
}
|
||||
bFormatExtracted = true;
|
||||
}
|
||||
199
Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp
Normal file
199
Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSSubsystem.h"
|
||||
#include "Sound/SoundConcurrency.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogSUDSSubsystem)
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
TMap<FName, FSUDSValue> USUDSSubsystem::Test_DummyGlobalVariables;
|
||||
#endif
|
||||
|
||||
void USUDSSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
// Default to a single voice line being played at once
|
||||
VoiceConcurrency = NewObject<USoundConcurrency>(this);
|
||||
VoiceConcurrency->Concurrency.MaxCount = 1;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::Deinitialize()
|
||||
{
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetMaxConcurrentVoicedLines(int ConcurrentLines)
|
||||
{
|
||||
if (IsValid(VoiceConcurrency))
|
||||
{
|
||||
VoiceConcurrency->Concurrency.MaxCount = ConcurrentLines;
|
||||
}
|
||||
}
|
||||
|
||||
int USUDSSubsystem::GetMaxConcurrentVoicedLines() const
|
||||
{
|
||||
if (IsValid(VoiceConcurrency))
|
||||
{
|
||||
return VoiceConcurrency->Concurrency.MaxCount;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::ResetGlobalState(bool bResetVariables)
|
||||
{
|
||||
if (bResetVariables)
|
||||
GlobalVariableState.Empty();
|
||||
}
|
||||
|
||||
FSUDSGlobalState USUDSSubsystem::GetSavedGlobalState() const
|
||||
{
|
||||
return FSUDSGlobalState(GlobalVariableState);
|
||||
}
|
||||
|
||||
void USUDSSubsystem::RestoreSavedGlobalState(const FSUDSGlobalState& State)
|
||||
{
|
||||
ResetGlobalState();
|
||||
GlobalVariableState.Append(State.GetGlobalVariables());
|
||||
}
|
||||
|
||||
|
||||
FText USUDSSubsystem::GetGlobalVariableText(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
if (Arg->GetType() == ESUDSValueType::Text)
|
||||
{
|
||||
return Arg->GetTextValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return FText();
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableInt(FName Name, int32 Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
int USUDSSubsystem::GetGlobalVariableInt(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Int:
|
||||
return Arg->GetIntValue();
|
||||
case ESUDSValueType::Float:
|
||||
UE_LOG(LogSUDSSubsystem, Warning, TEXT("Casting variable %s to int, data loss may occur"), *Name.ToString());
|
||||
return Arg->GetFloatValue();
|
||||
default:
|
||||
case ESUDSValueType::Gender:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible integer type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableFloat(FName Name, float Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
float USUDSSubsystem::GetGlobalVariableFloat(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Int:
|
||||
return Arg->GetIntValue();
|
||||
case ESUDSValueType::Float:
|
||||
return Arg->GetFloatValue();
|
||||
default:
|
||||
case ESUDSValueType::Gender:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible float type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableGender(FName Name, ETextGender Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
ETextGender USUDSSubsystem::GetGlobalVariableGender(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Gender:
|
||||
return Arg->GetGenderValue();
|
||||
default:
|
||||
case ESUDSValueType::Int:
|
||||
case ESUDSValueType::Float:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible gender type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return ETextGender::Neuter;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableBoolean(FName Name, bool Value)
|
||||
{
|
||||
// Use explicit FSUDSValue constructor to avoid default int conversion
|
||||
SetGlobalVariable(Name, FSUDSValue(Value));
|
||||
}
|
||||
|
||||
bool USUDSSubsystem::GetGlobalVariableBoolean(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Boolean:
|
||||
return Arg->GetBooleanValue();
|
||||
case ESUDSValueType::Int:
|
||||
return Arg->GetIntValue() != 0;
|
||||
default:
|
||||
case ESUDSValueType::Float:
|
||||
case ESUDSValueType::Gender:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible boolean type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableName(FName Name, FName Value)
|
||||
{
|
||||
SetGlobalVariable(Name, FSUDSValue(Value, false));
|
||||
}
|
||||
|
||||
FName USUDSSubsystem::GetGlobalVariableName(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
if (Arg->GetType() == ESUDSValueType::Name)
|
||||
{
|
||||
return Arg->GetNameValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::UnSetGlobalVariable(FName Name)
|
||||
{
|
||||
GlobalVariableState.Remove(Name);
|
||||
}
|
||||
100
Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp
Normal file
100
Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSValue.h"
|
||||
|
||||
#include "UObject/Class.h"
|
||||
#include "Internationalization/Text.h"
|
||||
|
||||
FArchive& operator<<(FArchive& Ar, FSUDSValue& Value)
|
||||
{
|
||||
// Custom serialisation since we can't auto-serialise union, TOptional
|
||||
uint8 TypeAsInt = (uint8)Value.Type;
|
||||
Ar << TypeAsInt;
|
||||
if (Ar.IsLoading())
|
||||
Value.Type = static_cast<ESUDSValueType>(TypeAsInt);
|
||||
|
||||
// This gets/sets float value too
|
||||
Ar << Value.IntValue;
|
||||
|
||||
if (Value.Type == ESUDSValueType::Text)
|
||||
{
|
||||
FText Text = Value.TextValue.Get(FText::GetEmpty());
|
||||
Ar << Text;
|
||||
if (Ar.IsLoading())
|
||||
Value.TextValue = Text;
|
||||
}
|
||||
else if (Value.Type == ESUDSValueType::Variable || Value.Type == ESUDSValueType::Name)
|
||||
{
|
||||
FString VarNameStr = Value.Name.Get(NAME_None).ToString();
|
||||
Ar << VarNameStr;
|
||||
if (Ar.IsLoading())
|
||||
Value.Name = FName(VarNameStr);
|
||||
}
|
||||
|
||||
return Ar;
|
||||
}
|
||||
|
||||
void operator<<(FStructuredArchive::FSlot Slot, FSUDSValue& Value)
|
||||
{
|
||||
FStructuredArchive::FRecord Record = Slot.EnterRecord();
|
||||
Record
|
||||
<< SA_VALUE(TEXT("Type"), Value.Type)
|
||||
<< SA_VALUE(TEXT("IntValue"), Value.IntValue); // gets/sets float/boolean/gender too
|
||||
|
||||
if (Value.Type == ESUDSValueType::Text)
|
||||
{
|
||||
Record << SA_VALUE(TEXT("TextValue"), Value.TextValue);
|
||||
}
|
||||
else if (Value.Type == ESUDSValueType::Variable || Value.Type == ESUDSValueType::Name)
|
||||
{
|
||||
Record << SA_VALUE(TEXT("Name"), Value.Name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FString FSUDSValue::ToString() const
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ESUDSValueType::Text:
|
||||
return GetTextValue().ToString();
|
||||
case ESUDSValueType::Int:
|
||||
return FString::FromInt(GetIntValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FString::SanitizeFloat(GetFloatValue());
|
||||
case ESUDSValueType::Boolean:
|
||||
return GetBooleanValue() ? "True" : "False";
|
||||
case ESUDSValueType::Gender:
|
||||
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
|
||||
return LexToString(GetGenderValue());
|
||||
#else
|
||||
return *StaticEnum<ETextGender>()->GetNameStringByValue((int64)GetGenderValue());
|
||||
#endif
|
||||
case ESUDSValueType::Name:
|
||||
return GetNameValue().ToString();
|
||||
case ESUDSValueType::Variable:
|
||||
return GetVariableNameValue().ToString();
|
||||
default:
|
||||
case ESUDSValueType::Empty:
|
||||
return "Empty";
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSValue::ExportTextItem(FString& ValueStr,
|
||||
FSUDSValue const& DefaultValue,
|
||||
UObject* Parent,
|
||||
int32 PortFlags,
|
||||
UObject* ExportRootScope) const
|
||||
{
|
||||
// This is used to generate the blueprint debugger, but also used in serialisation
|
||||
// We need to only implement it for debugging to avoid breaking anything else
|
||||
if (0 != (PortFlags & EPropertyPortFlags::PPF_BlueprintDebugView))
|
||||
{
|
||||
ValueStr.Appendf(TEXT("Type=%s Value=%s"), *StaticEnum<ESUDSValueType>()->GetDisplayValueAsText(Type).ToString(), *ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use the default for everything else
|
||||
return false;
|
||||
|
||||
}
|
||||
15
Plugins/SUDS/Source/SUDS/Public/SUDS.h
Normal file
15
Plugins/SUDS/Source/SUDS/Public/SUDS.h
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class FSUDSModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
23
Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h
Normal file
23
Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
|
||||
// Use DECLARE_LOG_CATEGORY_CLASS not DECLARE_LOG_CATEGORY_EXTERN because we use UE_LOG in headers
|
||||
DECLARE_LOG_CATEGORY_CLASS(LogSUDS, Warning, All)
|
||||
|
||||
#define SUDS_RANDOMITEM_VAR "SUDS.RandomItem"
|
||||
|
||||
struct FSUDSConstants
|
||||
{
|
||||
/// Reserved variable named use to create random results from select nodes
|
||||
static const FName RandomItemSelectIndexVarName;
|
||||
|
||||
};
|
||||
|
||||
#if ENGINE_MINOR_VERSION >= 5
|
||||
#define SUDS_GET_TEXT_KEY(Text) FTextInspector::GetTextId(Text).GetKey().ToString()
|
||||
#else
|
||||
#define SUDS_GET_TEXT_KEY(Text) FTextInspector::GetTextId(Text).GetKey().GetChars()
|
||||
#endif
|
||||
694
Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h
Normal file
694
Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h
Normal file
@@ -0,0 +1,694 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSDialogue.generated.h"
|
||||
|
||||
class USUDSScriptNodeGosub;
|
||||
class USUDSScriptNodeText;
|
||||
struct FSUDSScriptEdge;
|
||||
class USUDSScriptNode;
|
||||
class USUDSScript;
|
||||
class UDialogueWave;
|
||||
class UDialogueVoice;
|
||||
class USoundBase;
|
||||
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueSpeakerLine, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueChoice, class USUDSDialogue*, Dialogue, int, ChoiceIndex);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueProceeding, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueStarting, class USUDSDialogue*, Dialogue, FName, AtLabel);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueFinished, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDialogueEvent, class USUDSDialogue*, Dialogue, FName, EventName, const TArray<FSUDSValue>&, Arguments);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnVariableChangedEvent, class USUDSDialogue*, Dialogue, FName, VariableName, const FSUDSValue&, Value, bool, bFromScript);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnVariableRequestedEvent, class USUDSDialogue*, Dialogue, FName, VariableName);
|
||||
|
||||
#if WITH_EDITOR
|
||||
// Non-dynamic events for editor use
|
||||
DECLARE_DELEGATE_TwoParams(FOnDialogueSpeakerLineInternal, class USUDSDialogue* /* Dialogue */, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_ThreeParams(FOnDialogueChoiceInternal, class USUDSDialogue* /* Dialogue*/, int /*ChoiceIndex*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_OneParam(FOnDialogueProceedingInternal, class USUDSDialogue* /*Dialogue*/);
|
||||
DECLARE_DELEGATE_TwoParams(FOnDialogueStartingInternal, class USUDSDialogue* /*Dialogue*/, FName /*AtLabel*/);
|
||||
DECLARE_DELEGATE_OneParam(FOnDialogueFinishedInternal, class USUDSDialogue* /*Dialogue*/);
|
||||
DECLARE_DELEGATE_FourParams(FOnDialogueEventInternal, class USUDSDialogue* /*Dialogue*/, FName /*EventName*/, const TArray<FSUDSValue>& /*Arguments*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_FiveParams(FOnDialogueVarChangedByScriptInternal, class USUDSDialogue* /* Dialogue*/, FName /*VariableName*/, const FSUDSValue& /*Value*/, const FString& /*ExprString*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_ThreeParams(FOnDialogueVarChangedByCodeInternal, class USUDSDialogue* /* Dialogue*/, FName /*VariableName*/, const FSUDSValue& /*Value*/);
|
||||
DECLARE_DELEGATE_FourParams(FOnDialogueSelectEval, class USUDSDialogue* /*Dialogue*/, const FString& /*ConditionString*/, bool /*bResult*/, int /*SourceLineNo*/);
|
||||
#endif
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSDialogue, Verbose, All);
|
||||
|
||||
/// Copy of the internal state of a dialogue
|
||||
USTRUCT(BlueprintType)
|
||||
struct FSUDSDialogueState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
FString TextNodeID;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> Variables;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TArray<FString> ChoicesTaken;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TArray<FString> ReturnStack;
|
||||
|
||||
public:
|
||||
FSUDSDialogueState() {}
|
||||
|
||||
FSUDSDialogueState(const FString& TxtID,
|
||||
const TMap<FName, FSUDSValue>& InVars,
|
||||
const TSet<FString>& InChoices,
|
||||
const TArray<FString>& InReturnStack) : TextNodeID(TxtID),
|
||||
Variables(InVars),
|
||||
ChoicesTaken(InChoices.Array()),
|
||||
ReturnStack(InReturnStack)
|
||||
{
|
||||
}
|
||||
|
||||
const FString& GetTextNodeID() const { return TextNodeID; }
|
||||
const TMap<FName, FSUDSValue>& GetVariables() const { return Variables; }
|
||||
const TArray<FString>& GetChoicesTaken() const { return ChoicesTaken; }
|
||||
const TArray<FString>& GetReturnStack() const { return ReturnStack; }
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSDialogueState& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSDialogueState& Value);
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* A Dialogue is a runtime instance of a Script (the asset on which the dialogue is based)
|
||||
* An Dialogue always stops on a speaker line, which may have player choices. It progresses when you call Continue()
|
||||
* or Choose() and will run that continuation until it hits the next speaker line. In between, other things may occur
|
||||
* such as setting variables, raising events etc, depending on the script.
|
||||
* Each dialogue instance has its own state, so you can invoke the same Script multiple times as different dialogues if you want.
|
||||
* Each dialogue maintains its own internal state, which includes a set of variables.
|
||||
* Dialogues can have Participants, which are objects closely involved in the dialogue and which have the best access to
|
||||
* supply and retrieve variables and get events first. Other objects can simply listen to the exposed events; while they
|
||||
* can manipulate dialogue state too, they have less controllable access in terms of *when* this happens. It's best to
|
||||
* have at least one Participant driving state on the dialogue (relaying it to external objects), and to have read-only
|
||||
* users like UIs use the event delegates instead.
|
||||
* Dialogues need to be owned by an object, mainly for garbage collection. It's recommended that you set the owner to
|
||||
* one of the NPCs in the dialogue.
|
||||
* You can save/restore the state of a dialogue via GetSavedState/RestoreSavedState.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSDialogue : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
/// Event raised when dialogue progresses and a new speaker line, potentially with new choices, is ready to be displayed
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueSpeakerLine OnSpeakerLine;
|
||||
/// Event raised when a choice is made in the dialogue by the player. At this point, the dialogue has not progressed
|
||||
/// as a result of that choice so the index passed can be used to reference the choice
|
||||
/// This event is ONLY raised if there's a choice of paths, not for just continuing a linear path.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueChoice OnChoice;
|
||||
/// Event raised when the dialog is about to proceed away from the current speaker line (because of a choice or continue)
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueProceeding OnProceeding;
|
||||
/// Event raised when an event is sent from the dialogue script. Any listeners or participants can process the event.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueEvent OnEvent;
|
||||
/// Event raised when a variable is changed. "FromScript" is true if the variable was set by the script, false if set from code
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnVariableChangedEvent OnVariableChanged;
|
||||
/// Event raised when a variable is requested by the dialogue script. You can use this hook to set variables in the
|
||||
/// dialogue on-demand rather than up-front; anything set during this hook will be immediately used by the dialogue
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnVariableRequestedEvent OnVariableRequested;
|
||||
/// Event raised when the dialogue is starting, before the first speaker line
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueStarting OnStarting;
|
||||
/// Event raised when the dialogue finishes
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueFinished OnFinished;
|
||||
protected:
|
||||
UPROPERTY()
|
||||
TObjectPtr<const USUDSScript> BaseScript;
|
||||
UPROPERTY()
|
||||
TObjectPtr<USUDSScriptNodeText> CurrentSpeakerNode;
|
||||
UPROPERTY()
|
||||
TObjectPtr<const USUDSScriptNode> CurrentRootChoiceNode;
|
||||
|
||||
/// External objects which want to closely participate in the dialogue (not just listen to events)
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UObject>> Participants;
|
||||
|
||||
|
||||
/// All of the dialogue variables
|
||||
/// Dialogue variable state is all held locally. Dialogue participants can retrieve or set values in state.
|
||||
/// All state is saved with the dialogue. Variables can be used as text substitution parameters, conditionals,
|
||||
/// or communication with external state.
|
||||
typedef TMap<FName, FSUDSValue> FSUDSValueMap;
|
||||
FSUDSValueMap VariableState;
|
||||
|
||||
/// Stack of Gosub nodes to return to
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<USUDSScriptNodeGosub>> GosubReturnStack;
|
||||
|
||||
/// Set of all the TextIDs of choices taken already in this dialogue
|
||||
TSet<FString> ChoicesTaken;
|
||||
|
||||
TSet<FName> CurrentRequestedParamNames;
|
||||
bool bParamNamesExtracted;
|
||||
|
||||
/// Cached derived info
|
||||
mutable FText CurrentSpeakerDisplayName;
|
||||
/// All valid choices
|
||||
TArray<FSUDSScriptEdge> CurrentChoices;
|
||||
int CurrentSourceLineNo;
|
||||
static const FText DummyText;
|
||||
static const FString DummyString;
|
||||
|
||||
void InitVariables();
|
||||
void RunUntilNextSpeakerNodeOrEnd(USUDSScriptNode* FromNode, bool bRaiseAtEnd);
|
||||
const USUDSScriptNode* WalkToNextChoiceNode(USUDSScriptNode* FromNode, bool bExecute);
|
||||
USUDSScriptNode* RecurseWalkToNextChoiceOrTextNode(USUDSScriptNode* Node, bool bExecute, TArray<TObjectPtr<USUDSScriptNodeGosub>>& LocalGosubStack);
|
||||
const USUDSScriptNode* RunUntilNextChoiceNode(USUDSScriptNode* FromTextNode);
|
||||
const USUDSScriptNode* FindNextChoiceNode(USUDSScriptNode* FromNode);
|
||||
void SetCurrentSpeakerNode(USUDSScriptNodeText* Node, bool bQuietly);
|
||||
void SortParticipants();
|
||||
void RaiseStarting(FName StartLabel);
|
||||
void RaiseFinished();
|
||||
void RaiseNewSpeakerLine();
|
||||
void RaiseChoiceMade(int Index, int LineNo);
|
||||
void RaiseProceeding();
|
||||
void RaiseVariableChange(const FName& VarName, const FSUDSValue& Value, bool bFromScript, int LineNo);
|
||||
void RaiseVariableRequested(const FName& VarName, int LineNo);
|
||||
void RaiseExpressionVariablesRequested(const FSUDSExpression& Expression, int LineNo);
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const;
|
||||
|
||||
USUDSScriptNode* GetNextNode(USUDSScriptNode* Node);
|
||||
bool IsChoiceOrTextNode(ESUDSScriptNodeType Type);
|
||||
USUDSScriptNode* RunNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunSelectNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunSetVariableNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunEventNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunGosubNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunReturnNode(USUDSScriptNode* Node);
|
||||
void UpdateChoices();
|
||||
void RecurseAppendChoices(const USUDSScriptNode* Node, TArray<FSUDSScriptEdge>& OutChoices);
|
||||
USoundBase* GetSoundForCurrentLine(bool bAllowAnyTarget) const;
|
||||
UDialogueVoice* GetTargetVoice() const;
|
||||
class USoundConcurrency* GetVoiceSoundConcurrency() const;
|
||||
|
||||
FText ResolveParameterisedText(const TArray<FName> Params, const FTextFormat& TextFormat, int LineNo);
|
||||
void GetTextFormatArgs(const TArray<FName>& ArgNames, FFormatNamedArguments& OutArgs) const;
|
||||
bool CurrentNodeHasChoices() const;
|
||||
void SetVariableImpl(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo)
|
||||
{
|
||||
const FSUDSValue OldValue = GetVariable(Name);
|
||||
if (!IsVariableSet(Name) ||
|
||||
(OldValue != Value).GetBooleanValue())
|
||||
{
|
||||
VariableState.Add(Name, Value);
|
||||
RaiseVariableChange(Name, Value, bFromScript, LineNo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public:
|
||||
USUDSDialogue();
|
||||
// virtual ~USUDSDialogue() override
|
||||
// {
|
||||
// UE_LOG(LogTemp, Warning, TEXT("*********** Destroyed Dialogue!"));
|
||||
// }
|
||||
void Initialise(const USUDSScript* Script);
|
||||
|
||||
/// Get the script asset this dialogue is based on
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
const USUDSScript* GetScript() const { return BaseScript; }
|
||||
|
||||
/**
|
||||
* Begin the dialogue. Make sure you've added all participants before calling this.
|
||||
* This may not be the first time you've started this dialogue. All previous state is maintained to enable you
|
||||
* for example to take branching paths based on whether you've spoken to this character before.
|
||||
* If you want to reset *all* state, call Restart(true). However this is an extreme case; if you want to just
|
||||
* reset some variables then use the header section of the script to set variables to a default starting point.
|
||||
* @param Label The start point for this dialogue. If None, starts from the beginning.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void Start(FName Label = NAME_None);
|
||||
|
||||
|
||||
/**
|
||||
* Add a participant to this dialogue instance.
|
||||
* Participants are objects which want to be more closely involved in the dialogue. As opposed to event listeners,
|
||||
* participants get advance notice of events in the dialogue, and are also called in a known order, determined by
|
||||
* their priority. If you're providing variables to the dialogue, it is best to do it as a participant since it
|
||||
* gives you much more control.
|
||||
* @param Participant The participant object, which must implement ISUDSParticipant
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void AddParticipant(UObject* Participant);
|
||||
|
||||
/// Retrieve participants from this dialogue
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TArray<UObject*>& GetParticipants() const { return ObjectPtrDecay(Participants); }
|
||||
|
||||
/**
|
||||
* Set the complete list of participants for this dialogue instance.
|
||||
* Participants are objects which want to be more closely involved in the dialogue. As opposed to event listeners,
|
||||
* participants get advance notice of events in the dialogue, and are also called in a known order, determined by
|
||||
* their priority. If you're providing variables to the dialogue, it is best to do it as a participant since it
|
||||
* gives you much more control.
|
||||
* @param NewParticipants List of new participants. Each should implement ISUDSParticipant
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetParticipants(const TArray<UObject*>& NewParticipants);
|
||||
|
||||
|
||||
/// Get the speech text for the current dialogue node
|
||||
/// Any parameters required will be requested from participants in the dialogue and replaced
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
FText GetText();
|
||||
|
||||
/// Get the DialogueWave associated with the current dialogue node
|
||||
/// Returns null if there is no wave for this line.
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
UDialogueWave* GetWave() const;
|
||||
|
||||
/// Return whether the current dialogue node has a Dialogue Wave associated with it
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsCurrentLineVoiced() const;
|
||||
|
||||
/// Get the ID of the current speaker
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
const FString& GetSpeakerID() const;
|
||||
|
||||
/// Get the display name of the current speaker
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
FText GetSpeakerDisplayName() const;
|
||||
|
||||
/// Get the Dialogue Voice belonging to the current speaker, if voiced (Null otherwise)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
UDialogueVoice* GetSpeakerVoice() const;
|
||||
|
||||
/// Get the Dialogue Voice belonging to the named participant, if voiced (Null otherwise)
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
UDialogueVoice* GetVoice(FString Name) const;
|
||||
|
||||
/** If the current line is voiced, plays it in 2D.
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "2", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
void PlayVoicedLine2D(float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, plays it at the given location.
|
||||
* @param Location World position to play dialogue at
|
||||
* @param Rotation World rotation to play dialogue at
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param AttenuationSettings Override attenuation settings package to play sound with
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
void PlayVoicedLineAtLocation(FVector Location,
|
||||
FRotator Rotation,
|
||||
float VolumeMultiplier = 1.f,
|
||||
float PitchMultiplier = 1.f,
|
||||
USoundAttenuation* AttenuationSettings = nullptr,
|
||||
bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, spawn a sound for it in 2D. Use this if you want to control the sound while it's playing.
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "2", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
UAudioComponent* SpawnVoicedLine2D(float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, spawn a sound for it at the given location. Unlike PlayVoicedLineAtLocation you can
|
||||
* attach this sound to a moving object if you want
|
||||
* @param Location World position to play dialogue at
|
||||
* @param Rotation World rotation to play dialogue at
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param AttenuationSettings Override attenuation settings package to play sound with
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
UAudioComponent* SpawnVoicedLineAtLocation(FVector Location,
|
||||
FRotator Rotation,
|
||||
float VolumeMultiplier = 1.f,
|
||||
float PitchMultiplier = 1.f,
|
||||
USoundAttenuation* AttenuationSettings = nullptr,
|
||||
bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, get the sound which would be played for it.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
USoundBase* GetVoicedLineSound(bool bLooselyMatchTarget = true);
|
||||
|
||||
/**
|
||||
* Get the number of choices available from this node.
|
||||
* Note, this will return 1 in the case of just linear text progression. The difference between just linked text
|
||||
* lines and a choice with only 1 option is whether the choice text is blank or not.
|
||||
* See also IsSimpleContinue()
|
||||
* @return The number of choices available
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
int GetNumberOfChoices() const;
|
||||
|
||||
/**
|
||||
* Return whether to progress from here is a simple continue (no choices, no text), meaning you probably want
|
||||
* to display a simpler prompt to the player.
|
||||
* This will return false even if there's only one choice, if that choice has text associated with it.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsSimpleContinue() const;
|
||||
|
||||
/**
|
||||
* Get the text associated with a choice.
|
||||
* @param Index The index of the choice
|
||||
* @return The text. This may be blank if this represents just a link between 2 nodes and not a choice at all.
|
||||
* Note that if you want to have only 1 choice but with associated text, this is fine and should be a choice
|
||||
* line just like any other.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FText GetChoiceText(int Index);
|
||||
|
||||
/// Get all the current choices available, if you prefer this format
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TArray<FSUDSScriptEdge>& GetChoices() const;
|
||||
|
||||
/** Returns whether the choice at the given index has been taken previously.
|
||||
* This is saved in dialogue state so will be remembered across save/restore.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool HasChoiceIndexBeenTakenPreviously(int Index);
|
||||
|
||||
/** Returns whether a choice has been taken previously.
|
||||
* This is saved in dialogue state so will be remembered across save/restore.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool HasChoiceBeenTakenPreviously(const FSUDSScriptEdge& Choice);
|
||||
|
||||
|
||||
/**
|
||||
* Continues the dialogue if (and ONLY if) there is only one valid path/choice out of the current node.
|
||||
* @return True if the dialogue continues after this, false if the dialogue is now at an end.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool Continue();
|
||||
|
||||
/**
|
||||
* Picks one of the available choices
|
||||
* If there's only 1 you can still call this with Index = 0, but also see Continue
|
||||
* @param Index The index of the choice to make
|
||||
* @return True if the dialogue continues, false if it has now reached the end.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool Choose(int Index);
|
||||
|
||||
/// Returns true if the dialogue has reached the end
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsEnded() const;
|
||||
|
||||
/// Returns whether the current speaker line is the last line of dialogue, i.e. there are no
|
||||
/// further choices and the next continue will end the dialogue. This allows you to anticipate
|
||||
/// the end of dialogue (IsEnded() will still return false until the last continue is taken)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsFinalLine() const;
|
||||
|
||||
|
||||
/// End the dialogue early
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void End(bool bQuietly);
|
||||
|
||||
/// Get the source line number of the current position of the dialogue (returns 0 if not applicable)
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
int GetCurrentSourceLine() const;
|
||||
|
||||
|
||||
/**
|
||||
* Restart the dialogue, either from the start or from a named label.
|
||||
* @param bResetState Whether to reset ALL dialogue state, as if the dialogue had been created anew. You mostly don't want
|
||||
* to do this; if you have certain things you want to reset every time, then use [set] commands in the header section
|
||||
* which runs every time the dialogue starts.
|
||||
* @param StartLabel Label to start running from; if None start from the beginning.
|
||||
* @param bReRunHeader If true (default), re-runs the header nodes before starting. Header nodes let you initialise
|
||||
* state that should always be reset when the dialogue is restarted
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void Restart(bool bResetState = false, FName StartLabel = NAME_None, bool bReRunHeader = true);
|
||||
|
||||
/**
|
||||
* Reset the state of this dialogue.
|
||||
* @param bResetVariables If true, resets all variable state
|
||||
* @param bResetPosition If true, resets the current position in the dialogue (which speaker line is next)
|
||||
* @param bResetVisited If true, resets the memory of which choices have been made
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void ResetState(bool bResetVariables = true, bool bResetPosition = true, bool bResetVisited = true);
|
||||
|
||||
/** Retrieve a copy of the state of this dialogue.
|
||||
* This is useful for saving the state of this dialogue.
|
||||
* @return A static copy of the current state of this dialogue. This struct can be serialised with your save data,
|
||||
* and contains both the state of variables and the current speaking node ID.
|
||||
* @note If you save/load mid-dialogue then you're need to have written Text ID's into the source text to ensure they
|
||||
* stay the same between edits, as you do for localisation. If you only save/load after dialogue has ended then
|
||||
* you don't need to worry about this since the dialogue will always start from the beginning
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSDialogueState GetSavedState() const;
|
||||
|
||||
/** Restore the saved state of this dialogue.
|
||||
* This is useful for restoring the state of this dialogue. It will attempt to restore both the value of variables,
|
||||
* and the current speaking node in the dialogue. If you expect to be able to restore to a point mid-dialogue,
|
||||
* it's important that Text IDs are defined in your source file (as for localisation) since that's used as the
|
||||
* identifier of the current speaking node. If you only save/load after dialogue has ended then you don't need
|
||||
* to worry about this as dialogue will restart each time.
|
||||
* @param State Dialogue state that you previously retrieved from GetSavedState().
|
||||
* @note After restoring, you'll want to either call Start() or Continue(), depending on whether you restored
|
||||
* mid-dialogue or not (see IsEnded() to tell whether you did)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void RestoreSavedState(const FSUDSDialogueState& State);
|
||||
|
||||
/// Get the set of text parameters that are actually being asked for in the current state of the dialogue.
|
||||
/// This will include parameters in the text, and parameters in any current choices being displayed.
|
||||
/// Use this if you want to be more specific about what parameters you supply when ISUDSParticipant::UpdateDialogueParameters
|
||||
/// is called.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TSet<FName> GetParametersInUse();
|
||||
|
||||
|
||||
/// Set a variable in dialogue state
|
||||
/// This is mostly only useful if you happen to already have a general purpose FSUDSValue.
|
||||
/// See SetVariableText, SetVariableInt etc for literal-friendly versions
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariable(FName Name, FSUDSValue Value)
|
||||
{
|
||||
SetVariableImpl(Name, Value, false, 0);
|
||||
}
|
||||
|
||||
/// Get a variable in dialogue state as a general value type
|
||||
/// See GetDialogueText, GetDialogueInt etc for more type friendly versions, but if you want to access the state
|
||||
/// as a type-flexible value then you can do so with this function.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetVariable(FName Name) const
|
||||
{
|
||||
if (const auto Arg = VariableState.Find(Name))
|
||||
{
|
||||
return *Arg;
|
||||
}
|
||||
return FSUDSValue();
|
||||
}
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool IsVariableSet(FName Name) const
|
||||
{
|
||||
return VariableState.Contains(Name);
|
||||
}
|
||||
|
||||
/// Get all variables
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TMap<FName, FSUDSValue>& GetVariables() const { return VariableState; }
|
||||
|
||||
/**
|
||||
* Set a text dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableText(FName Name, FText Value)
|
||||
{
|
||||
SetVariable(Name, Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a text dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FText GetVariableText(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a dialogue variable on the passed in parameters collection.
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableInt(FName Name, int32 Value);
|
||||
|
||||
/**
|
||||
* Get an int dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
int GetVariableInt(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a float dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableFloat(FName Name, float Value);
|
||||
|
||||
/**
|
||||
* Get a float dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
float GetVariableFloat(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a gender dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableGender(FName Name, ETextGender Value);
|
||||
|
||||
/**
|
||||
* Get a gender dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
ETextGender GetVariableGender(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a boolean dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableBoolean(FName Name, bool Value);
|
||||
|
||||
/**
|
||||
* Get a boolean dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool GetVariableBoolean(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a name dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableName(FName Name, FName Value);
|
||||
|
||||
/**
|
||||
* Get a name dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FName GetVariableName(FName Name) const;
|
||||
|
||||
|
||||
/**
|
||||
* Remove the definition of a variable.
|
||||
* This has much same effect as setting the variable back to the default value for this type, since attempting to
|
||||
* retrieve a missing variable result in a default value.
|
||||
* @param Name The name of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void UnSetVariable(FName Name);
|
||||
|
||||
/**
|
||||
* Get a piece of user-specified metadata for the current speaker line.
|
||||
* User metadata is assigned by adding a special comment before a speaker line, and can be used
|
||||
* for anything where you need additional data associated with a particular line (as opposed to
|
||||
* setting a variable or sending an event).
|
||||
* @param Key The metadata key
|
||||
* @return The current metadata value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetSpeakerLineUserMetadata(FName Key) const;
|
||||
|
||||
/**
|
||||
* Get all user-specified metadata for the current speaker line.
|
||||
* User metadata is assigned by adding a special comment before a speaker line, and can be used
|
||||
* for anything where you need additional data associated with a particular line (as opposed to
|
||||
* setting a variable or sending an event).
|
||||
* @return All key/value user metadata for the current speaker line
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> GetAllSpeakerLineUserMetadata() const;
|
||||
|
||||
/**
|
||||
* Get a piece of user-specified metadata for a choice in the current list of choices.
|
||||
* User metadata is assigned by adding a special comment before a choice line, and can be used
|
||||
* for anything where you need additional data associated with a particular choice, such as RPG
|
||||
* stat requirements.
|
||||
* @param Index The choice index
|
||||
* @param Key The metadata key
|
||||
* @return The current metadata value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetChoiceUserMetadata(int Index, FName Key) const;
|
||||
|
||||
/**
|
||||
* Get all user-specified metadata for a choice in the current list of choices.
|
||||
* User metadata is assigned by adding a special comment before a choice line, and can be used
|
||||
* for anything where you need additional data associated with a particular choice, such as RPG
|
||||
* stat requirements.
|
||||
* @param Index The choice index
|
||||
* @return All key/value user metadata for the choice
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> GetAllChoiceUserMetadata(int Index) const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
FOnDialogueSpeakerLineInternal InternalOnSpeakerLine;
|
||||
FOnDialogueChoiceInternal InternalOnChoice;
|
||||
FOnDialogueProceedingInternal InternalOnProceeding;
|
||||
FOnDialogueEventInternal InternalOnEvent;
|
||||
FOnDialogueVarChangedByScriptInternal InternalOnSetVar;
|
||||
FOnDialogueVarChangedByCodeInternal InternalOnSetVarByCode;
|
||||
FOnDialogueSelectEval InternalOnSelectEval;
|
||||
FOnDialogueStartingInternal InternalOnStarting;
|
||||
FOnDialogueFinishedInternal InternalOnFinished;
|
||||
#endif
|
||||
};
|
||||
234
Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h
Normal file
234
Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "SUDSValue.h"
|
||||
#include "SUDSExpression.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSExpressionItemType : uint8
|
||||
{
|
||||
Null = 0 UMETA(Hidden),
|
||||
// Operators (must be 0-127, in order of precedence, highest first - gaps left in case we need them)
|
||||
Not = 4,
|
||||
Multiply = 10,
|
||||
Divide = 11,
|
||||
Modulo = 12,
|
||||
Add = 20,
|
||||
Subtract = 21,
|
||||
Less = 30,
|
||||
LessEqual = 31,
|
||||
Greater = 32,
|
||||
GreaterEqual = 33,
|
||||
Equal = 34,
|
||||
NotEqual = 35,
|
||||
And = 40,
|
||||
Or = 41,
|
||||
|
||||
LParens = 100,
|
||||
RParens = 101,
|
||||
|
||||
// Operands (must be 128+)
|
||||
Operand = 128
|
||||
|
||||
};
|
||||
|
||||
/// An item in an expression queue, can be operator or operand
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSExpressionItem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
ESUDSExpressionItemType Type;
|
||||
|
||||
// Value if an operand node
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
FSUDSValue OperandValue;
|
||||
|
||||
public:
|
||||
|
||||
FSUDSExpressionItem() : Type(ESUDSExpressionItemType::Operand) {}
|
||||
FSUDSExpressionItem(ESUDSExpressionItemType Operator) : Type(Operator) {}
|
||||
|
||||
FSUDSExpressionItem(const FSUDSValue& LiteralOrVariable)
|
||||
: Type(ESUDSExpressionItemType::Operand),
|
||||
OperandValue(LiteralOrVariable)
|
||||
{
|
||||
}
|
||||
|
||||
ESUDSExpressionItemType GetType() const { return Type; }
|
||||
// Only valid if optype is operand
|
||||
const FSUDSValue& GetOperandValue() const { return OperandValue; }
|
||||
void SetOperandValue(const FSUDSValue& NewVal) { OperandValue = NewVal; }
|
||||
|
||||
bool IsOperator() const { return static_cast<uint8>(Type) < 128; }
|
||||
bool IsOperand() const { return !IsOperator(); }
|
||||
bool IsBinaryOperator() const
|
||||
{
|
||||
// Only not is unary right now
|
||||
return Type != ESUDSExpressionItemType::Not;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/// An expression holds an executable expression, whether it's a simple single literal
|
||||
/// or a compound expression with variables
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSExpression
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
// The output queue in Reverse Polish Notation order
|
||||
UPROPERTY()
|
||||
TArray<FSUDSExpressionItem> Queue;
|
||||
|
||||
/// Whether the tree is valid to execute
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
bool bIsValid;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FName> VariableNames;
|
||||
|
||||
|
||||
/// The original string version of the expression, for reference
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
FString SourceString;
|
||||
|
||||
FSUDSExpressionItem EvaluateOperator(ESUDSExpressionItemType Op,
|
||||
const FSUDSExpressionItem& Arg1,
|
||||
const FSUDSExpressionItem& Arg2,
|
||||
const TMap<FName, FSUDSValue>& Variables,
|
||||
const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
FSUDSValue EvaluateOperand(const FSUDSValue& Operand, const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
|
||||
bool Validate();
|
||||
|
||||
public:
|
||||
|
||||
FSUDSExpression() : bIsValid(true) {}
|
||||
|
||||
/// Initialise an expression tree just with a single literal or variable
|
||||
FSUDSExpression(const FSUDSValue& LiteralOrVariable)
|
||||
{
|
||||
Queue.Add(FSUDSExpressionItem(LiteralOrVariable));
|
||||
if (LiteralOrVariable.IsVariable())
|
||||
VariableNames.Add(LiteralOrVariable.GetVariableNameValue());
|
||||
bIsValid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse an expression from a string
|
||||
* @param Expression The string to parse
|
||||
* @param OutParseError If there are any errors, pointer to a string to complete with the details
|
||||
* @return Whether the parsing was successful
|
||||
*/
|
||||
bool ParseFromString(const FString& Expression, FString* OutParseError);
|
||||
|
||||
/// Reset the expression to return true
|
||||
void Reset();
|
||||
|
||||
|
||||
/// Evaluate the expression and return the result, using a given variable state
|
||||
FSUDSValue Evaluate(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
|
||||
/// Evaluate the expression and return the result as a boolean, using a given variable state
|
||||
bool EvaluateBoolean(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables, const FString& ErrorContext) const;
|
||||
|
||||
/// Get the original source of the expression as a string
|
||||
const FString& GetSourceString() const { return SourceString; }
|
||||
|
||||
/// Whether this expression can be run (or is empty)
|
||||
bool IsValid() const { return bIsValid; }
|
||||
|
||||
/// Whether this expression is blank
|
||||
bool IsEmpty() const { return Queue.IsEmpty(); }
|
||||
|
||||
/// Get the list of variables this expression needs
|
||||
const TArray<FName>& GetVariableNames() const { return VariableNames; }
|
||||
|
||||
/// Return whether this expression is a generated random condition
|
||||
bool IsRandomCondition() const;
|
||||
|
||||
/**
|
||||
* Attempt to parse an operand from a string. Returns true if this string is a valid operand, which means a literal
|
||||
* (int, float, quoted string, boolean, gender), or a variable reference ({VariableName})
|
||||
* @param ValueStr The string to parse
|
||||
* @param OutVal The operand value which will be populated if successful
|
||||
* @return True if successful, false if not
|
||||
*/
|
||||
static bool ParseOperand(const FString& ValueStr, FSUDSValue& OutVal);
|
||||
|
||||
// Attempt to parse an operator from an incoming string
|
||||
static ESUDSExpressionItemType ParseOperator(const FString& OpStr);
|
||||
|
||||
/// Access the internal RPN execution queue
|
||||
const TArray<FSUDSExpressionItem>& GetQueue() { return Queue; }
|
||||
|
||||
/// Return whether this is a single literal
|
||||
bool IsLiteral() const
|
||||
{
|
||||
return bIsValid && Queue.Num() == 1 && Queue[0].IsOperand() && Queue[0].GetOperandValue().GetType() != ESUDSValueType::Variable;
|
||||
}
|
||||
|
||||
/// Helper method to get literal values
|
||||
FSUDSValue GetLiteralValue() const
|
||||
{
|
||||
check(IsLiteral());
|
||||
return Queue[0].GetOperandValue();
|
||||
}
|
||||
|
||||
/// Return whenter this is a text literal
|
||||
bool IsTextLiteral() const
|
||||
{
|
||||
return bIsValid && Queue.Num() == 1 && Queue[0].IsOperand() && Queue[0].GetOperandValue().GetType() == ESUDSValueType::Text;
|
||||
}
|
||||
|
||||
/// Helper method to get a text literal value, for easier localisation
|
||||
FText GetTextLiteralValue() const
|
||||
{
|
||||
check(IsTextLiteral());
|
||||
return GetLiteralValue().GetTextValue();
|
||||
}
|
||||
/// Helper method to override a text literal
|
||||
void SetTextLiteralValue(const FText& NewLiteral)
|
||||
{
|
||||
check(IsTextLiteral());
|
||||
Queue[0].SetOperandValue(NewLiteral);
|
||||
}
|
||||
|
||||
/// Helper method to get boolean literal value
|
||||
bool GetBooleanLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Boolean);
|
||||
return GetLiteralValue().GetBooleanValue();
|
||||
}
|
||||
/// Helper method to get int literal value
|
||||
int GetIntLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Int);
|
||||
return GetLiteralValue().GetIntValue();
|
||||
}
|
||||
/// Helper method to get float literal value
|
||||
float GetFloatLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Float);
|
||||
return GetLiteralValue().GetFloatValue();
|
||||
}
|
||||
/// Helper method to get gender literal value
|
||||
ETextGender GetGenderLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Gender);
|
||||
return GetLiteralValue().GetGenderValue();
|
||||
}
|
||||
/// Helper method to get name literal value
|
||||
FName GetNameLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Name);
|
||||
return GetLiteralValue().GetNameValue();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
145
Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h
Normal file
145
Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "SUDSLibrary.generated.h"
|
||||
|
||||
class USUDSScript;
|
||||
class USUDSDialogue;
|
||||
UCLASS()
|
||||
class SUDS_API USUDSLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with no participants.
|
||||
* You should subsequently call "SetParticipants" or "AddParticipant" on the returned dialogue if you expect any
|
||||
* parameters or speaker names to work.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogue(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with an initial set of participants.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param Participants List of participants, each of which must implement the ISUDSParticipant interface to be used.
|
||||
* Participants are objects that want to be closely involved in the dialogue to provide variables and receive all events.
|
||||
* Other objects can subscribe to events separately but do not have as much control.
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogueWithParticipants(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
const TArray<UObject*>& Participants,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with a single participants.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param Participant The participant, which must implement the ISUDSParticipant interface to be used.
|
||||
* Participants are objects that want to be closely involved in the dialogue to provide variables and receive all events.
|
||||
* Other objects can subscribe to events separately but do not have as much control.
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogueWithParticipant(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
UObject* Participant,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
/**
|
||||
* Try to extract a text value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param TextValue The text value
|
||||
* @return True if the value was of type text and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsText(const FSUDSValue& Value, FText& TextValue);
|
||||
/**
|
||||
* Try to extract a boolean value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param BoolValue The boolean value
|
||||
* @return True if the value was of type boolean and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsBoolean(const FSUDSValue& Value, bool& BoolValue);
|
||||
/**
|
||||
* Try to extract an integer value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param IntValue The integer value
|
||||
* @return True if the value was of type integer and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsInt(const FSUDSValue& Value, int& IntValue);
|
||||
/**
|
||||
* Try to extract a float value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param FloatValue The float value
|
||||
* @return True if the value was of type float and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsFloat(const FSUDSValue& Value, float& FloatValue);
|
||||
/**
|
||||
* Try to extract a gender value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param GenderValue The gender value
|
||||
* @return True if the value was of type gender and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsGender(const FSUDSValue& Value, ETextGender& GenderValue);
|
||||
/**
|
||||
* Try to extract a Name value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param NameValue The Name value
|
||||
* @return True if the value was of type Name and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsName(const FSUDSValue& Value, FName& NameValue);
|
||||
|
||||
/** Retrieve the type of a SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @return The value type
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static ESUDSValueType GetDialogueValueType(const FSUDSValue& Value);
|
||||
|
||||
/** Determine whether a SUDS value is empty (uninitialised).
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @return Whether the value is empty
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static bool GetDialogueValueIsEmpty(const FSUDSValue& Value);
|
||||
|
||||
/**
|
||||
* Determine whether a SUDS variable name refers to a global variable
|
||||
* @param Name The full name of the variable
|
||||
* @param OutName The name of the variable, trimmed if necessary to remove a global prefix
|
||||
* @return Whether the variable was global
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static bool IsDialogueVariableGlobal(const FName& Name, UPARAM(ref) FName& OutName);
|
||||
};
|
||||
129
Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h
Normal file
129
Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "SUDSParticipant.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
UINTERFACE(MinimalAPI)
|
||||
class USUDSParticipant : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to be implemented by participant objects in a given dialogue.
|
||||
* A participant is simply any object which wants to be closely involved in supplying data to, or retrieving data from,
|
||||
* the dialogue. Although you could do this simply by subscribing to the delegate events on a dialogue, the advantage
|
||||
* of making a participant is that you have better control over the ordering of multiple participants, in case for example
|
||||
* there's some common variable that they both want to set.
|
||||
* It's also a clearer interface to look for vs ad-hoc delegate hooks.
|
||||
* Generally we recommend that:
|
||||
* - Objects providing data to the dialogue should implement ISUDSParticipant
|
||||
* - Anything that just wants to observe the dialogue (like a UI) should just listen to events
|
||||
*
|
||||
* Participants are *guaranteed* to be called earlier than delegates, which means you can set variables from the
|
||||
* Participant callback and when the UI delegate reads text back, all substitution variables will be up to date.
|
||||
*
|
||||
*/
|
||||
class SUDS_API ISUDSParticipant
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Called when a dialogue involving this participant is starting.
|
||||
* The implementation should probably set any starting variables referenced by the dialogue here (or you can do that
|
||||
* later during other functions). At this point there is no active speaker line, we're bootstrapping.
|
||||
* @param Dialogue The dialogue
|
||||
* @param AtLabel The label that the dialogue has started at (None if starting at the beginning)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueStarting(USUDSDialogue* Dialogue, FName AtLabel);
|
||||
|
||||
/**
|
||||
* Called when a dialogue finishes.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueFinished(USUDSDialogue* Dialogue);
|
||||
|
||||
/**
|
||||
* Called when a new speaker line, potentially with attached choices, has become active in the dialogue.
|
||||
* This participant can provide any variable updates if it needs to at this point.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueSpeakerLine(USUDSDialogue* Dialogue);
|
||||
|
||||
/**
|
||||
* Called when a choice is made by the player.
|
||||
* At this point, the dialogue has not progressed as a result of that choice, so the index passed can be used to
|
||||
* reference the choice.
|
||||
* This event is ONLY raised if there's a choice of paths, not for just continuing a linear path.
|
||||
* See OnDialogueProceeding for a more general callback.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
* @param ChoiceIndex The index of the choice that was made
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueChoiceMade(USUDSDialogue* Dialogue, int ChoiceIndex);
|
||||
|
||||
/**
|
||||
* Called just before proceeding with the dialogue from the current speaker line; just after either a choice is made by the player
|
||||
* or the dialogue is just prompted to proceed with its single path.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueProceeding(USUDSDialogue* Dialogue);
|
||||
|
||||
|
||||
/**
|
||||
* Called when an event is raised from dialogue
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param EventName The name of the event that has been raised
|
||||
* @param Arguments
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueEvent(USUDSDialogue* Dialogue, FName EventName, const TArray<FSUDSValue>& Arguments);
|
||||
|
||||
/**
|
||||
* Called when a variable changes value in the dialogue
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param VariableName The name of the variable which has changed value
|
||||
* @param Value The new value
|
||||
* @param bFromScript True if the value changed because of a script line, false if it changed because of code calling SetVariable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueVariableChanged(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& Value, bool bFromScript);
|
||||
|
||||
/**
|
||||
* Called when a variable value is requested by the dialogue script.
|
||||
* While you can set variables on the dialogue at any time and they're persistent, you can implement this method to
|
||||
* provide on-demand variable values (call SetVariable on the dialogue) if you want. This hook is called just before
|
||||
* the variables are used.
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param VariableName The name of the variable which has changed value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueVariableRequested(USUDSDialogue* Dialogue, FName VariableName);
|
||||
|
||||
/**
|
||||
* Return the priority of this participant (default 0).
|
||||
* If for some reason you need to control the order multiple participants in a dialogue are called,
|
||||
* override this method; higher priority participants will be called *later* so that their variables etc override
|
||||
* previously set values.
|
||||
* @return Relative priority, default 0, higher numbers override lower ones.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
int GetDialogueParticipantPriority() const;
|
||||
|
||||
};
|
||||
|
||||
|
||||
114
Plugins/SUDS/Source/SUDS/Public/SUDSScript.h
Normal file
114
Plugins/SUDS/Source/SUDS/Public/SUDSScript.h
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSScript.generated.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
class USUDSScriptNodeText;
|
||||
class USUDSScriptNodeGosub;
|
||||
/**
|
||||
* A single SUDS script asset.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScript : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/// Array of nodes (static after import)
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<TObjectPtr<USUDSScriptNode>> Nodes;
|
||||
|
||||
/// Map of labels to nodes
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
TMap<FName, int> LabelList;
|
||||
|
||||
// Header equivalents for startup
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<TObjectPtr<USUDSScriptNode>> HeaderNodes;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TMap<FName, int> HeaderLabelList;
|
||||
|
||||
/// Array of all speaker IDs found in this script
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
TArray<FString> Speakers;
|
||||
|
||||
/// When using VO, Dialogue Voice assets are associated with speaker IDs
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="SUDS")
|
||||
TMap<FString, TObjectPtr<UDialogueVoice>> SpeakerVoices;
|
||||
|
||||
bool DoesAnyPathAfterLeadToChoice(USUDSScriptNode* FromNode);
|
||||
int RecurseLookForChoice(USUDSScriptNode* CurrNode);
|
||||
|
||||
public:
|
||||
void StartImport(TArray<TObjectPtr<USUDSScriptNode>>** Nodes,
|
||||
TArray<TObjectPtr<USUDSScriptNode>>** HeaderNodes,
|
||||
TMap<FName, int>** LabelList,
|
||||
TMap<FName, int>** ppHeaderLabelList,
|
||||
TArray<FString>** SpeakerList);
|
||||
void FinishImport();
|
||||
|
||||
const TArray<USUDSScriptNode*>& GetNodes() const { return ObjectPtrDecay(Nodes); }
|
||||
const TArray<USUDSScriptNode*>& GetHeaderNodes() const { return ObjectPtrDecay(HeaderNodes); }
|
||||
const TMap<FName, int>& GetLabelList() const { return LabelList; }
|
||||
const TMap<FName, int>& GetHeaderLabelList() const { return HeaderLabelList; }
|
||||
|
||||
|
||||
/// Get the first header node, if any (header nodes are run every time the script starts)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS")
|
||||
USUDSScriptNode* GetHeaderNode() const;
|
||||
|
||||
/// Get the first node of the script, if starting from the beginning
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS")
|
||||
USUDSScriptNode* GetFirstNode() const;
|
||||
|
||||
/// Get the next node after a given node, ONLY if there's only one way to go
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNode* GetNextNode(const USUDSScriptNode* Node) const;
|
||||
|
||||
/// Get the first node of the script following a label, or null if the label wasn't found
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNode* GetNodeByLabel(const FName& Label) const;
|
||||
|
||||
/// Try to find a speaker node by its text ID
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNodeText* GetNodeByTextID(const FString& TextID) const;
|
||||
/// Try to find a gosub node by its gosub ID
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNodeGosub* GetNodeByGosubID(const FString& ID) const;
|
||||
|
||||
|
||||
/// Get the list of speakers
|
||||
const TArray<FString>& GetSpeakers() const { return Speakers; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
UDialogueVoice* GetSpeakerVoice(const FString& SpeakerID) const;
|
||||
|
||||
/// Set up the speaker voice association
|
||||
void SetSpeakerVoice(const FString& SpeakerID, UDialogueVoice* Voice);
|
||||
const TMap<FString, UDialogueVoice*>& GetSpeakerVoices() const { return ObjectPtrDecay(SpeakerVoices); }
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// Import data for this
|
||||
UPROPERTY(VisibleAnywhere, Instanced, Category=ImportSettings)
|
||||
TObjectPtr<class UAssetImportData> AssetImportData;
|
||||
|
||||
// UObject interface
|
||||
virtual void PostInitProperties() override;
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
|
||||
#else
|
||||
virtual void GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const override;
|
||||
#endif
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
// End of UObject interface
|
||||
#endif
|
||||
|
||||
};
|
||||
88
Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h
Normal file
88
Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptEdge.generated.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSEdgeType : uint8
|
||||
{
|
||||
/// A simple continuation; usually for sequences with no choices
|
||||
Continue,
|
||||
/// A decision made by the player from a list of choices
|
||||
Decision,
|
||||
/// A conditional path, taken automatically based on the situation
|
||||
Condition,
|
||||
/// An edge which forms a chain of nodes which are supposed to be considered together
|
||||
/// This links a text node to its choices, and also potentially compound choices underneath if there are selects
|
||||
Chained
|
||||
|
||||
};
|
||||
/**
|
||||
* Edge in the script graph. An edge leads to another node (unidirectional)
|
||||
* Edges can have conditions which mean whether they're valid or not, either as automatic
|
||||
* choices or player choices.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSScriptEdge
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
// Text, if a user choice. Always references a string table
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FText Text;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
ESUDSEdgeType Type;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TWeakObjectPtr<USUDSScriptNode> TargetNode;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FSUDSExpression Condition;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
int SourceLineNo;
|
||||
|
||||
/// User metadata associated with this edge. May include derived expressions
|
||||
UPROPERTY()
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
|
||||
mutable bool bFormatExtracted = false;
|
||||
mutable TArray<FName> ParameterNames;
|
||||
mutable FTextFormat TextFormat;
|
||||
|
||||
void ExtractFormat() const;
|
||||
|
||||
public:
|
||||
FSUDSScriptEdge(): Type(ESUDSEdgeType::Continue), SourceLineNo(0)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSScriptEdge(USUDSScriptNode* ToNode, ESUDSEdgeType InType, int LineNo);
|
||||
|
||||
FSUDSScriptEdge(const FText& InText, USUDSScriptNode* ToNode, int LineNo);
|
||||
|
||||
FText GetText() const { return Text; }
|
||||
FString GetTextID() const;
|
||||
ESUDSEdgeType GetType() const { return Type; }
|
||||
TWeakObjectPtr<USUDSScriptNode> GetTargetNode() const { return TargetNode; }
|
||||
const FSUDSExpression& GetCondition() const { return Condition; }
|
||||
int GetSourceLineNo() const { return SourceLineNo; }
|
||||
const TMap<FName, FSUDSExpression>& GetUserMetadata() const { return UserMetadata; }
|
||||
|
||||
void SetText(const FText& Text);
|
||||
void SetType(ESUDSEdgeType InType) { Type = InType; }
|
||||
void SetTargetNode(const TWeakObjectPtr<USUDSScriptNode>& InTargetNode);
|
||||
void SetCondition(const FSUDSExpression& InCondition) { Condition = InCondition; }
|
||||
void SetUserMetadata(const TMap<FName, FSUDSExpression>& Meta) { UserMetadata = Meta; }
|
||||
|
||||
const FTextFormat& GetTextFormat() const;
|
||||
const TArray<FName>& GetParameterNames() const;
|
||||
bool HasParameters() const;
|
||||
};
|
||||
81
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h
Normal file
81
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptEdge.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSScriptNode.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSScriptNodeType : uint8
|
||||
{
|
||||
/// Text node, displaying a line of dialogue
|
||||
Text,
|
||||
/// Choice node, displaying a series of user choices which navigate to other nodes
|
||||
Choice,
|
||||
/// Select node, automatically selecting one which navigates to another node based on state
|
||||
Select,
|
||||
/// Set variable node
|
||||
SetVariable,
|
||||
/// Event node
|
||||
Event,
|
||||
/// Gosub node
|
||||
Gosub,
|
||||
/// Return node
|
||||
Return,
|
||||
};
|
||||
/**
|
||||
* A node in the script graph.
|
||||
* Nodes are either text, or branch points (user choice or automatic branching logic)
|
||||
* Text nodes always lead to a single next step, be that another text node or a branch.
|
||||
* Branch nodes are separate from the text so that jumping can return to a choice point without emitting more text.
|
||||
* At runtime if a text node's next step is a user choice, it will be available immediately. Otherwise it's not
|
||||
* evaluated until the dialogue progresses.
|
||||
* Edges connect everything. Edges may have text if they're user choices (even if that's a single choice), and
|
||||
* may have conditions.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScriptNode : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/// Type of node
|
||||
/// To make it easier to check rather than having to cast to subtypes blindly. And also not all types need a subtype
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
ESUDSScriptNodeType NodeType = ESUDSScriptNodeType::Text;
|
||||
/// Links to other nodes
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<FSUDSScriptEdge> Edges;
|
||||
/// The line number in the script that this node came from
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
int SourceLineNo;
|
||||
|
||||
|
||||
public:
|
||||
USUDSScriptNode();
|
||||
|
||||
ESUDSScriptNodeType GetNodeType() const { return NodeType; }
|
||||
const TArray<FSUDSScriptEdge>& GetEdges() const { return Edges; }
|
||||
int GetSourceLineNo() const { return SourceLineNo; }
|
||||
|
||||
void AddEdge(const FSUDSScriptEdge& NewEdge);
|
||||
void InitChoice(int LineNo);
|
||||
void InitSelect(int LineNo);
|
||||
void InitReturn(int LineNo);
|
||||
|
||||
int GetEdgeCount() const { return Edges.Num(); }
|
||||
const FSUDSScriptEdge* GetEdge(int Index) const
|
||||
{
|
||||
if (Edges.IsValidIndex(Index))
|
||||
{
|
||||
return &Edges[Index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// Determine if this node is a Select node that's representing a [random]
|
||||
bool IsRandomSelect() const;
|
||||
};
|
||||
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h
Normal file
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeEvent.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeEvent : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
// Variable identifier
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName EventName;
|
||||
|
||||
/// Literal arguments
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<FSUDSExpression> Args;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& EvtName, const TArray<FSUDSExpression>& InArgs, int LineNo);
|
||||
FName GetEventName() const { return EventName; }
|
||||
const TArray<FSUDSExpression>& GetArgs() const { return Args; }
|
||||
|
||||
|
||||
};
|
||||
49
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h
Normal file
49
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeGosub : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
/// Name of the label which we'll jump to before returning
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName LabelName;
|
||||
|
||||
/// Generated ID for use when saving state
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FString GosubID;
|
||||
|
||||
/// Convenience flag to let you know whether this node MAY HAVE any choices directly after it
|
||||
/// Internally this also lets us know to look for the next choice node after returning
|
||||
/// It's possible that where there are conditionals ahead, there are only choices on some of the paths.
|
||||
/// This flag is to let us know to look for choices, but if conditionals apply we may not find any using actual dialogue state.
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
bool bHasChoices = false;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& Label, const FString ID, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Gosub;
|
||||
LabelName = FName(Label);
|
||||
GosubID = ID;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
FName GetLabelName() const { return LabelName; }
|
||||
const FString& GetGosubID() const { return GosubID; }
|
||||
/// Whether on one select path or another a choice was found
|
||||
/// Doesn't help if within a Gosub as call site may be anywhere
|
||||
bool MayHaveChoices() const { return bHasChoices; }
|
||||
|
||||
void NotifyMayHaveChoices() { bHasChoices = true; }
|
||||
|
||||
};
|
||||
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h
Normal file
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeSet.generated.h"
|
||||
|
||||
/**
|
||||
* Set variable node
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeSet : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
// Variable identifier
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName Identifier;
|
||||
|
||||
/// Expression to provide value to set
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FSUDSExpression Expression;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& VarName, const FSUDSExpression& InExpression, int LineNo);
|
||||
const FName& GetIdentifier() const { return Identifier; }
|
||||
const FSUDSExpression& GetExpression() const { return Expression; }
|
||||
|
||||
};
|
||||
72
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h
Normal file
72
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeText.generated.h"
|
||||
|
||||
class UDialogueWave;
|
||||
|
||||
/**
|
||||
* A node which contains speaker text
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScriptNodeText : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
/// Identifier of the speaker for text nodes
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
FString SpeakerID;
|
||||
/// Text, always references a string table. Parameters will not have been completed.
|
||||
/// Note: if you're using voiced dialogue, see the Wave property and its subtitle functionality
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
FText Text;
|
||||
/// DialogueWave asset link for voiced dialogue
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="SUDS")
|
||||
TObjectPtr<UDialogueWave> Wave;
|
||||
|
||||
/// Convenience flag to let you know whether this text node MAY HAVE choices attached
|
||||
/// If false, there's only one way to proceed from here and no text associated with that
|
||||
/// If true, either there can be > 1 choice options, or a single choice with associated text (this can be when
|
||||
/// you have no choice but want text rather than just a continue button)
|
||||
/// Internally this also lets us know to look for the next choice node
|
||||
/// It's possible that where there are conditionals ahead, there are only choices on some of the paths.
|
||||
/// This flag is to let us know to look for choices, but if conditionals apply we may not find any using actual dialogue state.
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
bool bHasChoices = false;
|
||||
|
||||
/// User metadata associated with this speaker line. May include derived expressions
|
||||
UPROPERTY()
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
mutable bool bFormatExtracted = false;
|
||||
mutable TArray<FName> ParameterNames;
|
||||
mutable FTextFormat TextFormat;
|
||||
|
||||
void ExtractFormat() const;
|
||||
|
||||
public:
|
||||
const FString& GetSpeakerID() const { return SpeakerID; }
|
||||
const FText& GetText() const { return Text; }
|
||||
FString GetTextID() const;
|
||||
UDialogueWave* GetWave() const { return Wave; }
|
||||
/// Whether on one select path or another a choice was found
|
||||
/// Doesn't help if within a Gosub as call site may be anywhere
|
||||
bool MayHaveChoices() const { return bHasChoices; }
|
||||
|
||||
void Init(const FString& SpeakerID, const FText& Text, int LineNo);
|
||||
void SetWave(UDialogueWave* InWave) { Wave = InWave; }
|
||||
const FTextFormat& GetTextFormat() const;
|
||||
const TArray<FName>& GetParameterNames() const;
|
||||
bool HasParameters() const;
|
||||
|
||||
void NotifyMayHaveChoices() { bHasChoices = true; }
|
||||
|
||||
const TMap<FName, FSUDSExpression>& GetUserMetadata() const { return UserMetadata; }
|
||||
void SetUserMetadata(const TMap<FName, FSUDSExpression>& Meta) { UserMetadata = Meta; }
|
||||
|
||||
|
||||
};
|
||||
292
Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h
Normal file
292
Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "SUDSSubsystem.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
class USUDSScript;
|
||||
class USoundConcurrency;
|
||||
struct FSoundConcurrencySettings;
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSSubsystem, Log, All);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnGlobalVariableChangedEvent, FName, VariableName, const FSUDSValue&, Value, bool, bFromScript);
|
||||
|
||||
/// Copy of the global state of the system
|
||||
USTRUCT(BlueprintType)
|
||||
struct FSUDSGlobalState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS")
|
||||
TMap<FName, FSUDSValue> GlobalVariables;
|
||||
|
||||
public:
|
||||
FSUDSGlobalState() {}
|
||||
|
||||
FSUDSGlobalState(const TMap<FName, FSUDSValue>& InGlobalVars) : GlobalVariables(InGlobalVars)
|
||||
{
|
||||
}
|
||||
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const { return GlobalVariables; }
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSGlobalState& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSGlobalState& Value);
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/// Event raised when a global variable is changed. "FromScript" is true if the variable was set by the script, false if set from code
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnGlobalVariableChangedEvent OnGlobalVariableChanged;
|
||||
|
||||
protected:
|
||||
UPROPERTY()
|
||||
TObjectPtr<USoundConcurrency> VoiceConcurrency;
|
||||
|
||||
/// Global variable state
|
||||
TMap<FName, FSUDSValue> GlobalVariableState;
|
||||
|
||||
void SetGlobalVariableImpl(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo)
|
||||
{
|
||||
const FSUDSValue OldValue = GetGlobalVariable(Name);
|
||||
if (!IsGlobalVariableSet(Name) ||
|
||||
(OldValue != Value).GetBooleanValue())
|
||||
{
|
||||
GlobalVariableState.Add(Name, Value);
|
||||
OnGlobalVariableChanged.Broadcast(Name, Value, bFromScript);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Sets the number of voiced lines that can be played at once. Defaults to 1, so that when a new voiced line is
|
||||
* played while another is still playing, the previous one is stopped. If you would prefer that they overlap, set
|
||||
* this to >1
|
||||
* @param ConcurrentLines The number of voice lines that can be played at once.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Settings")
|
||||
void SetMaxConcurrentVoicedLines(int ConcurrentLines);
|
||||
|
||||
/**
|
||||
* Gets the number of voiced lines that can be played at once. Defaults to 1, so that when a new voiced line is
|
||||
* played while another is still playing, the previous one is stopped.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Settings")
|
||||
int GetMaxConcurrentVoicedLines() const;
|
||||
|
||||
USoundConcurrency* GetVoicedLineConcurrency() const { return VoiceConcurrency; }
|
||||
|
||||
|
||||
/**
|
||||
* Reset the global state of the system.
|
||||
* @param bResetVariables If true, resets all variable state
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global State")
|
||||
void ResetGlobalState(bool bResetVariables = true);
|
||||
|
||||
/** Retrieve a copy of the global state of the system.
|
||||
* This is useful for saving the state of e.g. global variables.
|
||||
* @return A static copy of the current global state. This struct can be serialised with your save data,
|
||||
* and contains the state of global variables.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global State")
|
||||
FSUDSGlobalState GetSavedGlobalState() const;
|
||||
|
||||
/** Restore the global saved state.
|
||||
* This is useful for restoring global state eg global variables when you're loading a game.
|
||||
* @param State Global state that you previously retrieved from GetSavedGlobalState().
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void RestoreSavedGlobalState(const FSUDSGlobalState& State);
|
||||
|
||||
/// Set a global variable
|
||||
/// This is mostly only useful if you happen to already have a general purpose FSUDSValue.
|
||||
/// See SetGlobalVariableText, SetGlobalVariableInt etc for literal-friendly versions
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariable(FName Name, FSUDSValue Value)
|
||||
{
|
||||
SetGlobalVariableImpl(Name, Value, false, 0);
|
||||
}
|
||||
|
||||
/// Internal use only
|
||||
void InternalSetGlobalVariable(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo) { SetGlobalVariableImpl(Name, Value, bFromScript, LineNo); }
|
||||
|
||||
/// Get a variable in dialogue state as a general value type
|
||||
/// See GetDialogueText, GetDialogueInt etc for more type friendly versions, but if you want to access the state
|
||||
/// as a type-flexible value then you can do so with this function.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FSUDSValue GetGlobalVariable(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
return *Arg;
|
||||
}
|
||||
return FSUDSValue();
|
||||
}
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
bool IsGlobalVariableSet(FName Name) const
|
||||
{
|
||||
return GlobalVariableState.Contains(Name);
|
||||
}
|
||||
|
||||
/// Get all variables
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const { return GlobalVariableState; }
|
||||
|
||||
/**
|
||||
* Set a text global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableText(FName Name, FText Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a text global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FText GetGlobalVariableText(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a global variable on the passed in parameters collection.
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableInt(FName Name, int32 Value);
|
||||
|
||||
/**
|
||||
* Get an int global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
int GetGlobalVariableInt(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a float global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableFloat(FName Name, float Value);
|
||||
|
||||
/**
|
||||
* Get a float global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
float GetGlobalVariableFloat(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a gender global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableGender(FName Name, ETextGender Value);
|
||||
|
||||
/**
|
||||
* Get a gender global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
ETextGender GetGlobalVariableGender(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a boolean global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableBoolean(FName Name, bool Value);
|
||||
|
||||
/**
|
||||
* Get a boolean global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
bool GetGlobalVariableBoolean(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a name global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableName(FName Name, FName Value);
|
||||
|
||||
/**
|
||||
* Get a name global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FName GetGlobalVariableName(FName Name) const;
|
||||
|
||||
|
||||
/**
|
||||
* Remove the definition of a global variable.
|
||||
* This has much same effect as setting the variable back to the default value for this type, since attempting to
|
||||
* retrieve a missing variable result in a default value.
|
||||
* @param Name The name of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void UnSetGlobalVariable(FName Name);
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/// Only for use by tests / editor tools when real subsystem isn't running
|
||||
static TMap<FName, FSUDSValue> Test_DummyGlobalVariables;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
inline USUDSSubsystem* GetSUDSSubsystem(UWorld* WorldContext)
|
||||
{
|
||||
if (IsValid(WorldContext) && WorldContext->IsGameWorld())
|
||||
{
|
||||
auto GI = WorldContext->GetGameInstance();
|
||||
if (IsValid(GI))
|
||||
return GI->GetSubsystem<USUDSSubsystem>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
416
Plugins/SUDS/Source/SUDS/Public/SUDSValue.h
Normal file
416
Plugins/SUDS/Source/SUDS/Public/SUDSValue.h
Normal file
@@ -0,0 +1,416 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "SUDSCommon.h"
|
||||
#include "SUDSValue.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSValueType : uint8
|
||||
{
|
||||
Text = 0,
|
||||
Int = 1,
|
||||
Float = 2,
|
||||
Boolean = 3,
|
||||
Gender = 4,
|
||||
Name = 5,
|
||||
/// Access the value of another variable
|
||||
Variable = 10,
|
||||
|
||||
Empty = 99
|
||||
};
|
||||
/// Struct which can hold any of the value types that SUDS needs to use, in a Blueprint friendly manner
|
||||
/// For getting / setting these values from blueprints, see blueprint library functions SetSUDSValue<Type>() / GetSUDSValue<Type>()
|
||||
/// For convenience these are wrapped in USUDSDialogue but in e.g. event callbacks they're not
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSValue
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
ESUDSValueType Type;
|
||||
union
|
||||
{
|
||||
int32 IntValue;
|
||||
float FloatValue;
|
||||
};
|
||||
TOptional<FText> TextValue;
|
||||
// Used for variables and name values
|
||||
TOptional<FName> Name;
|
||||
public:
|
||||
|
||||
FSUDSValue() : Type(ESUDSValueType::Empty), IntValue(0), TextValue(FText::GetEmpty()) {}
|
||||
|
||||
FSUDSValue(const int32 Value)
|
||||
: Type(ESUDSValueType::Int) { IntValue = Value; }
|
||||
|
||||
FSUDSValue(const float Value)
|
||||
: Type(ESUDSValueType::Float) { FloatValue = Value; }
|
||||
|
||||
FSUDSValue(const FText& Value)
|
||||
: Type(ESUDSValueType::Text),
|
||||
IntValue(0),
|
||||
TextValue(Value)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSValue(FText&& Value)
|
||||
: Type(ESUDSValueType::Text), IntValue(0), TextValue(MoveTemp(Value))
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSValue(ETextGender Value)
|
||||
: Type(ESUDSValueType::Gender), IntValue(0)
|
||||
{
|
||||
IntValue = static_cast<int32>(Value);
|
||||
}
|
||||
|
||||
FSUDSValue(bool Value)
|
||||
: Type(ESUDSValueType::Boolean), IntValue(0)
|
||||
{
|
||||
IntValue = Value ? 1 : 0;
|
||||
}
|
||||
|
||||
FSUDSValue(const FName& ReferencedName, bool bIsVariable)
|
||||
: Type(bIsVariable ? ESUDSValueType::Variable : ESUDSValueType::Name),
|
||||
IntValue(0),
|
||||
Name(ReferencedName)
|
||||
{
|
||||
}
|
||||
|
||||
// Construct a default value of a given type
|
||||
explicit FSUDSValue(ESUDSValueType ValType)
|
||||
: Type(ValType), IntValue(0)
|
||||
{
|
||||
}
|
||||
|
||||
/// Whether this value is empty, i.e. hasn't been set to anything
|
||||
FORCEINLINE bool IsEmpty() const
|
||||
{
|
||||
return Type == ESUDSValueType::Empty;
|
||||
}
|
||||
|
||||
FORCEINLINE ESUDSValueType GetType() const
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
|
||||
FORCEINLINE int32 GetIntValue() const
|
||||
{
|
||||
// We don't warn for unset variables / uninitialised values, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Int && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as int but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return IntValue;
|
||||
}
|
||||
|
||||
FORCEINLINE float GetFloatValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && !IsNumeric() && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as float but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Type == ESUDSValueType::Int)
|
||||
{
|
||||
// Allow int widening to float
|
||||
return GetIntValue();
|
||||
}
|
||||
return FloatValue;
|
||||
}
|
||||
|
||||
FORCEINLINE const FText& GetTextValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Text && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as text but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (TextValue.IsSet())
|
||||
return TextValue.GetValue();
|
||||
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
|
||||
FORCEINLINE ETextGender GetGenderValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Gender && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as float but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return static_cast<ETextGender>(IntValue);
|
||||
}
|
||||
|
||||
FORCEINLINE bool GetBooleanValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Boolean && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as boolean but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return IntValue != 0;
|
||||
}
|
||||
|
||||
FORCEINLINE FName GetNameValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Name && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as Name but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Name.IsSet())
|
||||
return Name.GetValue();
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
FORCEINLINE FName GetVariableNameValue() const
|
||||
{
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as variable name but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Name.IsSet())
|
||||
return Name.GetValue();
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
FORCEINLINE bool IsVariable() const
|
||||
{
|
||||
return Type == ESUDSValueType::Variable;
|
||||
}
|
||||
|
||||
bool IsNumeric() const
|
||||
{
|
||||
return Type == ESUDSValueType::Float || Type == ESUDSValueType::Int;
|
||||
}
|
||||
|
||||
FFormatArgumentValue ToFormatArg() const
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
default:
|
||||
case ESUDSValueType::Text:
|
||||
return FFormatArgumentValue(TextValue.GetValue());
|
||||
case ESUDSValueType::Int:
|
||||
return FFormatArgumentValue(GetIntValue());
|
||||
case ESUDSValueType::Boolean:
|
||||
return FFormatArgumentValue(GetBooleanValue());
|
||||
case ESUDSValueType::Gender:
|
||||
return FFormatArgumentValue(GetGenderValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FFormatArgumentValue(GetFloatValue());
|
||||
case ESUDSValueType::Name:
|
||||
// return something useful from FName even though technically shouldn't use in visible text
|
||||
return FFormatArgumentValue(FText::FromName(GetNameValue()));
|
||||
case ESUDSValueType::Empty:
|
||||
case ESUDSValueType::Variable:
|
||||
return FFormatArgumentValue();
|
||||
}
|
||||
}
|
||||
|
||||
void SetValue(const FSUDSValue& RValue)
|
||||
{
|
||||
*this = RValue;
|
||||
}
|
||||
|
||||
/// Not operation, technically only valid on booleans, but not enforced so as to allow unset vars & conversions
|
||||
FSUDSValue operator!() const
|
||||
{
|
||||
return FSUDSValue(!GetBooleanValue());
|
||||
}
|
||||
|
||||
FSUDSValue operator*(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() * Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
*
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator/(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() / Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
/
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator%(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() % Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
float lval = (Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue());
|
||||
float rval = (Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue());
|
||||
// Prevent NaN and errors reported when executing FMath::Fmod with invalid values.
|
||||
return FSUDSValue(rval != 0 ? FMath::Fmod(lval, rval) : 0.0f);
|
||||
}
|
||||
FSUDSValue operator+(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() + Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
+
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator-(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() - Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
-
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator<(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// Don't check types here. We'll fall back on int comparisons which is important for cases where
|
||||
// a variable hasn't been set
|
||||
// result is boolean so no need to protect types
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
<
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
|
||||
}
|
||||
FSUDSValue operator==(const FSUDSValue& Rhs) const
|
||||
{
|
||||
if (IsNumeric() || Rhs.IsNumeric())
|
||||
{
|
||||
if (GetType() == ESUDSValueType::Float || Rhs.GetType() == ESUDSValueType::Float)
|
||||
{
|
||||
// For floats, use tolerance
|
||||
return FSUDSValue(FMath::IsNearlyEqual(
|
||||
GetType() == ESUDSValueType::Int ? (float)GetIntValue() : GetFloatValue(),
|
||||
Rhs.GetType() == ESUDSValueType::Int ? (float)Rhs.GetIntValue() : Rhs.GetFloatValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
return FSUDSValue(GetIntValue() == Rhs.GetIntValue());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// no auto conversion here
|
||||
// however, tolerate unresolved variables, they will return initial values
|
||||
if (Type != Rhs.Type && Type != ESUDSValueType::Variable && Rhs.Type != ESUDSValueType::Variable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare using type from whichever one isn't a variable (get values on unset variables will return defaults)
|
||||
ESUDSValueType UseType = IsVariable() ? Rhs.GetType() : GetType();
|
||||
switch (UseType)
|
||||
{
|
||||
case ESUDSValueType::Text:
|
||||
return FSUDSValue(GetTextValue().EqualTo(Rhs.GetTextValue()));
|
||||
case ESUDSValueType::Boolean:
|
||||
return FSUDSValue(GetBooleanValue() == Rhs.GetBooleanValue());
|
||||
case ESUDSValueType::Gender:
|
||||
return FSUDSValue(GetGenderValue() == Rhs.GetGenderValue());
|
||||
case ESUDSValueType::Variable:
|
||||
return FSUDSValue(GetVariableNameValue() == Rhs.GetVariableNameValue());
|
||||
case ESUDSValueType::Name:
|
||||
return FSUDSValue(GetNameValue() == Rhs.GetNameValue());
|
||||
// deal with int/float again here, this mops up cases where one side is an unset variable
|
||||
case ESUDSValueType::Int:
|
||||
return FSUDSValue(GetIntValue() == Rhs.GetIntValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FSUDSValue(GetFloatValue() == Rhs.GetFloatValue());
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
return FSUDSValue(false);
|
||||
}
|
||||
FSUDSValue operator<=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
if ((*this < Rhs).GetBooleanValue())
|
||||
return FSUDSValue(true);
|
||||
return (*this == Rhs);
|
||||
}
|
||||
|
||||
FSUDSValue operator>(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return Rhs < *this;
|
||||
}
|
||||
|
||||
FSUDSValue operator>=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return Rhs <= *this;
|
||||
}
|
||||
|
||||
FSUDSValue operator!=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return !(*this == Rhs);
|
||||
}
|
||||
|
||||
FSUDSValue operator&&(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We always let unset variables degrade to false
|
||||
check(Type == ESUDSValueType::Boolean || Type == ESUDSValueType::Variable);
|
||||
check(Rhs.Type == ESUDSValueType::Boolean || Rhs.Type == ESUDSValueType::Variable);
|
||||
return FSUDSValue(GetBooleanValue() && Rhs.GetBooleanValue());
|
||||
}
|
||||
|
||||
FSUDSValue operator||(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We always let unset variables degrade to false
|
||||
check(Type == ESUDSValueType::Boolean || Type == ESUDSValueType::Variable);
|
||||
check(Rhs.Type == ESUDSValueType::Boolean || Rhs.Type == ESUDSValueType::Variable);
|
||||
return FSUDSValue(GetBooleanValue() || Rhs.GetBooleanValue());
|
||||
}
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSValue& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSValue& Value);
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
FString ToString() const;
|
||||
|
||||
bool ExportTextItem(FString& ValueStr, FSUDSValue const& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const;
|
||||
};
|
||||
template<>
|
||||
struct TStructOpsTypeTraits<FSUDSValue> : public TStructOpsTypeTraitsBase2<FSUDSValue>
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithSerializer = true,
|
||||
WithExportTextItem = true
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
53
Plugins/SUDS/Source/SUDS/SUDS.Build.cs
Normal file
53
Plugins/SUDS/Source/SUDS/SUDS.Build.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDS : ModuleRules
|
||||
{
|
||||
public SUDS(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditor.h"
|
||||
|
||||
#include "ISettingsModule.h"
|
||||
#include "ISettingsSection.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSScriptActions.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Styling/SlateStyle.h"
|
||||
#include "Styling/SlateStyleRegistry.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FSUDSModule"
|
||||
|
||||
void FSUDSEditorModule::StartupModule()
|
||||
{
|
||||
ScriptActions = MakeShared<FSUDSScriptActions>();
|
||||
FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
auto SudsPlugin = IPluginManager::Get().FindPlugin(TEXT("SUDS"));
|
||||
if (SudsPlugin.IsValid())
|
||||
{
|
||||
const FString IconDir = SudsPlugin->GetBaseDir() + "/Content/Editor/Slate/Icons";
|
||||
const FVector2D Sz16 = FVector2D(16.0f, 16.0f);
|
||||
const FVector2D Sz64 = FVector2D(64.0f, 64.0f);
|
||||
|
||||
StyleSet = MakeShared<FSlateStyleSet>("SUDSStyleSet");
|
||||
StyleSet->Set(TEXT("ClassIcon.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_16x.png"), Sz16));
|
||||
StyleSet->Set(TEXT("ClassThumbnail.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_64x.png"), Sz64));
|
||||
|
||||
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
// register settings
|
||||
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
|
||||
if (SettingsModule)
|
||||
{
|
||||
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "SUDS Editor",
|
||||
LOCTEXT("SUDSEditorSettingsName", "SUDS Editor"),
|
||||
LOCTEXT("SUDSEditorSettingsDescription", "Configure the editor parts of SUDS."),
|
||||
GetMutableDefault<USUDSEditorSettings>()
|
||||
);
|
||||
}
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Started"))
|
||||
|
||||
}
|
||||
|
||||
void FSUDSEditorModule::ShutdownModule()
|
||||
{
|
||||
if (StyleSet.IsValid())
|
||||
{
|
||||
FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return;
|
||||
FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Shut Down"))
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FSUDSEditorModule, SUDSEditor)
|
||||
DEFINE_LOG_CATEGORY(LogSUDSEditor);
|
||||
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
@@ -0,0 +1,280 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeSet.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
void FSUDSEditorScriptTools::WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
const auto& SrcData = Script->AssetImportData->SourceData;
|
||||
if (SrcData.SourceFiles.Num() == 1)
|
||||
{
|
||||
TArray<FString> Lines;
|
||||
FString SourceFile = SrcData.SourceFiles[0].RelativeFilename;
|
||||
auto Package = Script->GetPackage();
|
||||
if (FPaths::IsRelative(SourceFile) && Package)
|
||||
{
|
||||
FString PackagePath = FPackageName::LongPackageNameToFilename(FPackageName::GetLongPackagePath(Package->GetPathName()));
|
||||
SourceFile = FPaths::ConvertRelativePathToFull(PackagePath, SourceFile);
|
||||
}
|
||||
if (FFileHelper::LoadFileToStringArray(Lines, *SourceFile))
|
||||
{
|
||||
const int PrevErrs = Logger.NumErrors();
|
||||
const bool bHeaderChanges = WriteBackTextIDsFromNodes(Script->GetHeaderNodes(), Lines, Script->GetName(), Logger);
|
||||
const bool bBodyChanges = WriteBackTextIDsFromNodes(Script->GetNodes(), Lines, Script->GetName(), Logger);
|
||||
|
||||
if (Logger.NumErrors() > PrevErrs)
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Errors prevented saving updates to %s."), *Script->GetName())));
|
||||
}
|
||||
else if (bHeaderChanges || bBodyChanges)
|
||||
{
|
||||
// We need to re-hash file in memory before saving to prevent re-import
|
||||
int32 Length = 10;
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
Length += Line.Len() + UE_ARRAY_COUNT(LINE_TERMINATOR);
|
||||
}
|
||||
FString CombinedString;
|
||||
CombinedString.Reserve(Length);
|
||||
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
CombinedString += Line;
|
||||
CombinedString += LINE_TERMINATOR;
|
||||
}
|
||||
|
||||
// Write source file back; always encode as UTF8 not default ANSI/UTF16
|
||||
// Do this before updating asset import data so it picks up new file timestamp
|
||||
FFileHelper::SaveStringToFile(FStringView(CombinedString), *SourceFile, FFileHelper::EEncodingOptions::ForceUTF8);
|
||||
|
||||
/* BEGIN try to prevent re-import prompt
|
||||
* This unfortunately didn't work, so removed & let it reimport
|
||||
|
||||
const FMD5Hash FileHash = FSUDSScriptImporter::CalculateHash(*CombinedString, CombinedString.Len());
|
||||
Script->AssetImportData->Update(SourceFile, FileHash);
|
||||
|
||||
// Need to mark asset dirty since hash has changed
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
|
||||
// Even this doesn't work:
|
||||
GUnrealEd->AutoReimportManager->IgnoreFileModification(SourceFile);
|
||||
|
||||
* END try to prevent re-import
|
||||
*/
|
||||
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Successfully updated %s"), *SourceFile)));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("No changes were required to %s"), *SourceFile)));
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT("Error opening source asset %s"), *SourceFile)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(
|
||||
FString::Printf(TEXT("No source files associated with asset %s"), *Script->GetName())));
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
bool bAnyChanges = false;
|
||||
// For each speaker line, set line and choice edge, use source line no to append text ID
|
||||
for (const auto& N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (const auto* TN = Cast<USUDSScriptNodeText>(N))
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(TN->GetText(), TN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::SetVariable)
|
||||
{
|
||||
if (const auto* SN = Cast<USUDSScriptNodeSet>(N))
|
||||
{
|
||||
if (SN->GetExpression().IsTextLiteral())
|
||||
{
|
||||
FText Literal = SN->GetExpression().GetTextLiteralValue();
|
||||
|
||||
bAnyChanges = WriteBackTextID(Literal, SN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Choice)
|
||||
{
|
||||
// Edges
|
||||
for (auto& Edge : N->GetEdges())
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(Edge.GetText(), Edge.GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Gosub)
|
||||
{
|
||||
if (const auto* GN = Cast<USUDSScriptNodeGosub>(N))
|
||||
// Need to write back Gosub IDs so that saved return stacks work
|
||||
bAnyChanges = WriteBackGosubID(GN->GetGosubID(), GN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
return bAnyChanges;
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
if (AssetText.IsEmpty())
|
||||
return false;
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back TextID to '%s', source line number %d is invalid (Text: '%s')"),
|
||||
*NameForErrors,
|
||||
LineNo,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
const FString TextID = SUDS_GET_TEXT_KEY(AssetText);
|
||||
FString ExistingTextID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundExisting = FSUDSScriptImporter::RetrieveTextIDFromLine(SourceLineView, ExistingTextID, ExistingNum);
|
||||
|
||||
// Existing TextID - replace if already there
|
||||
if (!bFoundExisting || ExistingTextID != TextID)
|
||||
{
|
||||
if (TextIDCheckMatch(AssetText, SourceLine))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *TextID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set TextID on line %d of %s but source file did not contain expected text '%s'"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::TextIDCheckMatch(const FText& AssetText, const FString& SourceLine)
|
||||
{
|
||||
// Text from our asset must be present in the text in the source line
|
||||
// However, in case this is a multi-line string, take the first line only
|
||||
FString AssetStr = AssetText.ToString();
|
||||
{
|
||||
FString L, R;
|
||||
if (AssetStr.Split("\n", &L, &R))
|
||||
{
|
||||
AssetStr = L.TrimStartAndEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Bear in mind that this line might be a text line, a choice or a set line
|
||||
// therefore we only check if the source line contains the asset text
|
||||
|
||||
return SourceLine.Contains(AssetStr);
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackGosubID(const FString& GosubID,
|
||||
int LineNo,
|
||||
TArray<FString>& Lines,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger& Logger)
|
||||
{
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back GosubID to '%s', source line number %d is invalid"),
|
||||
*NameForErrors,
|
||||
LineNo)));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
FString ExistingID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundID = FSUDSScriptImporter::RetrieveGosubIDFromLine(SourceLineView, ExistingID, ExistingNum);
|
||||
|
||||
if (!bFoundID || ExistingID != GosubID)
|
||||
{
|
||||
// Check it's a gosub line
|
||||
if (SourceLine.Contains(TEXT("gosub")) || SourceLine.Contains(TEXT("go sub")))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *GosubID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set GosubID on line %d of %s but source file did not have a gosubu on that line"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors)));
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
bool USUDSEditorSettings::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (AlwaysAutoGenerateVoiceOverAssetsOnImport)
|
||||
return true;
|
||||
|
||||
for (auto Dir : DirectoriesToAutoGenerateVoiceOverAssetsOnImport)
|
||||
{
|
||||
if (FPaths::IsUnderDirectory(PackagePath, Dir.Path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueVoiceAssetLocation, DialogueVoiceAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueWaveAssetLocation, DialogueWaveAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetOutputDir(ESUDSAssetLocation Location,
|
||||
const FString& SharedPath,
|
||||
const FString& PackagePath,
|
||||
const FString& ScriptName)
|
||||
{
|
||||
switch(Location)
|
||||
{
|
||||
default:
|
||||
case ESUDSAssetLocation::SharedDirectory:
|
||||
return SharedPath;
|
||||
case ESUDSAssetLocation::SharedDirectorySubdir:
|
||||
return FPaths::Combine(SharedPath, ScriptName);
|
||||
case ESUDSAssetLocation::ScriptDirectory:
|
||||
return PackagePath;
|
||||
case ESUDSAssetLocation::ScriptDirectorySubdir:
|
||||
return FPaths::Combine(PackagePath, ScriptName);
|
||||
}
|
||||
}
|
||||
1590
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
1590
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
File diff suppressed because it is too large
Load Diff
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
@@ -0,0 +1,343 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Framework/Commands/Commands.h"
|
||||
#include "Framework/Text/BaseTextLayoutMarshaller.h"
|
||||
#include "Toolkits/AssetEditorToolkit.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Widgets/Views/STableRow.h"
|
||||
|
||||
class SMultiLineEditableTextBox;
|
||||
struct FSUDSValue;
|
||||
class USUDSDialogue;
|
||||
class USUDSScript;
|
||||
|
||||
#define LOCTEXT_NAMESPACE "SUDS"
|
||||
|
||||
class FSUDSToolbarCommands
|
||||
: public TCommands<FSUDSToolbarCommands>
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSToolbarCommands()
|
||||
: TCommands<FSUDSToolbarCommands>(
|
||||
"SUDS",
|
||||
LOCTEXT("SUDS", "Steves Unreal Dialogue System"),
|
||||
NAME_None,
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0
|
||||
FAppStyle::GetAppStyleSetName()
|
||||
#else
|
||||
FEditorStyle::GetStyleSetName()
|
||||
#endif
|
||||
)
|
||||
{ }
|
||||
|
||||
public:
|
||||
|
||||
// TCommands interface
|
||||
|
||||
virtual void RegisterCommands() override
|
||||
{
|
||||
UI_COMMAND(StartDialogue, "Start Dialogue", "Start/restart dialogue", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(WriteBackTextIDs, "Write String Keys", "Write string keys back to script source to stabilise for localisation / voice asset links", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(GenerateVOAssets, "Generate Voice Assets", "Generate DialogueVoice and DialogueWave assets for VO", EUserInterfaceActionType::Button, FInputChord());
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
TSharedPtr<FUICommandInfo> StartDialogue;
|
||||
TSharedPtr<FUICommandInfo> WriteBackTextIDs;
|
||||
TSharedPtr<FUICommandInfo> GenerateVOAssets;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorOutputRow
|
||||
{
|
||||
public:
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
FSlateColor BgColour;
|
||||
|
||||
FSUDSEditorOutputRow(const FText& InPrefix,
|
||||
const FText& InLine,
|
||||
const FSlateColor& InPrefixColour,
|
||||
const FSlateColor& InLineColour,
|
||||
const FSlateColor& InBgColour) :
|
||||
Prefix(InPrefix),
|
||||
Line(InLine),
|
||||
PrefixColour(InPrefixColour),
|
||||
LineColour(InLineColour),
|
||||
BgColour(InBgColour)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorOutputItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorOutputItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FText, Prefix)
|
||||
SLATE_ARGUMENT(FText, Line)
|
||||
SLATE_ARGUMENT(FSlateColor, PrefixColour)
|
||||
SLATE_ARGUMENT(FSlateColor, LineColour)
|
||||
SLATE_ARGUMENT(FSlateColor, BgColour)
|
||||
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorVariableRow
|
||||
{
|
||||
public:
|
||||
FName Name;
|
||||
FSUDSValue Value;
|
||||
bool bIsManualOverride;
|
||||
|
||||
FSUDSEditorVariableRow(const FName& InName, const FSUDSValue& InValue, bool bIsManual) : Name(InName),
|
||||
Value(InValue),
|
||||
bIsManualOverride(bIsManual)
|
||||
{
|
||||
}
|
||||
|
||||
friend bool operator<(const FSUDSEditorVariableRow& Lhs, const FSUDSEditorVariableRow& RHS)
|
||||
{
|
||||
return Lhs.Name.LexicalLess(RHS.Name);
|
||||
}
|
||||
|
||||
friend bool operator<(const TSharedPtr<FSUDSEditorVariableRow>& Lhs, const TSharedPtr<FSUDSEditorVariableRow>& RHS)
|
||||
{
|
||||
return *Lhs < *RHS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorVariableItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorVariableItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FName, VariableName)
|
||||
SLATE_ARGUMENT(FSUDSValue, VariableValue)
|
||||
SLATE_ARGUMENT(bool, bIsManualOverride)
|
||||
SLATE_ARGUMENT(class FSUDSEditorToolkit*, Parent)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
public:
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FName VariableName;
|
||||
FSUDSValue VariableValue;
|
||||
bool bIsManualOverride = false;
|
||||
class FSUDSEditorToolkit* Parent = nullptr;
|
||||
|
||||
TSharedRef<class SWidget> GetGenderMenu();
|
||||
void OnGenderSelected(ETextGender TextGender);
|
||||
virtual FVector2D ComputeDesiredSize(float) const override;
|
||||
|
||||
};
|
||||
|
||||
struct FSUDSTraceLogMessage
|
||||
{
|
||||
TSharedRef<FString> Message;
|
||||
FName Category;
|
||||
FSlateColor Colour;
|
||||
|
||||
FSUDSTraceLogMessage(FName InCategory, const FString& InMessage, const FSlateColor& InColour)
|
||||
: Message(MakeShared<FString>(InMessage))
|
||||
, Category(InCategory)
|
||||
, Colour(InColour)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class FSUDSTraceLogMarshaller : public FBaseTextLayoutMarshaller
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSTraceLogMarshaller();
|
||||
|
||||
// ITextLayoutMarshaller
|
||||
virtual void SetText(const FString& SourceString, FTextLayout& TargetTextLayout) override;
|
||||
virtual void GetText(FString& TargetString, const FTextLayout& SourceTextLayout) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
protected:
|
||||
TArray< TSharedPtr<FSUDSTraceLogMessage> > Messages;
|
||||
};
|
||||
|
||||
// Trace log widget, really just so we can tick and automatically scroll to the end
|
||||
class SSUDSTraceLog : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSTraceLog)
|
||||
{}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
SSUDSTraceLog() : bIsUserScrolled(false) {}
|
||||
|
||||
void Construct(const FArguments& InArgs);
|
||||
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
void ScrollToEnd();
|
||||
|
||||
protected:
|
||||
bool bIsUserScrolled;
|
||||
TSharedPtr<FSUDSTraceLogMarshaller> TraceLogMarshaller;
|
||||
TSharedPtr<SMultiLineEditableTextBox> TraceLogTextBox;
|
||||
|
||||
void OnUserScrolled(float X);
|
||||
|
||||
};
|
||||
|
||||
class FSUDSEditorToolkit : public FAssetEditorToolkit
|
||||
{
|
||||
public:
|
||||
void InitEditor(const TArray<UObject*>& InObjects);
|
||||
|
||||
virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
|
||||
virtual FText GetBaseToolkitName() const override;
|
||||
virtual FName GetToolkitFName() const override;
|
||||
virtual FLinearColor GetWorldCentricTabColorScale() const override;
|
||||
virtual FString GetWorldCentricTabPrefix() const override;
|
||||
void UserEditVariable(const FName& Name, FSUDSValue Value);
|
||||
void DeleteVariable(const FName& Name);
|
||||
|
||||
protected:
|
||||
virtual void OnClose() override;
|
||||
|
||||
private:
|
||||
USUDSScript* Script = nullptr;
|
||||
USUDSDialogue* Dialogue = nullptr;
|
||||
float VarColumnWidth = 120;
|
||||
float PrefixColumnWidth = 100;
|
||||
FName StartLabel = NAME_None;
|
||||
bool bResetVarsOnStart = true;
|
||||
FDelegateHandle ReimportDelegateHandle;
|
||||
// FSUDSEditorDialogueRow needs to held by a TSharedPtr for SListView
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorOutputRow>>> OutputListView;
|
||||
TArray<TSharedPtr<FSUDSEditorOutputRow>> OutputRows;
|
||||
TSharedPtr<SVerticalBox> ChoicesBox;
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorVariableRow>>> VariablesListView;
|
||||
TArray<TSharedPtr<FSUDSEditorVariableRow>> VariableRows;
|
||||
TSharedPtr<SSUDSTraceLog> TraceLog;
|
||||
TMap<FName, FSUDSValue> ManualOverrideVariables;
|
||||
|
||||
static FName DialogueOutputTabName;
|
||||
static FName DetailsTabName;
|
||||
static FName VariablesTabName;
|
||||
static FName LogTabName;
|
||||
|
||||
|
||||
const FSlateColor SpeakerColour = FLinearColor(1.0f, 1.0f, 0.6f, 1.0f);
|
||||
const FSlateColor ChoiceColour = FLinearColor(0.4f, 1.0f, 0.4f, 1.0f);
|
||||
const FSlateColor EventColour = FLinearColor(0.2f, 0.6f, 1.0f, 1.0f);
|
||||
const FSlateColor VarSetColour = FLinearColor(0.8f, 0.6f, 0.9f, 1.0f);
|
||||
const FSlateColor VarEditColour = FLinearColor(0.6f, 0.3f, 1.0f, 1.0f);
|
||||
const FSlateColor StartColour = FLinearColor(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
const FSlateColor FinishColour = FLinearColor(1.0f, 0.3f, 0.3f, 1.0f);
|
||||
const FSlateColor SelectColour = FLinearColor(1.0f, 0.0f, 0.5f, 1.0f);
|
||||
const FSlateColor RowBgColour1 = FLinearColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
const FSlateColor RowBgColour2 = FLinearColor(0.3f, 0.3f, 0.3f, 1.0f);
|
||||
|
||||
void ExtendToolbar(FToolBarBuilder& ToolbarBuilder, TWeakPtr<SDockTab> Tab);
|
||||
TSharedRef<class SWidget> GetStartLabelMenu();
|
||||
FText GetSelectedStartLabel() const;
|
||||
void OnStartLabelSelected(FName Label);
|
||||
ECheckBoxState GetResetVarsCheckState() const;
|
||||
void OnResetVarsCheckStateChanged(ECheckBoxState NewState);
|
||||
FReply AddVariableClicked();
|
||||
void UpdateVariables();
|
||||
void EnsureTabsVisible();
|
||||
void StartDialogue();
|
||||
void DestroyDialogue();
|
||||
void UpdateOutput();
|
||||
void UpdateChoiceButtons();
|
||||
void AddOutputRow(const FText& Prefix, const FText& Line, const FSlateColor& PrefixColour, const FSlateColor& LineColour);
|
||||
void AddTraceLogRow(const FName& Category, int SourceLineNo, const FString& Message);
|
||||
|
||||
void AddDialogueStep(const FName& Category, int SourceLineNo, const FText& Description, const FText& Prefix);
|
||||
|
||||
void OnDialogueChoice(USUDSDialogue* Dialogue, int ChoiceIndex, int LineNo);
|
||||
void OnDialogueEvent(USUDSDialogue* Dialogue, FName EventName, const TArray<FSUDSValue>& Args, int LineNo);
|
||||
void OnDialogueFinished(USUDSDialogue* Dialogue);
|
||||
void OnDialogueProceeding(USUDSDialogue* Dialogue);
|
||||
void OnDialogueStarting(USUDSDialogue* Dialogue, FName LabelName);
|
||||
void OnDialogueSpeakerLine(USUDSDialogue* Dialogue, int LineNo);
|
||||
void OnDialogueSetVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue, const FString& ExpressionStr, int LineNo);
|
||||
void OnDialogueUserEditedVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue);
|
||||
void OnDialogueSelectEval(USUDSDialogue* Dialogue, const FString& ExpressionStr, bool bSuccess, int LineNo);
|
||||
void OnDialogueRandomEval(USUDSDialogue* Dialogue, const int RandomOutcome, int LineNo);
|
||||
|
||||
TSharedRef<ITableRow> OnGenerateRowForOutput(
|
||||
TSharedPtr<FSUDSEditorOutputRow> FsudsEditorDialogueRow,
|
||||
const TSharedRef<STableViewBase>& TableViewBase);
|
||||
TSharedRef<ITableRow> OnGenerateRowForVariable(TSharedPtr<FSUDSEditorVariableRow> Row,
|
||||
const TSharedRef<STableViewBase>& Table);
|
||||
FSlateColor GetColourForCategory(const FName& Category);
|
||||
void OnPostReimport(UObject* Object, bool bSuccess);
|
||||
void Clear();
|
||||
void WriteBackTextIDs();
|
||||
void GenerateVOAssets();
|
||||
|
||||
};
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
|
||||
#include "ObjectTools.h"
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Internationalization/Regex.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
#include "Sound/SoundWave.h"
|
||||
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// First check for problems
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
bool bError = false;
|
||||
if ((Settings->DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueVoiceAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueVoiceAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Voice assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueVoiceAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if ((Settings->DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueWaveAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueWaveAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueWaveAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if (bError) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
TMap<FString, UDialogueVoice*> UnsavedVoices;
|
||||
GenerateVoiceAssets(Script, Flags, Logger, UnsavedVoices);
|
||||
GenerateWaveAssets(Script, Flags, UnsavedVoices, Logger);
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateVoiceAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger, TMap<FString, UDialogueVoice*> &OutCreatedVoices)
|
||||
{
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
ParentDir = GetVoiceOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Speaker : Script->GetSpeakers())
|
||||
{
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
FString PackageName;
|
||||
FString AssetName;
|
||||
if (GetSpeakerVoiceAssetNames(Script, Speaker, PackageName, AssetName))
|
||||
{
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Voice! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure we still link the existing voice
|
||||
Script->SetSpeakerVoice(Speaker, Cast<UDialogueVoice>(Assets[0].GetAsset()));
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
// Either way nothing more to do
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here then Dialogue Voice didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for voice asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating voice asset %s"), *PackageName);
|
||||
|
||||
UDialogueVoice* NewVoiceAsset = NewObject<UDialogueVoice>(Package, FName(AssetName), Flags);
|
||||
OutCreatedVoices.Add(Speaker, NewVoiceAsset);
|
||||
// there's nothing else to create here, voice is mostly a placeholder with the rest set up later by user
|
||||
FAssetRegistryModule::AssetCreated(NewVoiceAsset);
|
||||
|
||||
Script->SetSpeakerVoice(Speaker, NewVoiceAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
NewVoiceAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoicePackageName(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName)
|
||||
{
|
||||
FString NotUsed;
|
||||
return GetSpeakerVoiceAssetNames(Script, SpeakerID, OutPackageName, NotUsed);
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
FString Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
FString ParentDir = GetVoiceOutputDir(Script);
|
||||
|
||||
OutAssetName = FString::Printf(TEXT("%s%s"), *Prefix, *ObjectTools::SanitizeObjectName(SpeakerID));
|
||||
OutPackageName = UPackageTools::SanitizePackageName(ParentDir / OutAssetName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UDialogueVoice* FSUDSEditorVoiceOverTools::FindSpeakerVoice(USUDSScript* Script,
|
||||
const FString& SpeakerID)
|
||||
{
|
||||
FString PackageName;
|
||||
if (GetSpeakerVoicePackageName(Script, SpeakerID, PackageName))
|
||||
{
|
||||
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
return Cast<UDialogueVoice>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*> UnsavedVoices, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// We need to identify specific lines of dialogue to specify a wave asset to go with them, which means we
|
||||
// need to use the Text ID, just like we do for translations. This means that really, you shouldn't start to
|
||||
// assign sounds to wave assets until you've written the text IDs back to the script
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = FString::Printf(TEXT("%s%s_"), *Settings->DialogueWaveAssetPrefix, *GetScriptNameAsPrefix(Script));
|
||||
ParentDir = GetWaveOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate wave assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto Line = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
// We use the TextID to identify a specific line, just like for translation
|
||||
FString TextID = Line->GetTextID();
|
||||
|
||||
// Remove the '@' characters from the text ID for creating the asset name, they just turn into excessive '_'s
|
||||
const FRegexPattern TextIDPattern(TEXT("\\@([0-9a-fA-F]+)\\@"));
|
||||
FRegexMatcher TextIDRegex(TextIDPattern, TextID);
|
||||
if (TextIDRegex.FindNext())
|
||||
{
|
||||
TextID = TextIDRegex.GetCaptureGroup(1);
|
||||
}
|
||||
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
const FString SanitizedName = FString::Printf(TEXT("%s%s"),
|
||||
*Prefix,
|
||||
*ObjectTools::SanitizeObjectName(TextID));
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(ParentDir / SanitizedName);
|
||||
|
||||
UDialogueWave* WaveAsset = nullptr;
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueWave::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Wave! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
WaveAsset = Cast<UDialogueWave>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UDialogueVoice* SpeakerVoice = nullptr;
|
||||
// Finding speaker assets via FAssetRegistryModule does not work on unsaved assets, so we need to check
|
||||
// the unsaved voices that have been created in the previous step first
|
||||
if (auto pSV = UnsavedVoices.Find(Line->GetSpeakerID()))
|
||||
{
|
||||
SpeakerVoice = *pSV;
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
// Now try assets
|
||||
SpeakerVoice = FindSpeakerVoice(Script, Line->GetSpeakerID());
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
FString SpeakerPackageName;
|
||||
GetSpeakerVoicePackageName(Script, Line->GetSpeakerID(), SpeakerPackageName);
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Error: speaker voice asset for %s is missing, expected to be at %s"), *Line->GetSpeakerID(), *SpeakerPackageName);
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for wave asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
FString NativeLangText = Line->GetText().ToString();
|
||||
USoundWave* SoundWave = nullptr;
|
||||
if (!WaveAsset)
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating wave asset %s"), *PackageName);
|
||||
WaveAsset = NewObject<UDialogueWave>(Package, FName(SanitizedName), Flags);
|
||||
FAssetRegistryModule::AssetCreated(WaveAsset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We will always update the existing asset, but will warn if the text is changing
|
||||
if (WaveAsset->ContextMappings.Num() > 0)
|
||||
{
|
||||
auto Mapping = WaveAsset->ContextMappings[0];
|
||||
if (IsValid(Mapping.SoundWave))
|
||||
{
|
||||
SoundWave = Mapping.SoundWave;
|
||||
// This is OK if the context is what we were going to create anyway, we'll just leave it alone
|
||||
if (Mapping.Context.Speaker != SpeakerVoice ||
|
||||
WaveAsset->SpokenText != NativeLangText)
|
||||
{
|
||||
// No good, user has assigned sound to this dialogue wave, but we need to change it to
|
||||
// a different speaker / line
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave %s has an assigned Sound Wave but the speaker or text has changed. You should update the sound wave!"),
|
||||
*PackageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set spoken text - native language only. Dialogue Wave handles the localisation of this separately,
|
||||
// we can't link it to our String Table unfortunately.
|
||||
WaveAsset->SpokenText = NativeLangText;
|
||||
// Set dialogue context
|
||||
// We need a Speaker Voice, we'll leave the sound and targets blank
|
||||
WaveAsset->UpdateContext(WaveAsset->ContextMappings[0], SoundWave, SpeakerVoice, TArray<UDialogueVoice*>());
|
||||
|
||||
// Now assign the wave to the line
|
||||
Line->SetWave(WaveAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
WaveAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetVoiceOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetVoiceOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetWaveOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetWaveOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetScriptNameAsPrefix(USUDSScript* Script)
|
||||
{
|
||||
FString Name = Script->GetName();
|
||||
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
if (Settings->StripScriptPrefixesWhenGeneratingNames)
|
||||
{
|
||||
int32 Index = INDEX_NONE;
|
||||
if (Name.FindChar('_', Index))
|
||||
{
|
||||
if (Index < Name.Len() - 1)
|
||||
{
|
||||
Name = Name.RightChop(Index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Name;
|
||||
}
|
||||
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "IMessageLogListing.h"
|
||||
#include "MessageLogModule.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
FSUDSMessageLogger::~FSUDSMessageLogger()
|
||||
{
|
||||
if (bWriteToMessageLog)
|
||||
{
|
||||
//Always clear the old message after an import or re-import
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
// Should NOT clear messages here, otherwise when FSUDSMessageLogger is used multiple times in the import process,
|
||||
// each one is clearing messages from previous stages of the import.
|
||||
//LogListing->ClearMessages();
|
||||
|
||||
if(ErrorMessages.Num() > 0)
|
||||
{
|
||||
LogListing->AddMessages(MoveTemp(ErrorMessages));
|
||||
LogListing->NotifyIfAnyMessages(NSLOCTEXT("SUDS", "ImportErrors", "There were issues with the import."), EMessageSeverity::Warning);
|
||||
MessageLogModule.OpenMessageLog(LogTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasErrors() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumErrors() const
|
||||
{
|
||||
int Errs = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
++Errs;
|
||||
}
|
||||
}
|
||||
return Errs;
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasWarnings() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumWarnings() const
|
||||
{
|
||||
int Count = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::AddMessage(EMessageSeverity::Type Severity, const FText& Text)
|
||||
{
|
||||
ErrorMessages.Add(FTokenizedMessage::Create(Severity, Text));
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::ClearMessages()
|
||||
{
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
LogListing->ClearMessages();
|
||||
}
|
||||
|
||||
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptActions.h"
|
||||
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorToolkit.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "ToolMenuSection.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/MessageDialog.h"
|
||||
#include "Widgets/Views/SListView.h"
|
||||
|
||||
FText FSUDSScriptActions::GetName() const
|
||||
{
|
||||
return INVTEXT("SUDS Script");
|
||||
}
|
||||
|
||||
FString FSUDSScriptActions::GetObjectDisplayName(UObject* Object) const
|
||||
{
|
||||
return Object->GetName();
|
||||
}
|
||||
|
||||
UClass* FSUDSScriptActions::GetSupportedClass() const
|
||||
{
|
||||
return USUDSScript::StaticClass();
|
||||
}
|
||||
|
||||
FColor FSUDSScriptActions::GetTypeColor() const
|
||||
{
|
||||
return FColor::Orange;
|
||||
}
|
||||
|
||||
uint32 FSUDSScriptActions::GetCategories()
|
||||
{
|
||||
return EAssetTypeCategories::Misc;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const
|
||||
{
|
||||
for (auto& Asset : TypeAssets)
|
||||
{
|
||||
const auto Script = CastChecked<USUDSScript>(Asset);
|
||||
if (Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutSourceFilePaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSScriptActions::HasActions(const TArray<UObject*>& InObjects) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor)
|
||||
{
|
||||
MakeShared<FSUDSEditorToolkit>()->InitEditor(InObjects);
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section)
|
||||
{
|
||||
const auto Scripts = GetTypedWeakObjectPtrs<USUDSScript>(InObjects);
|
||||
|
||||
Section.AddMenuEntry(
|
||||
"WriteBackTextIDs",
|
||||
NSLOCTEXT("SUDS", "WriteBackTextIDs", "Write Back String Keys"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"WriteBackTextIDsTooltip",
|
||||
"Write string table keys back to source script to make them constant for localisation."),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Details"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::WriteBackTextIDs, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
Section.AddMenuEntry(
|
||||
"GenerateVOAssets",
|
||||
NSLOCTEXT("SUDS", "GenerateVOAssets", "Generate Voice Assets"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"GenerateVOAssetsTooltip",
|
||||
"Generate Dialogue Voice / Dialogue Wave assets for the selected scripts"),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Toolbar.Export"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::GenerateVOAssets, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to write string keys back to the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto Script : Scripts)
|
||||
{
|
||||
if (Script.IsValid())
|
||||
{
|
||||
FSUDSEditorScriptTools::WriteBackTextIDs(Script.Get(), Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FSUDSScriptActions::GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to generate Dialogue Voice / Dialogue Wave assets for the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto WeakScript : Scripts)
|
||||
{
|
||||
if (auto Script = WeakScript.Get())
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Script, Flags, &Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Internationalization/StringTable.h"
|
||||
#include "Editor.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "Internationalization/StringTableCore.h"
|
||||
|
||||
|
||||
USUDSScriptFactory::USUDSScriptFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
bEditorImport = true;
|
||||
bText = true;
|
||||
Formats.Add(TEXT("sud;SUDS Script File"));
|
||||
}
|
||||
|
||||
UObject* USUDSScriptFactory::FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn)
|
||||
{
|
||||
Flags |= RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
|
||||
USUDSScript* Result = nullptr;
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type);
|
||||
|
||||
const FString FactoryCurrentFilename = UFactory::GetCurrentFilename();
|
||||
FString CurrentSourcePath;
|
||||
FString FilenameNoExtension;
|
||||
FString UnusedExtension;
|
||||
FPaths::Split(FactoryCurrentFilename, CurrentSourcePath, FilenameNoExtension, UnusedExtension);
|
||||
const FString LongPackagePath = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetPathName());
|
||||
|
||||
const FString NameForErrors(InName.ToString());
|
||||
|
||||
// Now parse this using utility
|
||||
if(Importer.ImportFromBuffer(Buffer, BufferEnd - Buffer, NameForErrors, &Logger, false))
|
||||
{
|
||||
|
||||
// Populate with data
|
||||
Result = NewObject<USUDSScript>(InParent, InName, Flags);
|
||||
UStringTable* StringTable = CreateStringTable(InParent, InName, Result, Flags, &Logger);
|
||||
Importer.PopulateAsset(Result, StringTable);
|
||||
|
||||
// Register source info
|
||||
const FMD5Hash Hash = FSUDSScriptImporter::CalculateHash(Buffer, BufferEnd - Buffer);
|
||||
Result->AssetImportData->Update(FactoryCurrentFilename, Hash);
|
||||
|
||||
// VO assets at import time?
|
||||
if (ShouldGenerateVoiceAssets(LongPackagePath))
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Result, Flags, &Logger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostImport(this, Result);
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool USUDSScriptFactory::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
return Settings->ShouldGenerateVoiceAssets(PackagePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UStringTable* USUDSScriptFactory::CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
auto Settings = GetDefault<USUDSEditorSettings>();
|
||||
const bool bCreateSeparatePackage = Settings && Settings->bCreateStringTablesAsSeparatePackages;
|
||||
|
||||
const FName StringTableName = FName(InName.ToString() + "Strings");
|
||||
UStringTable* Table = nullptr;
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
const FString PackageDir = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(PackageDir / StringTableName.ToString());
|
||||
|
||||
if (bCreateSeparatePackage)
|
||||
{
|
||||
|
||||
// Create string table as its own package (.uasset)
|
||||
|
||||
{
|
||||
// Destroy any string table packed together with the script
|
||||
const FString ScriptPackageName = Script->GetPackage()->GetPathName();
|
||||
TArray<FAssetData> ScriptAssets;
|
||||
Registry->GetAssetsByPackageName(*ScriptPackageName, ScriptAssets);
|
||||
TArray<UObject*> StringTableAssets;
|
||||
for (auto& Asset : ScriptAssets)
|
||||
{
|
||||
if (Asset.GetClass() == UStringTable::StaticClass())
|
||||
{
|
||||
StringTableAssets.Add(Asset.GetAsset());
|
||||
}
|
||||
}
|
||||
if (StringTableAssets.Num() > 0)
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects(StringTableAssets, false );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UStringTable::StaticClass()))
|
||||
{
|
||||
Table = Cast<UStringTable>(Assets[0].GetAsset());
|
||||
Table->GetMutableStringTable()->ClearSourceStrings();
|
||||
Table->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a String Table! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Table)
|
||||
{
|
||||
// String Table didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for string table %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating string table %s"), *PackageName);
|
||||
|
||||
// This constructor registers the string table with FStringTableRegistry
|
||||
Table = NewObject<UStringTable>(Package, StringTableName, Flags);
|
||||
Package->FullyLoad();
|
||||
Table->MarkPackageDirty();
|
||||
FAssetRegistryModule::AssetCreated(Table);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Destroy separate strings package if it exists (could flip this option off after importing)
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
// Note: we need to force deletion because old string table will be referenced
|
||||
// Unfortunately there is no ObjectTools::ForceDeleteAssets, only the inner
|
||||
// ForceDeleteObjects, so we need to do it ourselves
|
||||
ForceDeleteAssets(Assets);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default route, create string table inside script package
|
||||
Table = NewObject<UStringTable>(ScriptParent, StringTableName, Flags);
|
||||
|
||||
}
|
||||
|
||||
return Table;
|
||||
|
||||
}
|
||||
|
||||
void USUDSScriptFactory::ForceDeleteAssets(const TArray<FAssetData>& AssetsToDelete)
|
||||
{
|
||||
// Copied from ObjectTools::DeleteAssets, except that we're using ForceDeleteObjects so we
|
||||
// don't have to display any confusing messages about moving the string table into the shared asset
|
||||
|
||||
TArray<TWeakObjectPtr<UPackage>> PackageFilesToDelete;
|
||||
TArray<UObject*> ObjectsToDelete;
|
||||
for ( int i = 0; i < AssetsToDelete.Num(); i++ )
|
||||
{
|
||||
const FAssetData& AssetData = AssetsToDelete[i];
|
||||
UObject *ObjectToDelete = AssetData.GetAsset({ ULevel::LoadAllExternalObjectsTag });
|
||||
// Assets can be loaded even when their underlying type/class no longer exists...
|
||||
if ( ObjectToDelete!=nullptr )
|
||||
{
|
||||
ObjectsToDelete.Add( ObjectToDelete );
|
||||
}
|
||||
else if ( AssetData.IsUAsset() )
|
||||
{
|
||||
// ... In this cases there is no underlying asset or type so remove the package itself directly after confirming it's valid to do so.
|
||||
FString PackageFilename;
|
||||
if( !FPackageName::DoesPackageExist( AssetData.PackageName.ToString(), &PackageFilename ) )
|
||||
{
|
||||
// Could not determine filename for package so we can not delete
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = FindPackage(nullptr, *AssetData.PackageName.ToString());
|
||||
if ( Package )
|
||||
{
|
||||
PackageFilesToDelete.Add(Package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32 NumObjectsToDelete = ObjectsToDelete.Num();
|
||||
if ( NumObjectsToDelete > 0 )
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects( ObjectsToDelete, false );
|
||||
}
|
||||
|
||||
const int32 NumPackagesToDelete = PackageFilesToDelete.Num();
|
||||
if (NumPackagesToDelete > 0)
|
||||
{
|
||||
TArray<UPackage*> PackagePointers;
|
||||
for ( const auto& PkgIt : PackageFilesToDelete )
|
||||
{
|
||||
UPackage* Package = PkgIt.Get();
|
||||
if ( Package )
|
||||
{
|
||||
PackagePointers.Add(Package);
|
||||
}
|
||||
}
|
||||
|
||||
if ( PackagePointers.Num() > 0 )
|
||||
{
|
||||
const bool bPerformReferenceCheck = true;
|
||||
ObjectTools::CleanupAfterSuccessfulDelete(PackagePointers, bPerformReferenceCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2765
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
2765
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptReimportFactory.h"
|
||||
|
||||
#include "SUDSEditor.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
|
||||
USUDSScriptReimportFactory::USUDSScriptReimportFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
// We need to have a unique priority vs the original factory, so go after
|
||||
ImportPriority = DefaultImportPriority - 1;
|
||||
}
|
||||
|
||||
bool USUDSScriptReimportFactory::CanReimport(UObject* Obj, TArray<FString>& OutFilenames)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutFilenames);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void USUDSScriptReimportFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && ensure(NewReimportPaths.Num() == 1))
|
||||
{
|
||||
Script->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EReimportResult::Type USUDSScriptReimportFactory::Reimport(UObject* Obj)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (!Script)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// Make sure file is valid and exists
|
||||
const FString Filename = Script->AssetImportData->GetFirstFilename();
|
||||
if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// When a new script is created, it actually lives at the same address as the incoming one. UE must re-use objects
|
||||
// when you put them back at the same outer & asset name?
|
||||
// This means if we want to preserve anything from the previously imported object, such as generated VO asset links,
|
||||
// we need to copy those out now.
|
||||
TMap<FString, UDialogueVoice*> PrevSpeakerVoices = Script->GetSpeakerVoices();
|
||||
// Store the TextID -> DialogueWave, but also store the line text as well so we can detect whether it matches & warn if not
|
||||
TMap<FString, TPair<FString, UDialogueWave*> > PrevWaves;
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto W = TN->GetWave())
|
||||
{
|
||||
PrevWaves.Add(TN->GetTextID(), TPair<FString, UDialogueWave*>(TN->GetText().ToString(), W));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the import again
|
||||
EReimportResult::Type Result = EReimportResult::Failed;
|
||||
bool OutCanceled = false;
|
||||
|
||||
if (ImportObject(Script->GetClass(), Script->GetOuter(), *Script->GetName(), RF_Public | RF_Standalone, Filename, nullptr, OutCanceled) != nullptr)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("Imported successfully"));
|
||||
|
||||
FSUDSMessageLogger Logger;
|
||||
// Now, try to restore the speaker voice / line wave links from before
|
||||
for (auto SpeakerID : Script->GetSpeakers())
|
||||
{
|
||||
if (auto pVoice = PrevSpeakerVoices.Find(SpeakerID))
|
||||
{
|
||||
Script->SetSpeakerVoice(SpeakerID, *pVoice);
|
||||
}
|
||||
}
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto pWavePair = PrevWaves.Find(TN->GetTextID()))
|
||||
{
|
||||
// Set the wave link either way
|
||||
TN->SetWave(pWavePair->Value);
|
||||
|
||||
// Check that the text is the same; if it's not, then lines have potentially changed
|
||||
// we still live with the assignment we have (might be a minor edit) but user should be aware
|
||||
if (TN->GetText().ToString() != pWavePair->Key)
|
||||
{
|
||||
Logger.Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"TextID %s is linked to Dialogue Wave %s, but text has changed. Check whether this line is linked to the correct wave, and consider Writing String Keys back to script before making more script changes in future."),
|
||||
*TN->GetTextID(),
|
||||
*pWavePair->Value->GetName());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Script->AssetImportData->Update(Filename);
|
||||
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
Result = EReimportResult::Succeeded;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OutCanceled)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import canceled"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import failed"));
|
||||
}
|
||||
|
||||
Result = EReimportResult::Failed;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
int32 USUDSScriptReimportFactory::GetPriority() const
|
||||
{
|
||||
return ImportPriority;
|
||||
}
|
||||
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class FSUDSScriptActions;
|
||||
class FSlateStyleSet;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSEditor, Verbose, All);
|
||||
|
||||
class FSUDSEditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
protected:
|
||||
TSharedPtr<FSUDSScriptActions> ScriptActions;
|
||||
TSharedPtr<FSlateStyleSet> StyleSet;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class USUDSScript;
|
||||
class USUDSScriptNode;
|
||||
struct FSUDSMessageLogger;
|
||||
|
||||
class FSUDSEditorScriptTools
|
||||
{
|
||||
public:
|
||||
|
||||
static void WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackGosubID(const FString& GosubID, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool TextIDCheckMatch(const FText& AssetText, const FString& SourceLine);
|
||||
|
||||
|
||||
};
|
||||
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSEditorSettings.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSAssetLocation : uint8
|
||||
{
|
||||
/// Use a single flat shared directory
|
||||
SharedDirectory,
|
||||
/// Use a shared base directory, but create subfolders based on the script asset name
|
||||
SharedDirectorySubdir,
|
||||
/// Place asset alongside the script that originated it
|
||||
ScriptDirectory,
|
||||
/// Place asset in a subfolder of the folder containing the script that generated it, named the same as the script
|
||||
ScriptDirectorySubdir
|
||||
};
|
||||
|
||||
class USUDSScript;
|
||||
/**
|
||||
* Settings for editor-specific aspects of SUDS (no effect at runtime)
|
||||
*/
|
||||
UCLASS(config = Editor, defaultconfig, meta=(DisplayName="SUDS Editor"))
|
||||
class SUDSEDITOR_API USUDSEditorSettings : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Voice assets for speakers in scripts when generated", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueVoiceAssetLocation = ESUDSAssetLocation::SharedDirectory;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Voice assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueVoiceAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Wave assets for speaker lines in scripts", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueWaveAssetLocation = ESUDSAssetLocation::ScriptDirectorySubdir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Wave assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueWaveAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Voice assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueVoiceAssetPrefix = "DV_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Wave assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueWaveAssetPrefix = "DW_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "When generating subdirectories and wave asset names from script names, whether to strip characters before the first '_' to avoid including script prefix", RelativeToGameContentDir, LongPackageName))
|
||||
bool StripScriptPrefixesWhenGeneratingNames = true;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Whether to auto-generate Dialogue Voice/Wave assets for ALL dialogue scripts on import. Note: you can always generate VO assets manually."))
|
||||
bool AlwaysAutoGenerateVoiceOverAssetsOnImport = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Auto-generate Dialogue Voice/Wave assets for scripts in these directories (and subdirectories) on import. Note: you can always generate VO assets manually.", RelativeToGameContentDir, LongPackageName))
|
||||
TArray<FDirectoryPath> DirectoriesToAutoGenerateVoiceOverAssetsOnImport;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "Whether to generate a spoken line for choices (default false)."))
|
||||
bool AlwaysGenerateSpeakerLinesFromChoices = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "The SpeakerID to use for speaker lines generated from choices"))
|
||||
FString SpeakerIdForGeneratedLinesFromChoices = "Player";
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Assets", AdvancedDisplay, meta = (Tooltip = "Whether to create string tables as a separate package (.uasset) from the SUDS Script, which will cause them to appear separately in the Content Browser (requires script re-import)"))
|
||||
bool bCreateStringTablesAsSeparatePackages = true;
|
||||
|
||||
USUDSEditorSettings() {}
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FString GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
FString GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
static FString GetOutputDir(ESUDSAssetLocation Location, const FString& SharedPath, const FString& PackagePath, const FString& ScriptName);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
|
||||
class SUDSEDITOR_API FSUDSEditorVoiceOverTools
|
||||
{
|
||||
public:
|
||||
static void GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
protected:
|
||||
static void GenerateVoiceAssets(USUDSScript* Script,
|
||||
EObjectFlags Flags,
|
||||
FSUDSMessageLogger *Logger,
|
||||
TMap<FString, UDialogueVoice*> &OutCreatedVoices);
|
||||
static void GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*>, FSUDSMessageLogger* Logger);
|
||||
static bool GetSpeakerVoicePackageName(USUDSScript* Script, const FString& SpeakerID, FString& OutPackageName);
|
||||
static bool GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName);
|
||||
static UDialogueVoice* FindSpeakerVoice(USUDSScript* Script, const FString& SpeakerID);
|
||||
static FString GetScriptNameAsPrefix(USUDSScript* Script);
|
||||
|
||||
static FString GetVoiceOutputDir(USUDSScript* Script);
|
||||
static FString GetWaveOutputDir(USUDSScript* Script);
|
||||
|
||||
};
|
||||
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Logging/TokenizedMessage.h"
|
||||
|
||||
class FTokenizedMessage;
|
||||
|
||||
struct SUDSEDITOR_API FSUDSMessageLogger
|
||||
{
|
||||
protected:
|
||||
TArray<TSharedRef<FTokenizedMessage>> ErrorMessages;
|
||||
|
||||
bool bWriteToMessageLog = true;
|
||||
public:
|
||||
FSUDSMessageLogger() {}
|
||||
FSUDSMessageLogger(bool bWriteToMsg) : bWriteToMessageLog(bWriteToMsg) {}
|
||||
~FSUDSMessageLogger();
|
||||
|
||||
void SetWriteToMessageLog(bool bWrite) { bWriteToMessageLog = bWrite; }
|
||||
bool HasErrors() const;
|
||||
int NumErrors() const;
|
||||
bool HasWarnings() const;
|
||||
int NumWarnings() const;
|
||||
|
||||
bool GetWriteToMessageLog() const { return bWriteToMessageLog; }
|
||||
void AddMessage(EMessageSeverity::Type Severity, const FText& Text);
|
||||
|
||||
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6
|
||||
template <typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, UE::Core::TCheckedFormatString<FString::FmtCharType, Types...> Fmt, Types... Args)
|
||||
#else
|
||||
template <typename FmtType, typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, const FmtType& Fmt, Types... Args)
|
||||
#endif
|
||||
{
|
||||
EMessageSeverity::Type Sev = EMessageSeverity::Info;
|
||||
switch(Verbosity)
|
||||
{
|
||||
case ELogVerbosity::Fatal:
|
||||
case ELogVerbosity::Error:
|
||||
Sev = EMessageSeverity::Error;
|
||||
break;
|
||||
case ELogVerbosity::Warning:
|
||||
Sev = EMessageSeverity::Warning;
|
||||
break;
|
||||
default: ;
|
||||
}
|
||||
AddMessage(Sev, FText::FromString(FString::Printf(Fmt, Args...)));
|
||||
}
|
||||
|
||||
const TArray<TSharedRef<FTokenizedMessage>>& GetErrorMessages() const { return ErrorMessages; }
|
||||
|
||||
/// Clear messages in preparation for an import
|
||||
static void ClearMessages();
|
||||
|
||||
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AssetTypeActions_Base.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
class USUDSScript;
|
||||
struct FSUDSMessageLogger;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class FSUDSScriptActions : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FString GetObjectDisplayName(UObject* Object) const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
virtual void GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const override;
|
||||
virtual bool IsImportedAsset() const override { return true; }
|
||||
virtual void GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section) override;
|
||||
virtual bool HasActions(const TArray<UObject*>& InObjects) const override;
|
||||
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor) override;
|
||||
|
||||
protected:
|
||||
void WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
void GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "Factories/Factory.h"
|
||||
#include "SUDSScriptFactory.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDSEDITOR_API USUDSScriptFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USUDSScriptFactory();
|
||||
protected:
|
||||
virtual UObject* FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn) override;
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FSUDSScriptImporter Importer;
|
||||
|
||||
void ForceDeleteAssets(const TArray<FAssetData>& Assets);
|
||||
UStringTable* CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
};
|
||||
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
@@ -0,0 +1,520 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSImporter, Verbose, All);
|
||||
|
||||
struct SUDSEDITOR_API FSUDSParsedEdge
|
||||
{
|
||||
public:
|
||||
/// Text associated with this edge (if a player choice option)
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// The line this edge was created on
|
||||
int SourceLineNo;
|
||||
/// Condition expression that applies to this edge (for select nodes)
|
||||
FSUDSExpression ConditionExpression;
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate choice lines
|
||||
/// Could be used to disable a choice if you don't have the stats to take it, or anything else
|
||||
/// where you need a bit of extra information about a line that doesn't suit setting a dialogue variable for.
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
int SourceNodeIdx = -1;
|
||||
int TargetNodeIdx = -1;
|
||||
|
||||
FSUDSParsedEdge(int LineNo) : SourceLineNo(LineNo){}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta)
|
||||
: Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo)
|
||||
: SourceLineNo(LineNo),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
*this = FSUDSParsedEdge(-1);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
enum class ESUDSParsedNodeType : uint8
|
||||
{
|
||||
/// Text node, displaying a line of dialogue
|
||||
Text,
|
||||
/// Choice node, displaying a series of user choices which navigate to other nodes
|
||||
Choice,
|
||||
/// Select node, automatically selecting one which navigates to another node based on state (also Random)
|
||||
Select,
|
||||
/// Goto node, redirects execution somewhere else
|
||||
/// Gotos are only nodes in the parsing structure, because they need to be discoverable as a fallthrough destination
|
||||
/// When converting to runtime use they just become edges
|
||||
Goto,
|
||||
/// Gosub node, acts like goto except stores return location
|
||||
Gosub,
|
||||
/// Return node, returns from a gosub
|
||||
Return,
|
||||
/// Set variable node
|
||||
SetVariable,
|
||||
/// Event node
|
||||
Event
|
||||
};
|
||||
|
||||
/// Intermediate parsed node from script text
|
||||
/// This will be converted into a final asset later
|
||||
struct SUDSEDITOR_API FSUDSParsedNode
|
||||
{
|
||||
public:
|
||||
ESUDSParsedNodeType NodeType;
|
||||
int OriginalIndent;
|
||||
/// Identifier is speaker ID, goto label, variable name etc
|
||||
FString Identifier;
|
||||
/// Text in native language
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// Expression, for nodes that use it (e.g. set)
|
||||
FSUDSExpression Expression;
|
||||
/// Event arguments, for event nodes
|
||||
TArray<FSUDSExpression> EventArgs;
|
||||
/// Labels which lead to this node
|
||||
TArray<FString> Labels;
|
||||
/// Edges leading to other nodes
|
||||
TArray<FSUDSParsedEdge> Edges;
|
||||
/// The line this node was created on
|
||||
int SourceLineNo;
|
||||
/// Whether this is a valid fall-through target
|
||||
bool AllowFallthrough = true;
|
||||
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate speaker lines
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
// Path hierarchy of choice nodes leading to this node, of the form "/C002/C006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ChoicePath;
|
||||
// Path hierarchy of select nodes leading to this node, of the form "/S002/S006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ConditionalPath;
|
||||
|
||||
/// Although multiple edges can lead here, this index is for the auto-connected parent (may be nothing)
|
||||
int ParentNodeIdx = -1;
|
||||
|
||||
FSUDSParsedNode(ESUDSParsedNodeType InNodeType, int Indent, int LineNo) : NodeType(InNodeType),
|
||||
OriginalIndent(Indent),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& Label,
|
||||
const FString& GosubID,
|
||||
int Indent,
|
||||
int LineNo) : NodeType(ESUDSParsedNodeType::Gosub),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(Label),
|
||||
TextID(GosubID),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& InSpeaker, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Text),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(InSpeaker),
|
||||
Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& GotoLabel, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Goto), OriginalIndent(Indent), Identifier(GotoLabel), SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName, const FSUDSExpression& InExpr, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName,
|
||||
const FSUDSExpression& InExpr,
|
||||
const FString& InTextID,
|
||||
int Indent,
|
||||
int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
TextID(InTextID),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
};
|
||||
class SUDSEDITOR_API FSUDSScriptImporter
|
||||
{
|
||||
public:
|
||||
bool ImportFromBuffer(const TCHAR* Buffer, int32 Len, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void PopulateAsset(USUDSScript* Asset, UStringTable* StringTable);
|
||||
static FMD5Hash CalculateHash(const TCHAR* Buffer, int32 Len);
|
||||
static const FString EndGotoLabel;
|
||||
protected:
|
||||
static const FString TreePathSeparator;
|
||||
|
||||
enum class EConditionalStage : uint8
|
||||
{
|
||||
IfStage,
|
||||
ElseIfStage,
|
||||
ElseStage,
|
||||
RandomStage,
|
||||
RandomOptionStage
|
||||
};
|
||||
enum class EConditionalParent : uint8
|
||||
{
|
||||
Select,
|
||||
ElseIfStage,
|
||||
ElseStage
|
||||
};
|
||||
|
||||
// Struct for tracking if/elseif blocks
|
||||
struct ConditionalContext
|
||||
{
|
||||
/// Index of parent select node where else will be added
|
||||
int SelectNodeIdx = -1;
|
||||
/// Previous block index (parent for nesting)
|
||||
int PreviousBlockIdx = -1;
|
||||
/// Track whether we're in if/elseif/else
|
||||
EConditionalStage Stage;
|
||||
/// String identifying the current condition; for elseif or else contains original "if" context
|
||||
FString ConditionPathElement;
|
||||
|
||||
ConditionalContext(int InSelectNodeIdx, int InPrevBlockIdx, EConditionalStage InStage, const FString& InCondStr) :
|
||||
SelectNodeIdx(InSelectNodeIdx),
|
||||
PreviousBlockIdx(InPrevBlockIdx),
|
||||
Stage(InStage),
|
||||
ConditionPathElement(InCondStr)
|
||||
{
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/// Struct for tracking indents
|
||||
struct IndentContext
|
||||
{
|
||||
public:
|
||||
// The index of the Node which is the parent of this context
|
||||
// This potentially changes every time a sequential text node is encountered in the same context, so it's
|
||||
// always pointing to the last node encountered at this level, for connection
|
||||
int LastNodeIdx = -1;
|
||||
|
||||
/// The outermost indent level where this context lives
|
||||
/// You can indent things that don't create a new context, e.g.
|
||||
/// 1. Indent a text line under another text line: this is the same as no indent, just a continuation
|
||||
/// 2. Indent choices or conditions under a text line
|
||||
/// This is just good for readability, but does not create a new context, it's just a linear sequence
|
||||
/// Therefore the ThresholdIndent tracks the outermost indent relating to the current linear sequence, to know
|
||||
/// when you do in fact need to pop the current context off the stack.
|
||||
int ThresholdIndent = 0;
|
||||
|
||||
int LastTextNodeIdx = -1;
|
||||
|
||||
/// The path entry for this indent, to be combined with all previous levels to provide full path context
|
||||
FString PathEntry;
|
||||
|
||||
IndentContext(int NodeIdx, int Indent, const FString& Path) : LastNodeIdx(NodeIdx), ThresholdIndent(Indent), LastTextNodeIdx(-1), PathEntry(Path) {}
|
||||
|
||||
};
|
||||
|
||||
/// A tree of nodes. Contained to separate header nodes from body nodes
|
||||
struct ParsedTree
|
||||
{
|
||||
public:
|
||||
/// The indent context stack representing where we are in the indentation tree while parsing
|
||||
/// There must always be 1 level (root)
|
||||
TArray<IndentContext> IndentLevelStack;
|
||||
/// When encountering conditions and choice lines, we are building up details for an edge to another node, but
|
||||
/// we currently don't know the target node. We keep these pending details here
|
||||
int EdgeInProgressNodeIdx = -1;
|
||||
int EdgeInProgressEdgeIdx = -1;
|
||||
/// List of all nodes, appended to as parsing progresses
|
||||
/// Ordering is important, these nodes must be in the order encountered in the file
|
||||
TArray<FSUDSParsedNode> Nodes;
|
||||
/// Record of goto labels to node index, built up during parsing (forward refs are OK so not complete until end of parsing)
|
||||
TMap<FString, int> GotoLabelList;
|
||||
/// Goto labels which have been encountered but we haven't found a destination yet
|
||||
TArray<FString> PendingGotoLabels;
|
||||
/// Goto labels that lead directly to another goto and thus are just aliases
|
||||
TMap<FString, FString> AliasedGotoLabels;
|
||||
|
||||
/// Conditional blocks
|
||||
/// "if" creates a new context, uses current as parent
|
||||
/// "elseif" and "else" also create new contexts, but copies parent from current (sibling)
|
||||
/// "endif" ends the context
|
||||
/// You cannot have conditionals that were started in an indent context ending outside it
|
||||
TArray<ConditionalContext> ConditionalBlocks;
|
||||
/// Index of the current conditional block, if any
|
||||
int CurrentConditionalBlockIdx = -1;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
IndentLevelStack.Reset();
|
||||
EdgeInProgressNodeIdx = EdgeInProgressEdgeIdx -1;
|
||||
Nodes.Reset();
|
||||
GotoLabelList.Reset();
|
||||
PendingGotoLabels.Reset();
|
||||
AliasedGotoLabels.Reset();
|
||||
ConditionalBlocks.Reset();
|
||||
CurrentConditionalBlockIdx = -1;
|
||||
}
|
||||
};
|
||||
|
||||
ParsedTree HeaderTree;
|
||||
ParsedTree BodyTree;
|
||||
|
||||
struct ParsedMetadata
|
||||
{
|
||||
public:
|
||||
FName Key;
|
||||
FString Value;
|
||||
int IndentLevel;
|
||||
|
||||
ParsedMetadata(const FName& InKey, const FString& InValue, int InIndentLevel)
|
||||
: Key(InKey),
|
||||
Value(InValue),
|
||||
IndentLevel(InIndentLevel)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/// Text Metadata applied to speaker lines / choices until reset
|
||||
/// For each key there's a stack of metadata, with most indented at the top
|
||||
TMap<FName, TArray<ParsedMetadata>> PersistentMetadata;
|
||||
/// Text Metadata applied just to the next speaker line or choice
|
||||
TMap<FName, ParsedMetadata> TransientMetadata;
|
||||
/// User metadata for the next speaker line or choice
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
/// List of speakers, detected during parsing of lines of text
|
||||
TArray<FString> ReferencedSpeakers;
|
||||
|
||||
const int TabIndentValue = 4;
|
||||
bool bHeaderDone = false;
|
||||
bool bTooLateForHeader = false;
|
||||
bool bHeaderInProgress = false;
|
||||
TOptional<bool> bOverrideGenerateSpeakerLineForChoice;
|
||||
TOptional<FString> OverrideChoiceSpeakerID;
|
||||
bool bTextInProgress = false;
|
||||
int ChoiceUniqueId = 0;
|
||||
/// For generating text IDs
|
||||
int TextIDHighestNumber = 0;
|
||||
/// For generating gosub IDs
|
||||
int GosubIDHighestNumber = 0;
|
||||
/// Parse a single line
|
||||
bool ParseLine(const FStringView& Line, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseHeaderLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseBodyLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseCommentMetadataLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool IsLastNodeOfType(const ParsedTree& Tree, ESUDSParsedNodeType Type);
|
||||
bool ParseChoiceLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
FSUDSParsedEdge* GetEdgeInProgress(ParsedTree& Tree);
|
||||
void EnsureChoiceNodeExistsAboveSelect(ParsedTree& Tree, int IndentLevel, int LineNo);
|
||||
static bool IsConditionalLine(const FStringView& Line);
|
||||
bool ParseConditionalLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseElseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
|
||||
bool ParseElseLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
static bool IsRandomLine(const FStringView& Line);
|
||||
bool ParseRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseBeginRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseRandomOptionLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLabelLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGosubLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseReturnLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseSetLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEventLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseTextLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseImportSettingLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
TMap<FName, FString> GetTextMetadataForNextEntry(int CurrentLineIndent);
|
||||
TMap<FName, FSUDSExpression> ConsumeUserMetadata();
|
||||
bool IsCommentLine(const FStringView& TrimmedLine);
|
||||
FStringView TrimLine(const FStringView& Line, int& OutIndentLevel) const;
|
||||
int FindChoiceAfterTextNode(const FSUDSScriptImporter::ParsedTree& Tree, int TextNodeIdx, const FString& ConditionalPath);
|
||||
int RecurseChoiceNodeFindDeepest(const ParsedTree& Tree, int FromChoiceIdx, const FString& ConditionalPath);
|
||||
int RecurseSelectNodeFindDeepestChoice(const ParsedTree& Tree, int FromSelectNode, const FString& ConditionalPath);
|
||||
int FindEdge(const FSUDSScriptImporter::ParsedTree& Tree, int ParentNodeIdx, int TargetNodeIndex);
|
||||
FString MakeIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseConditionPathElement(int SelectNodeIdx);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel, int FromIndex, const FString& ConditionPath);
|
||||
void PopIndent(ParsedTree& Tree);
|
||||
void PushIndent(ParsedTree& Tree, int NodeIdx, int Indent, const FString& Path);
|
||||
FString GetCurrentTreePath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetCurrentTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetTreeConditionalPath(const ParsedTree& Tree, int NodeIndex);
|
||||
void SetFallthroughForNewNode(FSUDSScriptImporter::ParsedTree& Tree, FSUDSParsedNode& NewNode);
|
||||
int AppendNode(ParsedTree& Tree, const FSUDSParsedNode& InNode);
|
||||
bool SelectNodeIsMissingElsePath(const FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& Node);
|
||||
bool PostImportSanityCheck(const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& OrigChoiceNode, const FSUDSParsedNode& CurrChoiceNode, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode, const FSUDSParsedEdge& Edge, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
void ConnectRemainingNodes(ParsedTree& Tree, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void GenerateTextIDs(ParsedTree& BodyTree);
|
||||
int FindFallthroughNodeIndex(ParsedTree& Tree, int StartNodeIndex, const FString& FromChoicePath, const FString& FromConditionalPath);
|
||||
bool RetrieveAndRemoveTextID(FStringView& InOutLine, FString& OutTextID);
|
||||
bool RetrieveAndRemoveGosubID(FStringView& InOutLine, FString& OutTextID);
|
||||
FString GenerateTextID();
|
||||
const FSUDSParsedNode* GetNode(const ParsedTree& Tree, int Index = 0);
|
||||
int GetGotoTargetNodeIndex(const ParsedTree& Tree, const FString& InLabel);
|
||||
void PopulateAssetFromTree(USUDSScript* Asset,
|
||||
const ParsedTree& Tree,
|
||||
TArray<TObjectPtr<class USUDSScriptNode>>* pOutNodes,
|
||||
TMap<FName, int>* pOutLabels,
|
||||
UStringTable* StringTable);
|
||||
|
||||
public:
|
||||
const FSUDSParsedNode* GetNode(int Index = 0);
|
||||
const FSUDSParsedNode* GetHeaderNode(int Index = 0);
|
||||
/// Resolve a goto label to a target index (after import), or -1 if not resolvable
|
||||
int GetGotoTargetNodeIndex(const FString& Label);
|
||||
static bool RetrieveTextIDFromLine(FStringView& InOutLine, FString& OutTextID, int& OutNumber);
|
||||
static bool RetrieveGosubIDFromLine(FStringView& InOutLine, FString& OutID, int& OutNumber);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "EditorReimportHandler.h"
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "SUDSScriptReimportFactory.generated.h"
|
||||
|
||||
// Reimports a USUDSScriptFactory asset
|
||||
// Necessary to get the import pop-up
|
||||
UCLASS()
|
||||
class USUDSScriptReimportFactory : public USUDSScriptFactory, public FReimportHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
USUDSScriptReimportFactory();
|
||||
|
||||
//~ Begin FReimportHandler Interface
|
||||
virtual bool CanReimport(UObject* Obj, TArray<FString>& OutFilenames) override;
|
||||
virtual void SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths) override;
|
||||
virtual EReimportResult::Type Reimport(UObject* Obj) override;
|
||||
virtual int32 GetPriority() const override;
|
||||
//~ End FReimportHandler Interface
|
||||
};
|
||||
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDSEditor : ModuleRules
|
||||
{
|
||||
public SUDSEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"SUDS"
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"InputCore", // needed by come Slate widgets
|
||||
"Projects", // So that we can use the IPluginManager, required for our custom style
|
||||
"ToolMenus",
|
||||
"MessageLog",
|
||||
"UnrealEd",
|
||||
"EditorStyle"
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
21
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp
Normal file
21
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "SudsTestModule.h"
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogSudsTestModule)
|
||||
|
||||
void FSudsTestModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
UE_LOG(LogSudsTestModule, Log, TEXT("SUDSTest Module Started"))
|
||||
}
|
||||
|
||||
void FSudsTestModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
UE_LOG(LogSudsTestModule, Log, TEXT("SUDSTest Module Stopped"))
|
||||
}
|
||||
|
||||
|
||||
IMPLEMENT_MODULE(FSudsTestModule, SUDSTest)
|
||||
14
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h
Normal file
14
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSudsTestModule, All, All)
|
||||
|
||||
class FSudsTestModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
274
Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp
Normal file
274
Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestParticipant.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString ChoicesAsSpeakerLineEnableInScriptInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
NPC: I see
|
||||
* Choice 1a
|
||||
NPC: Sure
|
||||
*- Not a speaker line!
|
||||
Player: I had to say this separately
|
||||
* Choice 2
|
||||
NPC: Totally
|
||||
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerLineInScript,
|
||||
"SUDSTest.TestChoiceSpeakerLineInScript",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerLineInScript::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineEnableInScriptInput), ChoicesAsSpeakerLineEnableInScriptInput.Len(), "ChoicesAsSpeakerLineEnableInScriptInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
const FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
|
||||
|
||||
// Test choice which disables speaker line
|
||||
Dlg->Choose(1);
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "Player", "I had to say this separately");
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const FString ChoicesAsSpeakerLineSetSpeakerIdInScriptInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
[importsetting SpeakerIDForGeneratedLinesFromChoices `TheDude`]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
NPC: You said that choice
|
||||
* Choice 2
|
||||
NPC: Also that one
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerSetSpeakerIdInScriptInput,
|
||||
"SUDSTest.TestChoiceSpeakerSetSpeakerIdInScriptInput",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerSetSpeakerIdInScriptInput::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineSetSpeakerIdInScriptInput), ChoicesAsSpeakerLineSetSpeakerIdInScriptInput.Len(), "ChoicesAsSpeakerLineSetSpeakerIdInScriptInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
const FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
// Confirm that we got the choice as a speaker line, with custom speaker ID
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "TheDude", "Choice 1");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "You said that choice");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
const FString ChoicesAsSpeakerLineChainChoiceInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
* Choice 1a
|
||||
NPC: Hey this worked
|
||||
* Choice 1b
|
||||
* Choice 2
|
||||
NPC: Also that one
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerChainedChoiceInput,
|
||||
"SUDSTest.TestChoiceSpeakerChainedChoiceInput",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerChainedChoiceInput::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineChainChoiceInput), ChoicesAsSpeakerLineChainChoiceInput.Len(), "ChoicesAsSpeakerLineChainChoiceInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line, with custom speaker ID
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
TestEqual("Choice 2nd level text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice 2nd leveltext", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 1b"));
|
||||
Dlg->Choose(0);
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1a");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "Hey this worked");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
const FString ChoicesAsSpeakerLineWithStringKeysInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello @0012@
|
||||
* Choice 1 @0016@
|
||||
NPC: I see @0017@
|
||||
* Choice 1a @0018@
|
||||
NPC: Sure
|
||||
*- Not a speaker line!
|
||||
Player: I had to say this separately
|
||||
* Choice 2
|
||||
NPC: Totally
|
||||
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoicesAsSpeakerLineWithStringKeys,
|
||||
"SUDSTest.TestChoicesAsSpeakerLineWithStringKeys",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoicesAsSpeakerLineWithStringKeys::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineWithStringKeysInput), ChoicesAsSpeakerLineWithStringKeysInput.Len(), "ChoicesAsSpeakerLineWithStringKeysInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0012@");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0016@");
|
||||
FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0017@");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0018@");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
|
||||
|
||||
ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
TestDialogueText(this, "Duplicated speaker line", Dlg, "Player", "Choice 1a");
|
||||
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
1045
Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp
Normal file
1045
Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user