diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp index 11b25b0..cb305f3 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp @@ -8,6 +8,9 @@ #include "HyperTwistReplay/HyperTwistReplayLibrary.h" #include "HyperTwistSimulation/HyperTwistViewerLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingClassicCubingLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingSpatialLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingViewerLibrary.h" #include "JsonObjectConverter.h" namespace HyperTwistPuzzleViewerComponentInternal @@ -288,6 +291,23 @@ namespace HyperTwistPuzzleViewerComponentInternal return PreviousBoundaryTimeMs; } + TArray ResolveProjectionBoundaryTimesMs( + const TArray& MoveList, + const TArray& CachedMoveBoundaryTimesMs + ) + { + return CachedMoveBoundaryTimesMs.Num() == MoveList.Num() + ? CachedMoveBoundaryTimesMs + : BuildMoveBoundaryTimesMs(MoveList); + } + + int32 ResolveProjectionMoveStartTimeMs(const TArray& MoveBoundaryTimesMs, const int32 MoveIndex) + { + return MoveIndex > 0 && MoveBoundaryTimesMs.IsValidIndex(MoveIndex - 1) + ? MoveBoundaryTimesMs[MoveIndex - 1] + : 0; + } + FHyperTwistViewerPositionSnapshot BuildPositionSnapshot( const TArray& MoveList, const TArray& MoveBoundaryTimesMs, @@ -466,6 +486,7 @@ void UHyperTwistPuzzleViewerComponent::LoadSceneContext(const FHyperTwistSimulat SceneContext = InContext; PlaybackState.bSceneContextLoaded = InContext.IsStructurallyValid(); + HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState(); // Reset playhead — loaded content has changed. PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped; @@ -485,6 +506,7 @@ void UHyperTwistPuzzleViewerComponent::LoadMoveList(const TArray ProjectionBoundaryTimesMs = + HyperTwistPuzzleViewerComponentInternal::ResolveProjectionBoundaryTimesMs(MoveList, MoveBoundaryTimesMs); + OutProjection.ProjectedMoves.Reserve(MoveList.Num()); + + for (int32 MoveIndex = 0; MoveIndex < MoveList.Num(); ++MoveIndex) + { + FHyperTwistViewerAdapterProjectionStep Step; + Step.MoveIndex = MoveIndex; + Step.Transform = MoveList[MoveIndex].Transform; + Step.DisplayLabel = MoveList[MoveIndex].DisplayLabel; + Step.StartsAtMs = HyperTwistPuzzleViewerComponentInternal::ResolveProjectionMoveStartTimeMs( + ProjectionBoundaryTimesMs, + MoveIndex + ); + Step.CompletesAtMs = ProjectionBoundaryTimesMs.IsValidIndex(MoveIndex) + ? ProjectionBoundaryTimesMs[MoveIndex] + : Step.StartsAtMs; + OutProjection.ProjectedMoves.Add(Step); + } + + return OutProjection.IsStructurallyValid(); +} + +bool UHyperTwistPuzzleViewerComponent::TryBootstrapEmbeddedClassicCubingHost( + const FHyperTwistViewerEmbeddedHostBootstrapRequest& Request, + FHyperTwistViewerEmbeddedHostBootstrapState& OutBootstrapState +) +{ + OutBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState(); + + if (!Request.IsStructurallyValid()) + { + HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState(); + return false; + } + + FHyperTwistViewerClassicCubingAdapterProjection Projection; + if (!TryBuildClassicCubingAdapterProjection(Projection)) + { + HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState(); + return false; + } + + const FHyperTwistTrainingViewerReferenceBundle BrowserViewerBundle = + UHyperTwistTrainingViewerLibrary::BuildBundledBrowserViewerReferenceBundle(); + const FHyperTwistTrainingSpatialReferenceBundle SpatialBundle = + UHyperTwistTrainingSpatialLibrary::BuildBundledBrowserSpatialReferenceBundle(); + + FHyperTwistTrainingViewerEmbedContract EmbedContract; + FHyperTwistTrainingSpatialSceneContract SceneContract; + + if (!UHyperTwistTrainingViewerLibrary::TryGetEmbedContractById( + BrowserViewerBundle, + BrowserViewerBundle.PrimaryEmbedContractId, + EmbedContract + ) + || !UHyperTwistTrainingSpatialLibrary::TryGetSceneContractById( + SpatialBundle, + SpatialBundle.PrimarySceneContractId, + SceneContract + )) + { + HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState(); + return false; + } + + HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState(); + HostBootstrapState.BootstrapId = FString::Printf(TEXT("%s/%s"), *Request.HostInstanceId, *Projection.SceneContextId); + HostBootstrapState.HostInstanceId = Request.HostInstanceId; + HostBootstrapState.AdapterProjectionId = Projection.ProjectionId; + HostBootstrapState.ViewerAdapterContractId = Projection.ViewerAdapterContractId; + HostBootstrapState.BrowserEmbedContractId = EmbedContract.ContractId; + HostBootstrapState.SpatialSceneContractId = SceneContract.ContractId; + HostBootstrapState.SceneContextId = Projection.SceneContextId; + HostBootstrapState.RenderStateProfile = Projection.RenderStateProfile; + HostBootstrapState.BindingMode = Request.BindingMode; + HostBootstrapState.bReadyForEmbed = true; + HostBootstrapState.bTimelineSurfaceBound = Request.bBindTimelineSurface; + HostBootstrapState.bScrubberSurfaceBound = Request.bBindScrubberSurface; + HostBootstrapState.bStatusSurfaceBound = Request.bBindStatusSurface; + HostBootstrapState.bPointerInputEnabled = Request.bAllowPointerInput; + HostBootstrapState.AdapterLifecycleHooks = Projection.LifecycleHooks; + HostBootstrapState.BrowserLifecycleHookIds = EmbedContract.LifecycleHookIds; + HostBootstrapState.SpatialLifecycleStages = SceneContract.LifecycleStages; + HostBootstrapState.ViewerSurfaceTags = Projection.ViewerSurfaceTags; + HostBootstrapState.SpatialEventSurfaceTags = SceneContract.EventSurfaceTags; + + if (!HostBootstrapState.IsStructurallyValid()) + { + HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState(); + return false; + } + + OutBootstrapState = HostBootstrapState; + return true; +} + // --------------------------------------------------------------------------- // Readout // --------------------------------------------------------------------------- diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h index 6d96714..70e0e6c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h @@ -58,8 +58,8 @@ DECLARE_MULTICAST_DELEGATE_OneParam( // Holds a loaded scene context and a flat move list, and manages a pure // state-machine playback cursor over that list. // -// Bound 2 scope: observer channels and deterministic cursor/timeline transport. -// Adapter lifecycle is Bound 3. +// Bound 2 adds observer channels and deterministic cursor/timeline transport. +// Bound 3 adds adapter projection and embedded-host bootstrap surfaces. // Viewport rendering is Bound 4. // --------------------------------------------------------------------------- @@ -104,6 +104,23 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Viewer") bool AdvancePlaybackByMs(int32 DeltaTimeMs); + // ------------------------------------------------------------------ + // Adapter and host bootstrap + // ------------------------------------------------------------------ + + // Build a deterministic projection packet for a consumer-side classic-cubing + // viewer adapter without taking classic-cubing ownership into this lane. + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Viewer") + bool TryBuildClassicCubingAdapterProjection(FHyperTwistViewerClassicCubingAdapterProjection& OutProjection) const; + + // Prepare a minimal embedded-host bootstrap state from the landed classic- + // cubing, browser-viewer, and browser-spatial contracts. + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Viewer") + bool TryBootstrapEmbeddedClassicCubingHost( + const FHyperTwistViewerEmbeddedHostBootstrapRequest& Request, + FHyperTwistViewerEmbeddedHostBootstrapState& OutBootstrapState + ); + // ------------------------------------------------------------------ // Readout accessors // ------------------------------------------------------------------ @@ -132,6 +149,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Viewer") FHyperTwistViewerDirectionState GetDirectionState() const { return DirectionState; } + UFUNCTION(BlueprintPure, Category = "HyperTwist|Viewer") + FHyperTwistViewerEmbeddedHostBootstrapState GetHostBootstrapState() const { return HostBootstrapState; } + // ------------------------------------------------------------------ // Delegates // ------------------------------------------------------------------ @@ -182,6 +202,9 @@ private: UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer") bool bReplayPacketLoaded = false; + UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer") + FHyperTwistViewerEmbeddedHostBootstrapState HostBootstrapState; + UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer") TArray MoveBoundaryTimesMs; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistViewerTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistViewerTypes.h index 1e2a047..312f521 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistViewerTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistViewerTypes.h @@ -42,6 +42,13 @@ enum class EHyperTwistViewerJumpKind : uint8 PlayWrap UMETA(DisplayName = "Play Wrap") }; +UENUM(BlueprintType) +enum class EHyperTwistViewerEmbeddedHostBindingMode : uint8 +{ + InlineCanvas UMETA(DisplayName = "Inline Canvas"), + ScissoredView UMETA(DisplayName = "Scissored View") +}; + // --------------------------------------------------------------------------- // Control action — what the caller wants the viewer to do // --------------------------------------------------------------------------- @@ -342,6 +349,228 @@ struct UNREALHYPERTWIST_API FHyperTwistViewerJumpEvent } }; +// --------------------------------------------------------------------------- +// Bound 3 adapter and embedded-host bootstrap records +// --------------------------------------------------------------------------- + +USTRUCT(BlueprintType) +struct UNREALHYPERTWIST_API FHyperTwistViewerAdapterProjectionStep +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + int32 MoveIndex = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FHyperTwistTransformation Transform; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString DisplayLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + int32 StartsAtMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + int32 CompletesAtMs = 0; + + bool IsStructurallyValid() const + { + return MoveIndex >= 0 + && Transform.IsStructurallyValid() + && !DisplayLabel.IsEmpty() + && StartsAtMs >= 0 + && CompletesAtMs >= StartsAtMs; + } +}; + +USTRUCT(BlueprintType) +struct UNREALHYPERTWIST_API FHyperTwistViewerClassicCubingAdapterProjection +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString ProjectionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString SemanticContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString GeometryContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString ViewerAdapterContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString SceneContextId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString RenderStateProfile; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FHyperTwistPuzzleState BaseState; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + int32 CurrentDisplayMoveIndex = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + int32 TotalTimelineTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FHyperTwistViewerPositionSnapshot PositionSnapshot; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray ProjectedMoves; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray ViewerSurfaceTags; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray LifecycleHooks; + + bool IsStructurallyValid() const + { + if (ProjectionId.IsEmpty() + || SemanticContractId.IsEmpty() + || GeometryContractId.IsEmpty() + || ViewerAdapterContractId.IsEmpty() + || SceneContextId.IsEmpty() + || RenderStateProfile.IsEmpty() + || !BaseState.IsStructurallyValid() + || !PositionSnapshot.IsStructurallyValid() + || TotalTimelineTimeMs < 0) + { + return false; + } + + for (const FHyperTwistViewerAdapterProjectionStep& Step : ProjectedMoves) + { + if (!Step.IsStructurallyValid()) + { + return false; + } + } + + return true; + } +}; + +USTRUCT(BlueprintType) +struct UNREALHYPERTWIST_API FHyperTwistViewerEmbeddedHostBootstrapRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString HostInstanceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + EHyperTwistViewerEmbeddedHostBindingMode BindingMode = EHyperTwistViewerEmbeddedHostBindingMode::InlineCanvas; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bBindTimelineSurface = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bBindScrubberSurface = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bBindStatusSurface = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bAllowPointerInput = true; + + bool IsStructurallyValid() const + { + return !HostInstanceId.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct UNREALHYPERTWIST_API FHyperTwistViewerEmbeddedHostBootstrapState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString BootstrapId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString HostInstanceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString AdapterProjectionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString ViewerAdapterContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString BrowserEmbedContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString SpatialSceneContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString SceneContextId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + FString RenderStateProfile; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + EHyperTwistViewerEmbeddedHostBindingMode BindingMode = EHyperTwistViewerEmbeddedHostBindingMode::InlineCanvas; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bReadyForEmbed = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bTimelineSurfaceBound = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bScrubberSurfaceBound = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bStatusSurfaceBound = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + bool bPointerInputEnabled = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray AdapterLifecycleHooks; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray BrowserLifecycleHookIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray SpatialLifecycleStages; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray ViewerSurfaceTags; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer") + TArray SpatialEventSurfaceTags; + + bool IsStructurallyValid() const + { + if (!bReadyForEmbed) + { + return BootstrapId.IsEmpty() + && HostInstanceId.IsEmpty() + && AdapterProjectionId.IsEmpty() + && ViewerAdapterContractId.IsEmpty() + && BrowserEmbedContractId.IsEmpty() + && SpatialSceneContractId.IsEmpty() + && SceneContextId.IsEmpty(); + } + + return !BootstrapId.IsEmpty() + && !HostInstanceId.IsEmpty() + && !AdapterProjectionId.IsEmpty() + && !ViewerAdapterContractId.IsEmpty() + && !BrowserEmbedContractId.IsEmpty() + && !SpatialSceneContractId.IsEmpty() + && !SceneContextId.IsEmpty() + && !RenderStateProfile.IsEmpty() + && BrowserLifecycleHookIds.Num() > 0 + && SpatialLifecycleStages.Num() > 0; + } +}; + // --------------------------------------------------------------------------- // Runtime surface - minimal compact replay-player bundle exposed through the // training runtime library without moving shell ownership out of simulation. diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound3AdapterBootstrapTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound3AdapterBootstrapTest.cpp new file mode 100644 index 0000000..4ae4a64 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound3AdapterBootstrapTest.cpp @@ -0,0 +1,210 @@ +// Copyright HyperTwist, Inc. All Rights Reserved. + +#include "Misc/AutomationTest.h" + +#include "HyperTwistBootstrap/HyperTwistContractLibrary.h" +#include "HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h" +#include "HyperTwistSimulation/HyperTwistViewerLibrary.h" +#include "JsonObjectConverter.h" + +#if WITH_AUTOMATION_TESTS + +namespace HyperTwistTwistyBound3AdapterBootstrapTestInternal +{ + template + FString SerializeStructToJson(const TStruct& Value) + { + FString Json; + FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0); + return Json; + } + + FHyperTwistReplayEvent MakeReplayEvent( + const FString& EventId, + const int32 Sequence, + const int32 TimeMs, + const EHyperTwistReplayEventType EventType, + const FString& PayloadJson + ) + { + FHyperTwistReplayEvent Event; + Event.EventId = EventId; + Event.Sequence = Sequence; + Event.TimeMs = TimeMs; + Event.EventType = EventType; + Event.PayloadJson = PayloadJson; + return Event; + } + + FHyperTwistReplayPacket MakeReplayPacket() + { + const FHyperTwistPuzzleState SampleState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState(); + + FHyperTwistStateSnapshot Snapshot; + Snapshot.SnapshotId = TEXT("snapshot-bound3"); + Snapshot.State = SampleState; + Snapshot.DerivedHash = TEXT("snapshot-bound3-hash"); + + FHyperTwistReplayMovePayload FirstMovePayload; + FirstMovePayload.Notation = TEXT("R"); + FirstMovePayload.TransformationRef = TEXT("classic/r"); + FirstMovePayload.Source = TEXT("unit-test"); + + FHyperTwistReplayMovePayload SecondMovePayload; + SecondMovePayload.Notation = TEXT("U"); + SecondMovePayload.TransformationRef = TEXT("classic/u"); + SecondMovePayload.Source = TEXT("unit-test"); + + FHyperTwistReplayMovePayload ThirdMovePayload; + ThirdMovePayload.Notation = TEXT("F'"); + ThirdMovePayload.TransformationRef = TEXT("classic/f-prime"); + ThirdMovePayload.Source = TEXT("unit-test"); + + FHyperTwistReplayPacket ReplayPacket; + ReplayPacket.ReplayId = TEXT("replay-bound3-001"); + ReplayPacket.SessionId = TEXT("session-bound3-001"); + ReplayPacket.PuzzleDefinition = SampleState.Definition; + ReplayPacket.CaptureMode = EHyperTwistCaptureMode::Imported; + ReplayPacket.Events = { + MakeReplayEvent( + TEXT("evt-001"), + 1, + 0, + EHyperTwistReplayEventType::StateSnapshot, + SerializeStructToJson(Snapshot) + ), + MakeReplayEvent( + TEXT("evt-002"), + 2, + 0, + EHyperTwistReplayEventType::TimerStart, + TEXT("") + ), + MakeReplayEvent( + TEXT("evt-003"), + 3, + 200, + EHyperTwistReplayEventType::Move, + SerializeStructToJson(FirstMovePayload) + ), + MakeReplayEvent( + TEXT("evt-004"), + 4, + 400, + EHyperTwistReplayEventType::Move, + SerializeStructToJson(SecondMovePayload) + ), + MakeReplayEvent( + TEXT("evt-005"), + 5, + 700, + EHyperTwistReplayEventType::Move, + SerializeStructToJson(ThirdMovePayload) + ), + MakeReplayEvent( + TEXT("evt-006"), + 6, + 1000, + EHyperTwistReplayEventType::SolveEnd, + TEXT("") + ) + }; + + return ReplayPacket; + } +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistTwistyBound3AdapterBoundaryTest, + "HyperTwist.CleanRoom.TwistyJs.Bound3.AdapterBoundary", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistTwistyBound3AdapterBoundaryTest::RunTest(const FString& Parameters) +{ + UHyperTwistPuzzleViewerComponent* Viewer = NewObject(); + TestNotNull(TEXT("Viewer component instance must be created."), Viewer); + TestTrue( + TEXT("Replay packet load must succeed for the Bound 3 adapter test."), + Viewer->LoadReplayPacket(HyperTwistTwistyBound3AdapterBootstrapTestInternal::MakeReplayPacket()) + ); + + Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest()); + TestTrue(TEXT("Projection test playback advance should move the cursor."), Viewer->AdvancePlaybackByMs(250)); + + FHyperTwistViewerClassicCubingAdapterProjection Projection; + TestTrue( + TEXT("The viewer must build a consumer-side classic-cubing adapter projection."), + Viewer->TryBuildClassicCubingAdapterProjection(Projection) + ); + TestTrue(TEXT("The adapter projection must be structurally valid."), Projection.IsStructurallyValid()); + TestEqual(TEXT("The semantic contract id must come from the landed classic-cubing lane."), Projection.SemanticContractId, FString(TEXT("classic-cubing-semantics-stack"))); + TestEqual(TEXT("The geometry contract id must come from the landed classic-cubing lane."), Projection.GeometryContractId, FString(TEXT("classic-cubing-geometry-stack"))); + TestEqual(TEXT("The viewer adapter contract id must come from the landed classic-cubing lane."), Projection.ViewerAdapterContractId, FString(TEXT("classic-cubing-viewer-adapter-stack"))); + TestEqual(TEXT("The projection should preserve the viewer's scene context id."), Projection.SceneContextId, Viewer->GetSceneContext().SceneContextId); + TestEqual(TEXT("The projection should preserve the viewer's total timeline time."), Projection.TotalTimelineTimeMs, 1000); + TestEqual(TEXT("The projection should preserve the current display move index."), Projection.CurrentDisplayMoveIndex, 1); + TestEqual(TEXT("The projection should preserve the current position snapshot time."), Projection.PositionSnapshot.CurrentTimeMs, 250); + TestEqual(TEXT("The deterministic move projection should include all replay moves."), Projection.ProjectedMoves.Num(), 3); + TestEqual(TEXT("The first projected step should start at 0ms."), Projection.ProjectedMoves[0].StartsAtMs, 0); + TestEqual(TEXT("The first projected step should complete at 200ms."), Projection.ProjectedMoves[0].CompletesAtMs, 200); + TestEqual(TEXT("The second projected step should start at 200ms."), Projection.ProjectedMoves[1].StartsAtMs, 200); + TestEqual(TEXT("The second projected step should complete at 400ms."), Projection.ProjectedMoves[1].CompletesAtMs, 400); + TestEqual(TEXT("The third projected step should start at 400ms."), Projection.ProjectedMoves[2].StartsAtMs, 400); + TestEqual(TEXT("The third projected step should complete at 700ms."), Projection.ProjectedMoves[2].CompletesAtMs, 700); + TestTrue(TEXT("The projection should surface classic-cubing viewer lifecycle hooks."), Projection.LifecycleHooks.Contains(TEXT("timeline scrubbed"))); + TestTrue(TEXT("The projection should surface classic-cubing viewer surface tags."), Projection.ViewerSurfaceTags.Contains(TEXT("TwistyPlayer embed"))); + + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistTwistyBound3HostBootstrapTest, + "HyperTwist.CleanRoom.TwistyJs.Bound3.HostBootstrap", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistTwistyBound3HostBootstrapTest::RunTest(const FString& Parameters) +{ + UHyperTwistPuzzleViewerComponent* Viewer = NewObject(); + TestNotNull(TEXT("Viewer component instance must be created."), Viewer); + TestTrue( + TEXT("Replay packet load must succeed for the Bound 3 bootstrap test."), + Viewer->LoadReplayPacket(HyperTwistTwistyBound3AdapterBootstrapTestInternal::MakeReplayPacket()) + ); + + FHyperTwistViewerEmbeddedHostBootstrapRequest Request; + Request.HostInstanceId = TEXT("compact-host-01"); + Request.BindingMode = EHyperTwistViewerEmbeddedHostBindingMode::ScissoredView; + Request.bBindTimelineSurface = true; + Request.bBindScrubberSurface = true; + Request.bBindStatusSurface = true; + Request.bAllowPointerInput = true; + + FHyperTwistViewerEmbeddedHostBootstrapState BootstrapState; + TestTrue( + TEXT("The viewer must prepare a minimal embedded host bootstrap state."), + Viewer->TryBootstrapEmbeddedClassicCubingHost(Request, BootstrapState) + ); + TestTrue(TEXT("The bootstrap state must be structurally valid."), BootstrapState.IsStructurallyValid()); + TestEqual(TEXT("The bootstrap state should preserve the host instance id."), BootstrapState.HostInstanceId, Request.HostInstanceId); + TestEqual(TEXT("The bootstrap state should reference the classic-cubing viewer adapter contract."), BootstrapState.ViewerAdapterContractId, FString(TEXT("classic-cubing-viewer-adapter-stack"))); + TestEqual(TEXT("The bootstrap state should reference the landed browser viewer embed contract."), BootstrapState.BrowserEmbedContractId, FString(TEXT("browser-viewer-embed"))); + TestEqual(TEXT("The bootstrap state should reference the landed browser spatial scene contract."), BootstrapState.SpatialSceneContractId, FString(TEXT("browser-spatial-scene-stack"))); + TestEqual(TEXT("The bootstrap state should preserve the selected binding mode."), BootstrapState.BindingMode, EHyperTwistViewerEmbeddedHostBindingMode::ScissoredView); + TestTrue(TEXT("The bootstrap state should mark the embedded host ready."), BootstrapState.bReadyForEmbed); + TestTrue(TEXT("The bootstrap state should bind the timeline surface when requested."), BootstrapState.bTimelineSurfaceBound); + TestTrue(TEXT("The bootstrap state should bind the scrubber surface when requested."), BootstrapState.bScrubberSurfaceBound); + TestTrue(TEXT("The bootstrap state should bind the status surface when requested."), BootstrapState.bStatusSurfaceBound); + TestTrue(TEXT("The bootstrap state should keep pointer input enabled when requested."), BootstrapState.bPointerInputEnabled); + TestTrue(TEXT("The bootstrap state should carry classic-cubing adapter lifecycle hooks."), BootstrapState.AdapterLifecycleHooks.Contains(TEXT("viewer ready"))); + TestTrue(TEXT("The bootstrap state should carry browser embed lifecycle hook ids."), BootstrapState.BrowserLifecycleHookIds.Contains(TEXT("hook/model-loaded"))); + TestTrue(TEXT("The bootstrap state should carry spatial scene lifecycle stages."), BootstrapState.SpatialLifecycleStages.Contains(TEXT("create canvas root"))); + TestTrue(TEXT("The bootstrap state should carry scissored-view event routing support."), BootstrapState.SpatialEventSurfaceTags.Contains(TEXT("scissored view embedding"))); + TestEqual(TEXT("The stored bootstrap state should match the returned bootstrap id."), Viewer->GetHostBootstrapState().BootstrapId, BootstrapState.BootstrapId); + TestEqual(TEXT("The stored bootstrap state should preserve the projection id."), Viewer->GetHostBootstrapState().AdapterProjectionId, BootstrapState.AdapterProjectionId); + + return true; +} + +#endif // WITH_AUTOMATION_TESTS