diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp index ec4a79e..7023942 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistPuzzleViewerComponent.cpp @@ -186,6 +186,18 @@ namespace HyperTwistPuzzleViewerComponentInternal continue; } + const FHyperTwistPuzzleDefinitionRef& PacketDefinition = ReplayPacket.PuzzleDefinition; + const FHyperTwistPuzzleDefinitionRef& SnapshotDefinition = Snapshot.State.Definition; + const bool bDefinitionMatchesPacket = + PacketDefinition.PuzzleId == SnapshotDefinition.PuzzleId + && PacketDefinition.PuzzleFamily == SnapshotDefinition.PuzzleFamily + && PacketDefinition.Dimension == SnapshotDefinition.Dimension + && PacketDefinition.NotationProfile == SnapshotDefinition.NotationProfile; + if (!bDefinitionMatchesPacket) + { + continue; + } + OutContext.SceneContextId = !ReplayPacket.ReplayId.IsEmpty() ? FString::Printf(TEXT("replay/%s"), *ReplayPacket.ReplayId) : Snapshot.SnapshotId; @@ -662,8 +674,7 @@ bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayP { SceneContext = ReplaySceneContext; } - else if (!SceneContext.IsStructurallyValid() - || SceneContext.PuzzleState.Definition.PuzzleId != ReplayPacket.PuzzleDefinition.PuzzleId) + else { SceneContext = FHyperTwistSimulationSceneContext(); } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp index 5998d0d..2fcb38c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistTwistyBound1ReplayShellTest.cpp @@ -208,4 +208,81 @@ bool FHyperTwistTwistyBound1ReplayShellTest::RunTest(const FString& Parameters) return true; } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistTwistyBound1ReplaySceneContextMismatchGuardTest, + "HyperTwist.CleanRoom.TwistyJs.Bound1.ReplaySceneContextMismatchGuard", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistTwistyBound1ReplaySceneContextMismatchGuardTest::RunTest(const FString& Parameters) +{ + static_cast(Parameters); + + UHyperTwistPuzzleViewerComponent* Viewer = NewObject(); + TestNotNull(TEXT("Viewer component instance must be created."), Viewer); + if (Viewer == nullptr) + { + return false; + } + + FHyperTwistReplayPacket ReplayPacket; + ReplayPacket.ReplayId = TEXT("replay-bound1-mismatch"); + ReplayPacket.SessionId = TEXT("session-bound1-mismatch"); + ReplayPacket.PuzzleDefinition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition(); + ReplayPacket.CaptureMode = EHyperTwistCaptureMode::Imported; + + FHyperTwistStateSnapshot Snapshot; + Snapshot.SnapshotId = TEXT("snapshot-mismatch"); + Snapshot.State = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState(); + Snapshot.DerivedHash = TEXT("snapshot-mismatch-hash"); + + ReplayPacket.Events = { + HyperTwistTwistyBound1ReplayShellTestInternal::MakeReplayEvent( + TEXT("evt-001"), + 1, + 0, + EHyperTwistReplayEventType::StateSnapshot, + HyperTwistTwistyBound1ReplayShellTestInternal::SerializeStructToJson(Snapshot) + ), + HyperTwistTwistyBound1ReplayShellTestInternal::MakeReplayEvent( + TEXT("evt-002"), + 2, + 100, + EHyperTwistReplayEventType::Move, + TEXT("{\"notation\":\"R\",\"transformationRef\":\"classic/r\",\"source\":\"unit-test\"}") + ) + }; + + TestTrue( + TEXT("The initial valid replay packet should populate a scene context before the mismatch case."), + Viewer->LoadReplayPacket(ReplayPacket) + ); + TestTrue( + TEXT("The initial valid replay packet should mark the scene context as loaded."), + Viewer->GetPlaybackState().bSceneContextLoaded + ); + + Snapshot.State.Definition = UHyperTwistContractLibrary::MakeSampleClassicPuzzleDefinition(); + ReplayPacket.Events[0].PayloadJson = + HyperTwistTwistyBound1ReplayShellTestInternal::SerializeStructToJson(Snapshot); + + TestTrue( + TEXT("Replay packet load should still succeed when the packet remains structurally valid."), + Viewer->LoadReplayPacket(ReplayPacket) + ); + + const FHyperTwistViewerPlaybackState PlaybackState = Viewer->GetPlaybackState(); + TestTrue(TEXT("Mismatch replay packets should still report the replay as loaded."), PlaybackState.bReplayLoaded); + TestFalse( + TEXT("Mismatch replay snapshots must not materialize a misleading scene context."), + PlaybackState.bSceneContextLoaded + ); + TestFalse( + TEXT("Mismatch replay snapshots must clear any viewer scene-context state."), + Viewer->GetSceneContext().IsStructurallyValid() + ); + + return true; +} + #endif // WITH_AUTOMATION_TESTS