diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp index fd5b077..ec4a79e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp @@ -133,6 +133,26 @@ namespace HyperTwistPuzzleViewerComponentInternal return Result; } + FHyperTwistProjectionSettings BuildReplayProjectionSettings( + const FHyperTwistPuzzleDefinitionRef& PuzzleDefinition + ) + { + FHyperTwistProjectionSettings Settings; + if (PuzzleDefinition.PuzzleFamily == EHyperTwistPuzzleFamily::Hypercube + || PuzzleDefinition.Dimension > 3) + { + Settings.ProjectionKind = EHyperTwistProjectionKind::HyperProjection; + Settings.ProjectionProfile = TEXT("hyper-default"); + Settings.ProjectionDepth = 2.0f; + return Settings; + } + + Settings.ProjectionKind = EHyperTwistProjectionKind::ClassicPerspective; + Settings.ProjectionProfile = TEXT("classic-default"); + Settings.ProjectionDepth = 1.0f; + return Settings; + } + int32 ResolvePlaybackTerminalTimeMs( const TArray& MoveBoundaryTimesMs, const FHyperTwistDerivedReplaySummary& ReplaySummary, @@ -170,6 +190,7 @@ namespace HyperTwistPuzzleViewerComponentInternal ? FString::Printf(TEXT("replay/%s"), *ReplayPacket.ReplayId) : Snapshot.SnapshotId; OutContext.PuzzleState = Snapshot.State; + OutContext.ProjectionSettings = BuildReplayProjectionSettings(ReplayPacket.PuzzleDefinition); OutContext.RenderStateProfile = ReplayPacket.PuzzleDefinition.PuzzleFamily == EHyperTwistPuzzleFamily::ClassicCube ? TEXT("classic-replay") : TEXT("replay-default"); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationTypes.h index db24359..adce671 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationTypes.h @@ -25,6 +25,11 @@ struct FHyperTwistProjectionSettings UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") float ProjectionDepth = 1.0f; + + bool IsStructurallyValid() const + { + return !ProjectionProfile.IsEmpty() && ProjectionDepth > 0.0f; + } }; USTRUCT(BlueprintType) @@ -46,6 +51,9 @@ struct FHyperTwistSimulationSceneContext bool IsStructurallyValid() const { - return !SceneContextId.IsEmpty() && PuzzleState.IsStructurallyValid() && !RenderStateProfile.IsEmpty(); + return !SceneContextId.IsEmpty() + && PuzzleState.IsStructurallyValid() + && ProjectionSettings.IsStructurallyValid() + && !RenderStateProfile.IsEmpty(); } }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp index 9b459fb..dcb8997 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp @@ -440,6 +440,27 @@ bool FHyperTwistClassicCubeReplayViewerLoadTest::RunTest(const FString& Paramete const FHyperTwistViewerPlaybackState PlaybackState = ViewerComponent->GetPlaybackState(); TestTrue(TEXT("The viewer must mark the replay as loaded."), PlaybackState.bReplayLoaded); TestTrue(TEXT("The viewer must materialize scene context from the replay snapshot."), PlaybackState.bSceneContextLoaded); + const FHyperTwistSimulationSceneContext ReplaySceneContext = ViewerComponent->GetSceneContext(); + TestTrue(TEXT("The classic-cube replay scene context must stay structurally valid."), ReplaySceneContext.IsStructurallyValid()); + TestEqual( + TEXT("The classic-cube replay scene context must use the generic classic projection kind."), + ReplaySceneContext.ProjectionSettings.ProjectionKind, + EHyperTwistProjectionKind::ClassicPerspective + ); + TestEqual( + TEXT("The classic-cube replay scene context must use the generic classic projection profile."), + ReplaySceneContext.ProjectionSettings.ProjectionProfile, + FString(TEXT("classic-default")) + ); + TestTrue( + TEXT("The classic-cube replay scene context must preserve a positive generic projection depth."), + FMath::IsNearlyEqual(ReplaySceneContext.ProjectionSettings.ProjectionDepth, 1.0f) + ); + TestEqual( + TEXT("The classic-cube replay scene context must keep the bounded classic replay render-state profile."), + ReplaySceneContext.RenderStateProfile, + FString(TEXT("classic-replay")) + ); TestEqual(TEXT("The viewer must reconstruct the expected move count."), PlaybackState.TotalMoveCount, 10); TestEqual(TEXT("The viewer must retain the replay id after load."), PlaybackState.ReplayId, Packet.ReplayId); return true; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp index 20540e0..5998d0d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp @@ -120,6 +120,27 @@ bool FHyperTwistTwistyBound1ReplayShellTest::RunTest(const FString& Parameters) TestEqual(TEXT("Replay load must initialize current time to the start boundary."), InitialState.CurrentTimeMs, 0); TestEqual(TEXT("Replay load must derive total replay time from replay events."), InitialState.TotalTimeMs, 900); TestTrue(TEXT("Replay load should derive a scene context from a snapshot event when available."), InitialState.bSceneContextLoaded); + const FHyperTwistSimulationSceneContext ReplaySceneContext = Viewer->GetSceneContext(); + TestTrue(TEXT("Replay-backed scene context must stay structurally valid."), ReplaySceneContext.IsStructurallyValid()); + TestEqual( + TEXT("Hypercube replay scene context should use the generic hyper projection kind."), + ReplaySceneContext.ProjectionSettings.ProjectionKind, + EHyperTwistProjectionKind::HyperProjection + ); + TestEqual( + TEXT("Hypercube replay scene context should use the generic hyper projection profile."), + ReplaySceneContext.ProjectionSettings.ProjectionProfile, + FString(TEXT("hyper-default")) + ); + TestTrue( + TEXT("Hypercube replay scene context should preserve a positive generic projection depth."), + FMath::IsNearlyEqual(ReplaySceneContext.ProjectionSettings.ProjectionDepth, 2.0f) + ); + TestEqual( + TEXT("Hypercube replay scene context should keep the bounded generic replay render-state profile."), + ReplaySceneContext.RenderStateProfile, + FString(TEXT("replay-default")) + ); TestEqual(TEXT("Initial replay-backed scrubber position must stay at the start boundary."), UHyperTwistViewerLibrary::GetScrubberNormalizedPosition(InitialState), 0.0f); TestEqual(TEXT("Timeline label must reflect replay-backed duration."), UHyperTwistViewerLibrary::GetTimelineLabel(InitialState), FString(TEXT("0.00s / 0.90s"))); TestEqual(TEXT("Replay summary must count move events."), Viewer->GetReplaySummary().MoveCount, 2);