From 07336e8268636fd2e0b72a0d0d0f30ed55999f36 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Fri, 15 May 2026 18:27:29 +0200 Subject: [PATCH] Add cubing alg bound 3 validation profiles --- .../HyperTwistAlgorithmAutomationTests.cpp | 220 ++++++++++++ .../HyperTwistAlgorithmValidation.cpp | 334 ++++++++++++++++++ .../HyperTwistAlgorithmValidation.h | 79 +++++ 3 files changed, 633 insertions(+) create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.h diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmAutomationTests.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmAutomationTests.cpp index 0585786..d93ec40 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmAutomationTests.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmAutomationTests.cpp @@ -6,6 +6,7 @@ #include "HyperTwistAlgorithm/HyperTwistAlgorithmParser.h" #include "HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h" #include "HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.h" +#include "HyperTwistAlgorithm/HyperTwistAlgorithmValidation.h" #include "HyperTwistCore/HyperTwistCoreLibrary.h" namespace HyperTwistAlgorithmAutomationTestInternal @@ -73,6 +74,45 @@ namespace HyperTwistAlgorithmAutomationTestInternal { return UHyperTwistAlgorithmSerializer::SerializeAlgorithm(Sequence); } + + bool ValidationResultsMatch( + const FHyperTwistAlgorithmValidationResult& A, + const FHyperTwistAlgorithmValidationResult& B) + { + if (A.bIsValid != B.bIsValid + || A.ProfileName != B.ProfileName + || A.Issues.Num() != B.Issues.Num()) + { + return false; + } + + for (int32 Index = 0; Index < A.Issues.Num(); ++Index) + { + if (A.Issues[Index].IssueCode != B.Issues[Index].IssueCode + || A.Issues[Index].NodePath != B.Issues[Index].NodePath + || A.Issues[Index].Message != B.Issues[Index].Message) + { + return false; + } + } + + return true; + } + + bool ResultContainsIssue( + const FHyperTwistAlgorithmValidationResult& Result, + const EHyperTwistAlgorithmValidationIssueCode IssueCode) + { + for (const FHyperTwistAlgorithmValidationIssue& Issue : Result.Issues) + { + if (Issue.IssueCode == IssueCode) + { + return true; + } + } + + return false; + } } IMPLEMENT_SIMPLE_AUTOMATION_TEST( @@ -110,6 +150,21 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( "HyperTwist.Algorithm.Bound2.StructuredJsonRebaseline", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistAlgorithmValidationFlatOnlyAutomationTest, + "HyperTwist.Algorithm.Bound3.FlatOnly", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistAlgorithmValidationClassicWcaAutomationTest, + "HyperTwist.Algorithm.Bound3.ClassicWca", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistAlgorithmValidationJsonParityAutomationTest, + "HyperTwist.Algorithm.Bound3.JsonProfileParity", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) + bool FHyperTwistAlgorithmCanonicalizationAutomationTest::RunTest(const FString& Parameters) { (void)Parameters; @@ -489,4 +544,169 @@ bool FHyperTwistAlgorithmTraversalStructuredJsonAutomationTest::RunTest(const FS return true; } +bool FHyperTwistAlgorithmValidationFlatOnlyAutomationTest::RunTest(const FString& Parameters) +{ + (void)Parameters; + + using namespace HyperTwistAlgorithmAutomationTestInternal; + + FHyperTwistAlgorithmSequence FlatSequence; + if (!TryParseSequence(*this, TEXT("R . // note\nU"), FlatSequence)) + { + return false; + } + + const FHyperTwistAlgorithmValidationResult FlatResult = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceFlatOnly(FlatSequence); + TestTrue(TEXT("Flat-only profile accepts atomic moves and annotations"), FlatResult.bIsValid); + TestEqual(TEXT("Flat-only valid case has no issues"), FlatResult.Issues.Num(), 0); + + FHyperTwistAlgorithmSequence GroupSequence; + if (!TryParseSequence(*this, TEXT("(R U)'"), GroupSequence)) + { + return false; + } + + const FHyperTwistAlgorithmValidationResult GroupResult = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceFlatOnly(GroupSequence); + TestFalse(TEXT("Flat-only profile rejects group containers"), GroupResult.bIsValid); + TestTrue( + TEXT("Flat-only profile returns typed container failure"), + ResultContainsIssue(GroupResult, EHyperTwistAlgorithmValidationIssueCode::ContainerNotAllowed)); + + FHyperTwistAlgorithmSequence CommutatorSequence; + if (!TryParseSequence(*this, TEXT("[R, U]"), CommutatorSequence)) + { + return false; + } + + const FHyperTwistAlgorithmValidationResult CommutatorResult = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceFlatOnly(CommutatorSequence); + TestFalse(TEXT("Flat-only profile rejects commutators"), CommutatorResult.bIsValid); + TestTrue( + TEXT("Flat-only commutator rejection uses typed container failure"), + ResultContainsIssue(CommutatorResult, EHyperTwistAlgorithmValidationIssueCode::ContainerNotAllowed)); + + return true; +} + +bool FHyperTwistAlgorithmValidationClassicWcaAutomationTest::RunTest(const FString& Parameters) +{ + (void)Parameters; + + using namespace HyperTwistAlgorithmAutomationTestInternal; + + const TArray ValidCases = { + TEXT("R U R' U'"), + TEXT("3Rw"), + TEXT("2-3Uw"), + TEXT("x y z"), + TEXT("M E S"), + TEXT("u") + }; + + for (const FString& Input : ValidCases) + { + FHyperTwistAlgorithmSequence Sequence; + if (!TryParseSequence(*this, Input, Sequence)) + { + return false; + } + + const FHyperTwistAlgorithmValidationResult Result = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceClassicWca(Sequence); + TestTrue(FString::Printf(TEXT("classic-wca accepts '%s'"), *Input), Result.bIsValid); + if (!Result.bIsValid) + { + AddError(FString::Printf( + TEXT("classic-wca rejected '%s' with %d issues"), + *Input, + Result.Issues.Num())); + } + } + + struct FInvalidCase + { + const TCHAR* Input; + EHyperTwistAlgorithmValidationIssueCode ExpectedCode; + }; + + const TArray InvalidCases = { + { TEXT("P"), EHyperTwistAlgorithmValidationIssueCode::UnknownMoveFamily }, + { TEXT("0R"), EHyperTwistAlgorithmValidationIssueCode::LayerIndexMustBePositive }, + { TEXT("3-2Uw"), EHyperTwistAlgorithmValidationIssueCode::LayerRangeMustAscend }, + { TEXT("2x"), EHyperTwistAlgorithmValidationIssueCode::LayerPrefixNotAllowedForFamily }, + { TEXT("2M"), EHyperTwistAlgorithmValidationIssueCode::LayerPrefixNotAllowedForFamily } + }; + + for (const FInvalidCase& Case : InvalidCases) + { + FHyperTwistAlgorithmSequence Sequence; + if (!TryParseSequence(*this, FString(Case.Input), Sequence)) + { + return false; + } + + const FHyperTwistAlgorithmValidationResult Result = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceClassicWca(Sequence); + TestFalse(FString::Printf(TEXT("classic-wca rejects '%s'"), Case.Input), Result.bIsValid); + TestTrue( + FString::Printf(TEXT("classic-wca reports typed issue for '%s'"), Case.Input), + ResultContainsIssue(Result, Case.ExpectedCode)); + } + + return true; +} + +bool FHyperTwistAlgorithmValidationJsonParityAutomationTest::RunTest(const FString& Parameters) +{ + (void)Parameters; + + using namespace HyperTwistAlgorithmAutomationTestInternal; + + FHyperTwistAlgorithmSequence InvalidTextSequence; + if (!TryParseSequence(*this, TEXT("(R U)'"), InvalidTextSequence)) + { + return false; + } + + FHyperTwistAlgorithmSequence InvalidJsonSequence; + if (!TryRoundTripJson(*this, InvalidTextSequence, InvalidJsonSequence, TEXT("flat-only invalid sequence"))) + { + return false; + } + + const FHyperTwistAlgorithmValidationResult InvalidTextResult = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceFlatOnly(InvalidTextSequence); + const FHyperTwistAlgorithmValidationResult InvalidJsonResult = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceFlatOnly(InvalidJsonSequence); + TestTrue( + TEXT("Flat-only validation matches between text-ingested and JSON-ingested sequences"), + ValidationResultsMatch(InvalidTextResult, InvalidJsonResult)); + + FHyperTwistAlgorithmSequence ValidTextSequence; + if (!TryParseSequence(*this, TEXT("2-3Uw R x"), ValidTextSequence)) + { + return false; + } + + FHyperTwistAlgorithmSequence ValidJsonSequence; + if (!TryRoundTripJson(*this, ValidTextSequence, ValidJsonSequence, TEXT("classic-wca valid sequence"))) + { + return false; + } + + const FHyperTwistAlgorithmValidationResult ValidTextResult = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceClassicWca(ValidTextSequence); + const FHyperTwistAlgorithmValidationResult ValidJsonResult = + UHyperTwistAlgorithmValidationLibrary::ValidateSequenceClassicWca(ValidJsonSequence); + TestTrue(TEXT("classic-wca text-ingested sequence validates"), ValidTextResult.bIsValid); + TestTrue(TEXT("classic-wca JSON-ingested sequence validates"), ValidJsonResult.bIsValid); + TestTrue( + TEXT("classic-wca validation matches between text-ingested and JSON-ingested sequences"), + ValidationResultsMatch(ValidTextResult, ValidJsonResult)); + + return true; +} + #endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.cpp new file mode 100644 index 0000000..4b0f4d0 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.cpp @@ -0,0 +1,334 @@ +// Clean-room implementation for HyperTwist algorithm-language subsystem +// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A) +// Source: Behavioral contracts from cubing/alg.js Model A handoff (GPL-3.0-or-later lane) +// No source code inspection. Clean-room workflow only. +// Date: 2026-05-15 +// Bound 3 - validation profiles and typed validation failures + +#include "HyperTwistAlgorithm/HyperTwistAlgorithmValidation.h" + +namespace HyperTwistAlgorithmValidationInternal +{ + struct FResolvedValidationProfile + { + FString CanonicalName = TEXT("permissive"); + bool bRejectContainers = false; + bool bClassicCubeLegality = false; + bool bSupported = true; + }; + + FString NormalizeProfileKey(const FString& InProfile) + { + FString Normalized = InProfile.TrimStartAndEnd(); + Normalized.ToLowerInline(); + return Normalized; + } + + FResolvedValidationProfile ResolveValidationProfile(const FString& InProfile) + { + const FString ProfileKey = NormalizeProfileKey(InProfile); + FResolvedValidationProfile Result; + + if (ProfileKey.IsEmpty() + || ProfileKey == TEXT("permissive") + || ProfileKey == TEXT("unknown-family-ok")) + { + return Result; + } + + if (ProfileKey == TEXT("flat-only")) + { + Result.CanonicalName = TEXT("flat-only"); + Result.bRejectContainers = true; + return Result; + } + + if (ProfileKey == TEXT("classic-wca") + || ProfileKey == TEXT("classic-cube")) + { + Result.CanonicalName = TEXT("classic-wca"); + Result.bClassicCubeLegality = true; + return Result; + } + + if (ProfileKey == TEXT("classic-wca-flat") + || ProfileKey == TEXT("classic-wca-flat-only") + || ProfileKey == TEXT("flat-only+classic-wca") + || ProfileKey == TEXT("classic-cube-flat")) + { + Result.CanonicalName = TEXT("classic-wca-flat"); + Result.bRejectContainers = true; + Result.bClassicCubeLegality = true; + return Result; + } + + Result.CanonicalName = ProfileKey; + Result.bSupported = false; + return Result; + } + + void AddIssue( + FHyperTwistAlgorithmValidationResult& OutResult, + const EHyperTwistAlgorithmValidationIssueCode IssueCode, + const FString& NodePath, + const FString& Message) + { + OutResult.bIsValid = false; + + FHyperTwistAlgorithmValidationIssue Issue; + Issue.IssueCode = IssueCode; + Issue.NodePath = NodePath; + Issue.Message = Message; + OutResult.Issues.Add(MoveTemp(Issue)); + } + + bool IsSingleCharacterInSet(const FString& Family, const TCHAR* AllowedCharacters) + { + return Family.Len() == 1 && FCString::Strchr(AllowedCharacters, Family[0]) != nullptr; + } + + bool IsClassicFaceFamily(const FString& Family) + { + return IsSingleCharacterInSet(Family, TEXT("ULFRBD")); + } + + bool IsClassicWideLowercaseFamily(const FString& Family) + { + return IsSingleCharacterInSet(Family, TEXT("ulfrbd")); + } + + bool IsClassicRotationFamily(const FString& Family) + { + return IsSingleCharacterInSet(Family, TEXT("xyzXYZ")); + } + + bool IsClassicSliceFamily(const FString& Family) + { + return IsSingleCharacterInSet(Family, TEXT("MESmes")); + } + + bool IsKnownClassicFamily(const FHyperTwistAlgorithmBlockMove& Move) + { + return IsClassicFaceFamily(Move.Family) + || IsClassicWideLowercaseFamily(Move.Family) + || IsClassicRotationFamily(Move.Family) + || IsClassicSliceFamily(Move.Family); + } + + FString NodeTypeToContainerLabel(const EHyperTwistAlgorithmNodeType NodeType) + { + switch (NodeType) + { + case EHyperTwistAlgorithmNodeType::Group: + return TEXT("group"); + case EHyperTwistAlgorithmNodeType::Commutator: + return TEXT("commutator"); + case EHyperTwistAlgorithmNodeType::Conjugate: + return TEXT("conjugate"); + default: + return TEXT("container"); + } + } + + void ValidateBlockMove( + const FHyperTwistAlgorithmBlockMove& Move, + const FString& NodePath, + const FResolvedValidationProfile& Profile, + FHyperTwistAlgorithmValidationResult& OutResult) + { + if (!Profile.bClassicCubeLegality) + { + return; + } + + if (!IsKnownClassicFamily(Move)) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::UnknownMoveFamily, + NodePath, + FString::Printf( + TEXT("Move family '%s' is not allowed by profile '%s'."), + *Move.Family, + *Profile.CanonicalName)); + return; + } + + const bool bHasSingleLayerPrefix = Move.GetCanonicalMoveType() == EHyperTwistAlgorithmMoveType::InnerSlice; + const bool bHasRangePrefix = Move.GetCanonicalMoveType() == EHyperTwistAlgorithmMoveType::RangedSlice; + + if (bHasSingleLayerPrefix && Move.GetCanonicalInnerLayer() <= 0) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::LayerIndexMustBePositive, + NodePath, + FString::Printf( + TEXT("Move '%s' uses layer index %d, but layer indices must be positive."), + *Move.Family, + Move.GetCanonicalInnerLayer())); + } + + if (bHasRangePrefix) + { + if (Move.GetCanonicalInnerLayer() <= 0 || Move.GetCanonicalOuterLayer() <= 0) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::LayerRangeMustBePositive, + NodePath, + FString::Printf( + TEXT("Move '%s' uses range %d-%d, but both range endpoints must be positive."), + *Move.Family, + Move.GetCanonicalInnerLayer(), + Move.GetCanonicalOuterLayer())); + } + + if (Move.GetCanonicalInnerLayer() >= Move.GetCanonicalOuterLayer()) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::LayerRangeMustAscend, + NodePath, + FString::Printf( + TEXT("Move '%s' uses range %d-%d, but the range must ascend with start < end."), + *Move.Family, + Move.GetCanonicalInnerLayer(), + Move.GetCanonicalOuterLayer())); + } + } + + if ((bHasSingleLayerPrefix || bHasRangePrefix) + && (IsClassicRotationFamily(Move.Family) || IsClassicSliceFamily(Move.Family))) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::LayerPrefixNotAllowedForFamily, + NodePath, + FString::Printf( + TEXT("Move family '%s' does not allow layer indices or ranges in profile '%s'."), + *Move.Family, + *Profile.CanonicalName)); + } + } + + void ValidateNodeList( + const TArray& Nodes, + const FString& PathPrefix, + const FResolvedValidationProfile& Profile, + FHyperTwistAlgorithmValidationResult& OutResult); + + void ValidateNode( + const FHyperTwistAlgorithmNode& Node, + const FString& NodePath, + const FResolvedValidationProfile& Profile, + FHyperTwistAlgorithmValidationResult& OutResult) + { + switch (Node.NodeType) + { + case EHyperTwistAlgorithmNodeType::BlockMove: + ValidateBlockMove(Node.BlockMove, NodePath, Profile, OutResult); + break; + + case EHyperTwistAlgorithmNodeType::Group: + if (Profile.bRejectContainers) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::ContainerNotAllowed, + NodePath, + FString::Printf( + TEXT("Profile '%s' does not allow group containers."), + *Profile.CanonicalName)); + } + ValidateNodeList(Node.Group.Inner, NodePath + TEXT(".items"), Profile, OutResult); + break; + + case EHyperTwistAlgorithmNodeType::Commutator: + if (Profile.bRejectContainers) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::ContainerNotAllowed, + NodePath, + FString::Printf( + TEXT("Profile '%s' does not allow commutator containers."), + *Profile.CanonicalName)); + } + ValidateNodeList(Node.Commutator.A, NodePath + TEXT(".left"), Profile, OutResult); + ValidateNodeList(Node.Commutator.B, NodePath + TEXT(".right"), Profile, OutResult); + break; + + case EHyperTwistAlgorithmNodeType::Conjugate: + if (Profile.bRejectContainers) + { + AddIssue( + OutResult, + EHyperTwistAlgorithmValidationIssueCode::ContainerNotAllowed, + NodePath, + FString::Printf( + TEXT("Profile '%s' does not allow conjugate containers."), + *Profile.CanonicalName)); + } + ValidateNodeList(Node.Conjugate.A, NodePath + TEXT(".left"), Profile, OutResult); + ValidateNodeList(Node.Conjugate.B, NodePath + TEXT(".right"), Profile, OutResult); + break; + + case EHyperTwistAlgorithmNodeType::Pause: + case EHyperTwistAlgorithmNodeType::Newline: + case EHyperTwistAlgorithmNodeType::Comment: + case EHyperTwistAlgorithmNodeType::Sequence: + default: + break; + } + } + + void ValidateNodeList( + const TArray& Nodes, + const FString& PathPrefix, + const FResolvedValidationProfile& Profile, + FHyperTwistAlgorithmValidationResult& OutResult) + { + for (int32 Index = 0; Index < Nodes.Num(); ++Index) + { + const FString NodePath = FString::Printf(TEXT("%s[%d]"), *PathPrefix, Index); + ValidateNode(Nodes[Index], NodePath, Profile, OutResult); + } + } +} + +FHyperTwistAlgorithmValidationResult UHyperTwistAlgorithmValidationLibrary::ValidateSequenceForProfile( + const FHyperTwistAlgorithmSequence& Sequence, + const FString& NotationProfile) +{ + using namespace HyperTwistAlgorithmValidationInternal; + + FHyperTwistAlgorithmValidationResult Result; + const FResolvedValidationProfile Profile = ResolveValidationProfile(NotationProfile); + Result.ProfileName = Profile.CanonicalName; + + if (!Profile.bSupported) + { + AddIssue( + Result, + EHyperTwistAlgorithmValidationIssueCode::UnsupportedProfile, + TEXT("profile"), + FString::Printf(TEXT("Validation profile '%s' is not supported."), *NotationProfile)); + return Result; + } + + ValidateNodeList(Sequence.Nodes, TEXT("root"), Profile, Result); + return Result; +} + +FHyperTwistAlgorithmValidationResult UHyperTwistAlgorithmValidationLibrary::ValidateSequenceFlatOnly( + const FHyperTwistAlgorithmSequence& Sequence) +{ + return ValidateSequenceForProfile(Sequence, TEXT("flat-only")); +} + +FHyperTwistAlgorithmValidationResult UHyperTwistAlgorithmValidationLibrary::ValidateSequenceClassicWca( + const FHyperTwistAlgorithmSequence& Sequence) +{ + return ValidateSequenceForProfile(Sequence, TEXT("classic-wca")); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.h new file mode 100644 index 0000000..3036769 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmValidation.h @@ -0,0 +1,79 @@ +#pragma once + +// Clean-room implementation for HyperTwist algorithm-language subsystem +// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A) +// Source: Behavioral contracts from cubing/alg.js Model A handoff (GPL-3.0-or-later lane) +// No source code inspection. Clean-room workflow only. +// Date: 2026-05-15 +// Bound 3 - validation profiles and typed validation failures + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h" +#include "HyperTwistAlgorithmValidation.generated.h" + +UENUM(BlueprintType) +enum class EHyperTwistAlgorithmValidationIssueCode : uint8 +{ + UnsupportedProfile UMETA(DisplayName = "UnsupportedProfile"), + ContainerNotAllowed UMETA(DisplayName = "ContainerNotAllowed"), + UnknownMoveFamily UMETA(DisplayName = "UnknownMoveFamily"), + LayerIndexMustBePositive UMETA(DisplayName = "LayerIndexMustBePositive"), + LayerRangeMustBePositive UMETA(DisplayName = "LayerRangeMustBePositive"), + LayerRangeMustAscend UMETA(DisplayName = "LayerRangeMustAscend"), + LayerPrefixNotAllowedForFamily UMETA(DisplayName = "LayerPrefixNotAllowedForFamily") +}; + +USTRUCT(BlueprintType) +struct FHyperTwistAlgorithmValidationIssue +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm|Validation") + EHyperTwistAlgorithmValidationIssueCode IssueCode = + EHyperTwistAlgorithmValidationIssueCode::UnsupportedProfile; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm|Validation") + FString NodePath; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm|Validation") + FString Message; +}; + +USTRUCT(BlueprintType) +struct FHyperTwistAlgorithmValidationResult +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm|Validation") + bool bIsValid = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm|Validation") + FString ProfileName = TEXT("permissive"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm|Validation") + TArray Issues; +}; + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistAlgorithmValidationLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Validation") + static FHyperTwistAlgorithmValidationResult ValidateSequenceForProfile( + const FHyperTwistAlgorithmSequence& Sequence, + const FString& NotationProfile + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Validation") + static FHyperTwistAlgorithmValidationResult ValidateSequenceFlatOnly( + const FHyperTwistAlgorithmSequence& Sequence + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Validation") + static FHyperTwistAlgorithmValidationResult ValidateSequenceClassicWca( + const FHyperTwistAlgorithmSequence& Sequence + ); +};