Add twisty Bound 3 adapter bootstrap packet

This commit is contained in:
axiomlogicnexus 2026-05-18 03:48:11 +02:00
parent bcd1f9ff4e
commit dc621d97e4
4 changed files with 634 additions and 2 deletions

View file

@ -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<int32> ResolveProjectionBoundaryTimesMs(
const TArray<FHyperTwistViewerMoveEntry>& MoveList,
const TArray<int32>& CachedMoveBoundaryTimesMs
)
{
return CachedMoveBoundaryTimesMs.Num() == MoveList.Num()
? CachedMoveBoundaryTimesMs
: BuildMoveBoundaryTimesMs(MoveList);
}
int32 ResolveProjectionMoveStartTimeMs(const TArray<int32>& MoveBoundaryTimesMs, const int32 MoveIndex)
{
return MoveIndex > 0 && MoveBoundaryTimesMs.IsValidIndex(MoveIndex - 1)
? MoveBoundaryTimesMs[MoveIndex - 1]
: 0;
}
FHyperTwistViewerPositionSnapshot BuildPositionSnapshot(
const TArray<FHyperTwistViewerMoveEntry>& MoveList,
const TArray<int32>& 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<FHyperTwistView
ReplayPacket = FHyperTwistReplayPacket();
ReplaySummary = FHyperTwistDerivedReplaySummary();
bReplayPacketLoaded = false;
HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState();
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
@ -508,6 +530,7 @@ bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayP
ReplaySummary = UHyperTwistReplayLibrary::DeriveReplaySummary(InReplayPacket);
bReplayPacketLoaded = true;
MoveList = HyperTwistPuzzleViewerComponentInternal::BuildReplayMoveList(InReplayPacket);
HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState();
FHyperTwistSimulationSceneContext ReplaySceneContext;
if (HyperTwistPuzzleViewerComponentInternal::TryBuildSceneContextFromReplay(InReplayPacket, ReplaySceneContext))
@ -706,6 +729,153 @@ bool UHyperTwistPuzzleViewerComponent::AdvancePlaybackByMs(const int32 DeltaTime
return PlaybackState.CurrentTimeMs != PreviousTimeMs;
}
bool UHyperTwistPuzzleViewerComponent::TryBuildClassicCubingAdapterProjection(
FHyperTwistViewerClassicCubingAdapterProjection& OutProjection
) const
{
OutProjection = FHyperTwistViewerClassicCubingAdapterProjection();
if (!SceneContext.IsStructurallyValid())
{
return false;
}
const FHyperTwistTrainingClassicCubingReferenceBundle Bundle =
UHyperTwistTrainingClassicCubingLibrary::BuildBundledClassicCubingReferenceBundle();
FHyperTwistTrainingClassicCubingSemanticContract SemanticContract;
FHyperTwistTrainingClassicCubingGeometryContract GeometryContract;
FHyperTwistTrainingClassicCubingViewerAdapterContract ViewerAdapterContract;
if (!UHyperTwistTrainingClassicCubingLibrary::TryGetSemanticContractById(
Bundle,
Bundle.PrimarySemanticContractId,
SemanticContract
)
|| !UHyperTwistTrainingClassicCubingLibrary::TryGetGeometryContractById(
Bundle,
Bundle.PrimaryGeometryContractId,
GeometryContract
)
|| !UHyperTwistTrainingClassicCubingLibrary::TryGetViewerAdapterContractById(
Bundle,
Bundle.PrimaryViewerAdapterContractId,
ViewerAdapterContract
))
{
return false;
}
OutProjection.ProjectionId = FString::Printf(TEXT("classic-cubing/%s"), *SceneContext.SceneContextId);
OutProjection.SemanticContractId = SemanticContract.ContractId;
OutProjection.GeometryContractId = GeometryContract.ContractId;
OutProjection.ViewerAdapterContractId = ViewerAdapterContract.ContractId;
OutProjection.SceneContextId = SceneContext.SceneContextId;
OutProjection.RenderStateProfile = SceneContext.RenderStateProfile;
OutProjection.BaseState = SceneContext.PuzzleState;
OutProjection.CurrentDisplayMoveIndex = PlaybackState.CurrentMoveIndex;
OutProjection.TotalTimelineTimeMs = PlaybackState.TotalTimeMs;
OutProjection.PositionSnapshot = PositionSnapshot;
OutProjection.ViewerSurfaceTags = ViewerAdapterContract.ViewerSurfaceTags;
OutProjection.LifecycleHooks = ViewerAdapterContract.LifecycleHooks;
const TArray<int32> 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
// ---------------------------------------------------------------------------

View file

@ -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<int32> MoveBoundaryTimesMs;

View file

@ -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<FHyperTwistViewerAdapterProjectionStep> ProjectedMoves;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> ViewerSurfaceTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> 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<FString> AdapterLifecycleHooks;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> BrowserLifecycleHookIds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> SpatialLifecycleStages;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> ViewerSurfaceTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> 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.

View file

@ -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 <typename TStruct>
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<UHyperTwistPuzzleViewerComponent>();
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<UHyperTwistPuzzleViewerComponent>();
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