Add Bound 2 viewer timeline transport
This commit is contained in:
parent
eaeece995a
commit
bcd1f9ff4e
4 changed files with 1004 additions and 44 deletions
|
|
@ -102,6 +102,48 @@ namespace HyperTwistPuzzleViewerComponentInternal
|
|||
return Result;
|
||||
}
|
||||
|
||||
TArray<int32> BuildMoveBoundaryTimesMs(const TArray<FHyperTwistViewerMoveEntry>& MoveList)
|
||||
{
|
||||
TArray<int32> Result;
|
||||
Result.Reserve(MoveList.Num());
|
||||
|
||||
int32 PreviousBoundaryTimeMs = 0;
|
||||
for (int32 MoveIndex = 0; MoveIndex < MoveList.Num(); ++MoveIndex)
|
||||
{
|
||||
const FHyperTwistViewerMoveEntry& Entry = MoveList[MoveIndex];
|
||||
int32 BoundaryTimeMs = 0;
|
||||
if (Entry.HintPositionSeconds > 0.0f)
|
||||
{
|
||||
BoundaryTimeMs = FMath::RoundToInt(Entry.HintPositionSeconds * 1000.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
BoundaryTimeMs = (MoveIndex + 1) * 1000;
|
||||
}
|
||||
|
||||
BoundaryTimeMs = FMath::Max(BoundaryTimeMs, PreviousBoundaryTimeMs);
|
||||
Result.Add(BoundaryTimeMs);
|
||||
PreviousBoundaryTimeMs = BoundaryTimeMs;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
int32 ResolvePlaybackTerminalTimeMs(
|
||||
const TArray<int32>& MoveBoundaryTimesMs,
|
||||
const FHyperTwistDerivedReplaySummary& ReplaySummary,
|
||||
const bool bReplayLoaded
|
||||
)
|
||||
{
|
||||
const int32 LastMoveBoundaryTimeMs = MoveBoundaryTimesMs.Num() > 0 ? MoveBoundaryTimesMs.Last() : 0;
|
||||
if (bReplayLoaded)
|
||||
{
|
||||
return FMath::Max(ReplaySummary.TotalTimeMs, LastMoveBoundaryTimeMs);
|
||||
}
|
||||
|
||||
return LastMoveBoundaryTimeMs;
|
||||
}
|
||||
|
||||
bool TryBuildSceneContextFromReplay(
|
||||
const FHyperTwistReplayPacket& ReplayPacket,
|
||||
FHyperTwistSimulationSceneContext& OutContext
|
||||
|
|
@ -161,6 +203,230 @@ namespace HyperTwistPuzzleViewerComponentInternal
|
|||
|
||||
return FMath::Max(0, ReplaySummary.TotalTimeMs);
|
||||
}
|
||||
|
||||
int32 FindLastCompletedMoveIndexForTime(const TArray<int32>& MoveBoundaryTimesMs, const int32 CurrentTimeMs)
|
||||
{
|
||||
int32 LastCompletedMoveIndex = INDEX_NONE;
|
||||
for (int32 MoveIndex = 0; MoveIndex < MoveBoundaryTimesMs.Num(); ++MoveIndex)
|
||||
{
|
||||
if (MoveBoundaryTimesMs[MoveIndex] <= CurrentTimeMs)
|
||||
{
|
||||
LastCompletedMoveIndex = MoveIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return LastCompletedMoveIndex;
|
||||
}
|
||||
|
||||
int32 FindDisplayMoveIndexForTime(
|
||||
const TArray<int32>& MoveBoundaryTimesMs,
|
||||
const int32 CurrentTimeMs,
|
||||
const int32 TerminalTimeMs
|
||||
)
|
||||
{
|
||||
if (MoveBoundaryTimesMs.Num() == 0)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
if (CurrentTimeMs <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (TerminalTimeMs > 0 && CurrentTimeMs >= TerminalTimeMs)
|
||||
{
|
||||
return MoveBoundaryTimesMs.Num() - 1;
|
||||
}
|
||||
|
||||
for (int32 MoveIndex = 0; MoveIndex < MoveBoundaryTimesMs.Num(); ++MoveIndex)
|
||||
{
|
||||
if (CurrentTimeMs < MoveBoundaryTimesMs[MoveIndex])
|
||||
{
|
||||
return MoveIndex;
|
||||
}
|
||||
|
||||
if (CurrentTimeMs == MoveBoundaryTimesMs[MoveIndex] && MoveIndex + 1 < MoveBoundaryTimesMs.Num())
|
||||
{
|
||||
return MoveIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return MoveBoundaryTimesMs.Num() - 1;
|
||||
}
|
||||
|
||||
int32 ResolveNextMoveBoundaryTimeMs(const TArray<int32>& MoveBoundaryTimesMs, const int32 CurrentTimeMs)
|
||||
{
|
||||
for (const int32 MoveBoundaryTimeMs : MoveBoundaryTimesMs)
|
||||
{
|
||||
if (MoveBoundaryTimeMs > CurrentTimeMs)
|
||||
{
|
||||
return MoveBoundaryTimeMs;
|
||||
}
|
||||
}
|
||||
|
||||
return CurrentTimeMs;
|
||||
}
|
||||
|
||||
int32 ResolvePreviousMoveBoundaryTimeMs(const TArray<int32>& MoveBoundaryTimesMs, const int32 CurrentTimeMs)
|
||||
{
|
||||
int32 PreviousBoundaryTimeMs = 0;
|
||||
for (const int32 MoveBoundaryTimeMs : MoveBoundaryTimesMs)
|
||||
{
|
||||
if (MoveBoundaryTimeMs >= CurrentTimeMs)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
PreviousBoundaryTimeMs = MoveBoundaryTimeMs;
|
||||
}
|
||||
|
||||
return PreviousBoundaryTimeMs;
|
||||
}
|
||||
|
||||
FHyperTwistViewerPositionSnapshot BuildPositionSnapshot(
|
||||
const TArray<FHyperTwistViewerMoveEntry>& MoveList,
|
||||
const TArray<int32>& MoveBoundaryTimesMs,
|
||||
const int32 CurrentTimeMs,
|
||||
const int32 TerminalTimeMs,
|
||||
const EHyperTwistViewerPlaybackDirection PlaybackDirection
|
||||
)
|
||||
{
|
||||
FHyperTwistViewerPositionSnapshot Snapshot;
|
||||
Snapshot.CurrentTimeMs = FMath::Max(0, CurrentTimeMs);
|
||||
Snapshot.bAtStart = Snapshot.CurrentTimeMs <= 0;
|
||||
Snapshot.bAtEnd = TerminalTimeMs > 0 ? Snapshot.CurrentTimeMs >= TerminalTimeMs : MoveList.Num() == 0;
|
||||
|
||||
if (MoveList.Num() == 0)
|
||||
{
|
||||
Snapshot.bAtBoundary = true;
|
||||
return Snapshot;
|
||||
}
|
||||
|
||||
Snapshot.BaseCompletedMoveIndex = FindLastCompletedMoveIndexForTime(MoveBoundaryTimesMs, Snapshot.CurrentTimeMs);
|
||||
Snapshot.BaseBoundaryTimeMs = Snapshot.BaseCompletedMoveIndex != INDEX_NONE
|
||||
? MoveBoundaryTimesMs[Snapshot.BaseCompletedMoveIndex]
|
||||
: 0;
|
||||
|
||||
const int32 NextMoveIndex = Snapshot.BaseCompletedMoveIndex + 1;
|
||||
if (!Snapshot.bAtEnd
|
||||
&& NextMoveIndex >= 0
|
||||
&& MoveList.IsValidIndex(NextMoveIndex)
|
||||
&& MoveBoundaryTimesMs.IsValidIndex(NextMoveIndex))
|
||||
{
|
||||
const int32 SegmentStartTimeMs = Snapshot.BaseBoundaryTimeMs;
|
||||
const int32 SegmentEndTimeMs = MoveBoundaryTimesMs[NextMoveIndex];
|
||||
if (Snapshot.CurrentTimeMs > SegmentStartTimeMs && Snapshot.CurrentTimeMs < SegmentEndTimeMs)
|
||||
{
|
||||
FHyperTwistViewerInFlightMoveProgress Progress;
|
||||
Progress.MoveIndex = NextMoveIndex;
|
||||
Progress.MoveEntry = MoveList[NextMoveIndex];
|
||||
Progress.Direction = PlaybackDirection == EHyperTwistViewerPlaybackDirection::None
|
||||
? EHyperTwistViewerPlaybackDirection::Forward
|
||||
: PlaybackDirection;
|
||||
Progress.StartedAtMs = SegmentStartTimeMs;
|
||||
Progress.EndsAtMs = SegmentEndTimeMs;
|
||||
|
||||
const int32 SegmentDurationMs = FMath::Max(1, SegmentEndTimeMs - SegmentStartTimeMs);
|
||||
Progress.Fraction = FMath::Clamp(
|
||||
static_cast<float>(Snapshot.CurrentTimeMs - SegmentStartTimeMs) / static_cast<float>(SegmentDurationMs),
|
||||
0.0f,
|
||||
1.0f
|
||||
);
|
||||
|
||||
Snapshot.InFlightMoves.Add(Progress);
|
||||
}
|
||||
}
|
||||
|
||||
Snapshot.bAtBoundary = Snapshot.InFlightMoves.Num() == 0;
|
||||
return Snapshot;
|
||||
}
|
||||
|
||||
FHyperTwistViewerDirectionState BuildDirectionState(
|
||||
const EHyperTwistViewerPlaybackMode Mode,
|
||||
const EHyperTwistViewerPlaybackDirection Direction
|
||||
)
|
||||
{
|
||||
FHyperTwistViewerDirectionState State;
|
||||
State.Mode = Mode;
|
||||
State.Direction = Direction;
|
||||
State.bIsContinuousPlayback = Mode == EHyperTwistViewerPlaybackMode::Playing;
|
||||
State.bIsPaused = Mode == EHyperTwistViewerPlaybackMode::Paused;
|
||||
State.bIsStepping = Mode == EHyperTwistViewerPlaybackMode::Stepping;
|
||||
return State;
|
||||
}
|
||||
|
||||
bool ArePlaybackStatesEquivalent(
|
||||
const FHyperTwistViewerPlaybackState& Left,
|
||||
const FHyperTwistViewerPlaybackState& Right
|
||||
)
|
||||
{
|
||||
return Left.Mode == Right.Mode
|
||||
&& Left.Direction == Right.Direction
|
||||
&& Left.CurrentMoveIndex == Right.CurrentMoveIndex
|
||||
&& Left.TotalMoveCount == Right.TotalMoveCount
|
||||
&& Left.CurrentTimeMs == Right.CurrentTimeMs
|
||||
&& Left.TotalTimeMs == Right.TotalTimeMs
|
||||
&& Left.InspectionTimeMs == Right.InspectionTimeMs
|
||||
&& Left.bAtStart == Right.bAtStart
|
||||
&& Left.bAtEnd == Right.bAtEnd
|
||||
&& Left.bSceneContextLoaded == Right.bSceneContextLoaded
|
||||
&& Left.bReplayLoaded == Right.bReplayLoaded
|
||||
&& Left.ReplayId == Right.ReplayId
|
||||
&& Left.PuzzleId == Right.PuzzleId
|
||||
&& Left.ReplayResult == Right.ReplayResult
|
||||
&& Left.StatusLabel == Right.StatusLabel;
|
||||
}
|
||||
|
||||
bool ArePositionSnapshotsEquivalent(
|
||||
const FHyperTwistViewerPositionSnapshot& Left,
|
||||
const FHyperTwistViewerPositionSnapshot& Right
|
||||
)
|
||||
{
|
||||
if (Left.CurrentTimeMs != Right.CurrentTimeMs
|
||||
|| Left.BaseCompletedMoveIndex != Right.BaseCompletedMoveIndex
|
||||
|| Left.BaseBoundaryTimeMs != Right.BaseBoundaryTimeMs
|
||||
|| Left.bAtBoundary != Right.bAtBoundary
|
||||
|| Left.bAtStart != Right.bAtStart
|
||||
|| Left.bAtEnd != Right.bAtEnd
|
||||
|| Left.InFlightMoves.Num() != Right.InFlightMoves.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32 ProgressIndex = 0; ProgressIndex < Left.InFlightMoves.Num(); ++ProgressIndex)
|
||||
{
|
||||
const FHyperTwistViewerInFlightMoveProgress& LeftProgress = Left.InFlightMoves[ProgressIndex];
|
||||
const FHyperTwistViewerInFlightMoveProgress& RightProgress = Right.InFlightMoves[ProgressIndex];
|
||||
if (LeftProgress.MoveIndex != RightProgress.MoveIndex
|
||||
|| LeftProgress.Direction != RightProgress.Direction
|
||||
|| LeftProgress.StartedAtMs != RightProgress.StartedAtMs
|
||||
|| LeftProgress.EndsAtMs != RightProgress.EndsAtMs
|
||||
|| !FMath::IsNearlyEqual(LeftProgress.Fraction, RightProgress.Fraction)
|
||||
|| LeftProgress.MoveEntry.DisplayLabel != RightProgress.MoveEntry.DisplayLabel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AreDirectionStatesEquivalent(
|
||||
const FHyperTwistViewerDirectionState& Left,
|
||||
const FHyperTwistViewerDirectionState& Right
|
||||
)
|
||||
{
|
||||
return Left.Mode == Right.Mode
|
||||
&& Left.Direction == Right.Direction
|
||||
&& Left.bIsContinuousPlayback == Right.bIsContinuousPlayback
|
||||
&& Left.bIsPaused == Right.bIsPaused
|
||||
&& Left.bIsStepping == Right.bIsStepping;
|
||||
}
|
||||
}
|
||||
|
||||
UHyperTwistPuzzleViewerComponent::UHyperTwistPuzzleViewerComponent()
|
||||
|
|
@ -169,6 +435,7 @@ UHyperTwistPuzzleViewerComponent::UHyperTwistPuzzleViewerComponent()
|
|||
|
||||
// Initialise playback state to a clean stopped-empty state.
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
PlaybackState.CurrentMoveIndex = INDEX_NONE;
|
||||
PlaybackState.TotalMoveCount = 0;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
|
|
@ -184,7 +451,7 @@ UHyperTwistPuzzleViewerComponent::UHyperTwistPuzzleViewerComponent()
|
|||
void UHyperTwistPuzzleViewerComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
// Nothing to drive on BeginPlay in Bound 1 — tick and animation are Bound 2.
|
||||
// Host-driven deterministic transport remains explicit in Bound 2.
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -193,29 +460,37 @@ void UHyperTwistPuzzleViewerComponent::BeginPlay()
|
|||
|
||||
void UHyperTwistPuzzleViewerComponent::LoadSceneContext(const FHyperTwistSimulationSceneContext& InContext)
|
||||
{
|
||||
const FHyperTwistViewerPlaybackState PreviousPlaybackState = PlaybackState;
|
||||
const FHyperTwistViewerPositionSnapshot PreviousPositionSnapshot = PositionSnapshot;
|
||||
const FHyperTwistViewerDirectionState PreviousDirectionState = DirectionState;
|
||||
|
||||
SceneContext = InContext;
|
||||
PlaybackState.bSceneContextLoaded = InContext.IsStructurallyValid();
|
||||
|
||||
// Reset playhead — loaded content has changed.
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
|
||||
PlaybackState.CurrentMoveIndex = MoveList.Num() > 0 ? 0 : INDEX_NONE;
|
||||
PlaybackState.TotalMoveCount = MoveList.Num();
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
|
||||
RebuildAndBroadcast();
|
||||
RebuildAndBroadcast(PreviousPlaybackState, PreviousPositionSnapshot, PreviousDirectionState);
|
||||
}
|
||||
|
||||
void UHyperTwistPuzzleViewerComponent::LoadMoveList(const TArray<FHyperTwistViewerMoveEntry>& InMoveList)
|
||||
{
|
||||
const FHyperTwistViewerPlaybackState PreviousPlaybackState = PlaybackState;
|
||||
const FHyperTwistViewerPositionSnapshot PreviousPositionSnapshot = PositionSnapshot;
|
||||
const FHyperTwistViewerDirectionState PreviousDirectionState = DirectionState;
|
||||
|
||||
MoveList = InMoveList;
|
||||
ReplayPacket = FHyperTwistReplayPacket();
|
||||
ReplaySummary = FHyperTwistDerivedReplaySummary();
|
||||
bReplayPacketLoaded = false;
|
||||
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
|
||||
PlaybackState.TotalMoveCount = MoveList.Num();
|
||||
PlaybackState.CurrentMoveIndex = MoveList.Num() > 0 ? 0 : INDEX_NONE;
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
|
||||
RebuildAndBroadcast();
|
||||
RebuildAndBroadcast(PreviousPlaybackState, PreviousPositionSnapshot, PreviousDirectionState);
|
||||
}
|
||||
|
||||
bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayPacket& InReplayPacket)
|
||||
|
|
@ -225,6 +500,10 @@ bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayP
|
|||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistViewerPlaybackState PreviousPlaybackState = PlaybackState;
|
||||
const FHyperTwistViewerPositionSnapshot PreviousPositionSnapshot = PositionSnapshot;
|
||||
const FHyperTwistViewerDirectionState PreviousDirectionState = DirectionState;
|
||||
|
||||
ReplayPacket = InReplayPacket;
|
||||
ReplaySummary = UHyperTwistReplayLibrary::DeriveReplaySummary(InReplayPacket);
|
||||
bReplayPacketLoaded = true;
|
||||
|
|
@ -242,10 +521,10 @@ bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayP
|
|||
}
|
||||
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
|
||||
PlaybackState.TotalMoveCount = MoveList.Num();
|
||||
PlaybackState.CurrentMoveIndex = MoveList.Num() > 0 ? 0 : INDEX_NONE;
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
|
||||
RebuildAndBroadcast();
|
||||
RebuildAndBroadcast(PreviousPlaybackState, PreviousPositionSnapshot, PreviousDirectionState);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -260,21 +539,42 @@ void UHyperTwistPuzzleViewerComponent::ApplyControlRequest(const FHyperTwistView
|
|||
return;
|
||||
}
|
||||
|
||||
const int32 LastIndex = MoveList.Num() - 1;
|
||||
const bool bHasMoves = MoveList.Num() > 0;
|
||||
const FHyperTwistViewerPlaybackState PreviousPlaybackState = PlaybackState;
|
||||
const FHyperTwistViewerPositionSnapshot PreviousPositionSnapshot = PositionSnapshot;
|
||||
const FHyperTwistViewerDirectionState PreviousDirectionState = DirectionState;
|
||||
const int32 PreviousTimeMs = PlaybackState.CurrentTimeMs;
|
||||
const int32 PreviousMoveIndex = PlaybackState.CurrentMoveIndex;
|
||||
const bool bHasMoves = MoveList.Num() > 0;
|
||||
const auto ResolveJumpMoveIndex = [this](const int32 NewTimeMs)
|
||||
{
|
||||
return HyperTwistPuzzleViewerComponentInternal::FindDisplayMoveIndexForTime(
|
||||
MoveBoundaryTimesMs,
|
||||
NewTimeMs,
|
||||
PlaybackState.TotalTimeMs
|
||||
);
|
||||
};
|
||||
FHyperTwistViewerJumpEvent JumpEvent;
|
||||
FHyperTwistViewerJumpEvent* JumpEventPtr = nullptr;
|
||||
|
||||
switch (Request.Action)
|
||||
{
|
||||
case EHyperTwistViewerControlAction::Play:
|
||||
// Only allow play when there are moves and we are not already at the end.
|
||||
if (bHasMoves && PlaybackState.Mode != EHyperTwistViewerPlaybackMode::Playing)
|
||||
{
|
||||
// If stopped at end, wrap back to start before playing.
|
||||
if (PlaybackState.bAtEnd && PlaybackState.Mode == EHyperTwistViewerPlaybackMode::Stopped)
|
||||
if (PlaybackState.TotalTimeMs > 0
|
||||
&& PlaybackState.CurrentTimeMs >= PlaybackState.TotalTimeMs
|
||||
&& PlaybackState.Mode != EHyperTwistViewerPlaybackMode::Playing)
|
||||
{
|
||||
PlaybackState.CurrentMoveIndex = 0;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
JumpEvent.JumpKind = EHyperTwistViewerJumpKind::PlayWrap;
|
||||
JumpEvent.PreviousTimeMs = PreviousTimeMs;
|
||||
JumpEvent.NewTimeMs = 0;
|
||||
JumpEvent.PreviousMoveIndex = PreviousMoveIndex;
|
||||
JumpEvent.NewMoveIndex = ResolveJumpMoveIndex(0);
|
||||
JumpEventPtr = &JumpEvent;
|
||||
}
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Playing;
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::Forward;
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -287,18 +587,38 @@ void UHyperTwistPuzzleViewerComponent::ApplyControlRequest(const FHyperTwistView
|
|||
|
||||
case EHyperTwistViewerControlAction::Stop:
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
|
||||
PlaybackState.CurrentMoveIndex = bHasMoves ? 0 : INDEX_NONE;
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
if (PreviousTimeMs != 0)
|
||||
{
|
||||
JumpEvent.JumpKind = EHyperTwistViewerJumpKind::Stop;
|
||||
JumpEvent.PreviousTimeMs = PreviousTimeMs;
|
||||
JumpEvent.NewTimeMs = 0;
|
||||
JumpEvent.PreviousMoveIndex = PreviousMoveIndex;
|
||||
JumpEvent.NewMoveIndex = bHasMoves ? 0 : INDEX_NONE;
|
||||
JumpEventPtr = &JumpEvent;
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistViewerControlAction::StepForward:
|
||||
if (bHasMoves)
|
||||
{
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stepping;
|
||||
if (PlaybackState.CurrentMoveIndex < LastIndex)
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::Forward;
|
||||
const int32 NewTimeMs = HyperTwistPuzzleViewerComponentInternal::ResolveNextMoveBoundaryTimeMs(
|
||||
MoveBoundaryTimesMs,
|
||||
PlaybackState.CurrentTimeMs
|
||||
);
|
||||
if (NewTimeMs != PlaybackState.CurrentTimeMs)
|
||||
{
|
||||
PlaybackState.CurrentMoveIndex++;
|
||||
PlaybackState.CurrentTimeMs = NewTimeMs;
|
||||
JumpEvent.JumpKind = EHyperTwistViewerJumpKind::StepForward;
|
||||
JumpEvent.PreviousTimeMs = PreviousTimeMs;
|
||||
JumpEvent.NewTimeMs = NewTimeMs;
|
||||
JumpEvent.PreviousMoveIndex = PreviousMoveIndex;
|
||||
JumpEvent.NewMoveIndex = ResolveJumpMoveIndex(NewTimeMs);
|
||||
JumpEventPtr = &JumpEvent;
|
||||
}
|
||||
// At end — remain at last index, mode stays Stepping (caller can check bAtEnd).
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -306,9 +626,20 @@ void UHyperTwistPuzzleViewerComponent::ApplyControlRequest(const FHyperTwistView
|
|||
if (bHasMoves)
|
||||
{
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stepping;
|
||||
if (PlaybackState.CurrentMoveIndex > 0)
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::Backward;
|
||||
const int32 NewTimeMs = HyperTwistPuzzleViewerComponentInternal::ResolvePreviousMoveBoundaryTimeMs(
|
||||
MoveBoundaryTimesMs,
|
||||
PlaybackState.CurrentTimeMs
|
||||
);
|
||||
if (NewTimeMs != PlaybackState.CurrentTimeMs)
|
||||
{
|
||||
PlaybackState.CurrentMoveIndex--;
|
||||
PlaybackState.CurrentTimeMs = NewTimeMs;
|
||||
JumpEvent.JumpKind = EHyperTwistViewerJumpKind::StepBack;
|
||||
JumpEvent.PreviousTimeMs = PreviousTimeMs;
|
||||
JumpEvent.NewTimeMs = NewTimeMs;
|
||||
JumpEvent.PreviousMoveIndex = PreviousMoveIndex;
|
||||
JumpEvent.NewMoveIndex = ResolveJumpMoveIndex(NewTimeMs);
|
||||
JumpEventPtr = &JumpEvent;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -317,10 +648,25 @@ void UHyperTwistPuzzleViewerComponent::ApplyControlRequest(const FHyperTwistView
|
|||
{
|
||||
if (bHasMoves)
|
||||
{
|
||||
// Clamp to valid range.
|
||||
const int32 ClampedIndex = FMath::Clamp(Request.SeekTargetIndex, 0, LastIndex);
|
||||
PlaybackState.CurrentMoveIndex = ClampedIndex;
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stepping;
|
||||
const int32 NewTimeMs = ResolveSeekTargetTimeMs(Request.SeekTargetIndex);
|
||||
if (NewTimeMs > PlaybackState.CurrentTimeMs)
|
||||
{
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::Forward;
|
||||
}
|
||||
else if (NewTimeMs < PlaybackState.CurrentTimeMs)
|
||||
{
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::Backward;
|
||||
}
|
||||
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stepping;
|
||||
PlaybackState.CurrentTimeMs = NewTimeMs;
|
||||
|
||||
JumpEvent.JumpKind = EHyperTwistViewerJumpKind::Seek;
|
||||
JumpEvent.PreviousTimeMs = PreviousTimeMs;
|
||||
JumpEvent.NewTimeMs = NewTimeMs;
|
||||
JumpEvent.PreviousMoveIndex = PreviousMoveIndex;
|
||||
JumpEvent.NewMoveIndex = ResolveJumpMoveIndex(NewTimeMs);
|
||||
JumpEventPtr = &JumpEvent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -329,7 +675,35 @@ void UHyperTwistPuzzleViewerComponent::ApplyControlRequest(const FHyperTwistView
|
|||
break;
|
||||
}
|
||||
|
||||
RebuildAndBroadcast();
|
||||
RebuildAndBroadcast(PreviousPlaybackState, PreviousPositionSnapshot, PreviousDirectionState, JumpEventPtr);
|
||||
}
|
||||
|
||||
bool UHyperTwistPuzzleViewerComponent::AdvancePlaybackByMs(const int32 DeltaTimeMs)
|
||||
{
|
||||
if (DeltaTimeMs <= 0 || PlaybackState.Mode != EHyperTwistViewerPlaybackMode::Playing || MoveList.Num() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistViewerPlaybackState PreviousPlaybackState = PlaybackState;
|
||||
const FHyperTwistViewerPositionSnapshot PreviousPositionSnapshot = PositionSnapshot;
|
||||
const FHyperTwistViewerDirectionState PreviousDirectionState = DirectionState;
|
||||
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::Forward;
|
||||
const int32 PreviousTimeMs = PlaybackState.CurrentTimeMs;
|
||||
PlaybackState.CurrentTimeMs = FMath::Clamp(
|
||||
PlaybackState.CurrentTimeMs + DeltaTimeMs,
|
||||
0,
|
||||
PlaybackState.TotalTimeMs
|
||||
);
|
||||
|
||||
if (PlaybackState.TotalTimeMs > 0 && PlaybackState.CurrentTimeMs >= PlaybackState.TotalTimeMs)
|
||||
{
|
||||
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Paused;
|
||||
}
|
||||
|
||||
RebuildAndBroadcast(PreviousPlaybackState, PreviousPositionSnapshot, PreviousDirectionState);
|
||||
return PlaybackState.CurrentTimeMs != PreviousTimeMs;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -349,10 +723,42 @@ FHyperTwistViewerMoveEntry UHyperTwistPuzzleViewerComponent::GetCurrentMoveEntry
|
|||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UHyperTwistPuzzleViewerComponent::RebuildAndBroadcast()
|
||||
void UHyperTwistPuzzleViewerComponent::RebuildTimelineCache()
|
||||
{
|
||||
MoveBoundaryTimesMs = HyperTwistPuzzleViewerComponentInternal::BuildMoveBoundaryTimesMs(MoveList);
|
||||
}
|
||||
|
||||
int32 UHyperTwistPuzzleViewerComponent::ResolveSeekTargetTimeMs(const int32 TargetIndex) const
|
||||
{
|
||||
if (MoveBoundaryTimesMs.Num() == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (TargetIndex <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (TargetIndex >= MoveBoundaryTimesMs.Num() - 1)
|
||||
{
|
||||
return PlaybackState.TotalTimeMs > 0 ? PlaybackState.TotalTimeMs : MoveBoundaryTimesMs.Last();
|
||||
}
|
||||
|
||||
return MoveBoundaryTimesMs[TargetIndex];
|
||||
}
|
||||
|
||||
void UHyperTwistPuzzleViewerComponent::RebuildAndBroadcast(
|
||||
const FHyperTwistViewerPlaybackState& PreviousPlaybackState,
|
||||
const FHyperTwistViewerPositionSnapshot& PreviousPositionSnapshot,
|
||||
const FHyperTwistViewerDirectionState& PreviousDirectionState,
|
||||
const FHyperTwistViewerJumpEvent* JumpEvent
|
||||
)
|
||||
{
|
||||
const bool bHasMoves = MoveList.Num() > 0;
|
||||
|
||||
RebuildTimelineCache();
|
||||
|
||||
PlaybackState.TotalMoveCount = MoveList.Num();
|
||||
PlaybackState.bSceneContextLoaded = SceneContext.IsStructurallyValid();
|
||||
PlaybackState.bReplayLoaded = bReplayPacketLoaded;
|
||||
|
|
@ -361,15 +767,21 @@ void UHyperTwistPuzzleViewerComponent::RebuildAndBroadcast()
|
|||
? ReplayPacket.PuzzleDefinition.PuzzleId
|
||||
: (SceneContext.IsStructurallyValid() ? SceneContext.PuzzleState.Definition.PuzzleId : FString());
|
||||
PlaybackState.ReplayResult = bReplayPacketLoaded ? ReplaySummary.Result : FString();
|
||||
PlaybackState.TotalTimeMs = bReplayPacketLoaded ? FMath::Max(0, ReplaySummary.TotalTimeMs) : 0;
|
||||
PlaybackState.TotalTimeMs = HyperTwistPuzzleViewerComponentInternal::ResolvePlaybackTerminalTimeMs(
|
||||
MoveBoundaryTimesMs,
|
||||
ReplaySummary,
|
||||
bReplayPacketLoaded
|
||||
);
|
||||
PlaybackState.InspectionTimeMs = bReplayPacketLoaded ? FMath::Max(0, ReplaySummary.InspectionTimeMs) : 0;
|
||||
PlaybackState.CurrentTimeMs = FMath::Clamp(PlaybackState.CurrentTimeMs, 0, PlaybackState.TotalTimeMs);
|
||||
|
||||
if (!bHasMoves)
|
||||
{
|
||||
PlaybackState.CurrentMoveIndex = INDEX_NONE;
|
||||
PlaybackState.bAtStart = true;
|
||||
PlaybackState.bAtEnd = true;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
PlaybackState.bAtStart = true;
|
||||
PlaybackState.bAtEnd = true;
|
||||
PlaybackState.CurrentTimeMs = 0;
|
||||
|
||||
if (PlaybackState.bReplayLoaded)
|
||||
{
|
||||
|
|
@ -392,13 +804,24 @@ void UHyperTwistPuzzleViewerComponent::RebuildAndBroadcast()
|
|||
}
|
||||
else
|
||||
{
|
||||
const int32 LastIndex = MoveList.Num() - 1;
|
||||
PlaybackState.CurrentMoveIndex = FMath::Clamp(PlaybackState.CurrentMoveIndex, 0, LastIndex);
|
||||
PlaybackState.bAtStart = (PlaybackState.CurrentMoveIndex <= 0);
|
||||
PlaybackState.bAtEnd = (PlaybackState.CurrentMoveIndex >= LastIndex);
|
||||
PlaybackState.CurrentTimeMs = bReplayPacketLoaded
|
||||
? HyperTwistPuzzleViewerComponentInternal::DeriveCurrentTimeMs(MoveList, PlaybackState, ReplaySummary)
|
||||
: 0;
|
||||
DirectionState = HyperTwistPuzzleViewerComponentInternal::BuildDirectionState(
|
||||
PlaybackState.Mode,
|
||||
PlaybackState.Direction
|
||||
);
|
||||
PositionSnapshot = HyperTwistPuzzleViewerComponentInternal::BuildPositionSnapshot(
|
||||
MoveList,
|
||||
MoveBoundaryTimesMs,
|
||||
PlaybackState.CurrentTimeMs,
|
||||
PlaybackState.TotalTimeMs,
|
||||
PlaybackState.Direction
|
||||
);
|
||||
PlaybackState.CurrentMoveIndex = HyperTwistPuzzleViewerComponentInternal::FindDisplayMoveIndexForTime(
|
||||
MoveBoundaryTimesMs,
|
||||
PlaybackState.CurrentTimeMs,
|
||||
PlaybackState.TotalTimeMs
|
||||
);
|
||||
PlaybackState.bAtStart = PositionSnapshot.bAtStart;
|
||||
PlaybackState.bAtEnd = PositionSnapshot.bAtEnd;
|
||||
PlaybackState.StatusLabel = UHyperTwistViewerLibrary::GetControlBarReadout(PlaybackState, GetCurrentMoveEntry());
|
||||
if (PlaybackState.bReplayLoaded && !PlaybackState.ReplayResult.IsEmpty())
|
||||
{
|
||||
|
|
@ -406,5 +829,38 @@ void UHyperTwistPuzzleViewerComponent::RebuildAndBroadcast()
|
|||
}
|
||||
}
|
||||
|
||||
if (!bHasMoves)
|
||||
{
|
||||
DirectionState = HyperTwistPuzzleViewerComponentInternal::BuildDirectionState(
|
||||
PlaybackState.Mode,
|
||||
PlaybackState.Direction
|
||||
);
|
||||
PositionSnapshot = HyperTwistPuzzleViewerComponentInternal::BuildPositionSnapshot(
|
||||
MoveList,
|
||||
MoveBoundaryTimesMs,
|
||||
PlaybackState.CurrentTimeMs,
|
||||
PlaybackState.TotalTimeMs,
|
||||
PlaybackState.Direction
|
||||
);
|
||||
}
|
||||
|
||||
OnPlaybackStateChanged.Broadcast(PlaybackState);
|
||||
|
||||
if (!HyperTwistPuzzleViewerComponentInternal::ArePositionSnapshotsEquivalent(PreviousPositionSnapshot, PositionSnapshot))
|
||||
{
|
||||
OnPositionSnapshotUpdated.Broadcast(PositionSnapshot);
|
||||
OnPositionSnapshotUpdatedNative.Broadcast(PositionSnapshot);
|
||||
}
|
||||
|
||||
if (!HyperTwistPuzzleViewerComponentInternal::AreDirectionStatesEquivalent(PreviousDirectionState, DirectionState))
|
||||
{
|
||||
OnDirectionStateChanged.Broadcast(DirectionState);
|
||||
OnDirectionStateChangedNative.Broadcast(DirectionState);
|
||||
}
|
||||
|
||||
if (JumpEvent != nullptr && JumpEvent->IsStructurallyValid())
|
||||
{
|
||||
OnPlaybackJumped.Broadcast(*JumpEvent);
|
||||
OnPlaybackJumpedNative.Broadcast(*JumpEvent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,44 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
|
|||
const FHyperTwistViewerPlaybackState&, NewState
|
||||
);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
|
||||
FOnViewerPositionSnapshotUpdated,
|
||||
const FHyperTwistViewerPositionSnapshot&, NewSnapshot
|
||||
);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
|
||||
FOnViewerDirectionStateChanged,
|
||||
const FHyperTwistViewerDirectionState&, NewDirectionState
|
||||
);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
|
||||
FOnViewerPlaybackJumped,
|
||||
const FHyperTwistViewerJumpEvent&, JumpEvent
|
||||
);
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(
|
||||
FOnViewerPositionSnapshotUpdatedNative,
|
||||
const FHyperTwistViewerPositionSnapshot&
|
||||
);
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(
|
||||
FOnViewerDirectionStateChangedNative,
|
||||
const FHyperTwistViewerDirectionState&
|
||||
);
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(
|
||||
FOnViewerPlaybackJumpedNative,
|
||||
const FHyperTwistViewerJumpEvent&
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UHyperTwistPuzzleViewerComponent
|
||||
//
|
||||
// Holds a loaded scene context and a flat move list, and manages a pure
|
||||
// state-machine playback cursor over that list.
|
||||
//
|
||||
// Bound 1 scope: state management and delegate broadcasting only.
|
||||
// Tick-based animation is Bound 2. Adapter lifecycle is Bound 3.
|
||||
// Bound 2 scope: observer channels and deterministic cursor/timeline transport.
|
||||
// Adapter lifecycle is Bound 3.
|
||||
// Viewport rendering is Bound 4.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -64,10 +94,16 @@ public:
|
|||
// ------------------------------------------------------------------
|
||||
|
||||
// Apply a control request. Advances, retreats, or repositions the playhead.
|
||||
// Bound 1: pure state-machine. No animation is driven here.
|
||||
// Bound 2: state-machine plus direction/jump observer updates.
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Viewer")
|
||||
void ApplyControlRequest(const FHyperTwistViewerControlRequest& Request);
|
||||
|
||||
// Advance the playing cursor by a deterministic timeline delta.
|
||||
// Used by transport automation and host-driven playback without widening
|
||||
// into renderer ownership.
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Viewer")
|
||||
bool AdvancePlaybackByMs(int32 DeltaTimeMs);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Readout accessors
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -90,6 +126,12 @@ public:
|
|||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistDerivedReplaySummary GetReplaySummary() const { return ReplaySummary; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistViewerPositionSnapshot GetPositionSnapshot() const { return PositionSnapshot; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistViewerDirectionState GetDirectionState() const { return DirectionState; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Delegates
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -97,6 +139,19 @@ public:
|
|||
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Viewer")
|
||||
FOnViewerPlaybackStateChanged OnPlaybackStateChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Viewer")
|
||||
FOnViewerPositionSnapshotUpdated OnPositionSnapshotUpdated;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Viewer")
|
||||
FOnViewerDirectionStateChanged OnDirectionStateChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Viewer")
|
||||
FOnViewerPlaybackJumped OnPlaybackJumped;
|
||||
|
||||
FOnViewerPositionSnapshotUpdatedNative OnPositionSnapshotUpdatedNative;
|
||||
FOnViewerDirectionStateChangedNative OnDirectionStateChangedNative;
|
||||
FOnViewerPlaybackJumpedNative OnPlaybackJumpedNative;
|
||||
|
||||
protected:
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
|
@ -112,6 +167,12 @@ private:
|
|||
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistViewerPlaybackState PlaybackState;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistViewerPositionSnapshot PositionSnapshot;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistViewerDirectionState DirectionState;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistReplayPacket ReplayPacket;
|
||||
|
||||
|
|
@ -121,6 +182,18 @@ private:
|
|||
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
|
||||
bool bReplayPacketLoaded = false;
|
||||
|
||||
// Rebuild the playback state derived fields and broadcast the delegate.
|
||||
void RebuildAndBroadcast();
|
||||
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
|
||||
TArray<int32> MoveBoundaryTimesMs;
|
||||
|
||||
void RebuildTimelineCache();
|
||||
|
||||
int32 ResolveSeekTargetTimeMs(int32 TargetIndex) const;
|
||||
|
||||
// Rebuild the playback state derived fields and broadcast split observers.
|
||||
void RebuildAndBroadcast(
|
||||
const FHyperTwistViewerPlaybackState& PreviousPlaybackState,
|
||||
const FHyperTwistViewerPositionSnapshot& PreviousPositionSnapshot,
|
||||
const FHyperTwistViewerDirectionState& PreviousDirectionState,
|
||||
const FHyperTwistViewerJumpEvent* JumpEvent = nullptr
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,6 +24,24 @@ enum class EHyperTwistViewerPlaybackMode : uint8
|
|||
Stepping UMETA(DisplayName = "Stepping")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistViewerPlaybackDirection : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "None"),
|
||||
Forward UMETA(DisplayName = "Forward"),
|
||||
Backward UMETA(DisplayName = "Backward")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistViewerJumpKind : uint8
|
||||
{
|
||||
Seek UMETA(DisplayName = "Seek"),
|
||||
StepForward UMETA(DisplayName = "Step Forward"),
|
||||
StepBack UMETA(DisplayName = "Step Back"),
|
||||
Stop UMETA(DisplayName = "Stop"),
|
||||
PlayWrap UMETA(DisplayName = "Play Wrap")
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Control action — what the caller wants the viewer to do
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -82,6 +100,9 @@ struct UNREALHYPERTWIST_API FHyperTwistViewerPlaybackState
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
EHyperTwistViewerPlaybackMode Mode = EHyperTwistViewerPlaybackMode::Stopped;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
EHyperTwistViewerPlaybackDirection Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
|
||||
// Zero-based index of the move currently at the playhead.
|
||||
// INDEX_NONE (-1) when the move list is empty.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
|
|
@ -184,6 +205,143 @@ struct UNREALHYPERTWIST_API FHyperTwistViewerControlRequest
|
|||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct UNREALHYPERTWIST_API FHyperTwistViewerInFlightMoveProgress
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 MoveIndex = INDEX_NONE;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
FHyperTwistViewerMoveEntry MoveEntry;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
EHyperTwistViewerPlaybackDirection Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
float Fraction = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 StartedAtMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 EndsAtMs = 0;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return MoveIndex >= 0
|
||||
&& MoveEntry.IsStructurallyValid()
|
||||
&& FMath::IsFinite(Fraction)
|
||||
&& Fraction >= 0.0f
|
||||
&& Fraction <= 1.0f
|
||||
&& StartedAtMs >= 0
|
||||
&& EndsAtMs >= StartedAtMs;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct UNREALHYPERTWIST_API FHyperTwistViewerPositionSnapshot
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 CurrentTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 BaseCompletedMoveIndex = INDEX_NONE;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 BaseBoundaryTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
TArray<FHyperTwistViewerInFlightMoveProgress> InFlightMoves;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
bool bAtBoundary = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
bool bAtStart = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
bool bAtEnd = false;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
if (CurrentTimeMs < 0 || BaseBoundaryTimeMs < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BaseCompletedMoveIndex < INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FHyperTwistViewerInFlightMoveProgress& Progress : InFlightMoves)
|
||||
{
|
||||
if (!Progress.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct UNREALHYPERTWIST_API FHyperTwistViewerDirectionState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
EHyperTwistViewerPlaybackMode Mode = EHyperTwistViewerPlaybackMode::Stopped;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
EHyperTwistViewerPlaybackDirection Direction = EHyperTwistViewerPlaybackDirection::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
bool bIsContinuousPlayback = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
bool bIsPaused = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
bool bIsStepping = false;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct UNREALHYPERTWIST_API FHyperTwistViewerJumpEvent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
EHyperTwistViewerJumpKind JumpKind = EHyperTwistViewerJumpKind::Seek;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 PreviousTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 NewTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 PreviousMoveIndex = INDEX_NONE;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
|
||||
int32 NewMoveIndex = INDEX_NONE;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return PreviousTimeMs >= 0 && NewTimeMs >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runtime surface - minimal compact replay-player bundle exposed through the
|
||||
// training runtime library without moving shell ownership out of simulation.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
// 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 HyperTwistTwistyBound2TimelineTestInternal
|
||||
{
|
||||
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-bound2");
|
||||
Snapshot.State = SampleState;
|
||||
Snapshot.DerivedHash = TEXT("snapshot-bound2-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-bound2-001");
|
||||
ReplayPacket.SessionId = TEXT("session-bound2-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(
|
||||
FHyperTwistTwistyBound2ObserverModelTest,
|
||||
"HyperTwist.CleanRoom.TwistyJs.Bound2.ObserverModel",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistTwistyBound2ObserverModelTest::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 2 observer test."),
|
||||
Viewer->LoadReplayPacket(HyperTwistTwistyBound2TimelineTestInternal::MakeReplayPacket())
|
||||
);
|
||||
|
||||
TArray<FHyperTwistViewerPositionSnapshot> PositionEvents;
|
||||
TArray<FHyperTwistViewerDirectionState> DirectionEvents;
|
||||
TArray<FHyperTwistViewerJumpEvent> JumpEvents;
|
||||
|
||||
Viewer->OnPositionSnapshotUpdatedNative.AddLambda(
|
||||
[&PositionEvents](const FHyperTwistViewerPositionSnapshot& Snapshot)
|
||||
{
|
||||
PositionEvents.Add(Snapshot);
|
||||
}
|
||||
);
|
||||
Viewer->OnDirectionStateChangedNative.AddLambda(
|
||||
[&DirectionEvents](const FHyperTwistViewerDirectionState& State)
|
||||
{
|
||||
DirectionEvents.Add(State);
|
||||
}
|
||||
);
|
||||
Viewer->OnPlaybackJumpedNative.AddLambda(
|
||||
[&JumpEvents](const FHyperTwistViewerJumpEvent& JumpEvent)
|
||||
{
|
||||
JumpEvents.Add(JumpEvent);
|
||||
}
|
||||
);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest());
|
||||
TestEqual(TEXT("Play should emit one direction-state event."), DirectionEvents.Num(), 1);
|
||||
TestEqual(
|
||||
TEXT("Play should switch the direction-state channel to forward playback."),
|
||||
DirectionEvents.Last().Direction,
|
||||
EHyperTwistViewerPlaybackDirection::Forward
|
||||
);
|
||||
TestTrue(TEXT("Play should mark continuous playback active."), DirectionEvents.Last().bIsContinuousPlayback);
|
||||
TestEqual(TEXT("Play should not emit a jump event by itself."), JumpEvents.Num(), 0);
|
||||
|
||||
TestTrue(TEXT("Advance should move the playing cursor."), Viewer->AdvancePlaybackByMs(100));
|
||||
TestEqual(TEXT("Advancing the playing cursor should emit one position update."), PositionEvents.Num(), 1);
|
||||
TestEqual(TEXT("The first position update should advance to 100ms."), PositionEvents.Last().CurrentTimeMs, 100);
|
||||
TestEqual(TEXT("The first position update should keep the base boundary at the start."), PositionEvents.Last().BaseBoundaryTimeMs, 0);
|
||||
TestEqual(TEXT("The first position update should expose one in-flight move."), PositionEvents.Last().InFlightMoves.Num(), 1);
|
||||
TestEqual(TEXT("The in-flight move should be the first move."), PositionEvents.Last().InFlightMoves[0].MoveIndex, 0);
|
||||
TestTrue(
|
||||
TEXT("The first in-flight move should be halfway complete at 100ms of a 200ms segment."),
|
||||
FMath::IsNearlyEqual(PositionEvents.Last().InFlightMoves[0].Fraction, 0.5f)
|
||||
);
|
||||
TestEqual(TEXT("Continuous position updates should stay off the jump channel."), JumpEvents.Num(), 0);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePauseRequest());
|
||||
TestEqual(TEXT("Pause should emit a second direction-state event."), DirectionEvents.Num(), 2);
|
||||
TestTrue(TEXT("Pause should mark the direction-state channel paused."), DirectionEvents.Last().bIsPaused);
|
||||
|
||||
TestFalse(TEXT("Advancing while paused should not move the cursor."), Viewer->AdvancePlaybackByMs(50));
|
||||
TestEqual(TEXT("Advancing while paused should not emit another position update."), PositionEvents.Num(), 1);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeStepBackRequest());
|
||||
TestEqual(TEXT("Step back should emit a third direction-state event."), DirectionEvents.Num(), 3);
|
||||
TestEqual(
|
||||
TEXT("Step back should switch the direction-state channel to backward stepping."),
|
||||
DirectionEvents.Last().Direction,
|
||||
EHyperTwistViewerPlaybackDirection::Backward
|
||||
);
|
||||
TestTrue(TEXT("Step back should mark the direction-state channel stepping."), DirectionEvents.Last().bIsStepping);
|
||||
TestEqual(TEXT("Step back should emit one jump event."), JumpEvents.Num(), 1);
|
||||
TestEqual(TEXT("The first jump event should record step-back semantics."), JumpEvents.Last().JumpKind, EHyperTwistViewerJumpKind::StepBack);
|
||||
TestEqual(TEXT("Step back should jump from 100ms to 0ms."), JumpEvents.Last().PreviousTimeMs, 100);
|
||||
TestEqual(TEXT("Step back should land at the start boundary."), JumpEvents.Last().NewTimeMs, 0);
|
||||
TestEqual(TEXT("Step back should also emit a second position update."), PositionEvents.Num(), 2);
|
||||
TestTrue(TEXT("The step-back position update should be a boundary snapshot."), PositionEvents.Last().bAtBoundary);
|
||||
TestEqual(TEXT("The step-back position update should clear in-flight progress."), PositionEvents.Last().InFlightMoves.Num(), 0);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeSeekRequest(2));
|
||||
TestEqual(TEXT("Seek should emit a second jump event."), JumpEvents.Num(), 2);
|
||||
TestEqual(TEXT("The second jump event should record seek semantics."), JumpEvents.Last().JumpKind, EHyperTwistViewerJumpKind::Seek);
|
||||
TestEqual(TEXT("Seeking to the last move index should jump to the terminal replay time."), JumpEvents.Last().NewTimeMs, 1000);
|
||||
TestEqual(TEXT("Seek should emit a fourth direction-state event."), DirectionEvents.Num(), 4);
|
||||
TestEqual(TEXT("Seek should restore forward direction when seeking later."), DirectionEvents.Last().Direction, EHyperTwistViewerPlaybackDirection::Forward);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistTwistyBound2CursorTimelineTest,
|
||||
"HyperTwist.CleanRoom.TwistyJs.Bound2.CursorTimeline",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistTwistyBound2CursorTimelineTest::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 2 cursor test."),
|
||||
Viewer->LoadReplayPacket(HyperTwistTwistyBound2TimelineTestInternal::MakeReplayPacket())
|
||||
);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest());
|
||||
TestTrue(TEXT("Cursor advance should move the playhead during continuous playback."), Viewer->AdvancePlaybackByMs(100));
|
||||
|
||||
FHyperTwistViewerPositionSnapshot Snapshot = Viewer->GetPositionSnapshot();
|
||||
TestEqual(TEXT("A 100ms cursor advance should expose the correct current time."), Snapshot.CurrentTimeMs, 100);
|
||||
TestEqual(TEXT("The base boundary should stay at the start before the first move completes."), Snapshot.BaseCompletedMoveIndex, INDEX_NONE);
|
||||
TestEqual(TEXT("The first partial segment should expose one in-flight move."), Snapshot.InFlightMoves.Num(), 1);
|
||||
TestEqual(TEXT("The in-flight move should be the first move."), Snapshot.InFlightMoves[0].MoveIndex, 0);
|
||||
TestTrue(TEXT("The first partial segment should report a 0.5 fraction."), FMath::IsNearlyEqual(Snapshot.InFlightMoves[0].Fraction, 0.5f));
|
||||
|
||||
TestTrue(TEXT("A second cursor advance should continue through the timeline."), Viewer->AdvancePlaybackByMs(150));
|
||||
Snapshot = Viewer->GetPositionSnapshot();
|
||||
TestEqual(TEXT("The cursor should now be at 250ms."), Snapshot.CurrentTimeMs, 250);
|
||||
TestEqual(TEXT("The base completed move should now be move 0."), Snapshot.BaseCompletedMoveIndex, 0);
|
||||
TestEqual(TEXT("The cursor should now interpolate the second move."), Snapshot.InFlightMoves[0].MoveIndex, 1);
|
||||
TestTrue(TEXT("The second move should be 25% complete at 250ms."), FMath::IsNearlyEqual(Snapshot.InFlightMoves[0].Fraction, 0.25f));
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePauseRequest());
|
||||
const int32 PausedTimeMs = Viewer->GetPlaybackState().CurrentTimeMs;
|
||||
TestFalse(TEXT("The cursor must not advance while paused."), Viewer->AdvancePlaybackByMs(200));
|
||||
TestEqual(TEXT("Pause must preserve the current timeline position."), Viewer->GetPlaybackState().CurrentTimeMs, PausedTimeMs);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeStepForwardRequest());
|
||||
Snapshot = Viewer->GetPositionSnapshot();
|
||||
TestEqual(TEXT("Step forward should land on the next move boundary, not a partial continuous time."), Snapshot.CurrentTimeMs, 400);
|
||||
TestTrue(TEXT("A move-boundary step should land on a boundary snapshot."), Snapshot.bAtBoundary);
|
||||
TestEqual(TEXT("Step forward should expose no in-flight move at the boundary."), Snapshot.InFlightMoves.Num(), 0);
|
||||
TestEqual(TEXT("The base completed move should now be move 1."), Snapshot.BaseCompletedMoveIndex, 1);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeStepBackRequest());
|
||||
Snapshot = Viewer->GetPositionSnapshot();
|
||||
TestEqual(TEXT("Step back should return to the previous move boundary."), Snapshot.CurrentTimeMs, 200);
|
||||
TestEqual(TEXT("Step back should restore move 0 as the completed boundary anchor."), Snapshot.BaseCompletedMoveIndex, 0);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest());
|
||||
TestTrue(TEXT("Resume should continue from the paused move boundary."), Viewer->AdvancePlaybackByMs(100));
|
||||
TestEqual(TEXT("Resume should continue from the paused boundary instead of restarting."), Viewer->GetPlaybackState().CurrentTimeMs, 300);
|
||||
|
||||
Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeSeekRequest(2));
|
||||
Snapshot = Viewer->GetPositionSnapshot();
|
||||
TestEqual(TEXT("Seeking to the last move index should land on the terminal replay time."), Snapshot.CurrentTimeMs, 1000);
|
||||
TestTrue(TEXT("Seeking to the last move index should land at the end state."), Snapshot.bAtEnd);
|
||||
TestEqual(TEXT("The final completed boundary anchor should remain the last move."), Snapshot.BaseCompletedMoveIndex, 2);
|
||||
TestEqual(TEXT("The scrubber should remain coherent at the terminal end state."), UHyperTwistViewerLibrary::GetScrubberNormalizedPosition(Viewer->GetPlaybackState()), 1.0f);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_AUTOMATION_TESTS
|
||||
Loading…
Add table
Reference in a new issue