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

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

View File

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