Implement Bound 3 smart-device bridge persistence
This commit is contained in:
parent
feff696f8c
commit
14d25e314e
7 changed files with 2260 additions and 2 deletions
|
|
@ -119,6 +119,16 @@ namespace HyperTwistTrainingPersistenceLibraryInternal
|
|||
{
|
||||
ConsiderLatestUtc(LatestUtc, HistoryEntry.EventAtUtc);
|
||||
}
|
||||
for (const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : RepositoryState.StoredSmartDeviceSessions)
|
||||
{
|
||||
ConsiderLatestUtc(LatestUtc, SessionRecord.StartedAtUtc);
|
||||
ConsiderLatestUtc(LatestUtc, SessionRecord.LastUpdatedAtUtc);
|
||||
ConsiderLatestUtc(LatestUtc, SessionRecord.EndedAtUtc);
|
||||
}
|
||||
for (const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame : RepositoryState.StoredSmartDeviceEventFrames)
|
||||
{
|
||||
ConsiderLatestUtc(LatestUtc, EventFrame.OccurredAtUtc);
|
||||
}
|
||||
|
||||
return LatestUtc.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : LatestUtc;
|
||||
}
|
||||
|
|
@ -191,6 +201,20 @@ namespace HyperTwistTrainingPersistenceLibraryInternal
|
|||
OutUserIds.Add(HistoryEntry.UserId);
|
||||
}
|
||||
}
|
||||
for (const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : RepositoryState.StoredSmartDeviceSessions)
|
||||
{
|
||||
if (!SessionRecord.UserId.IsEmpty())
|
||||
{
|
||||
OutUserIds.Add(SessionRecord.UserId);
|
||||
}
|
||||
}
|
||||
for (const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame : RepositoryState.StoredSmartDeviceEventFrames)
|
||||
{
|
||||
if (!EventFrame.UserId.IsEmpty())
|
||||
{
|
||||
OutUserIds.Add(EventFrame.UserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString MakeQueueVerificationKey(const FString& UserId, const FString& QueueId)
|
||||
|
|
@ -425,6 +449,9 @@ UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
|
|||
Verification.ExpectedCoachSessionQueueCount = ExpectedRepositoryState.StoredCoachSessionQueues.Num();
|
||||
Verification.ExpectedCoachSessionQueueHistoryCount =
|
||||
ExpectedRepositoryState.StoredCoachSessionQueueHistory.Num();
|
||||
Verification.ExpectedSmartDeviceSessionCount = ExpectedRepositoryState.StoredSmartDeviceSessions.Num();
|
||||
Verification.ExpectedSmartDeviceEventFrameCount =
|
||||
ExpectedRepositoryState.StoredSmartDeviceEventFrames.Num();
|
||||
Verification.ExpectedIntegrityReport =
|
||||
UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(ExpectedRepositoryState);
|
||||
|
||||
|
|
@ -471,6 +498,9 @@ UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
|
|||
Verification.LoadedCoachSessionQueueCount = LoadedRepositoryState.StoredCoachSessionQueues.Num();
|
||||
Verification.LoadedCoachSessionQueueHistoryCount =
|
||||
LoadedRepositoryState.StoredCoachSessionQueueHistory.Num();
|
||||
Verification.LoadedSmartDeviceSessionCount = LoadedRepositoryState.StoredSmartDeviceSessions.Num();
|
||||
Verification.LoadedSmartDeviceEventFrameCount =
|
||||
LoadedRepositoryState.StoredSmartDeviceEventFrames.Num();
|
||||
Verification.LoadedIntegrityReport =
|
||||
UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(LoadedRepositoryState);
|
||||
|
||||
|
|
@ -484,7 +514,10 @@ UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
|
|||
&& Verification.ExpectedCoachActionPlanCount == Verification.LoadedCoachActionPlanCount
|
||||
&& Verification.ExpectedCoachSessionQueueCount == Verification.LoadedCoachSessionQueueCount
|
||||
&& Verification.ExpectedCoachSessionQueueHistoryCount
|
||||
== Verification.LoadedCoachSessionQueueHistoryCount;
|
||||
== Verification.LoadedCoachSessionQueueHistoryCount
|
||||
&& Verification.ExpectedSmartDeviceSessionCount == Verification.LoadedSmartDeviceSessionCount
|
||||
&& Verification.ExpectedSmartDeviceEventFrameCount
|
||||
== Verification.LoadedSmartDeviceEventFrameCount;
|
||||
|
||||
if (!Verification.bRepositoryCountsMatch)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -57,6 +57,279 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
|
|||
return ReferenceUtc.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : ReferenceUtc;
|
||||
}
|
||||
|
||||
TArray<FString> NormalizeNonEmptyUniqueStrings(const TArray<FString>& Values)
|
||||
{
|
||||
TArray<FString> Normalized;
|
||||
for (const FString& Value : Values)
|
||||
{
|
||||
if (!Value.IsEmpty())
|
||||
{
|
||||
Normalized.AddUnique(Value);
|
||||
}
|
||||
}
|
||||
|
||||
Normalized.Sort();
|
||||
return Normalized;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceIdentity NormalizeSmartDeviceIdentity(
|
||||
const FHyperTwistTrainingSmartDeviceIdentity& DeviceIdentity
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceIdentity Normalized = DeviceIdentity;
|
||||
if (Normalized.DeviceName.IsEmpty())
|
||||
{
|
||||
Normalized.DeviceName = Normalized.DeviceId;
|
||||
}
|
||||
|
||||
Normalized.CapabilityTags = NormalizeNonEmptyUniqueStrings(Normalized.CapabilityTags);
|
||||
return Normalized;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingSplitCapture> NormalizeSmartDeviceSplitCaptures(
|
||||
const TArray<FHyperTwistTrainingSplitCapture>& SplitCaptures
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingSplitCapture> Normalized;
|
||||
TSet<FString> SeenKeys;
|
||||
for (FHyperTwistTrainingSplitCapture SplitCapture : SplitCaptures)
|
||||
{
|
||||
if (!SplitCapture.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FString SplitKey = !SplitCapture.PhaseId.IsEmpty()
|
||||
? SplitCapture.PhaseId
|
||||
: FString::Printf(TEXT("timestamp-%d"), SplitCapture.TimestampMs);
|
||||
if (SeenKeys.Contains(SplitKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SplitCapture.TimestampMs = FMath::Max(0, SplitCapture.TimestampMs);
|
||||
if (SplitCapture.Label.IsEmpty())
|
||||
{
|
||||
SplitCapture.Label = SplitCapture.PhaseId;
|
||||
}
|
||||
|
||||
SeenKeys.Add(SplitKey);
|
||||
Normalized.Add(SplitCapture);
|
||||
}
|
||||
|
||||
Normalized.Sort([](
|
||||
const FHyperTwistTrainingSplitCapture& Left,
|
||||
const FHyperTwistTrainingSplitCapture& Right
|
||||
)
|
||||
{
|
||||
if (Left.TimestampMs != Right.TimestampMs)
|
||||
{
|
||||
return Left.TimestampMs < Right.TimestampMs;
|
||||
}
|
||||
|
||||
return Left.PhaseId < Right.PhaseId;
|
||||
});
|
||||
return Normalized;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame NormalizeSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceEventFrame Normalized = EventFrame;
|
||||
Normalized.DeviceIdentity = NormalizeSmartDeviceIdentity(Normalized.DeviceIdentity);
|
||||
if (Normalized.EventId.IsEmpty() && Normalized.SequenceNumber >= 0)
|
||||
{
|
||||
Normalized.EventId = FString::Printf(TEXT("sequence-%lld"), Normalized.SequenceNumber);
|
||||
}
|
||||
|
||||
Normalized.OccurredAtUtc = ResolveReferenceUtc(Normalized.OccurredAtUtc);
|
||||
Normalized.TotalTimeMs = FMath::Max(0, Normalized.TotalTimeMs);
|
||||
Normalized.RawSolveTimeMs = FMath::Max(0, Normalized.RawSolveTimeMs);
|
||||
Normalized.InspectionElapsedMs = FMath::Max(0, Normalized.InspectionElapsedMs);
|
||||
Normalized.MistakeCount = FMath::Max(0, Normalized.MistakeCount);
|
||||
if (Normalized.RawSolveTimeMs == 0 && Normalized.TotalTimeMs > 0)
|
||||
{
|
||||
Normalized.RawSolveTimeMs = Normalized.TotalTimeMs;
|
||||
}
|
||||
|
||||
Normalized.SplitCaptures = NormalizeSmartDeviceSplitCaptures(Normalized.SplitCaptures);
|
||||
return Normalized;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord NormalizeSmartDeviceSessionRecord(
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord Normalized = SessionRecord;
|
||||
Normalized.DeviceIdentity = NormalizeSmartDeviceIdentity(Normalized.DeviceIdentity);
|
||||
Normalized.AcceptedEventCount = FMath::Max(0, Normalized.AcceptedEventCount);
|
||||
Normalized.DuplicateEventCount = FMath::Max(0, Normalized.DuplicateEventCount);
|
||||
Normalized.LinkedAttemptIds = NormalizeNonEmptyUniqueStrings(Normalized.LinkedAttemptIds);
|
||||
if (!Normalized.LastLinkedAttemptId.IsEmpty())
|
||||
{
|
||||
Normalized.LinkedAttemptIds.AddUnique(Normalized.LastLinkedAttemptId);
|
||||
Normalized.LinkedAttemptIds.Sort();
|
||||
}
|
||||
|
||||
Normalized.LinkedAttemptCount = Normalized.LinkedAttemptIds.Num();
|
||||
if (Normalized.LastUpdatedAtUtc.IsEmpty())
|
||||
{
|
||||
Normalized.LastUpdatedAtUtc = !Normalized.EndedAtUtc.IsEmpty()
|
||||
? Normalized.EndedAtUtc
|
||||
: Normalized.StartedAtUtc;
|
||||
}
|
||||
|
||||
return Normalized;
|
||||
}
|
||||
|
||||
FString BuildSmartDeviceEventKey(const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame)
|
||||
{
|
||||
return EventFrame.BuildEventKey();
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingSmartDeviceSessionRecord> SortSmartDeviceSessionsByUpdateUtc(
|
||||
const TArray<FHyperTwistTrainingSmartDeviceSessionRecord>& SessionRecords
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingSmartDeviceSessionRecord> Sorted = SessionRecords;
|
||||
Sorted.Sort([](
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord& Left,
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord& Right
|
||||
)
|
||||
{
|
||||
if (Left.UserId != Right.UserId)
|
||||
{
|
||||
return Left.UserId < Right.UserId;
|
||||
}
|
||||
|
||||
const FString LeftSortUtc = !Left.LastUpdatedAtUtc.IsEmpty() ? Left.LastUpdatedAtUtc : Left.StartedAtUtc;
|
||||
const FString RightSortUtc = !Right.LastUpdatedAtUtc.IsEmpty() ? Right.LastUpdatedAtUtc : Right.StartedAtUtc;
|
||||
const int32 UtcCompare = CompareUtcStrings(LeftSortUtc, RightSortUtc);
|
||||
if (UtcCompare != 0)
|
||||
{
|
||||
return UtcCompare > 0;
|
||||
}
|
||||
|
||||
return Left.DeviceSessionId < Right.DeviceSessionId;
|
||||
});
|
||||
return Sorted;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingSmartDeviceEventFrame> SortSmartDeviceEventFramesByOccurredAtUtc(
|
||||
const TArray<FHyperTwistTrainingSmartDeviceEventFrame>& EventFrames
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingSmartDeviceEventFrame> Sorted = EventFrames;
|
||||
Sorted.Sort([](
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& Left,
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& Right
|
||||
)
|
||||
{
|
||||
const int32 UtcCompare = CompareUtcStrings(Left.OccurredAtUtc, Right.OccurredAtUtc);
|
||||
if (UtcCompare != 0)
|
||||
{
|
||||
return UtcCompare < 0;
|
||||
}
|
||||
if (Left.SequenceNumber != Right.SequenceNumber)
|
||||
{
|
||||
return Left.SequenceNumber < Right.SequenceNumber;
|
||||
}
|
||||
|
||||
return Left.EventId < Right.EventId;
|
||||
});
|
||||
return Sorted;
|
||||
}
|
||||
|
||||
void SyncSmartDeviceSessionsFromEvents(
|
||||
TArray<FHyperTwistTrainingSmartDeviceSessionRecord>& SessionRecords,
|
||||
const TArray<FHyperTwistTrainingSmartDeviceEventFrame>& EventFrames
|
||||
)
|
||||
{
|
||||
TMap<FString, int32> SessionIndexById;
|
||||
for (int32 Index = 0; Index < SessionRecords.Num(); ++Index)
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord = SessionRecords[Index];
|
||||
SessionRecord.AcceptedEventCount = 0;
|
||||
SessionRecord.LinkedAttemptIds.Reset();
|
||||
SessionRecord.LinkedAttemptCount = 0;
|
||||
SessionRecord.LastEventKey.Reset();
|
||||
SessionRecord.LastEventId.Reset();
|
||||
SessionRecord.LastEventKind = EHyperTwistTrainingSmartDeviceEventKind::None;
|
||||
SessionRecord.LastLinkedAttemptId.Reset();
|
||||
SessionIndexById.Add(SessionRecord.DeviceSessionId, Index);
|
||||
}
|
||||
|
||||
for (const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame : EventFrames)
|
||||
{
|
||||
const int32* SessionIndex = SessionIndexById.Find(EventFrame.DeviceSessionId);
|
||||
if (SessionIndex == nullptr || !SessionRecords.IsValidIndex(*SessionIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord = SessionRecords[*SessionIndex];
|
||||
SessionRecord.AcceptedEventCount += 1;
|
||||
SessionRecord.LastSequenceNumber = FMath::Max(SessionRecord.LastSequenceNumber, EventFrame.SequenceNumber);
|
||||
|
||||
if (!EventFrame.LinkedAttemptId.IsEmpty())
|
||||
{
|
||||
SessionRecord.LinkedAttemptIds.AddUnique(EventFrame.LinkedAttemptId);
|
||||
SessionRecord.LastLinkedAttemptId = EventFrame.LinkedAttemptId;
|
||||
}
|
||||
|
||||
const bool bUseAsLastEvent = SessionRecord.LastUpdatedAtUtc.IsEmpty()
|
||||
|| CompareUtcStrings(EventFrame.OccurredAtUtc, SessionRecord.LastUpdatedAtUtc) >= 0;
|
||||
if (!bUseAsLastEvent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SessionRecord.LastUpdatedAtUtc = EventFrame.OccurredAtUtc;
|
||||
SessionRecord.LastEventId = EventFrame.EventId;
|
||||
SessionRecord.LastEventKey = BuildSmartDeviceEventKey(EventFrame);
|
||||
SessionRecord.LastEventKind = EventFrame.EventKind;
|
||||
if (!EventFrame.CaseId.IsEmpty())
|
||||
{
|
||||
SessionRecord.CaseId = EventFrame.CaseId;
|
||||
}
|
||||
if (!EventFrame.StatusReason.IsEmpty())
|
||||
{
|
||||
SessionRecord.LastStatusReason = EventFrame.StatusReason;
|
||||
}
|
||||
|
||||
switch (EventFrame.EventKind)
|
||||
{
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::Connected:
|
||||
SessionRecord.ConnectionState = EHyperTwistTrainingSmartDeviceConnectionState::Connected;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::InspectionStarted:
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::SolveStarted:
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::SplitCaptured:
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::AttemptFinished:
|
||||
SessionRecord.ConnectionState = EHyperTwistTrainingSmartDeviceConnectionState::Streaming;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::Disconnected:
|
||||
SessionRecord.ConnectionState = EHyperTwistTrainingSmartDeviceConnectionState::Disconnected;
|
||||
SessionRecord.EndedAtUtc = EventFrame.OccurredAtUtc;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::Faulted:
|
||||
SessionRecord.ConnectionState = EHyperTwistTrainingSmartDeviceConnectionState::Faulted;
|
||||
SessionRecord.EndedAtUtc = EventFrame.OccurredAtUtc;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : SessionRecords)
|
||||
{
|
||||
SessionRecord.LinkedAttemptIds = NormalizeNonEmptyUniqueStrings(SessionRecord.LinkedAttemptIds);
|
||||
SessionRecord.LinkedAttemptCount = SessionRecord.LinkedAttemptIds.Num();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsDeferredOverflowDashboardOutcomeSource(const FString& SourceLabel)
|
||||
{
|
||||
return SourceLabel == TEXT("dashboard-guidance-outcome-deferred-overflow-queued")
|
||||
|
|
@ -6848,6 +7121,123 @@ FHyperTwistTrainingSessionTemplate UHyperTwistTrainingRepositoryLibrary::BuildMe
|
|||
return SessionTemplate.IsStructurallyValid() ? SessionTemplate : FHyperTwistTrainingSessionTemplate();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertSmartDeviceSessionRecord(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingRepositoryState UpdatedState = RepositoryState;
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord NormalizedSessionRecord =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceSessionRecord(SessionRecord);
|
||||
if (!NormalizedSessionRecord.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedState;
|
||||
}
|
||||
|
||||
const int32 ExistingIndex = UpdatedState.StoredSmartDeviceSessions.IndexOfByPredicate(
|
||||
[&NormalizedSessionRecord](const FHyperTwistTrainingSmartDeviceSessionRecord& ExistingRecord)
|
||||
{
|
||||
return ExistingRecord.DeviceSessionId == NormalizedSessionRecord.DeviceSessionId;
|
||||
}
|
||||
);
|
||||
if (ExistingIndex >= 0)
|
||||
{
|
||||
UpdatedState.StoredSmartDeviceSessions[ExistingIndex] = NormalizedSessionRecord;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdatedState.StoredSmartDeviceSessions.Add(NormalizedSessionRecord);
|
||||
}
|
||||
|
||||
return RepairRepositoryState(UpdatedState);
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingRepositoryLibrary::TryGetSmartDeviceSessionRecord(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& DeviceSessionId,
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord& OutSessionRecord
|
||||
)
|
||||
{
|
||||
OutSessionRecord = FHyperTwistTrainingSmartDeviceSessionRecord();
|
||||
if (DeviceSessionId.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord* ExistingRecord =
|
||||
RepositoryState.StoredSmartDeviceSessions.FindByPredicate(
|
||||
[&DeviceSessionId](const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord)
|
||||
{
|
||||
return SessionRecord.DeviceSessionId == DeviceSessionId;
|
||||
}
|
||||
);
|
||||
if (ExistingRecord == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutSessionRecord =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceSessionRecord(*ExistingRecord);
|
||||
return OutSessionRecord.IsStructurallyValid();
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingRepositoryLibrary::HasSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& DeviceSessionId,
|
||||
const FString& EventId
|
||||
)
|
||||
{
|
||||
if (DeviceSessionId.IsEmpty() || EventId.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString EventKey = FString::Printf(TEXT("%s\x1f%s"), *DeviceSessionId, *EventId);
|
||||
return RepositoryState.StoredSmartDeviceEventFrames.ContainsByPredicate(
|
||||
[&EventKey](const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame)
|
||||
{
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::BuildSmartDeviceEventKey(
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceEventFrame(EventFrame)
|
||||
) == EventKey;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RecordSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingRepositoryState UpdatedState = RepositoryState;
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame NormalizedEventFrame =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceEventFrame(EventFrame);
|
||||
const FString EventKey =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::BuildSmartDeviceEventKey(NormalizedEventFrame);
|
||||
if (!NormalizedEventFrame.IsStructurallyValid() || EventKey.IsEmpty())
|
||||
{
|
||||
return UpdatedState;
|
||||
}
|
||||
|
||||
const int32 ExistingIndex = UpdatedState.StoredSmartDeviceEventFrames.IndexOfByPredicate(
|
||||
[&EventKey](const FHyperTwistTrainingSmartDeviceEventFrame& ExistingFrame)
|
||||
{
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::BuildSmartDeviceEventKey(
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceEventFrame(ExistingFrame)
|
||||
) == EventKey;
|
||||
}
|
||||
);
|
||||
if (ExistingIndex >= 0)
|
||||
{
|
||||
UpdatedState.StoredSmartDeviceEventFrames[ExistingIndex] = NormalizedEventFrame;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdatedState.StoredSmartDeviceEventFrames.Add(NormalizedEventFrame);
|
||||
}
|
||||
|
||||
return RepairRepositoryState(UpdatedState);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertRunRecord(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingRunRecord& RunRecord
|
||||
|
|
@ -7919,11 +8309,19 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar
|
|||
}
|
||||
|
||||
TSet<FString> ValidRunSessionIds;
|
||||
TSet<FString> ValidAttemptIds;
|
||||
for (const FHyperTwistTrainingRunRecord& RunRecord : RepositoryState.RunRecords)
|
||||
{
|
||||
if (RunRecord.IsStructurallyValid())
|
||||
{
|
||||
ValidRunSessionIds.Add(RunRecord.Session.TrainingSessionId);
|
||||
for (const FHyperTwistTrainingAttempt& Attempt : RunRecord.Attempts)
|
||||
{
|
||||
if (Attempt.IsStructurallyValid())
|
||||
{
|
||||
ValidAttemptIds.Add(Attempt.AttemptId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8129,6 +8527,67 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar
|
|||
}
|
||||
}
|
||||
|
||||
Report.TotalSmartDeviceSessionCount = RepositoryState.StoredSmartDeviceSessions.Num();
|
||||
TSet<FString> SeenSmartDeviceSessionIds;
|
||||
TMap<FString, FHyperTwistTrainingSmartDeviceSessionRecord> ValidSmartDeviceSessionsById;
|
||||
for (const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : RepositoryState.StoredSmartDeviceSessions)
|
||||
{
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord NormalizedSessionRecord =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceSessionRecord(SessionRecord);
|
||||
if (!NormalizedSessionRecord.IsStructurallyValid())
|
||||
{
|
||||
Report.InvalidSmartDeviceSessionCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (SeenSmartDeviceSessionIds.Contains(NormalizedSessionRecord.DeviceSessionId))
|
||||
{
|
||||
Report.DuplicateSmartDeviceSessionCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
SeenSmartDeviceSessionIds.Add(NormalizedSessionRecord.DeviceSessionId);
|
||||
ValidSmartDeviceSessionsById.Add(NormalizedSessionRecord.DeviceSessionId, NormalizedSessionRecord);
|
||||
if (!ValidRunSessionIds.Contains(NormalizedSessionRecord.TrainingSessionId))
|
||||
{
|
||||
Report.MissingSmartDeviceSessionSourceSessionCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Report.TotalSmartDeviceEventFrameCount = RepositoryState.StoredSmartDeviceEventFrames.Num();
|
||||
TSet<FString> SeenSmartDeviceEventKeys;
|
||||
for (const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame : RepositoryState.StoredSmartDeviceEventFrames)
|
||||
{
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame NormalizedEventFrame =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceEventFrame(EventFrame);
|
||||
const FString EventKey =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::BuildSmartDeviceEventKey(NormalizedEventFrame);
|
||||
if (!NormalizedEventFrame.IsStructurallyValid() || EventKey.IsEmpty())
|
||||
{
|
||||
Report.InvalidSmartDeviceEventFrameCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (SeenSmartDeviceEventKeys.Contains(EventKey))
|
||||
{
|
||||
Report.DuplicateSmartDeviceEventFrameCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
SeenSmartDeviceEventKeys.Add(EventKey);
|
||||
if (!ValidRunSessionIds.Contains(NormalizedEventFrame.TrainingSessionId))
|
||||
{
|
||||
Report.MissingSmartDeviceEventSourceSessionCount += 1;
|
||||
}
|
||||
if (!ValidSmartDeviceSessionsById.Contains(NormalizedEventFrame.DeviceSessionId))
|
||||
{
|
||||
Report.MissingSmartDeviceEventSessionRecordCount += 1;
|
||||
}
|
||||
if (!NormalizedEventFrame.LinkedAttemptId.IsEmpty()
|
||||
&& !ValidAttemptIds.Contains(NormalizedEventFrame.LinkedAttemptId))
|
||||
{
|
||||
Report.MissingSmartDeviceEventAttemptLinkCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (Report.InvalidRunRecordCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
|
|
@ -8329,6 +8788,56 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar
|
|||
TEXT("drop-orphaned-coach-session-queue-history-entry-links")
|
||||
);
|
||||
}
|
||||
if (Report.InvalidSmartDeviceSessionCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
Report.RepairActions,
|
||||
TEXT("drop-invalid-smart-device-sessions")
|
||||
);
|
||||
}
|
||||
if (Report.DuplicateSmartDeviceSessionCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
Report.RepairActions,
|
||||
TEXT("deduplicate-smart-device-sessions-by-session-id")
|
||||
);
|
||||
}
|
||||
if (Report.MissingSmartDeviceSessionSourceSessionCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
Report.RepairActions,
|
||||
TEXT("clear-smart-device-session-links-to-missing-training-sessions")
|
||||
);
|
||||
}
|
||||
if (Report.InvalidSmartDeviceEventFrameCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
Report.RepairActions,
|
||||
TEXT("drop-invalid-smart-device-event-frames")
|
||||
);
|
||||
}
|
||||
if (Report.DuplicateSmartDeviceEventFrameCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
Report.RepairActions,
|
||||
TEXT("deduplicate-smart-device-event-frames-by-session-and-event-id")
|
||||
);
|
||||
}
|
||||
if (Report.MissingSmartDeviceEventSourceSessionCount > 0
|
||||
|| Report.MissingSmartDeviceEventSessionRecordCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
Report.RepairActions,
|
||||
TEXT("clear-smart-device-event-links-to-missing-sessions")
|
||||
);
|
||||
}
|
||||
if (Report.MissingSmartDeviceEventAttemptLinkCount > 0)
|
||||
{
|
||||
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
|
||||
Report.RepairActions,
|
||||
TEXT("clear-smart-device-event-links-to-missing-attempts")
|
||||
);
|
||||
}
|
||||
|
||||
return Report;
|
||||
}
|
||||
|
|
@ -8755,12 +9264,80 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR
|
|||
return Left.HistoryEntryId < Right.HistoryEntryId;
|
||||
});
|
||||
|
||||
TMap<FString, int32> SmartDeviceSessionIndexById;
|
||||
for (const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : RepositoryState.StoredSmartDeviceSessions)
|
||||
{
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord NormalizedSessionRecord =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceSessionRecord(SessionRecord);
|
||||
if (!NormalizedSessionRecord.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const int32* ExistingIndex = SmartDeviceSessionIndexById.Find(NormalizedSessionRecord.DeviceSessionId))
|
||||
{
|
||||
RepairedState.StoredSmartDeviceSessions[*ExistingIndex] = NormalizedSessionRecord;
|
||||
}
|
||||
else
|
||||
{
|
||||
SmartDeviceSessionIndexById.Add(
|
||||
NormalizedSessionRecord.DeviceSessionId,
|
||||
RepairedState.StoredSmartDeviceSessions.Add(NormalizedSessionRecord)
|
||||
);
|
||||
}
|
||||
}
|
||||
RepairedState.StoredSmartDeviceSessions =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::SortSmartDeviceSessionsByUpdateUtc(
|
||||
RepairedState.StoredSmartDeviceSessions
|
||||
);
|
||||
|
||||
TMap<FString, int32> SmartDeviceEventIndexByKey;
|
||||
for (const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame : RepositoryState.StoredSmartDeviceEventFrames)
|
||||
{
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame NormalizedEventFrame =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeSmartDeviceEventFrame(EventFrame);
|
||||
const FString EventKey =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::BuildSmartDeviceEventKey(NormalizedEventFrame);
|
||||
if (!NormalizedEventFrame.IsStructurallyValid() || EventKey.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const int32* ExistingIndex = SmartDeviceEventIndexByKey.Find(EventKey))
|
||||
{
|
||||
RepairedState.StoredSmartDeviceEventFrames[*ExistingIndex] = NormalizedEventFrame;
|
||||
}
|
||||
else
|
||||
{
|
||||
SmartDeviceEventIndexByKey.Add(
|
||||
EventKey,
|
||||
RepairedState.StoredSmartDeviceEventFrames.Add(NormalizedEventFrame)
|
||||
);
|
||||
}
|
||||
}
|
||||
RepairedState.StoredSmartDeviceEventFrames =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::SortSmartDeviceEventFramesByOccurredAtUtc(
|
||||
RepairedState.StoredSmartDeviceEventFrames
|
||||
);
|
||||
HyperTwistTrainingRepositoryLibraryInternal::SyncSmartDeviceSessionsFromEvents(
|
||||
RepairedState.StoredSmartDeviceSessions,
|
||||
RepairedState.StoredSmartDeviceEventFrames
|
||||
);
|
||||
|
||||
TSet<FString> ValidRunSessionIds;
|
||||
TSet<FString> ValidAttemptIds;
|
||||
for (const FHyperTwistTrainingRunRecord& RunRecord : RepairedState.RunRecords)
|
||||
{
|
||||
if (RunRecord.IsStructurallyValid())
|
||||
{
|
||||
ValidRunSessionIds.Add(RunRecord.Session.TrainingSessionId);
|
||||
for (const FHyperTwistTrainingAttempt& Attempt : RunRecord.Attempts)
|
||||
{
|
||||
if (Attempt.IsStructurallyValid())
|
||||
{
|
||||
ValidAttemptIds.Add(Attempt.AttemptId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8923,6 +9500,79 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR
|
|||
});
|
||||
RepairedState.StoredCoachSessionQueueHistory = MoveTemp(FilteredCoachSessionQueueHistory);
|
||||
|
||||
TArray<FHyperTwistTrainingSmartDeviceSessionRecord> FilteredSmartDeviceSessions;
|
||||
FilteredSmartDeviceSessions.Reserve(RepairedState.StoredSmartDeviceSessions.Num());
|
||||
for (FHyperTwistTrainingSmartDeviceSessionRecord SessionRecord : RepairedState.StoredSmartDeviceSessions)
|
||||
{
|
||||
if (!ValidRunSessionIds.Contains(SessionRecord.TrainingSessionId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TArray<FString> FilteredLinkedAttemptIds;
|
||||
for (const FString& AttemptId : SessionRecord.LinkedAttemptIds)
|
||||
{
|
||||
if (!AttemptId.IsEmpty() && ValidAttemptIds.Contains(AttemptId))
|
||||
{
|
||||
FilteredLinkedAttemptIds.AddUnique(AttemptId);
|
||||
}
|
||||
}
|
||||
|
||||
FilteredLinkedAttemptIds.Sort();
|
||||
SessionRecord.LinkedAttemptIds = MoveTemp(FilteredLinkedAttemptIds);
|
||||
if (!SessionRecord.LastLinkedAttemptId.IsEmpty()
|
||||
&& !ValidAttemptIds.Contains(SessionRecord.LastLinkedAttemptId))
|
||||
{
|
||||
SessionRecord.LastLinkedAttemptId.Reset();
|
||||
}
|
||||
SessionRecord.LinkedAttemptCount = SessionRecord.LinkedAttemptIds.Num();
|
||||
if (SessionRecord.IsStructurallyValid())
|
||||
{
|
||||
FilteredSmartDeviceSessions.Add(SessionRecord);
|
||||
}
|
||||
}
|
||||
FilteredSmartDeviceSessions =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::SortSmartDeviceSessionsByUpdateUtc(
|
||||
FilteredSmartDeviceSessions
|
||||
);
|
||||
RepairedState.StoredSmartDeviceSessions = MoveTemp(FilteredSmartDeviceSessions);
|
||||
|
||||
TSet<FString> ValidSmartDeviceSessionIds;
|
||||
for (const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : RepairedState.StoredSmartDeviceSessions)
|
||||
{
|
||||
ValidSmartDeviceSessionIds.Add(SessionRecord.DeviceSessionId);
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingSmartDeviceEventFrame> FilteredSmartDeviceEventFrames;
|
||||
FilteredSmartDeviceEventFrames.Reserve(RepairedState.StoredSmartDeviceEventFrames.Num());
|
||||
for (FHyperTwistTrainingSmartDeviceEventFrame EventFrame : RepairedState.StoredSmartDeviceEventFrames)
|
||||
{
|
||||
if (!ValidRunSessionIds.Contains(EventFrame.TrainingSessionId)
|
||||
|| !ValidSmartDeviceSessionIds.Contains(EventFrame.DeviceSessionId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EventFrame.LinkedAttemptId.IsEmpty() && !ValidAttemptIds.Contains(EventFrame.LinkedAttemptId))
|
||||
{
|
||||
EventFrame.LinkedAttemptId.Reset();
|
||||
}
|
||||
|
||||
if (EventFrame.IsStructurallyValid())
|
||||
{
|
||||
FilteredSmartDeviceEventFrames.Add(EventFrame);
|
||||
}
|
||||
}
|
||||
FilteredSmartDeviceEventFrames =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::SortSmartDeviceEventFramesByOccurredAtUtc(
|
||||
FilteredSmartDeviceEventFrames
|
||||
);
|
||||
RepairedState.StoredSmartDeviceEventFrames = MoveTemp(FilteredSmartDeviceEventFrames);
|
||||
HyperTwistTrainingRepositoryLibraryInternal::SyncSmartDeviceSessionsFromEvents(
|
||||
RepairedState.StoredSmartDeviceSessions,
|
||||
RepairedState.StoredSmartDeviceEventFrames
|
||||
);
|
||||
|
||||
return RepairedState;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "HyperTwistReplay/HyperTwistReplayLibrary.h"
|
||||
#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingClassicCubingLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h"
|
||||
|
|
@ -1951,6 +1952,38 @@ namespace HyperTwistTrainingSubsystemInternal
|
|||
{
|
||||
return Value >= 0.0f ? FString::Printf(TEXT("%.2f"), Value) : TEXT("n/a");
|
||||
}
|
||||
|
||||
int32 GetPenaltyOffsetMs(const EHyperTwistTrainingPenalty Penalty)
|
||||
{
|
||||
switch (Penalty)
|
||||
{
|
||||
case EHyperTwistTrainingPenalty::Plus2:
|
||||
return 2000;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
FString BuildSmartDeviceSessionId(
|
||||
const FString& TrainingSessionId,
|
||||
const FHyperTwistTrainingSmartDeviceIdentity& DeviceIdentity
|
||||
)
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("smart-device-session/%s/%s/%s"),
|
||||
*TrainingSessionId,
|
||||
*DeviceIdentity.ProviderId,
|
||||
*DeviceIdentity.DeviceId
|
||||
);
|
||||
}
|
||||
|
||||
FString BuildSmartDeviceAttemptId(
|
||||
const FString& TrainingSessionId,
|
||||
const FString& EventId
|
||||
)
|
||||
{
|
||||
return FString::Printf(TEXT("smart-device-attempt/%s/%s"), *TrainingSessionId, *EventId);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistTrainingSubsystem::SetActiveAlgJsSequence(const FHyperTwistAlgorithmSequence& Sequence)
|
||||
|
|
@ -2001,6 +2034,18 @@ namespace HyperTwistTrainingSubsystemInternal
|
|||
return ActiveLiveTimerState;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::HasActiveSmartDeviceSession() const
|
||||
{
|
||||
return bHasActiveSmartDeviceSession && ActiveSmartDeviceSessionRecord.IsStructurallyValid()
|
||||
&& ActiveSmartDeviceSessionRecord.IsOpen();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord
|
||||
UHyperTwistTrainingSubsystem::GetActiveSmartDeviceSessionRecord() const
|
||||
{
|
||||
return ActiveSmartDeviceSessionRecord;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::TryGetActiveImportedRuntimeSurface(
|
||||
FHyperTwistTrainingImportedRuntimeSurface& OutRuntimeSurface
|
||||
) const
|
||||
|
|
@ -4746,6 +4791,10 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromDe
|
|||
EHyperTwistTrainingDeliveryMode Mode
|
||||
)
|
||||
{
|
||||
if (HasActiveSmartDeviceSession())
|
||||
{
|
||||
StopActiveSmartDeviceSession(TEXT("replaced-by-new-training-run"));
|
||||
}
|
||||
if (ActiveRecognitionSessionState.bSessionOpen)
|
||||
{
|
||||
FString CloseError;
|
||||
|
|
@ -5309,6 +5358,302 @@ FHyperTwistTrainingDrillTimerCompletionResult UHyperTwistTrainingSubsystem::Subm
|
|||
return CompletionResult;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord UHyperTwistTrainingSubsystem::StartActiveSmartDeviceSession(
|
||||
const FHyperTwistTrainingSmartDeviceIdentity& DeviceIdentity,
|
||||
const FString& DeviceSessionId
|
||||
)
|
||||
{
|
||||
if (!CanUseSmartDeviceBridge()
|
||||
|| !ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
return FHyperTwistTrainingSmartDeviceSessionRecord();
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceIdentity NormalizedDeviceIdentity =
|
||||
NormalizeSmartDeviceIdentity(DeviceIdentity);
|
||||
if (!NormalizedDeviceIdentity.IsStructurallyValid())
|
||||
{
|
||||
return FHyperTwistTrainingSmartDeviceSessionRecord();
|
||||
}
|
||||
|
||||
const FString ResolvedDeviceSessionId = !DeviceSessionId.IsEmpty()
|
||||
? DeviceSessionId
|
||||
: HyperTwistTrainingSubsystemInternal::BuildSmartDeviceSessionId(
|
||||
ActiveRunState.Session.TrainingSessionId,
|
||||
NormalizedDeviceIdentity
|
||||
);
|
||||
if (ResolvedDeviceSessionId.IsEmpty())
|
||||
{
|
||||
return FHyperTwistTrainingSmartDeviceSessionRecord();
|
||||
}
|
||||
|
||||
if (HasActiveSmartDeviceSession())
|
||||
{
|
||||
const bool bMatchesCurrentSession =
|
||||
ActiveSmartDeviceSessionRecord.DeviceSessionId == ResolvedDeviceSessionId
|
||||
|| (ActiveSmartDeviceSessionRecord.TrainingSessionId == ActiveRunState.Session.TrainingSessionId
|
||||
&& ActiveSmartDeviceSessionRecord.DeviceIdentity.ProviderId == NormalizedDeviceIdentity.ProviderId
|
||||
&& ActiveSmartDeviceSessionRecord.DeviceIdentity.DeviceId == NormalizedDeviceIdentity.DeviceId);
|
||||
if (bMatchesCurrentSession)
|
||||
{
|
||||
return ActiveSmartDeviceSessionRecord;
|
||||
}
|
||||
|
||||
StopActiveSmartDeviceSession(TEXT("replaced-by-new-smart-device-session"));
|
||||
}
|
||||
|
||||
const FString NowUtc = FDateTime::UtcNow().ToIso8601();
|
||||
ActiveSmartDeviceSessionRecord = FHyperTwistTrainingSmartDeviceSessionRecord();
|
||||
ActiveSmartDeviceSessionRecord.DeviceSessionId = ResolvedDeviceSessionId;
|
||||
ActiveSmartDeviceSessionRecord.TrainingSessionId = ActiveRunState.Session.TrainingSessionId;
|
||||
ActiveSmartDeviceSessionRecord.UserId = ActiveRunState.Session.UserId;
|
||||
ActiveSmartDeviceSessionRecord.DeckId = ActiveRunState.Session.DeckId;
|
||||
ActiveSmartDeviceSessionRecord.CaseId = ActiveRunState.CurrentSelection.TrainingCase.CaseId;
|
||||
ActiveSmartDeviceSessionRecord.DeviceIdentity = NormalizedDeviceIdentity;
|
||||
ActiveSmartDeviceSessionRecord.StartedAtUtc = NowUtc;
|
||||
ActiveSmartDeviceSessionRecord.LastUpdatedAtUtc = NowUtc;
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Connected;
|
||||
ActiveSmartDeviceSessionRecord.LastStatusReason = TEXT("smart-device-session-started");
|
||||
bHasActiveSmartDeviceSession = ActiveSmartDeviceSessionRecord.IsStructurallyValid();
|
||||
if (bHasActiveSmartDeviceSession)
|
||||
{
|
||||
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertSmartDeviceSessionRecord(
|
||||
TrainingRepositoryState,
|
||||
ActiveSmartDeviceSessionRecord
|
||||
);
|
||||
RefreshRepositoryViews();
|
||||
}
|
||||
|
||||
return ActiveSmartDeviceSessionRecord;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord UHyperTwistTrainingSubsystem::StopActiveSmartDeviceSession(
|
||||
const FString& CloseReason
|
||||
)
|
||||
{
|
||||
if (!ActiveSmartDeviceSessionRecord.IsStructurallyValid())
|
||||
{
|
||||
return FHyperTwistTrainingSmartDeviceSessionRecord();
|
||||
}
|
||||
|
||||
const FString NowUtc = FDateTime::UtcNow().ToIso8601();
|
||||
const FString ResolvedReason = !CloseReason.IsEmpty()
|
||||
? CloseReason
|
||||
: TEXT("smart-device-session-stopped");
|
||||
ActiveSmartDeviceSessionRecord.LastUpdatedAtUtc = NowUtc;
|
||||
if (ActiveSmartDeviceSessionRecord.EndedAtUtc.IsEmpty())
|
||||
{
|
||||
ActiveSmartDeviceSessionRecord.EndedAtUtc = NowUtc;
|
||||
}
|
||||
if (ActiveSmartDeviceSessionRecord.ConnectionState
|
||||
!= EHyperTwistTrainingSmartDeviceConnectionState::Faulted
|
||||
&& ActiveSmartDeviceSessionRecord.ConnectionState
|
||||
!= EHyperTwistTrainingSmartDeviceConnectionState::Disconnected)
|
||||
{
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Closed;
|
||||
}
|
||||
ActiveSmartDeviceSessionRecord.LastStatusReason = ResolvedReason;
|
||||
bHasActiveSmartDeviceSession = false;
|
||||
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertSmartDeviceSessionRecord(
|
||||
TrainingRepositoryState,
|
||||
ActiveSmartDeviceSessionRecord
|
||||
);
|
||||
RefreshRepositoryViews();
|
||||
return ActiveSmartDeviceSessionRecord;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceIngestionResult UHyperTwistTrainingSubsystem::IngestSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceIngestionResult IngestionResult;
|
||||
IngestionResult.EventKind = EventFrame.EventKind;
|
||||
|
||||
if (!CanUseSmartDeviceBridge() || !HasActiveSmartDeviceSession())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-session-unavailable");
|
||||
return IngestionResult;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame NormalizedEventFrame =
|
||||
NormalizeSmartDeviceEventFrame(EventFrame);
|
||||
IngestionResult.DeviceSessionId = NormalizedEventFrame.DeviceSessionId;
|
||||
IngestionResult.EventId = NormalizedEventFrame.EventId;
|
||||
IngestionResult.EventKey = NormalizedEventFrame.BuildEventKey();
|
||||
if (!NormalizedEventFrame.IsStructurallyValid()
|
||||
|| NormalizedEventFrame.DeviceSessionId != ActiveSmartDeviceSessionRecord.DeviceSessionId
|
||||
|| NormalizedEventFrame.TrainingSessionId != ActiveRunState.Session.TrainingSessionId)
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-event-invalid");
|
||||
return IngestionResult;
|
||||
}
|
||||
|
||||
if (UHyperTwistTrainingRepositoryLibrary::HasSmartDeviceEventFrame(
|
||||
TrainingRepositoryState,
|
||||
NormalizedEventFrame.DeviceSessionId,
|
||||
NormalizedEventFrame.EventId))
|
||||
{
|
||||
ActiveSmartDeviceSessionRecord.DuplicateEventCount += 1;
|
||||
ActiveSmartDeviceSessionRecord.LastUpdatedAtUtc = NormalizedEventFrame.OccurredAtUtc;
|
||||
ActiveSmartDeviceSessionRecord.LastEventId = NormalizedEventFrame.EventId;
|
||||
ActiveSmartDeviceSessionRecord.LastEventKey = NormalizedEventFrame.BuildEventKey();
|
||||
ActiveSmartDeviceSessionRecord.LastEventKind = NormalizedEventFrame.EventKind;
|
||||
ActiveSmartDeviceSessionRecord.LastStatusReason = TEXT("smart-device-event-duplicate");
|
||||
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertSmartDeviceSessionRecord(
|
||||
TrainingRepositoryState,
|
||||
ActiveSmartDeviceSessionRecord
|
||||
);
|
||||
RefreshRepositoryViews();
|
||||
IngestionResult.bDuplicate = true;
|
||||
return IngestionResult;
|
||||
}
|
||||
|
||||
switch (NormalizedEventFrame.EventKind)
|
||||
{
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::Connected:
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Connected;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::InspectionStarted:
|
||||
if (!ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid() || !StartActiveLiveTimer())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-inspection-start-failed");
|
||||
return IngestionResult;
|
||||
}
|
||||
IngestionResult.bUpdatedTimerState = true;
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Streaming;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::SolveStarted:
|
||||
if (!ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-solve-start-failed");
|
||||
return IngestionResult;
|
||||
}
|
||||
if (!HasActiveLiveTimer() && !StartActiveLiveTimer())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-solve-start-failed");
|
||||
return IngestionResult;
|
||||
}
|
||||
RefreshActiveLiveTimerState();
|
||||
if (ActiveLiveTimerState.Phase == EHyperTwistTrainingLiveTimerPhase::Inspection
|
||||
&& !AdvanceActiveLiveTimerToSolvePhase())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-solve-phase-advance-failed");
|
||||
return IngestionResult;
|
||||
}
|
||||
IngestionResult.bUpdatedTimerState = true;
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Streaming;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::SplitCaptured:
|
||||
if (!HasActiveLiveTimer())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-split-without-live-timer");
|
||||
return IngestionResult;
|
||||
}
|
||||
MergeSmartDeviceSplitCapturesIntoLiveTimer(NormalizedEventFrame.SplitCaptures);
|
||||
IngestionResult.bUpdatedTimerState = true;
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Streaming;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::AttemptFinished:
|
||||
{
|
||||
const FHyperTwistTrainingAttempt Attempt =
|
||||
BuildSmartDeviceAttemptFromEventFrame(NormalizedEventFrame);
|
||||
if (!Attempt.IsStructurallyValid())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-attempt-payload-invalid");
|
||||
return IngestionResult;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingRunStepResult StepResult =
|
||||
SubmitTrainingAttempt(Attempt, FMath::Max(Attempt.TotalTimeMs, 0));
|
||||
if (!StepResult.IsStructurallyValid())
|
||||
{
|
||||
IngestionResult.FailureReason = TEXT("smart-device-attempt-submit-failed");
|
||||
return IngestionResult;
|
||||
}
|
||||
|
||||
NormalizedEventFrame.LinkedAttemptId = Attempt.AttemptId;
|
||||
IngestionResult.LinkedAttemptId = Attempt.AttemptId;
|
||||
IngestionResult.bSubmittedAttempt = true;
|
||||
if (HasActiveLiveTimer())
|
||||
{
|
||||
ResetActiveLiveTimerState();
|
||||
}
|
||||
if (ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
ActiveSmartDeviceSessionRecord.CaseId = ActiveRunState.CurrentSelection.TrainingCase.CaseId;
|
||||
}
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Streaming;
|
||||
break;
|
||||
}
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::Disconnected:
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Disconnected;
|
||||
ActiveSmartDeviceSessionRecord.EndedAtUtc = NormalizedEventFrame.OccurredAtUtc;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::Faulted:
|
||||
ActiveSmartDeviceSessionRecord.ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Faulted;
|
||||
ActiveSmartDeviceSessionRecord.EndedAtUtc = NormalizedEventFrame.OccurredAtUtc;
|
||||
break;
|
||||
case EHyperTwistTrainingSmartDeviceEventKind::None:
|
||||
default:
|
||||
IngestionResult.FailureReason = TEXT("smart-device-event-kind-unsupported");
|
||||
return IngestionResult;
|
||||
}
|
||||
|
||||
ActiveSmartDeviceSessionRecord.LastUpdatedAtUtc = NormalizedEventFrame.OccurredAtUtc;
|
||||
ActiveSmartDeviceSessionRecord.LastSequenceNumber = FMath::Max(
|
||||
ActiveSmartDeviceSessionRecord.LastSequenceNumber,
|
||||
NormalizedEventFrame.SequenceNumber
|
||||
);
|
||||
ActiveSmartDeviceSessionRecord.LastEventId = NormalizedEventFrame.EventId;
|
||||
ActiveSmartDeviceSessionRecord.LastEventKey = NormalizedEventFrame.BuildEventKey();
|
||||
ActiveSmartDeviceSessionRecord.LastEventKind = NormalizedEventFrame.EventKind;
|
||||
ActiveSmartDeviceSessionRecord.LastStatusReason = !NormalizedEventFrame.StatusReason.IsEmpty()
|
||||
? NormalizedEventFrame.StatusReason
|
||||
: TEXT("smart-device-event-applied");
|
||||
const bool bShouldRetainAdvancedCaseId =
|
||||
NormalizedEventFrame.EventKind == EHyperTwistTrainingSmartDeviceEventKind::AttemptFinished
|
||||
&& ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid();
|
||||
if (!NormalizedEventFrame.CaseId.IsEmpty() && !bShouldRetainAdvancedCaseId)
|
||||
{
|
||||
ActiveSmartDeviceSessionRecord.CaseId = NormalizedEventFrame.CaseId;
|
||||
}
|
||||
if (!NormalizedEventFrame.LinkedAttemptId.IsEmpty())
|
||||
{
|
||||
ActiveSmartDeviceSessionRecord.LinkedAttemptIds.AddUnique(NormalizedEventFrame.LinkedAttemptId);
|
||||
ActiveSmartDeviceSessionRecord.LastLinkedAttemptId = NormalizedEventFrame.LinkedAttemptId;
|
||||
ActiveSmartDeviceSessionRecord.LinkedAttemptCount =
|
||||
ActiveSmartDeviceSessionRecord.LinkedAttemptIds.Num();
|
||||
}
|
||||
|
||||
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertSmartDeviceSessionRecord(
|
||||
TrainingRepositoryState,
|
||||
ActiveSmartDeviceSessionRecord
|
||||
);
|
||||
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::RecordSmartDeviceEventFrame(
|
||||
TrainingRepositoryState,
|
||||
NormalizedEventFrame
|
||||
);
|
||||
RefreshRepositoryViews();
|
||||
|
||||
if (NormalizedEventFrame.EventKind == EHyperTwistTrainingSmartDeviceEventKind::Disconnected
|
||||
|| NormalizedEventFrame.EventKind == EHyperTwistTrainingSmartDeviceEventKind::Faulted)
|
||||
{
|
||||
bHasActiveSmartDeviceSession = false;
|
||||
}
|
||||
|
||||
IngestionResult.bAccepted = true;
|
||||
return IngestionResult;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSession UHyperTwistTrainingSubsystem::CompleteActiveRun(bool bAbortSession)
|
||||
{
|
||||
if (!HasActiveRun())
|
||||
|
|
@ -5322,6 +5667,13 @@ FHyperTwistTrainingSession UHyperTwistTrainingSubsystem::CompleteActiveRun(bool
|
|||
CloseActiveRecognitionSession(CloseError);
|
||||
}
|
||||
|
||||
if (HasActiveSmartDeviceSession())
|
||||
{
|
||||
StopActiveSmartDeviceSession(
|
||||
bAbortSession ? TEXT("training-run-aborted") : TEXT("training-run-completed")
|
||||
);
|
||||
}
|
||||
|
||||
ResetActiveLiveTimerState();
|
||||
|
||||
ActiveRunState.Session = UHyperTwistTrainingLibrary::CompleteTrainingSession(ActiveRunState.Session, bAbortSession);
|
||||
|
|
@ -5390,6 +5742,10 @@ void UHyperTwistTrainingSubsystem::ClearActiveRun()
|
|||
FString CloseError;
|
||||
CloseActiveRecognitionSession(CloseError);
|
||||
}
|
||||
if (HasActiveSmartDeviceSession())
|
||||
{
|
||||
StopActiveSmartDeviceSession(TEXT("training-run-cleared"));
|
||||
}
|
||||
ResetActiveLiveTimerState();
|
||||
RetainedRepositoryViewUserId = !ActiveRunState.Session.UserId.IsEmpty()
|
||||
? ActiveRunState.Session.UserId
|
||||
|
|
@ -5905,6 +6261,275 @@ void UHyperTwistTrainingSubsystem::ClearActiveMethodDrillRun()
|
|||
bHasActiveMethodDrillRun = false;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::CanUseSmartDeviceBridge() const
|
||||
{
|
||||
return HasActiveRun()
|
||||
&& ActiveRunState.Session.IsStructurallyValid()
|
||||
&& ActiveRunState.ActiveDeck.IsStructurallyValid()
|
||||
&& ActiveRunState.Session.Mode == EHyperTwistTrainingDeliveryMode::SmartDevice;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::TryResolveSmartDeviceAdapterBoundaryId(
|
||||
const FString& RequestedBoundaryId,
|
||||
FString& OutBoundaryId
|
||||
) const
|
||||
{
|
||||
OutBoundaryId.Reset();
|
||||
|
||||
const FHyperTwistTrainingClassicCubingReferenceBundle ReferenceBundle =
|
||||
UHyperTwistTrainingClassicCubingLibrary::BuildBundledClassicCubingReferenceBundle();
|
||||
if (!ReferenceBundle.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString BoundaryId = !RequestedBoundaryId.IsEmpty()
|
||||
? RequestedBoundaryId
|
||||
: ReferenceBundle.PrimaryDeviceAdapterBoundaryId;
|
||||
FHyperTwistTrainingClassicCubingDeviceAdapterBoundary Boundary;
|
||||
if (!UHyperTwistTrainingClassicCubingLibrary::TryGetDeviceAdapterBoundaryById(
|
||||
ReferenceBundle,
|
||||
BoundaryId,
|
||||
Boundary))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutBoundaryId = Boundary.BoundaryId;
|
||||
return !OutBoundaryId.IsEmpty();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceIdentity UHyperTwistTrainingSubsystem::NormalizeSmartDeviceIdentity(
|
||||
const FHyperTwistTrainingSmartDeviceIdentity& DeviceIdentity
|
||||
) const
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceIdentity Normalized = DeviceIdentity;
|
||||
if (Normalized.ProviderId.IsEmpty() || Normalized.DeviceId.IsEmpty())
|
||||
{
|
||||
return FHyperTwistTrainingSmartDeviceIdentity();
|
||||
}
|
||||
if (Normalized.DeviceName.IsEmpty())
|
||||
{
|
||||
Normalized.DeviceName = Normalized.DeviceId;
|
||||
}
|
||||
|
||||
FString ResolvedBoundaryId;
|
||||
if (!TryResolveSmartDeviceAdapterBoundaryId(Normalized.AdapterBoundaryId, ResolvedBoundaryId))
|
||||
{
|
||||
return FHyperTwistTrainingSmartDeviceIdentity();
|
||||
}
|
||||
|
||||
Normalized.AdapterBoundaryId = ResolvedBoundaryId;
|
||||
Normalized.CapabilityTags.RemoveAll([](const FString& Value)
|
||||
{
|
||||
return Value.IsEmpty();
|
||||
});
|
||||
TArray<FString> UniqueCapabilityTags;
|
||||
for (const FString& CapabilityTag : Normalized.CapabilityTags)
|
||||
{
|
||||
UniqueCapabilityTags.AddUnique(CapabilityTag);
|
||||
}
|
||||
|
||||
Normalized.CapabilityTags = MoveTemp(UniqueCapabilityTags);
|
||||
Normalized.CapabilityTags.Sort();
|
||||
return Normalized;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame UHyperTwistTrainingSubsystem::NormalizeSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
) const
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceEventFrame Normalized = EventFrame;
|
||||
Normalized.DeviceIdentity = NormalizeSmartDeviceIdentity(Normalized.DeviceIdentity);
|
||||
if (Normalized.EventId.IsEmpty() && Normalized.SequenceNumber >= 0)
|
||||
{
|
||||
Normalized.EventId = FString::Printf(TEXT("sequence-%lld"), Normalized.SequenceNumber);
|
||||
}
|
||||
|
||||
if (Normalized.DeviceSessionId.IsEmpty())
|
||||
{
|
||||
Normalized.DeviceSessionId = ActiveSmartDeviceSessionRecord.DeviceSessionId;
|
||||
}
|
||||
if (Normalized.TrainingSessionId.IsEmpty())
|
||||
{
|
||||
Normalized.TrainingSessionId = ActiveRunState.Session.TrainingSessionId;
|
||||
}
|
||||
if (Normalized.UserId.IsEmpty())
|
||||
{
|
||||
Normalized.UserId = ActiveRunState.Session.UserId;
|
||||
}
|
||||
if (Normalized.DeckId.IsEmpty())
|
||||
{
|
||||
Normalized.DeckId = ActiveRunState.Session.DeckId;
|
||||
}
|
||||
if (Normalized.CaseId.IsEmpty())
|
||||
{
|
||||
Normalized.CaseId = ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid()
|
||||
? ActiveRunState.CurrentSelection.TrainingCase.CaseId
|
||||
: ActiveSmartDeviceSessionRecord.CaseId;
|
||||
}
|
||||
if (Normalized.OccurredAtUtc.IsEmpty())
|
||||
{
|
||||
Normalized.OccurredAtUtc = FDateTime::UtcNow().ToIso8601();
|
||||
}
|
||||
|
||||
Normalized.TotalTimeMs = FMath::Max(0, Normalized.TotalTimeMs);
|
||||
Normalized.RawSolveTimeMs = FMath::Max(0, Normalized.RawSolveTimeMs);
|
||||
Normalized.InspectionElapsedMs = FMath::Max(0, Normalized.InspectionElapsedMs);
|
||||
Normalized.MistakeCount = FMath::Max(0, Normalized.MistakeCount);
|
||||
if (Normalized.RawSolveTimeMs == 0 && Normalized.TotalTimeMs > 0)
|
||||
{
|
||||
Normalized.RawSolveTimeMs = Normalized.TotalTimeMs;
|
||||
}
|
||||
if (Normalized.StatusReason.IsEmpty())
|
||||
{
|
||||
Normalized.StatusReason = TEXT("smart-device-event");
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingSplitCapture> NormalizedSplitCaptures;
|
||||
TSet<FString> SeenSplitPhaseIds;
|
||||
for (FHyperTwistTrainingSplitCapture SplitCapture : Normalized.SplitCaptures)
|
||||
{
|
||||
if (!SplitCapture.IsStructurallyValid() || SeenSplitPhaseIds.Contains(SplitCapture.PhaseId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SplitCapture.TimestampMs = FMath::Max(0, SplitCapture.TimestampMs);
|
||||
if (SplitCapture.Label.IsEmpty())
|
||||
{
|
||||
SplitCapture.Label = SplitCapture.PhaseId;
|
||||
}
|
||||
|
||||
SeenSplitPhaseIds.Add(SplitCapture.PhaseId);
|
||||
NormalizedSplitCaptures.Add(SplitCapture);
|
||||
}
|
||||
NormalizedSplitCaptures.Sort([](
|
||||
const FHyperTwistTrainingSplitCapture& Left,
|
||||
const FHyperTwistTrainingSplitCapture& Right
|
||||
)
|
||||
{
|
||||
if (Left.TimestampMs != Right.TimestampMs)
|
||||
{
|
||||
return Left.TimestampMs < Right.TimestampMs;
|
||||
}
|
||||
|
||||
return Left.PhaseId < Right.PhaseId;
|
||||
});
|
||||
Normalized.SplitCaptures = MoveTemp(NormalizedSplitCaptures);
|
||||
return Normalized;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingAttempt UHyperTwistTrainingSubsystem::BuildSmartDeviceAttemptFromEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
) const
|
||||
{
|
||||
FHyperTwistTrainingAttempt Attempt;
|
||||
const FString CaseId = !EventFrame.CaseId.IsEmpty()
|
||||
? EventFrame.CaseId
|
||||
: (ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid()
|
||||
? ActiveRunState.CurrentSelection.TrainingCase.CaseId
|
||||
: FString());
|
||||
if (!CanUseSmartDeviceBridge() || EventFrame.EventId.IsEmpty() || CaseId.IsEmpty())
|
||||
{
|
||||
return Attempt;
|
||||
}
|
||||
|
||||
const EHyperTwistTrainingPenalty AppliedPenalty =
|
||||
EventFrame.AppliedPenalty != EHyperTwistTrainingPenalty::None
|
||||
? EventFrame.AppliedPenalty
|
||||
: (EventFrame.ManualPenalty != EHyperTwistTrainingPenalty::None
|
||||
? EventFrame.ManualPenalty
|
||||
: EventFrame.InspectionPenalty);
|
||||
const int32 RawSolveTimeMs = EventFrame.RawSolveTimeMs > 0
|
||||
? EventFrame.RawSolveTimeMs
|
||||
: EventFrame.TotalTimeMs;
|
||||
const int32 FinalTimeMs = EventFrame.TotalTimeMs > 0
|
||||
? EventFrame.TotalTimeMs
|
||||
: RawSolveTimeMs + HyperTwistTrainingSubsystemInternal::GetPenaltyOffsetMs(AppliedPenalty);
|
||||
|
||||
Attempt.AttemptId = HyperTwistTrainingSubsystemInternal::BuildSmartDeviceAttemptId(
|
||||
ActiveRunState.Session.TrainingSessionId,
|
||||
EventFrame.EventId
|
||||
);
|
||||
Attempt.TrainingSessionId = ActiveRunState.Session.TrainingSessionId;
|
||||
Attempt.CaseId = CaseId;
|
||||
Attempt.Result = EventFrame.AttemptResult;
|
||||
Attempt.TotalTimeMs = FMath::Max(0, FinalTimeMs);
|
||||
Attempt.ExecutionTimeMs = FMath::Max(0, RawSolveTimeMs);
|
||||
Attempt.RecognitionTimeMs = 0;
|
||||
Attempt.TimingBreakdown.InspectionElapsedMs = EventFrame.InspectionElapsedMs;
|
||||
Attempt.TimingBreakdown.RawSolveTimeMs = FMath::Max(0, RawSolveTimeMs);
|
||||
Attempt.TimingBreakdown.PenaltyOffsetMs =
|
||||
HyperTwistTrainingSubsystemInternal::GetPenaltyOffsetMs(AppliedPenalty);
|
||||
Attempt.TimingBreakdown.FinalTimeMs = FMath::Max(0, FinalTimeMs);
|
||||
Attempt.TimingBreakdown.InspectionPenalty = EventFrame.InspectionPenalty;
|
||||
Attempt.TimingBreakdown.ManualPenalty = EventFrame.ManualPenalty;
|
||||
Attempt.TimingBreakdown.AppliedPenalty = AppliedPenalty;
|
||||
Attempt.TimingBreakdown.SplitCaptures = EventFrame.SplitCaptures;
|
||||
Attempt.TimingBreakdown.bDidNotFinish =
|
||||
EventFrame.AttemptResult == EHyperTwistTrainingAttemptResult::Timeout
|
||||
|| EventFrame.AttemptResult == EHyperTwistTrainingAttemptResult::DNF
|
||||
|| EventFrame.AttemptResult == EHyperTwistTrainingAttemptResult::Aborted;
|
||||
Attempt.Mistakes = EventFrame.MistakeCount;
|
||||
Attempt.ReplayId = !EventFrame.ReplayId.IsEmpty()
|
||||
? EventFrame.ReplayId
|
||||
: ActiveRunState.ReplayPacket.ReplayId;
|
||||
Attempt.CompletedAtUtc = EventFrame.OccurredAtUtc;
|
||||
return Attempt;
|
||||
}
|
||||
|
||||
void UHyperTwistTrainingSubsystem::MergeSmartDeviceSplitCapturesIntoLiveTimer(
|
||||
const TArray<FHyperTwistTrainingSplitCapture>& SplitCaptures
|
||||
)
|
||||
{
|
||||
RefreshActiveLiveTimerState();
|
||||
if (!HasActiveLiveTimer())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TSet<FString> SeenSplitPhaseIds;
|
||||
for (const FHyperTwistTrainingSplitCapture& ExistingSplitCapture : ActiveLiveTimerState.SplitCaptures)
|
||||
{
|
||||
if (!ExistingSplitCapture.PhaseId.IsEmpty())
|
||||
{
|
||||
SeenSplitPhaseIds.Add(ExistingSplitCapture.PhaseId);
|
||||
}
|
||||
}
|
||||
|
||||
for (FHyperTwistTrainingSplitCapture SplitCapture : SplitCaptures)
|
||||
{
|
||||
if (!SplitCapture.IsStructurallyValid() || SeenSplitPhaseIds.Contains(SplitCapture.PhaseId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SplitCapture.TimestampMs = FMath::Max(0, SplitCapture.TimestampMs);
|
||||
if (SplitCapture.Label.IsEmpty())
|
||||
{
|
||||
SplitCapture.Label = SplitCapture.PhaseId;
|
||||
}
|
||||
|
||||
SeenSplitPhaseIds.Add(SplitCapture.PhaseId);
|
||||
ActiveLiveTimerState.SplitCaptures.Add(SplitCapture);
|
||||
}
|
||||
|
||||
ActiveLiveTimerState.SplitCaptures.Sort([](
|
||||
const FHyperTwistTrainingSplitCapture& Left,
|
||||
const FHyperTwistTrainingSplitCapture& Right
|
||||
)
|
||||
{
|
||||
if (Left.TimestampMs != Right.TimestampMs)
|
||||
{
|
||||
return Left.TimestampMs < Right.TimestampMs;
|
||||
}
|
||||
|
||||
return Left.PhaseId < Right.PhaseId;
|
||||
});
|
||||
ActiveLiveTimerState.LastUpdatedAtUtc = FDateTime::UtcNow().ToIso8601();
|
||||
}
|
||||
|
||||
UObject* UHyperTwistTrainingSubsystem::ResolveRecognitionVisionClientObject()
|
||||
{
|
||||
LoadConfig();
|
||||
|
|
|
|||
|
|
@ -130,6 +130,32 @@ public:
|
|||
const FString& TemplateId
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|SmartDevice")
|
||||
static FHyperTwistTrainingRepositoryState UpsertSmartDeviceSessionRecord(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|SmartDevice")
|
||||
static bool TryGetSmartDeviceSessionRecord(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& DeviceSessionId,
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord& OutSessionRecord
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|SmartDevice")
|
||||
static bool HasSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& DeviceSessionId,
|
||||
const FString& EventId
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|SmartDevice")
|
||||
static FHyperTwistTrainingRepositoryState RecordSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
|
||||
static FHyperTwistTrainingRepositoryState UpsertRunRecord(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
|
|
|
|||
|
|
@ -592,6 +592,26 @@ public:
|
|||
EHyperTwistTrainingAttemptResult Result
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|SmartDevice")
|
||||
bool HasActiveSmartDeviceSession() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|SmartDevice")
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord GetActiveSmartDeviceSessionRecord() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|SmartDevice")
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord StartActiveSmartDeviceSession(
|
||||
const FHyperTwistTrainingSmartDeviceIdentity& DeviceIdentity,
|
||||
const FString& DeviceSessionId
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|SmartDevice")
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord StopActiveSmartDeviceSession(const FString& CloseReason);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|SmartDevice")
|
||||
FHyperTwistTrainingSmartDeviceIngestionResult IngestSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
|
||||
FHyperTwistTrainingSession CompleteActiveRun(bool bAbortSession);
|
||||
|
||||
|
|
@ -622,6 +642,23 @@ private:
|
|||
void RefreshRepositoryViews();
|
||||
void ResetActiveLiveTimerState();
|
||||
void RefreshActiveLiveTimerState();
|
||||
bool CanUseSmartDeviceBridge() const;
|
||||
bool TryResolveSmartDeviceAdapterBoundaryId(
|
||||
const FString& RequestedBoundaryId,
|
||||
FString& OutBoundaryId
|
||||
) const;
|
||||
FHyperTwistTrainingSmartDeviceIdentity NormalizeSmartDeviceIdentity(
|
||||
const FHyperTwistTrainingSmartDeviceIdentity& DeviceIdentity
|
||||
) const;
|
||||
FHyperTwistTrainingSmartDeviceEventFrame NormalizeSmartDeviceEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
) const;
|
||||
FHyperTwistTrainingAttempt BuildSmartDeviceAttemptFromEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame
|
||||
) const;
|
||||
void MergeSmartDeviceSplitCapturesIntoLiveTimer(
|
||||
const TArray<FHyperTwistTrainingSplitCapture>& SplitCaptures
|
||||
);
|
||||
FHyperTwistTrainingLiveTimerState BuildActiveLiveTimerStateSnapshot(
|
||||
double NowSeconds,
|
||||
const FString& NowUtc
|
||||
|
|
@ -656,6 +693,9 @@ private:
|
|||
UPROPERTY()
|
||||
FHyperTwistTrainingLiveTimerState ActiveLiveTimerState;
|
||||
|
||||
UPROPERTY()
|
||||
FHyperTwistTrainingSmartDeviceSessionRecord ActiveSmartDeviceSessionRecord;
|
||||
|
||||
UPROPERTY()
|
||||
FString RetainedRepositoryViewUserId;
|
||||
|
||||
|
|
@ -806,6 +846,9 @@ private:
|
|||
UPROPERTY()
|
||||
bool bHasActiveMethodDrillRun = false;
|
||||
|
||||
UPROPERTY()
|
||||
bool bHasActiveSmartDeviceSession = false;
|
||||
|
||||
double ActiveLiveTimerStartedAtSeconds = 0.0;
|
||||
double ActiveLiveTimerSolveStartedAtSeconds = 0.0;
|
||||
double ActiveLiveTimerPausedAtSeconds = 0.0;
|
||||
|
|
|
|||
|
|
@ -191,6 +191,30 @@ enum class EHyperTwistTrainingLiveTimerPhase : uint8
|
|||
Completed UMETA(DisplayName = "Completed")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistTrainingSmartDeviceConnectionState : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "None"),
|
||||
Connected UMETA(DisplayName = "Connected"),
|
||||
Streaming UMETA(DisplayName = "Streaming"),
|
||||
Disconnected UMETA(DisplayName = "Disconnected"),
|
||||
Closed UMETA(DisplayName = "Closed"),
|
||||
Faulted UMETA(DisplayName = "Faulted")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistTrainingSmartDeviceEventKind : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "None"),
|
||||
Connected UMETA(DisplayName = "Connected"),
|
||||
InspectionStarted UMETA(DisplayName = "Inspection Started"),
|
||||
SolveStarted UMETA(DisplayName = "Solve Started"),
|
||||
SplitCaptured UMETA(DisplayName = "Split Captured"),
|
||||
AttemptFinished UMETA(DisplayName = "Attempt Finished"),
|
||||
Disconnected UMETA(DisplayName = "Disconnected"),
|
||||
Faulted UMETA(DisplayName = "Faulted")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistTrainingReviewGrade : uint8
|
||||
{
|
||||
|
|
@ -500,6 +524,245 @@ struct FHyperTwistTrainingLiveTimerState
|
|||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingSmartDeviceIdentity
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString ProviderId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeviceId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeviceName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeviceFamily;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString AdapterBoundaryId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FString> CapabilityTags;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !ProviderId.IsEmpty() && !DeviceId.IsEmpty() && !AdapterBoundaryId.IsEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingSmartDeviceSessionRecord
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeviceSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString TrainingSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString UserId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeckId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString CaseId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingSmartDeviceIdentity DeviceIdentity;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString StartedAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastUpdatedAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString EndedAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSmartDeviceConnectionState ConnectionState =
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 AcceptedEventCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 DuplicateEventCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 LinkedAttemptCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int64 LastSequenceNumber = -1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastEventKey;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastEventId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSmartDeviceEventKind LastEventKind = EHyperTwistTrainingSmartDeviceEventKind::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastLinkedAttemptId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FString> LinkedAttemptIds;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastStatusReason;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !DeviceSessionId.IsEmpty() && !TrainingSessionId.IsEmpty() && !UserId.IsEmpty()
|
||||
&& !DeckId.IsEmpty() && DeviceIdentity.IsStructurallyValid();
|
||||
}
|
||||
|
||||
bool IsOpen() const
|
||||
{
|
||||
return ConnectionState == EHyperTwistTrainingSmartDeviceConnectionState::Connected
|
||||
|| ConnectionState == EHyperTwistTrainingSmartDeviceConnectionState::Streaming;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingSmartDeviceEventFrame
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeviceSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString TrainingSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString UserId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeckId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString CaseId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingSmartDeviceIdentity DeviceIdentity;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString EventId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int64 SequenceNumber = -1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString OccurredAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSmartDeviceEventKind EventKind = EHyperTwistTrainingSmartDeviceEventKind::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingAttemptResult AttemptResult = EHyperTwistTrainingAttemptResult::Success;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 RawSolveTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 InspectionElapsedMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 MistakeCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingPenalty InspectionPenalty = EHyperTwistTrainingPenalty::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingPenalty ManualPenalty = EHyperTwistTrainingPenalty::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingPenalty AppliedPenalty = EHyperTwistTrainingPenalty::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FHyperTwistTrainingSplitCapture> SplitCaptures;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString ReplayId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LinkedAttemptId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString StatusReason;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !DeviceSessionId.IsEmpty() && !TrainingSessionId.IsEmpty() && !UserId.IsEmpty()
|
||||
&& !DeckId.IsEmpty() && !EventId.IsEmpty() && !OccurredAtUtc.IsEmpty()
|
||||
&& DeviceIdentity.IsStructurallyValid()
|
||||
&& EventKind != EHyperTwistTrainingSmartDeviceEventKind::None;
|
||||
}
|
||||
|
||||
FString BuildEventKey() const
|
||||
{
|
||||
return DeviceSessionId.IsEmpty() || EventId.IsEmpty()
|
||||
? FString()
|
||||
: FString::Printf(TEXT("%s\x1f%s"), *DeviceSessionId, *EventId);
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingSmartDeviceIngestionResult
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeviceSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString EventId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString EventKey;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LinkedAttemptId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSmartDeviceEventKind EventKind = EHyperTwistTrainingSmartDeviceEventKind::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bAccepted = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bDuplicate = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bCreatedSession = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bUpdatedTimerState = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bSubmittedAttempt = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString FailureReason;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return bAccepted || bDuplicate;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingCase
|
||||
{
|
||||
|
|
@ -7517,6 +7780,12 @@ struct FHyperTwistTrainingRepositoryState
|
|||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> StoredCoachSessionQueueHistory;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FHyperTwistTrainingSmartDeviceSessionRecord> StoredSmartDeviceSessions;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FHyperTwistTrainingSmartDeviceEventFrame> StoredSmartDeviceEventFrames;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
|
|
@ -7656,6 +7925,36 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 MissingCoachSessionQueueHistoryEntryCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalSmartDeviceSessionCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 InvalidSmartDeviceSessionCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 DuplicateSmartDeviceSessionCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 MissingSmartDeviceSessionSourceSessionCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalSmartDeviceEventFrameCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 InvalidSmartDeviceEventFrameCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 DuplicateSmartDeviceEventFrameCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 MissingSmartDeviceEventSourceSessionCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 MissingSmartDeviceEventSessionRecordCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 MissingSmartDeviceEventAttemptLinkCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FString> RepairActions;
|
||||
|
||||
|
|
@ -7695,7 +7994,15 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
|
|||
|| OutOfSyncCoachSessionQueueHistoryCount > 0
|
||||
|| MissingCoachSessionQueueHistorySourceSessionCount > 0
|
||||
|| MissingCoachSessionQueueHistoryQueueCount > 0
|
||||
|| MissingCoachSessionQueueHistoryEntryCount > 0;
|
||||
|| MissingCoachSessionQueueHistoryEntryCount > 0
|
||||
|| InvalidSmartDeviceSessionCount > 0
|
||||
|| DuplicateSmartDeviceSessionCount > 0
|
||||
|| MissingSmartDeviceSessionSourceSessionCount > 0
|
||||
|| InvalidSmartDeviceEventFrameCount > 0
|
||||
|| DuplicateSmartDeviceEventFrameCount > 0
|
||||
|| MissingSmartDeviceEventSourceSessionCount > 0
|
||||
|| MissingSmartDeviceEventSessionRecordCount > 0
|
||||
|| MissingSmartDeviceEventAttemptLinkCount > 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -7779,9 +8086,21 @@ struct FHyperTwistTrainingRepositoryRoundTripVerification
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 ExpectedCoachSessionQueueHistoryCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 ExpectedSmartDeviceSessionCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 ExpectedSmartDeviceEventFrameCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 LoadedCoachSessionQueueHistoryCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 LoadedSmartDeviceSessionCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 LoadedSmartDeviceEventFrameCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 VerifiedDerivedUserCount = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,562 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
|
||||
namespace HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal
|
||||
{
|
||||
FHyperTwistTrainingDeck MakeSmartDeviceDeck(
|
||||
const FString& DeckId,
|
||||
const int32 CaseCount,
|
||||
const bool bInspectionEnabled
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingDeck Deck;
|
||||
Deck.DeckId = DeckId;
|
||||
Deck.Title = TEXT("Bound3 Smart Device Deck");
|
||||
Deck.DeliveryModes = {EHyperTwistTrainingDeliveryMode::SmartDevice};
|
||||
Deck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Uniform;
|
||||
Deck.bSupportsSmartCube = true;
|
||||
Deck.TimingPolicy.bInspectionEnabled = bInspectionEnabled;
|
||||
Deck.TimingPolicy.InspectionDurationMs = bInspectionEnabled ? 15000 : 0;
|
||||
FHyperTwistTrainingSplitPhaseDefinition FirstPhase;
|
||||
FirstPhase.PhaseId = TEXT("f2l");
|
||||
FirstPhase.Label = TEXT("F2L");
|
||||
FirstPhase.Order = 0;
|
||||
Deck.TimingPolicy.SplitPhases.Add(FirstPhase);
|
||||
|
||||
FHyperTwistTrainingSplitPhaseDefinition SecondPhase;
|
||||
SecondPhase.PhaseId = TEXT("oll");
|
||||
SecondPhase.Label = TEXT("OLL");
|
||||
SecondPhase.Order = 1;
|
||||
Deck.TimingPolicy.SplitPhases.Add(SecondPhase);
|
||||
|
||||
for (int32 CaseIndex = 0; CaseIndex < CaseCount; ++CaseIndex)
|
||||
{
|
||||
FHyperTwistTrainingCase TrainingCase;
|
||||
TrainingCase.CaseId = FString::Printf(TEXT("bound3-case-%d"), CaseIndex + 1);
|
||||
TrainingCase.PromptLabel = FString::Printf(TEXT("Bound3 Case %d"), CaseIndex + 1);
|
||||
TrainingCase.AllowedDeliveryModes = {EHyperTwistTrainingDeliveryMode::SmartDevice};
|
||||
Deck.Cases.Add(TrainingCase);
|
||||
}
|
||||
|
||||
return Deck;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceIdentity MakeSmartDeviceIdentity(const FString& DeviceId)
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceIdentity DeviceIdentity;
|
||||
DeviceIdentity.ProviderId = TEXT("fixture-provider");
|
||||
DeviceIdentity.DeviceId = DeviceId;
|
||||
DeviceIdentity.DeviceName = DeviceId;
|
||||
DeviceIdentity.DeviceFamily = TEXT("smart-timer");
|
||||
DeviceIdentity.CapabilityTags = {TEXT("inspection"), TEXT("splits"), TEXT("timed-attempts")};
|
||||
return DeviceIdentity;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame MakeEventFrame(
|
||||
const FHyperTwistTrainingSmartDeviceIdentity& DeviceIdentity,
|
||||
const FString& DeviceSessionId,
|
||||
const FString& TrainingSessionId,
|
||||
const FString& UserId,
|
||||
const FString& DeckId,
|
||||
const FString& CaseId,
|
||||
const FString& EventId,
|
||||
const int64 SequenceNumber,
|
||||
const EHyperTwistTrainingSmartDeviceEventKind EventKind
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSmartDeviceEventFrame EventFrame;
|
||||
EventFrame.DeviceSessionId = DeviceSessionId;
|
||||
EventFrame.TrainingSessionId = TrainingSessionId;
|
||||
EventFrame.UserId = UserId;
|
||||
EventFrame.DeckId = DeckId;
|
||||
EventFrame.CaseId = CaseId;
|
||||
EventFrame.DeviceIdentity = DeviceIdentity;
|
||||
EventFrame.EventId = EventId;
|
||||
EventFrame.SequenceNumber = SequenceNumber;
|
||||
EventFrame.OccurredAtUtc = TEXT("2026-05-19T12:00:00Z");
|
||||
EventFrame.EventKind = EventKind;
|
||||
EventFrame.StatusReason = TEXT("fixture-event");
|
||||
return EventFrame;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord* FindSessionRecordById(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& DeviceSessionId
|
||||
)
|
||||
{
|
||||
return RepositoryState.StoredSmartDeviceSessions.FindByPredicate(
|
||||
[&DeviceSessionId](const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord)
|
||||
{
|
||||
return SessionRecord.DeviceSessionId == DeviceSessionId;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame* FindEventFrameById(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& EventId
|
||||
)
|
||||
{
|
||||
return RepositoryState.StoredSmartDeviceEventFrames.FindByPredicate(
|
||||
[&EventId](const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame)
|
||||
{
|
||||
return EventFrame.EventId == EventId;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistCubeDeskBound3FixtureIngestionTest,
|
||||
"HyperTwist.CleanRoom.CubeDesk.Bound3.FixtureIngestion",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistCubeDeskBound3FixtureIngestionTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UGameInstance* GameInstance = NewObject<UGameInstance>(GetTransientPackage());
|
||||
TestNotNull(TEXT("Game instance must exist for the smart-device subsystem test."), GameInstance);
|
||||
if (GameInstance == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject<UHyperTwistTrainingSubsystem>(GameInstance);
|
||||
TestNotNull(TEXT("Training subsystem must be created."), TrainingSubsystem);
|
||||
if (TrainingSubsystem == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingDeck Deck =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeSmartDeviceDeck(
|
||||
TEXT("bound3-fixture-deck"),
|
||||
1,
|
||||
true
|
||||
);
|
||||
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck(
|
||||
Deck,
|
||||
TEXT("bound3-user"),
|
||||
TEXT("bound3-fixture-session"),
|
||||
EHyperTwistTrainingDeliveryMode::SmartDevice
|
||||
);
|
||||
TestTrue(TEXT("A smart-device run must start successfully."), RunState.IsStructurallyValid());
|
||||
TestTrue(
|
||||
TEXT("A smart-device run must select an initial case for fixture ingestion."),
|
||||
RunState.CurrentSelection.TrainingCase.IsStructurallyValid()
|
||||
);
|
||||
if (!RunState.IsStructurallyValid() || !RunState.CurrentSelection.TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceIdentity DeviceIdentity =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeSmartDeviceIdentity(TEXT("fixture-device-1"));
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord DeviceSession =
|
||||
TrainingSubsystem->StartActiveSmartDeviceSession(DeviceIdentity, TEXT("bound3-device-session"));
|
||||
TestTrue(TEXT("The smart-device session must start successfully."), DeviceSession.IsStructurallyValid());
|
||||
TestTrue(TEXT("The subsystem must track the active smart-device session."), TrainingSubsystem->HasActiveSmartDeviceSession());
|
||||
if (!DeviceSession.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString CaseId = RunState.CurrentSelection.TrainingCase.CaseId;
|
||||
FHyperTwistTrainingSmartDeviceEventFrame InspectionStarted =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeEventFrame(
|
||||
DeviceIdentity,
|
||||
DeviceSession.DeviceSessionId,
|
||||
RunState.Session.TrainingSessionId,
|
||||
RunState.Session.UserId,
|
||||
RunState.Session.DeckId,
|
||||
CaseId,
|
||||
TEXT("fixture-inspection"),
|
||||
1,
|
||||
EHyperTwistTrainingSmartDeviceEventKind::InspectionStarted
|
||||
);
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult InspectionResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(InspectionStarted);
|
||||
TestTrue(TEXT("Inspection-start events must be accepted."), InspectionResult.bAccepted);
|
||||
TestTrue(TEXT("Inspection-start events must update timer state."), InspectionResult.bUpdatedTimerState);
|
||||
TestEqual(
|
||||
TEXT("Inspection-start events must place the live timer in inspection."),
|
||||
TrainingSubsystem->GetActiveLiveTimerState().Phase,
|
||||
EHyperTwistTrainingLiveTimerPhase::Inspection
|
||||
);
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame SolveStarted =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeEventFrame(
|
||||
DeviceIdentity,
|
||||
DeviceSession.DeviceSessionId,
|
||||
RunState.Session.TrainingSessionId,
|
||||
RunState.Session.UserId,
|
||||
RunState.Session.DeckId,
|
||||
CaseId,
|
||||
TEXT("fixture-solve-start"),
|
||||
2,
|
||||
EHyperTwistTrainingSmartDeviceEventKind::SolveStarted
|
||||
);
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult SolveResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(SolveStarted);
|
||||
TestTrue(TEXT("Solve-start events must be accepted."), SolveResult.bAccepted);
|
||||
TestEqual(
|
||||
TEXT("Solve-start events must advance the live timer into solving."),
|
||||
TrainingSubsystem->GetActiveLiveTimerState().Phase,
|
||||
EHyperTwistTrainingLiveTimerPhase::Solving
|
||||
);
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame SplitCaptured =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeEventFrame(
|
||||
DeviceIdentity,
|
||||
DeviceSession.DeviceSessionId,
|
||||
RunState.Session.TrainingSessionId,
|
||||
RunState.Session.UserId,
|
||||
RunState.Session.DeckId,
|
||||
CaseId,
|
||||
TEXT("fixture-split"),
|
||||
3,
|
||||
EHyperTwistTrainingSmartDeviceEventKind::SplitCaptured
|
||||
);
|
||||
FHyperTwistTrainingSplitCapture FirstSplitCapture;
|
||||
FirstSplitCapture.PhaseId = TEXT("f2l");
|
||||
FirstSplitCapture.Label = TEXT("F2L");
|
||||
FirstSplitCapture.TimestampMs = 1200;
|
||||
SplitCaptured.SplitCaptures.Add(FirstSplitCapture);
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult SplitResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(SplitCaptured);
|
||||
TestTrue(TEXT("Split-captured events must be accepted."), SplitResult.bAccepted);
|
||||
TestEqual(
|
||||
TEXT("Split-captured events must retain one live-timer split capture."),
|
||||
TrainingSubsystem->GetActiveLiveTimerState().SplitCaptures.Num(),
|
||||
1
|
||||
);
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame AttemptFinished =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeEventFrame(
|
||||
DeviceIdentity,
|
||||
DeviceSession.DeviceSessionId,
|
||||
RunState.Session.TrainingSessionId,
|
||||
RunState.Session.UserId,
|
||||
RunState.Session.DeckId,
|
||||
CaseId,
|
||||
TEXT("fixture-attempt-finished"),
|
||||
4,
|
||||
EHyperTwistTrainingSmartDeviceEventKind::AttemptFinished
|
||||
);
|
||||
AttemptFinished.AttemptResult = EHyperTwistTrainingAttemptResult::Success;
|
||||
AttemptFinished.TotalTimeMs = 3210;
|
||||
AttemptFinished.RawSolveTimeMs = 3010;
|
||||
AttemptFinished.InspectionElapsedMs = 1500;
|
||||
AttemptFinished.SplitCaptures = SplitCaptured.SplitCaptures;
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult AttemptResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(AttemptFinished);
|
||||
TestTrue(TEXT("Attempt-finished events must be accepted."), AttemptResult.bAccepted);
|
||||
TestTrue(TEXT("Attempt-finished events must submit a training attempt."), AttemptResult.bSubmittedAttempt);
|
||||
TestTrue(
|
||||
TEXT("Attempt-finished events must return a linked attempt id."),
|
||||
!AttemptResult.LinkedAttemptId.IsEmpty()
|
||||
);
|
||||
|
||||
const FHyperTwistTrainingRepositoryState RepositoryState = TrainingSubsystem->GetTrainingRepositoryState();
|
||||
TestEqual(
|
||||
TEXT("Fixture ingestion must persist all accepted device events."),
|
||||
RepositoryState.StoredSmartDeviceEventFrames.Num(),
|
||||
4
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Fixture ingestion must persist exactly one device session."),
|
||||
RepositoryState.StoredSmartDeviceSessions.Num(),
|
||||
1
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Fixture ingestion must persist exactly one smart-device attempt."),
|
||||
TrainingSubsystem->GetActiveRunState().Attempts.Num(),
|
||||
1
|
||||
);
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord* StoredSessionRecord =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::FindSessionRecordById(
|
||||
RepositoryState,
|
||||
DeviceSession.DeviceSessionId
|
||||
);
|
||||
TestNotNull(TEXT("The stored smart-device session record must exist."), StoredSessionRecord);
|
||||
if (StoredSessionRecord == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
TEXT("The stored smart-device session must retain one linked attempt."),
|
||||
StoredSessionRecord->LinkedAttemptCount,
|
||||
1
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The stored smart-device session must retain all accepted event frames."),
|
||||
StoredSessionRecord->AcceptedEventCount,
|
||||
4
|
||||
);
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceEventFrame* StoredAttemptEvent =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::FindEventFrameById(
|
||||
RepositoryState,
|
||||
TEXT("fixture-attempt-finished")
|
||||
);
|
||||
TestNotNull(TEXT("The stored attempt-finished event must exist."), StoredAttemptEvent);
|
||||
if (StoredAttemptEvent == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
TEXT("The stored attempt-finished event must retain its linked attempt id."),
|
||||
StoredAttemptEvent->LinkedAttemptId,
|
||||
AttemptResult.LinkedAttemptId
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistCubeDeskBound3DeviceSessionLifecycleTest,
|
||||
"HyperTwist.CleanRoom.CubeDesk.Bound3.DeviceSessionLifecycle",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistCubeDeskBound3DeviceSessionLifecycleTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UGameInstance* GameInstance = NewObject<UGameInstance>(GetTransientPackage());
|
||||
TestNotNull(TEXT("Game instance must exist for the device-session lifecycle test."), GameInstance);
|
||||
if (GameInstance == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject<UHyperTwistTrainingSubsystem>(GameInstance);
|
||||
TestNotNull(TEXT("Training subsystem must be created."), TrainingSubsystem);
|
||||
if (TrainingSubsystem == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingDeck Deck =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeSmartDeviceDeck(
|
||||
TEXT("bound3-lifecycle-deck"),
|
||||
1,
|
||||
false
|
||||
);
|
||||
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck(
|
||||
Deck,
|
||||
TEXT("bound3-user"),
|
||||
TEXT("bound3-lifecycle-session"),
|
||||
EHyperTwistTrainingDeliveryMode::SmartDevice
|
||||
);
|
||||
TestTrue(TEXT("A smart-device lifecycle run must start successfully."), RunState.IsStructurallyValid());
|
||||
if (!RunState.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceIdentity DeviceIdentity =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeSmartDeviceIdentity(TEXT("fixture-device-2"));
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord DeviceSession =
|
||||
TrainingSubsystem->StartActiveSmartDeviceSession(DeviceIdentity, TEXT("bound3-lifecycle-device-session"));
|
||||
TestTrue(TEXT("The smart-device session must start successfully."), DeviceSession.IsStructurallyValid());
|
||||
TestTrue(TEXT("The subsystem must report the session as active after start."), TrainingSubsystem->HasActiveSmartDeviceSession());
|
||||
if (!DeviceSession.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame Connected =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeEventFrame(
|
||||
DeviceIdentity,
|
||||
DeviceSession.DeviceSessionId,
|
||||
RunState.Session.TrainingSessionId,
|
||||
RunState.Session.UserId,
|
||||
RunState.Session.DeckId,
|
||||
RunState.CurrentSelection.TrainingCase.CaseId,
|
||||
TEXT("lifecycle-connected"),
|
||||
1,
|
||||
EHyperTwistTrainingSmartDeviceEventKind::Connected
|
||||
);
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult ConnectedResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(Connected);
|
||||
TestTrue(TEXT("Connected events must be accepted."), ConnectedResult.bAccepted);
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame Disconnected =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeEventFrame(
|
||||
DeviceIdentity,
|
||||
DeviceSession.DeviceSessionId,
|
||||
RunState.Session.TrainingSessionId,
|
||||
RunState.Session.UserId,
|
||||
RunState.Session.DeckId,
|
||||
RunState.CurrentSelection.TrainingCase.CaseId,
|
||||
TEXT("lifecycle-disconnected"),
|
||||
2,
|
||||
EHyperTwistTrainingSmartDeviceEventKind::Disconnected
|
||||
);
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult DisconnectedResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(Disconnected);
|
||||
TestTrue(TEXT("Disconnected events must be accepted."), DisconnectedResult.bAccepted);
|
||||
TestFalse(
|
||||
TEXT("The subsystem must stop reporting the session as active after disconnect."),
|
||||
TrainingSubsystem->HasActiveSmartDeviceSession()
|
||||
);
|
||||
|
||||
const FHyperTwistTrainingRepositoryState RepositoryState = TrainingSubsystem->GetTrainingRepositoryState();
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord* StoredSessionRecord =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::FindSessionRecordById(
|
||||
RepositoryState,
|
||||
DeviceSession.DeviceSessionId
|
||||
);
|
||||
TestNotNull(TEXT("The disconnected smart-device session must remain stored."), StoredSessionRecord);
|
||||
if (StoredSessionRecord == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
TEXT("The stored session must resolve to the disconnected lifecycle state."),
|
||||
StoredSessionRecord->ConnectionState,
|
||||
EHyperTwistTrainingSmartDeviceConnectionState::Disconnected
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The stored session must retain an ended-at timestamp after disconnect."),
|
||||
!StoredSessionRecord->EndedAtUtc.IsEmpty()
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistCubeDeskBound3RepositoryDedupeTest,
|
||||
"HyperTwist.CleanRoom.CubeDesk.Bound3.RepositoryDedupe",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistCubeDeskBound3RepositoryDedupeTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UGameInstance* GameInstance = NewObject<UGameInstance>(GetTransientPackage());
|
||||
TestNotNull(TEXT("Game instance must exist for the repository dedupe test."), GameInstance);
|
||||
if (GameInstance == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject<UHyperTwistTrainingSubsystem>(GameInstance);
|
||||
TestNotNull(TEXT("Training subsystem must be created."), TrainingSubsystem);
|
||||
if (TrainingSubsystem == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingDeck Deck =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeSmartDeviceDeck(
|
||||
TEXT("bound3-dedupe-deck"),
|
||||
1,
|
||||
false
|
||||
);
|
||||
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck(
|
||||
Deck,
|
||||
TEXT("bound3-user"),
|
||||
TEXT("bound3-dedupe-session"),
|
||||
EHyperTwistTrainingDeliveryMode::SmartDevice
|
||||
);
|
||||
TestTrue(TEXT("A smart-device dedupe run must start successfully."), RunState.IsStructurallyValid());
|
||||
if (!RunState.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceIdentity DeviceIdentity =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeSmartDeviceIdentity(TEXT("fixture-device-3"));
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord DeviceSession =
|
||||
TrainingSubsystem->StartActiveSmartDeviceSession(DeviceIdentity, TEXT("bound3-dedupe-device-session"));
|
||||
TestTrue(TEXT("The smart-device session must start successfully."), DeviceSession.IsStructurallyValid());
|
||||
if (!DeviceSession.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSmartDeviceEventFrame AttemptFinished =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::MakeEventFrame(
|
||||
DeviceIdentity,
|
||||
DeviceSession.DeviceSessionId,
|
||||
RunState.Session.TrainingSessionId,
|
||||
RunState.Session.UserId,
|
||||
RunState.Session.DeckId,
|
||||
RunState.CurrentSelection.TrainingCase.CaseId,
|
||||
TEXT("dedupe-attempt-finished"),
|
||||
1,
|
||||
EHyperTwistTrainingSmartDeviceEventKind::AttemptFinished
|
||||
);
|
||||
AttemptFinished.AttemptResult = EHyperTwistTrainingAttemptResult::Success;
|
||||
AttemptFinished.TotalTimeMs = 2450;
|
||||
AttemptFinished.RawSolveTimeMs = 2250;
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult FirstAttemptResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(AttemptFinished);
|
||||
TestTrue(TEXT("The first attempt-finished event must be accepted."), FirstAttemptResult.bAccepted);
|
||||
TestTrue(TEXT("The first attempt-finished event must submit an attempt."), FirstAttemptResult.bSubmittedAttempt);
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceIngestionResult DuplicateAttemptResult =
|
||||
TrainingSubsystem->IngestSmartDeviceEventFrame(AttemptFinished);
|
||||
TestTrue(TEXT("Duplicate attempt-finished events must be recognized as duplicates."), DuplicateAttemptResult.bDuplicate);
|
||||
|
||||
const FHyperTwistTrainingRepositoryState RepositoryState = TrainingSubsystem->GetTrainingRepositoryState();
|
||||
TestEqual(
|
||||
TEXT("Idempotent dedupe must retain only one stored smart-device event frame."),
|
||||
RepositoryState.StoredSmartDeviceEventFrames.Num(),
|
||||
1
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Idempotent dedupe must retain only one submitted attempt in the run state."),
|
||||
TrainingSubsystem->GetActiveRunState().Attempts.Num(),
|
||||
1
|
||||
);
|
||||
|
||||
const FHyperTwistTrainingSmartDeviceSessionRecord* StoredSessionRecord =
|
||||
HyperTwistCubeDeskBound3SmartDeviceBridgeTestInternal::FindSessionRecordById(
|
||||
RepositoryState,
|
||||
DeviceSession.DeviceSessionId
|
||||
);
|
||||
TestNotNull(TEXT("The stored smart-device session must exist after dedupe."), StoredSessionRecord);
|
||||
if (StoredSessionRecord == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
TEXT("The stored smart-device session must count one duplicate event."),
|
||||
StoredSessionRecord->DuplicateEventCount,
|
||||
1
|
||||
);
|
||||
|
||||
const FHyperTwistTrainingRepositoryRoundTripVerification Verification =
|
||||
UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
|
||||
RepositoryState,
|
||||
TEXT("Automation/HyperTwistCubeDeskBound3RepositoryRoundTrip.json")
|
||||
);
|
||||
TestTrue(TEXT("Repository roundtrip verification must pass after smart-device dedupe."), Verification.bDidRoundTripPass);
|
||||
TestEqual(
|
||||
TEXT("Roundtrip verification must retain one smart-device session."),
|
||||
Verification.LoadedSmartDeviceSessionCount,
|
||||
1
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Roundtrip verification must retain one smart-device event frame."),
|
||||
Verification.LoadedSmartDeviceEventFrameCount,
|
||||
1
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Reference in a new issue