Close Bound 2 session domain and analytics panels
This commit is contained in:
parent
366a7a8a0e
commit
a7e52432e2
7 changed files with 1151 additions and 11 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h"
|
||||
|
||||
#include "HyperTwistTraining/HyperTwistTrainingAnalyticsLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
|
||||
|
|
@ -45,6 +46,66 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
return Provenance;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSession BuildSessionFromSummary(
|
||||
const FHyperTwistTrainingSessionSummary& Summary,
|
||||
const FString& UserId,
|
||||
const EHyperTwistTrainingDeliveryMode Mode
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSession Session;
|
||||
Session.TrainingSessionId = Summary.TrainingSessionId;
|
||||
Session.DeckId = Summary.DeckId;
|
||||
Session.UserId = !Summary.UserId.IsEmpty() ? Summary.UserId : UserId;
|
||||
Session.Mode = Mode;
|
||||
Session.SessionState = Summary.SessionState;
|
||||
Session.CurrentCaseId = Summary.LastCaseId;
|
||||
Session.AttemptCount = Summary.AttemptCount;
|
||||
Session.EndedAtUtc = Summary.LastCompletedAtUtc;
|
||||
return Session;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSessionDomainBundle BuildDisplayedSessionDomainBundle(
|
||||
const FHyperTwistTrainingRunState& RunState,
|
||||
const FHyperTwistTrainingSessionSummary& RunSummary,
|
||||
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance,
|
||||
const FHyperTwistTrainingCoachPanelState& CoachPanelState,
|
||||
const FHyperTwistTrainingMethodDrillRunState& MethodDrillRunState,
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& ReportBundle
|
||||
)
|
||||
{
|
||||
const FHyperTwistTrainingAttempt SolveAttempt =
|
||||
RunState.Attempts.Num() > 0 ? RunState.Attempts.Last() : FHyperTwistTrainingAttempt();
|
||||
return UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
|
||||
RunState.Session,
|
||||
RunSummary,
|
||||
SolveAttempt,
|
||||
TemplateLaunchProvenance,
|
||||
CoachPanelState,
|
||||
MethodDrillRunState,
|
||||
ReportBundle
|
||||
);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSessionDomainBundle BuildDisplayedRecapSessionDomainBundle(
|
||||
const FHyperTwistTrainingSessionSummary& RunSummary,
|
||||
const FString& UserId,
|
||||
const EHyperTwistTrainingDeliveryMode Mode,
|
||||
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance,
|
||||
const FHyperTwistTrainingCoachPanelState& CoachPanelState,
|
||||
const FHyperTwistTrainingMethodDrillRunState& MethodDrillRunState
|
||||
)
|
||||
{
|
||||
return UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
|
||||
BuildSessionFromSummary(RunSummary, UserId, Mode),
|
||||
RunSummary,
|
||||
FHyperTwistTrainingAttempt(),
|
||||
TemplateLaunchProvenance,
|
||||
CoachPanelState,
|
||||
MethodDrillRunState,
|
||||
FHyperTwistTrainingAnalyticsReportBundle()
|
||||
);
|
||||
}
|
||||
|
||||
FHyperTwistCoachDashboardTemplateLaunchHistoryEntry BuildTemplateLaunchHistoryEntry(
|
||||
const FHyperTwistTrainingRunRecord& RunRecord
|
||||
)
|
||||
|
|
@ -14345,6 +14406,24 @@ UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() con
|
|||
const FString ActiveCaseId = CachedRunState.CurrentSelection.TrainingCase.CaseId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: CachedRunState.CurrentSelection.TrainingCase.CaseId;
|
||||
const FHyperTwistTrainingAnalyticsReportBundle AnalyticsReportBundle =
|
||||
UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingAnalyticsReportBundle(
|
||||
const_cast<UHyperTwistCoachDashboardWidget*>(this)
|
||||
);
|
||||
const FHyperTwistTrainingSessionDomainBundle SessionDomainBundle =
|
||||
HyperTwistCoachDashboardWidgetInternal::BuildDisplayedSessionDomainBundle(
|
||||
CachedRunState,
|
||||
CachedRunSummary,
|
||||
CachedRunState.TemplateLaunchProvenance,
|
||||
CachedCoachPanelState,
|
||||
CachedMethodDrillRunState,
|
||||
AnalyticsReportBundle
|
||||
);
|
||||
const FHyperTwistTrainingAnalyticsPanelContractBundle AnalyticsPanelBundle =
|
||||
UHyperTwistTrainingAnalyticsLibrary::BuildAnalyticsPanelContractBundle(
|
||||
SessionDomainBundle,
|
||||
AnalyticsReportBundle
|
||||
);
|
||||
|
||||
Surface.TrainingSessionId = CachedRunState.Session.TrainingSessionId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
|
|
@ -14353,6 +14432,15 @@ UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() con
|
|||
HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState);
|
||||
Surface.DeliveryModeLabel =
|
||||
HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode);
|
||||
Surface.SessionKindLabel = SessionDomainBundle.GameSession.SessionKindLabel.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: SessionDomainBundle.GameSession.SessionKindLabel;
|
||||
Surface.GameSessionId = SessionDomainBundle.GameSession.GameSessionId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: SessionDomainBundle.GameSession.GameSessionId;
|
||||
Surface.PrimaryAnalyticsPanelId = AnalyticsPanelBundle.PrimaryPanelId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: AnalyticsPanelBundle.PrimaryPanelId;
|
||||
Surface.ActiveDeckLabel = ActiveDeckLabel;
|
||||
Surface.ActiveCaseId = ActiveCaseId;
|
||||
Surface.LastCaseId = CachedRunSummary.LastCaseId.IsEmpty()
|
||||
|
|
@ -14367,6 +14455,7 @@ UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() con
|
|||
Surface.DNFCount = CachedRunSummary.DNFCount;
|
||||
Surface.BestTotalTimeMs = CachedRunSummary.BestTotalTimeMs;
|
||||
Surface.AverageTotalTimeMs = CachedRunSummary.AverageTotalTimeMs;
|
||||
Surface.VisibleAnalyticsPanelCount = AnalyticsPanelBundle.Panels.Num();
|
||||
Surface.bHasActiveRun = bHasActiveRun;
|
||||
|
||||
const bool bQueueRecoveryMemorySource =
|
||||
|
|
@ -14383,20 +14472,22 @@ UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() con
|
|||
? DisplayedActiveRunStatusLine
|
||||
: (bHasActiveRun
|
||||
? FString::Printf(
|
||||
TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"),
|
||||
TEXT("Session: %s | Kind: %s | Game: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d | Panel: %s"),
|
||||
*Surface.TrainingSessionId,
|
||||
*Surface.SessionStateLabel,
|
||||
*Surface.SessionKindLabel,
|
||||
*Surface.GameSessionId,
|
||||
*Surface.ActiveDeckLabel,
|
||||
*Surface.DeliveryModeLabel,
|
||||
*Surface.ActiveCaseId,
|
||||
Surface.RemainingCaseCount)
|
||||
Surface.RemainingCaseCount,
|
||||
*Surface.PrimaryAnalyticsPanelId)
|
||||
: (Surface.bClosedLoopIdleDashboard
|
||||
? TEXT("No active run. The coach-to-review loop is closed and idle.")
|
||||
: TEXT("No active run. Queue and coach guidance are ready for launch from this screen.")));
|
||||
Surface.SummaryLine = !DisplayedActiveRunSummaryLine.IsEmpty()
|
||||
? DisplayedActiveRunSummaryLine
|
||||
: FString::Printf(
|
||||
TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s"),
|
||||
TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s | Panels: %d"),
|
||||
Surface.AttemptCount,
|
||||
Surface.CompletedAttemptCount,
|
||||
Surface.SuccessCount,
|
||||
|
|
@ -14405,7 +14496,8 @@ UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() con
|
|||
Surface.DNFCount,
|
||||
Surface.BestTotalTimeMs,
|
||||
Surface.AverageTotalTimeMs,
|
||||
*Surface.LastCaseId);
|
||||
*Surface.LastCaseId,
|
||||
Surface.VisibleAnalyticsPanelCount);
|
||||
return Surface;
|
||||
}
|
||||
|
||||
|
|
@ -14461,6 +14553,36 @@ UHyperTwistCoachDashboardWidget::GetDisplayedRunRecapStatusInspectSurface() cons
|
|||
: (bHasDisplayedRunRecap && !DisplayedRunRecap->DeckId.IsEmpty()
|
||||
? DisplayedRunRecap->DeckId
|
||||
: TEXT("n/a"));
|
||||
const FHyperTwistTrainingAnalyticsReportBundle AnalyticsReportBundle =
|
||||
bHasCurrentRunRecap
|
||||
? UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingAnalyticsReportBundle(
|
||||
const_cast<UHyperTwistCoachDashboardWidget*>(this))
|
||||
: FHyperTwistTrainingAnalyticsReportBundle();
|
||||
const FHyperTwistTrainingSessionDomainBundle SessionDomainBundle =
|
||||
bHasCurrentRunRecap
|
||||
? HyperTwistCoachDashboardWidgetInternal::BuildDisplayedSessionDomainBundle(
|
||||
CachedRunState,
|
||||
CachedRunSummary,
|
||||
CachedRunState.TemplateLaunchProvenance,
|
||||
CachedCoachPanelState,
|
||||
CachedMethodDrillRunState,
|
||||
AnalyticsReportBundle)
|
||||
: (bHasDisplayedRunRecap
|
||||
? HyperTwistCoachDashboardWidgetInternal::BuildDisplayedRecapSessionDomainBundle(
|
||||
*DisplayedRunRecap,
|
||||
bHasCurrentRunRecap ? CachedRunState.Session.UserId : RetainedRunRecapUserId,
|
||||
DisplayedRunRecapMode,
|
||||
DisplayedTemplateLaunch != nullptr
|
||||
? *DisplayedTemplateLaunch
|
||||
: FHyperTwistTrainingTemplateLaunchProvenance(),
|
||||
CachedCoachPanelState,
|
||||
CachedMethodDrillRunState)
|
||||
: FHyperTwistTrainingSessionDomainBundle());
|
||||
const FHyperTwistTrainingAnalyticsPanelContractBundle AnalyticsPanelBundle =
|
||||
UHyperTwistTrainingAnalyticsLibrary::BuildAnalyticsPanelContractBundle(
|
||||
SessionDomainBundle,
|
||||
AnalyticsReportBundle
|
||||
);
|
||||
|
||||
Surface.SourceLabel = RunRecapSourceLabel;
|
||||
Surface.TrainingSessionId = bHasDisplayedRunRecap
|
||||
|
|
@ -14476,6 +14598,15 @@ UHyperTwistCoachDashboardWidget::GetDisplayedRunRecapStatusInspectSurface() cons
|
|||
Surface.DeliveryModeLabel = bHasDisplayedRunRecap
|
||||
? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecapMode)
|
||||
: TEXT("n/a");
|
||||
Surface.SessionKindLabel = SessionDomainBundle.GameSession.SessionKindLabel.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: SessionDomainBundle.GameSession.SessionKindLabel;
|
||||
Surface.GameSessionId = SessionDomainBundle.GameSession.GameSessionId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: SessionDomainBundle.GameSession.GameSessionId;
|
||||
Surface.PrimaryAnalyticsPanelId = AnalyticsPanelBundle.PrimaryPanelId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: AnalyticsPanelBundle.PrimaryPanelId;
|
||||
Surface.CompletedAtUtc = bHasDisplayedRunRecap && !DisplayedRunRecap->LastCompletedAtUtc.IsEmpty()
|
||||
? DisplayedRunRecap->LastCompletedAtUtc
|
||||
: TEXT("n/a");
|
||||
|
|
@ -14499,28 +14630,33 @@ UHyperTwistCoachDashboardWidget::GetDisplayedRunRecapStatusInspectSurface() cons
|
|||
|| DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::ReviewPlan
|
||||
|| DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::Coach);
|
||||
Surface.bHasActiveRun = bHasActiveRun;
|
||||
Surface.VisibleAnalyticsPanelCount = AnalyticsPanelBundle.Panels.Num();
|
||||
|
||||
Surface.StatusLine = !DisplayedRunRecapStatusLine.IsEmpty()
|
||||
? DisplayedRunRecapStatusLine
|
||||
: (bHasDisplayedRunRecap
|
||||
? (DisplayedTemplateLaunch != nullptr
|
||||
? FString::Printf(
|
||||
TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s | Template: %s (%s) | Source plan: %s"),
|
||||
TEXT("Source: %s | Session: %s | Kind: %s | Game: %s | Deck: %s | Completed at: %s | Template: %s (%s) | Source plan: %s | Panel: %s"),
|
||||
*Surface.SourceLabel,
|
||||
*Surface.TrainingSessionId,
|
||||
*Surface.SessionKindLabel,
|
||||
*Surface.GameSessionId,
|
||||
*Surface.DeckId,
|
||||
*Surface.SessionStateLabel,
|
||||
*Surface.CompletedAtUtc,
|
||||
*Surface.TemplateId,
|
||||
*Surface.TemplateKindLabel,
|
||||
*Surface.TemplateSourcePlanId)
|
||||
*Surface.TemplateSourcePlanId,
|
||||
*Surface.PrimaryAnalyticsPanelId)
|
||||
: FString::Printf(
|
||||
TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s"),
|
||||
TEXT("Source: %s | Session: %s | Kind: %s | Game: %s | Deck: %s | Completed at: %s | Panel: %s"),
|
||||
*Surface.SourceLabel,
|
||||
*Surface.TrainingSessionId,
|
||||
*Surface.SessionKindLabel,
|
||||
*Surface.GameSessionId,
|
||||
*Surface.DeckId,
|
||||
*Surface.SessionStateLabel,
|
||||
*Surface.CompletedAtUtc))
|
||||
*Surface.CompletedAtUtc,
|
||||
*Surface.PrimaryAnalyticsPanelId))
|
||||
: (bHasActiveRun
|
||||
? TEXT("Source: active run | recap will stabilize here when the run completes or aborts.")
|
||||
: TEXT("Source: none yet | complete a run to retain a session recap on this dashboard.")));
|
||||
|
|
|
|||
|
|
@ -269,6 +269,244 @@ namespace HyperTwistTrainingAnalyticsLibraryInternal
|
|||
Bundle.Reports.Add(Report);
|
||||
}
|
||||
|
||||
FString ResolveSessionKindLabel(const EHyperTwistTrainingSessionKind SessionKind)
|
||||
{
|
||||
const UEnum* SessionKindEnum = StaticEnum<EHyperTwistTrainingSessionKind>();
|
||||
return SessionKindEnum != nullptr
|
||||
? SessionKindEnum->GetDisplayNameTextByValue(static_cast<int64>(SessionKind)).ToString()
|
||||
: TEXT("Unknown");
|
||||
}
|
||||
|
||||
bool SessionUsesLiveCoachOverlay(const FHyperTwistTrainingSession& Session)
|
||||
{
|
||||
return Session.SessionState == EHyperTwistTrainingSessionState::Ready
|
||||
|| Session.SessionState == EHyperTwistTrainingSessionState::Active
|
||||
|| Session.SessionState == EHyperTwistTrainingSessionState::Paused
|
||||
|| Session.SessionState == EHyperTwistTrainingSessionState::Reviewing;
|
||||
}
|
||||
|
||||
EHyperTwistTrainingSessionKind ResolveSessionKind(
|
||||
const FHyperTwistTrainingSession& Session,
|
||||
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance,
|
||||
const FHyperTwistTrainingCoachPanelState& CoachPanelState,
|
||||
const FHyperTwistTrainingMethodDrillRunState& MethodDrillRunState
|
||||
)
|
||||
{
|
||||
if (!Session.IsStructurallyValid())
|
||||
{
|
||||
return EHyperTwistTrainingSessionKind::None;
|
||||
}
|
||||
|
||||
if (TemplateLaunchProvenance.IsStructurallyValid())
|
||||
{
|
||||
const bool bMethodDrillFollowUp =
|
||||
MethodDrillRunState.IsStructurallyValid()
|
||||
&& !TemplateLaunchProvenance.MethodSegmentId.IsEmpty()
|
||||
&& TemplateLaunchProvenance.MethodSegmentId
|
||||
== MethodDrillRunState.DrillDefinition.MethodDescriptor.SegmentId;
|
||||
if (bMethodDrillFollowUp)
|
||||
{
|
||||
return EHyperTwistTrainingSessionKind::MethodDrillFollowUp;
|
||||
}
|
||||
if (!TemplateLaunchProvenance.SourceReviewPlanId.IsEmpty()
|
||||
|| TemplateLaunchProvenance.bPreferReviewRun
|
||||
|| TemplateLaunchProvenance.bCoachReview)
|
||||
{
|
||||
return EHyperTwistTrainingSessionKind::ReviewRun;
|
||||
}
|
||||
|
||||
return EHyperTwistTrainingSessionKind::TemplateLaunch;
|
||||
}
|
||||
|
||||
if (SessionUsesLiveCoachOverlay(Session))
|
||||
{
|
||||
if (!CoachPanelState.ActiveQueueEntryId.IsEmpty())
|
||||
{
|
||||
return EHyperTwistTrainingSessionKind::CoachQueue;
|
||||
}
|
||||
if (CoachPanelState.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp)
|
||||
{
|
||||
return EHyperTwistTrainingSessionKind::CoachFollowUp;
|
||||
}
|
||||
if (CoachPanelState.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::Recommended)
|
||||
{
|
||||
return EHyperTwistTrainingSessionKind::CoachRecommended;
|
||||
}
|
||||
}
|
||||
|
||||
if (Session.Mode == EHyperTwistTrainingDeliveryMode::CoachReviewed)
|
||||
{
|
||||
return EHyperTwistTrainingSessionKind::ReviewRun;
|
||||
}
|
||||
|
||||
return EHyperTwistTrainingSessionKind::DirectRun;
|
||||
}
|
||||
|
||||
FString BuildGameSessionId(
|
||||
const EHyperTwistTrainingSessionKind SessionKind,
|
||||
const FString& TrainingSessionId
|
||||
)
|
||||
{
|
||||
if (TrainingSessionId.IsEmpty())
|
||||
{
|
||||
return TEXT("");
|
||||
}
|
||||
|
||||
switch (SessionKind)
|
||||
{
|
||||
case EHyperTwistTrainingSessionKind::ReviewRun:
|
||||
return FString::Printf(TEXT("game-session/review/%s"), *TrainingSessionId);
|
||||
case EHyperTwistTrainingSessionKind::CoachRecommended:
|
||||
return FString::Printf(TEXT("game-session/coach-recommended/%s"), *TrainingSessionId);
|
||||
case EHyperTwistTrainingSessionKind::CoachFollowUp:
|
||||
return FString::Printf(TEXT("game-session/coach-follow-up/%s"), *TrainingSessionId);
|
||||
case EHyperTwistTrainingSessionKind::CoachQueue:
|
||||
return FString::Printf(TEXT("game-session/coach-queue/%s"), *TrainingSessionId);
|
||||
case EHyperTwistTrainingSessionKind::TemplateLaunch:
|
||||
return FString::Printf(TEXT("game-session/template/%s"), *TrainingSessionId);
|
||||
case EHyperTwistTrainingSessionKind::MethodDrillFollowUp:
|
||||
return FString::Printf(TEXT("game-session/method-drill-follow-up/%s"), *TrainingSessionId);
|
||||
case EHyperTwistTrainingSessionKind::DirectRun:
|
||||
return FString::Printf(TEXT("game-session/training/%s"), *TrainingSessionId);
|
||||
case EHyperTwistTrainingSessionKind::None:
|
||||
default:
|
||||
return FString();
|
||||
}
|
||||
}
|
||||
|
||||
FString ResolveGameSessionSourceLabel(
|
||||
const EHyperTwistTrainingSessionKind SessionKind,
|
||||
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance,
|
||||
const FHyperTwistTrainingCoachPanelState& CoachPanelState
|
||||
)
|
||||
{
|
||||
if (SessionKind == EHyperTwistTrainingSessionKind::CoachQueue)
|
||||
{
|
||||
return !CoachPanelState.ActiveQueueEntryId.IsEmpty()
|
||||
? CoachPanelState.ActiveQueueEntryId
|
||||
: CoachPanelState.NextQueueSourceLabel;
|
||||
}
|
||||
if (SessionKind == EHyperTwistTrainingSessionKind::CoachFollowUp
|
||||
|| SessionKind == EHyperTwistTrainingSessionKind::CoachRecommended)
|
||||
{
|
||||
return CoachPanelState.PreferredGuidanceSourceLabel;
|
||||
}
|
||||
if (TemplateLaunchProvenance.IsStructurallyValid())
|
||||
{
|
||||
if (!TemplateLaunchProvenance.GuidanceLabel.IsEmpty())
|
||||
{
|
||||
return TemplateLaunchProvenance.GuidanceLabel;
|
||||
}
|
||||
if (!TemplateLaunchProvenance.SourceReviewPlanId.IsEmpty())
|
||||
{
|
||||
return TemplateLaunchProvenance.SourceReviewPlanId;
|
||||
}
|
||||
return TemplateLaunchProvenance.TemplateId;
|
||||
}
|
||||
if (!CoachPanelState.PreferredLaunchBudgetSourceLabel.IsEmpty())
|
||||
{
|
||||
return CoachPanelState.PreferredLaunchBudgetSourceLabel;
|
||||
}
|
||||
|
||||
return TEXT("training-session");
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSolveSession ResolveSolveSession(
|
||||
const FHyperTwistTrainingSession& Session,
|
||||
const FHyperTwistTrainingAttempt& SolveAttempt,
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& ReportBundle
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSolveSession SolveSession;
|
||||
if (SolveAttempt.IsStructurallyValid())
|
||||
{
|
||||
SolveSession.AttemptId = SolveAttempt.AttemptId;
|
||||
SolveSession.TrainingSessionId = SolveAttempt.TrainingSessionId;
|
||||
SolveSession.CaseId = SolveAttempt.CaseId;
|
||||
SolveSession.RecordedAtUtc = SolveAttempt.CompletedAtUtc;
|
||||
SolveSession.DeliveryMode = Session.Mode;
|
||||
SolveSession.Result = SolveAttempt.Result;
|
||||
SolveSession.TotalTimeMs = SolveAttempt.TotalTimeMs;
|
||||
SolveSession.ExecutionTimeMs = SolveAttempt.ExecutionTimeMs;
|
||||
SolveSession.RecognitionTimeMs = SolveAttempt.RecognitionTimeMs;
|
||||
SolveSession.ReplayId = SolveAttempt.ReplayId;
|
||||
SolveSession.bLiveTimerBacked = Session.Mode == EHyperTwistTrainingDeliveryMode::Timer;
|
||||
return SolveSession;
|
||||
}
|
||||
|
||||
if (ReportBundle.LiveTimerState.IsStructurallyValid() && !ReportBundle.LiveTimerState.CaseId.IsEmpty())
|
||||
{
|
||||
SolveSession.TrainingSessionId = ReportBundle.LiveTimerState.TrainingSessionId;
|
||||
SolveSession.CaseId = ReportBundle.LiveTimerState.CaseId;
|
||||
SolveSession.RecordedAtUtc = ReportBundle.LiveTimerState.LastUpdatedAtUtc;
|
||||
SolveSession.DeliveryMode = Session.Mode;
|
||||
SolveSession.TotalTimeMs = ReportBundle.LiveTimerState.SolveElapsedMs;
|
||||
SolveSession.ExecutionTimeMs = ReportBundle.LiveTimerState.SolveElapsedMs;
|
||||
SolveSession.bLiveTimerBacked = true;
|
||||
}
|
||||
|
||||
return SolveSession;
|
||||
}
|
||||
|
||||
FString ResolvePanelId(const FString& ReportId)
|
||||
{
|
||||
if (ReportId == TEXT("analytics/session-outcome-balance"))
|
||||
{
|
||||
return TEXT("panel/training-session-overview");
|
||||
}
|
||||
if (ReportId == TEXT("analytics/deck-history-progress"))
|
||||
{
|
||||
return TEXT("panel/session-history-progress");
|
||||
}
|
||||
if (ReportId == TEXT("analytics/timing-trend-rolling-windows"))
|
||||
{
|
||||
return TEXT("panel/timing-trend");
|
||||
}
|
||||
if (ReportId == TEXT("analytics/personal-best-benchmark"))
|
||||
{
|
||||
return TEXT("panel/personal-best");
|
||||
}
|
||||
if (ReportId == TEXT("analytics/current-case-vs-deck"))
|
||||
{
|
||||
return TEXT("panel/current-case-comparison");
|
||||
}
|
||||
if (ReportId == TEXT("analytics/live-split-ladder"))
|
||||
{
|
||||
return TEXT("panel/live-split-ladder");
|
||||
}
|
||||
|
||||
FString NormalizedReportId = ReportId;
|
||||
NormalizedReportId.ReplaceInline(TEXT("analytics/"), TEXT(""));
|
||||
NormalizedReportId.ReplaceInline(TEXT("_"), TEXT("-"));
|
||||
return FString::Printf(TEXT("panel/%s"), *NormalizedReportId);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingAnalyticsPanelContract BuildPanelContract(
|
||||
const FHyperTwistTrainingAnalyticsReportDefinition& Report,
|
||||
const FHyperTwistTrainingSessionDomainBundle& SessionDomainBundle,
|
||||
const FString& PrimaryReportId
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingAnalyticsPanelContract Contract;
|
||||
if (!Report.IsStructurallyValid())
|
||||
{
|
||||
return Contract;
|
||||
}
|
||||
|
||||
Contract.PanelId = ResolvePanelId(Report.ReportId);
|
||||
Contract.ReportId = Report.ReportId;
|
||||
Contract.Title = Report.Title;
|
||||
Contract.Summary = Report.Summary;
|
||||
Contract.VisualizationKindLabel =
|
||||
StaticEnum<EHyperTwistTrainingAnalyticsVisualizationKind>()->GetDisplayNameTextByValue(
|
||||
static_cast<int64>(Report.VisualizationKind)
|
||||
).ToString();
|
||||
Contract.SessionKindLabel = SessionDomainBundle.GameSession.SessionKindLabel;
|
||||
Contract.GameSessionId = SessionDomainBundle.GameSession.GameSessionId;
|
||||
Contract.bPrimaryPanel = Report.ReportId == PrimaryReportId;
|
||||
return Contract;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingAnalyticsReportDefinition BuildSessionOutcomeReport(
|
||||
const FHyperTwistTrainingSessionSummary& SessionSummary,
|
||||
const FString& DeckId
|
||||
|
|
@ -705,6 +943,76 @@ namespace HyperTwistTrainingAnalyticsLibraryInternal
|
|||
}
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSessionDomainBundle UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
|
||||
const FHyperTwistTrainingSession& Session,
|
||||
const FHyperTwistTrainingSessionSummary& TrainingSession,
|
||||
const FHyperTwistTrainingAttempt& SolveAttempt,
|
||||
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance,
|
||||
const FHyperTwistTrainingCoachPanelState& CoachPanelState,
|
||||
const FHyperTwistTrainingMethodDrillRunState& MethodDrillRunState,
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& ReportBundle
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingSessionDomainBundle Bundle;
|
||||
Bundle.Session = Session;
|
||||
Bundle.TrainingSession = TrainingSession;
|
||||
Bundle.SessionKind = HyperTwistTrainingAnalyticsLibraryInternal::ResolveSessionKind(
|
||||
Session,
|
||||
TemplateLaunchProvenance,
|
||||
CoachPanelState,
|
||||
MethodDrillRunState
|
||||
);
|
||||
Bundle.SolveSession = HyperTwistTrainingAnalyticsLibraryInternal::ResolveSolveSession(
|
||||
Session,
|
||||
SolveAttempt,
|
||||
ReportBundle
|
||||
);
|
||||
Bundle.bHasSolveSession = Bundle.SolveSession.IsStructurallyValid();
|
||||
|
||||
Bundle.GameSession.GameSessionId = HyperTwistTrainingAnalyticsLibraryInternal::BuildGameSessionId(
|
||||
Bundle.SessionKind,
|
||||
TrainingSession.TrainingSessionId.IsEmpty()
|
||||
? Session.TrainingSessionId
|
||||
: TrainingSession.TrainingSessionId
|
||||
);
|
||||
Bundle.GameSession.TrainingSessionId = TrainingSession.TrainingSessionId.IsEmpty()
|
||||
? Session.TrainingSessionId
|
||||
: TrainingSession.TrainingSessionId;
|
||||
Bundle.GameSession.UserId = !TrainingSession.UserId.IsEmpty() ? TrainingSession.UserId : Session.UserId;
|
||||
Bundle.GameSession.DeckId = !TrainingSession.DeckId.IsEmpty() ? TrainingSession.DeckId : Session.DeckId;
|
||||
Bundle.GameSession.TemplateId = TemplateLaunchProvenance.TemplateId;
|
||||
Bundle.GameSession.QueueEntryId = CoachPanelState.ActiveQueueEntryId;
|
||||
Bundle.GameSession.SourceLabel = HyperTwistTrainingAnalyticsLibraryInternal::ResolveGameSessionSourceLabel(
|
||||
Bundle.SessionKind,
|
||||
TemplateLaunchProvenance,
|
||||
CoachPanelState
|
||||
);
|
||||
Bundle.GameSession.SessionKind = Bundle.SessionKind;
|
||||
Bundle.GameSession.SessionKindLabel =
|
||||
HyperTwistTrainingAnalyticsLibraryInternal::ResolveSessionKindLabel(Bundle.SessionKind);
|
||||
Bundle.GameSession.PrimaryAnalyticsReportId = ReportBundle.PrimaryReportId;
|
||||
Bundle.GameSession.StartedAtUtc = Session.StartedAtUtc;
|
||||
Bundle.GameSession.CompletedAtUtc = !TrainingSession.LastCompletedAtUtc.IsEmpty()
|
||||
? TrainingSession.LastCompletedAtUtc
|
||||
: Session.EndedAtUtc;
|
||||
Bundle.GameSession.bCoachOrchestrated =
|
||||
Bundle.SessionKind == EHyperTwistTrainingSessionKind::CoachRecommended
|
||||
|| Bundle.SessionKind == EHyperTwistTrainingSessionKind::CoachFollowUp
|
||||
|| Bundle.SessionKind == EHyperTwistTrainingSessionKind::CoachQueue;
|
||||
Bundle.GameSession.bTemplateLaunched = TemplateLaunchProvenance.IsStructurallyValid();
|
||||
Bundle.GameSession.bReviewBacked = !TemplateLaunchProvenance.SourceReviewPlanId.IsEmpty()
|
||||
|| TemplateLaunchProvenance.bPreferReviewRun
|
||||
|| TemplateLaunchProvenance.bCoachReview
|
||||
|| Bundle.SessionKind == EHyperTwistTrainingSessionKind::ReviewRun;
|
||||
Bundle.GameSession.bHasLinkedMethodDrill =
|
||||
Bundle.SessionKind == EHyperTwistTrainingSessionKind::MethodDrillFollowUp
|
||||
|| (!TemplateLaunchProvenance.MethodSegmentId.IsEmpty()
|
||||
&& MethodDrillRunState.IsStructurallyValid()
|
||||
&& TemplateLaunchProvenance.MethodSegmentId
|
||||
== MethodDrillRunState.DrillDefinition.MethodDescriptor.SegmentId);
|
||||
return Bundle;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingAnalyticsReportBundle UHyperTwistTrainingAnalyticsLibrary::BuildActiveTrainingAnalyticsReportBundle(
|
||||
const FHyperTwistTrainingSessionSummary& SessionSummary,
|
||||
const FHyperTwistTrainingDeckHistorySummary& DeckHistorySummary,
|
||||
|
|
@ -792,6 +1100,40 @@ FHyperTwistTrainingAnalyticsReportBundle UHyperTwistTrainingAnalyticsLibrary::Bu
|
|||
return Bundle;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingAnalyticsPanelContractBundle UHyperTwistTrainingAnalyticsLibrary::BuildAnalyticsPanelContractBundle(
|
||||
const FHyperTwistTrainingSessionDomainBundle& SessionDomainBundle,
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& ReportBundle
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingAnalyticsPanelContractBundle Bundle;
|
||||
Bundle.PrimaryReportId = ReportBundle.PrimaryReportId;
|
||||
Bundle.SessionKindLabel = SessionDomainBundle.GameSession.SessionKindLabel;
|
||||
Bundle.GameSessionId = SessionDomainBundle.GameSession.GameSessionId;
|
||||
|
||||
for (const FHyperTwistTrainingAnalyticsReportDefinition& Report : ReportBundle.Reports)
|
||||
{
|
||||
const FHyperTwistTrainingAnalyticsPanelContract Contract =
|
||||
HyperTwistTrainingAnalyticsLibraryInternal::BuildPanelContract(
|
||||
Report,
|
||||
SessionDomainBundle,
|
||||
ReportBundle.PrimaryReportId
|
||||
);
|
||||
if (!Contract.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Bundle.PrimaryPanelId.IsEmpty() || Contract.bPrimaryPanel)
|
||||
{
|
||||
Bundle.PrimaryPanelId = Contract.PanelId;
|
||||
}
|
||||
|
||||
Bundle.Panels.Add(Contract);
|
||||
}
|
||||
|
||||
return Bundle;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingAnalyticsLibrary::TryGetReportById(
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& Bundle,
|
||||
const FString& ReportId,
|
||||
|
|
|
|||
|
|
@ -680,6 +680,47 @@ FHyperTwistTrainingAnalyticsReportBundle UHyperTwistTrainingRuntimeLibrary::GetA
|
|||
return FHyperTwistTrainingAnalyticsReportBundle();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingSessionDomainBundle UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingSessionDomainBundle(
|
||||
UObject* WorldContextObject
|
||||
)
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
|
||||
{
|
||||
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->GetActiveRunState();
|
||||
const FHyperTwistTrainingAnalyticsReportBundle ReportBundle = GetActiveTrainingAnalyticsReportBundle(
|
||||
WorldContextObject
|
||||
);
|
||||
const FHyperTwistTrainingAttempt SolveAttempt =
|
||||
RunState.Attempts.Num() > 0 ? RunState.Attempts.Last() : FHyperTwistTrainingAttempt();
|
||||
return UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
|
||||
RunState.Session,
|
||||
TrainingSubsystem->GetActiveRunSummary(),
|
||||
SolveAttempt,
|
||||
RunState.TemplateLaunchProvenance,
|
||||
TrainingSubsystem->GetActiveCoachPanelState(),
|
||||
TrainingSubsystem->GetActiveMethodDrillRunState(),
|
||||
ReportBundle
|
||||
);
|
||||
}
|
||||
|
||||
return FHyperTwistTrainingSessionDomainBundle();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingAnalyticsPanelContractBundle
|
||||
UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingAnalyticsPanelContractBundle(UObject* WorldContextObject)
|
||||
{
|
||||
const FHyperTwistTrainingAnalyticsReportBundle ReportBundle = GetActiveTrainingAnalyticsReportBundle(
|
||||
WorldContextObject
|
||||
);
|
||||
const FHyperTwistTrainingSessionDomainBundle SessionDomainBundle = GetActiveTrainingSessionDomainBundle(
|
||||
WorldContextObject
|
||||
);
|
||||
return UHyperTwistTrainingAnalyticsLibrary::BuildAnalyticsPanelContractBundle(
|
||||
SessionDomainBundle,
|
||||
ReportBundle
|
||||
);
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingRuntimeLibrary::TryGetActiveTrainingAnalyticsReport(
|
||||
UObject* WorldContextObject,
|
||||
const FString& ReportId,
|
||||
|
|
|
|||
|
|
@ -222,6 +222,17 @@ class UNREALHYPERTWIST_API UHyperTwistTrainingAnalyticsLibrary : public UBluepri
|
|||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Analytics")
|
||||
static FHyperTwistTrainingSessionDomainBundle BuildSessionDomainBundle(
|
||||
const FHyperTwistTrainingSession& Session,
|
||||
const FHyperTwistTrainingSessionSummary& TrainingSession,
|
||||
const FHyperTwistTrainingAttempt& SolveAttempt,
|
||||
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance,
|
||||
const FHyperTwistTrainingCoachPanelState& CoachPanelState,
|
||||
const FHyperTwistTrainingMethodDrillRunState& MethodDrillRunState,
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& ReportBundle
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Analytics")
|
||||
static FHyperTwistTrainingAnalyticsReportBundle BuildActiveTrainingAnalyticsReportBundle(
|
||||
const FHyperTwistTrainingSessionSummary& SessionSummary,
|
||||
|
|
@ -235,6 +246,12 @@ public:
|
|||
const FHyperTwistRecognitionReplaySummary& RecognitionReplaySummary
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Analytics")
|
||||
static FHyperTwistTrainingAnalyticsPanelContractBundle BuildAnalyticsPanelContractBundle(
|
||||
const FHyperTwistTrainingSessionDomainBundle& SessionDomainBundle,
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& ReportBundle
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Analytics")
|
||||
static bool TryGetReportById(
|
||||
const FHyperTwistTrainingAnalyticsReportBundle& Bundle,
|
||||
|
|
|
|||
|
|
@ -233,6 +233,16 @@ public:
|
|||
UObject* WorldContextObject
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Analytics", meta = (WorldContext = "WorldContextObject"))
|
||||
static FHyperTwistTrainingSessionDomainBundle GetActiveTrainingSessionDomainBundle(
|
||||
UObject* WorldContextObject
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Analytics", meta = (WorldContext = "WorldContextObject"))
|
||||
static FHyperTwistTrainingAnalyticsPanelContractBundle GetActiveTrainingAnalyticsPanelContractBundle(
|
||||
UObject* WorldContextObject
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Analytics", meta = (WorldContext = "WorldContextObject"))
|
||||
static bool TryGetActiveTrainingAnalyticsReport(
|
||||
UObject* WorldContextObject,
|
||||
|
|
|
|||
|
|
@ -150,6 +150,19 @@ enum class EHyperTwistTrainingSessionState : uint8
|
|||
Aborted UMETA(DisplayName = "Aborted")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistTrainingSessionKind : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "None"),
|
||||
DirectRun UMETA(DisplayName = "Direct Run"),
|
||||
ReviewRun UMETA(DisplayName = "Review Run"),
|
||||
CoachRecommended UMETA(DisplayName = "Coach Recommended"),
|
||||
CoachFollowUp UMETA(DisplayName = "Coach Follow-Up"),
|
||||
CoachQueue UMETA(DisplayName = "Coach Queue"),
|
||||
TemplateLaunch UMETA(DisplayName = "Template Launch"),
|
||||
MethodDrillFollowUp UMETA(DisplayName = "Method Drill Follow-Up")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistTrainingAttemptResult : uint8
|
||||
{
|
||||
|
|
@ -1872,6 +1885,202 @@ struct FHyperTwistTrainingRunRecord
|
|||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingSolveSession
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString AttemptId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString TrainingSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString CaseId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString RecordedAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingDeliveryMode DeliveryMode = EHyperTwistTrainingDeliveryMode::Timer;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingAttemptResult Result = EHyperTwistTrainingAttemptResult::Failure;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 ExecutionTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 RecognitionTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString ReplayId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bLiveTimerBacked = false;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !TrainingSessionId.IsEmpty() && !CaseId.IsEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingGameSessionSummary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString GameSessionId;
|
||||
|
||||
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 TemplateId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString QueueEntryId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SourceLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SessionKindLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString PrimaryAnalyticsReportId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString StartedAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString CompletedAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSessionKind SessionKind = EHyperTwistTrainingSessionKind::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bCoachOrchestrated = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bTemplateLaunched = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bReviewBacked = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bHasLinkedMethodDrill = false;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !GameSessionId.IsEmpty() && !TrainingSessionId.IsEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingSessionDomainBundle
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSessionKind SessionKind = EHyperTwistTrainingSessionKind::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingSolveSession SolveSession;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingSession Session;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingSessionSummary TrainingSession;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingGameSessionSummary GameSession;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bHasSolveSession = false;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return Session.IsStructurallyValid()
|
||||
|| !TrainingSession.TrainingSessionId.IsEmpty()
|
||||
|| GameSession.IsStructurallyValid()
|
||||
|| bHasSolveSession;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingAnalyticsPanelContract
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString PanelId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString ReportId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString Title;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString Summary;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString VisualizationKindLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SessionKindLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString GameSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bPrimaryPanel = false;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !PanelId.IsEmpty() && !ReportId.IsEmpty() && !Title.IsEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingAnalyticsPanelContractBundle
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString PrimaryPanelId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString PrimaryReportId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SessionKindLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString GameSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<FHyperTwistTrainingAnalyticsPanelContract> Panels;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !PrimaryPanelId.IsEmpty() && Panels.Num() > 0;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingDeckHistorySummary
|
||||
{
|
||||
|
|
@ -16636,6 +16845,15 @@ struct FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeliveryModeLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SessionKindLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString GameSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString PrimaryAnalyticsPanelId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString ActiveDeckLabel;
|
||||
|
||||
|
|
@ -16672,6 +16890,9 @@ struct FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 AverageTotalTimeMs = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 VisibleAnalyticsPanelCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bHasActiveRun = false;
|
||||
|
||||
|
|
@ -16715,6 +16936,15 @@ struct FHyperTwistTrainingCoachDashboardRunRecapStatusInspectSurface
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DeliveryModeLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SessionKindLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString GameSessionId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString PrimaryAnalyticsPanelId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString CompletedAtUtc;
|
||||
|
||||
|
|
@ -16745,6 +16975,9 @@ struct FHyperTwistTrainingCoachDashboardRunRecapStatusInspectSurface
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bHasActiveRun = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 VisibleAnalyticsPanelCount = 0;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !StatusLine.IsEmpty()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,361 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HyperTwistTraining/HyperTwistTrainingAnalyticsLibrary.h"
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
|
||||
namespace HyperTwistCubeDeskBound2SessionAnalyticsTestInternal
|
||||
{
|
||||
const FHyperTwistTrainingAnalyticsPanelContract* FindPanelById(
|
||||
const FHyperTwistTrainingAnalyticsPanelContractBundle& Bundle,
|
||||
const FString& PanelId
|
||||
)
|
||||
{
|
||||
for (const FHyperTwistTrainingAnalyticsPanelContract& Panel : Bundle.Panels)
|
||||
{
|
||||
if (Panel.PanelId == PanelId)
|
||||
{
|
||||
return &Panel;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistCubeDeskBound2SessionDomainTest,
|
||||
"HyperTwist.CleanRoom.CubeDesk.Bound2.SessionDomain",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistCubeDeskBound2SessionDomainTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FHyperTwistTrainingSession Session;
|
||||
Session.TrainingSessionId = TEXT("bound2-session");
|
||||
Session.DeckId = TEXT("deck/pll");
|
||||
Session.UserId = TEXT("bound2-user");
|
||||
Session.Mode = EHyperTwistTrainingDeliveryMode::Timer;
|
||||
Session.SessionState = EHyperTwistTrainingSessionState::Active;
|
||||
Session.CurrentCaseId = TEXT("pll-aa");
|
||||
Session.AttemptCount = 1;
|
||||
|
||||
FHyperTwistTrainingSessionSummary Summary;
|
||||
Summary.TrainingSessionId = Session.TrainingSessionId;
|
||||
Summary.DeckId = Session.DeckId;
|
||||
Summary.UserId = Session.UserId;
|
||||
Summary.SessionState = Session.SessionState;
|
||||
Summary.AttemptCount = 1;
|
||||
Summary.CompletedAttemptCount = 1;
|
||||
Summary.SuccessCount = 1;
|
||||
Summary.SuccessRate = 100.0f;
|
||||
Summary.LastCaseId = Session.CurrentCaseId;
|
||||
Summary.LastCompletedAtUtc = TEXT("2026-05-19T11:00:00Z");
|
||||
|
||||
FHyperTwistTrainingAttempt SolveAttempt;
|
||||
SolveAttempt.AttemptId = TEXT("attempt-1");
|
||||
SolveAttempt.TrainingSessionId = Session.TrainingSessionId;
|
||||
SolveAttempt.CaseId = Session.CurrentCaseId;
|
||||
SolveAttempt.Result = EHyperTwistTrainingAttemptResult::Success;
|
||||
SolveAttempt.TotalTimeMs = 1789;
|
||||
SolveAttempt.ExecutionTimeMs = 1320;
|
||||
SolveAttempt.RecognitionTimeMs = 469;
|
||||
SolveAttempt.ReplayId = TEXT("replay-1");
|
||||
SolveAttempt.CompletedAtUtc = Summary.LastCompletedAtUtc;
|
||||
|
||||
FHyperTwistTrainingCoachPanelState CoachPanelState;
|
||||
CoachPanelState.PreferredGuidanceLane = EHyperTwistTrainingCoachGuidanceLane::FollowUp;
|
||||
CoachPanelState.PreferredGuidanceSourceLabel = TEXT("follow-up-brief");
|
||||
|
||||
FHyperTwistTrainingAnalyticsReportBundle ReportBundle;
|
||||
ReportBundle.PrimaryReportId = TEXT("analytics/session-outcome-balance");
|
||||
|
||||
const FHyperTwistTrainingSessionDomainBundle FollowUpBundle =
|
||||
UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
|
||||
Session,
|
||||
Summary,
|
||||
SolveAttempt,
|
||||
FHyperTwistTrainingTemplateLaunchProvenance(),
|
||||
CoachPanelState,
|
||||
FHyperTwistTrainingMethodDrillRunState(),
|
||||
ReportBundle
|
||||
);
|
||||
|
||||
TestTrue(TEXT("Follow-up bundle must resolve as a structurally valid session-domain bundle."), FollowUpBundle.IsStructurallyValid());
|
||||
TestEqual(
|
||||
TEXT("Active follow-up sessions must resolve the new owned session kind."),
|
||||
FollowUpBundle.SessionKind,
|
||||
EHyperTwistTrainingSessionKind::CoachFollowUp
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Follow-up game-session ids must stay deterministic from the training session id."),
|
||||
FollowUpBundle.GameSession.GameSessionId,
|
||||
TEXT("game-session/coach-follow-up/bound2-session")
|
||||
);
|
||||
TestTrue(TEXT("Follow-up bundle must expose the solve/session split explicitly."), FollowUpBundle.bHasSolveSession);
|
||||
TestEqual(
|
||||
TEXT("Solve-session attempt ids must survive the domain separation layer."),
|
||||
FollowUpBundle.SolveSession.AttemptId,
|
||||
SolveAttempt.AttemptId
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The primary analytics report id must stay attached to the derived game-session summary."),
|
||||
FollowUpBundle.GameSession.PrimaryAnalyticsReportId,
|
||||
ReportBundle.PrimaryReportId
|
||||
);
|
||||
|
||||
FHyperTwistTrainingTemplateLaunchProvenance ReviewTemplate;
|
||||
ReviewTemplate.TemplateId = TEXT("template-review");
|
||||
ReviewTemplate.SourceReviewPlanId = TEXT("review-plan-1");
|
||||
ReviewTemplate.Title = TEXT("Review Template");
|
||||
ReviewTemplate.FocusDeckId = Session.DeckId;
|
||||
ReviewTemplate.SuggestedMode = Session.Mode;
|
||||
ReviewTemplate.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
|
||||
ReviewTemplate.RecommendedCaseCount = 5;
|
||||
ReviewTemplate.bPreferReviewRun = true;
|
||||
|
||||
const FHyperTwistTrainingSessionDomainBundle ReviewBundle =
|
||||
UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
|
||||
Session,
|
||||
Summary,
|
||||
SolveAttempt,
|
||||
ReviewTemplate,
|
||||
FHyperTwistTrainingCoachPanelState(),
|
||||
FHyperTwistTrainingMethodDrillRunState(),
|
||||
ReportBundle
|
||||
);
|
||||
|
||||
TestEqual(
|
||||
TEXT("Review-backed template launches must not collapse back into a generic training session."),
|
||||
ReviewBundle.SessionKind,
|
||||
EHyperTwistTrainingSessionKind::ReviewRun
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Review-backed template launches must mark the derived game session as review-backed."),
|
||||
ReviewBundle.GameSession.bReviewBacked
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistCubeDeskBound2AnalyticsPanelsTest,
|
||||
"HyperTwist.CleanRoom.CubeDesk.Bound2.AnalyticsPanels",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistCubeDeskBound2AnalyticsPanelsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FHyperTwistTrainingSession Session;
|
||||
Session.TrainingSessionId = TEXT("bound2-panel-session");
|
||||
Session.DeckId = TEXT("deck/oll");
|
||||
Session.UserId = TEXT("bound2-user");
|
||||
Session.Mode = EHyperTwistTrainingDeliveryMode::Timer;
|
||||
Session.SessionState = EHyperTwistTrainingSessionState::Completed;
|
||||
Session.CurrentCaseId = TEXT("oll-27");
|
||||
|
||||
FHyperTwistTrainingSessionSummary Summary;
|
||||
Summary.TrainingSessionId = Session.TrainingSessionId;
|
||||
Summary.DeckId = Session.DeckId;
|
||||
Summary.UserId = Session.UserId;
|
||||
Summary.SessionState = Session.SessionState;
|
||||
Summary.AttemptCount = 8;
|
||||
Summary.CompletedAttemptCount = 8;
|
||||
Summary.SuccessCount = 6;
|
||||
Summary.FailureCount = 1;
|
||||
Summary.TimeoutCount = 1;
|
||||
Summary.TotalTimeMs = 18100;
|
||||
Summary.BestTotalTimeMs = 1950;
|
||||
Summary.AverageTotalTimeMs = 2262;
|
||||
Summary.SuccessRate = 75.0f;
|
||||
Summary.LastCaseId = Session.CurrentCaseId;
|
||||
Summary.LastCompletedAtUtc = TEXT("2026-05-19T11:05:00Z");
|
||||
|
||||
FHyperTwistTrainingDeckHistorySummary DeckHistorySummary;
|
||||
DeckHistorySummary.UserId = Session.UserId;
|
||||
DeckHistorySummary.DeckId = Session.DeckId;
|
||||
DeckHistorySummary.SessionCount = 4;
|
||||
DeckHistorySummary.CompletedSessionCount = 3;
|
||||
DeckHistorySummary.TotalAttemptCount = 26;
|
||||
DeckHistorySummary.BestTotalTimeMs = 1890;
|
||||
DeckHistorySummary.BestRawSolveTimeMs = 1710;
|
||||
DeckHistorySummary.AverageTotalTimeMs = 2330;
|
||||
DeckHistorySummary.AverageRawSolveTimeMs = 2140;
|
||||
DeckHistorySummary.AverageInspectionTimeMs = 190;
|
||||
DeckHistorySummary.LastSessionId = Session.TrainingSessionId;
|
||||
DeckHistorySummary.LastCompletedAtUtc = Summary.LastCompletedAtUtc;
|
||||
|
||||
FHyperTwistTrainingRollingWindowStat RollingWindow;
|
||||
RollingWindow.WindowLabel = TEXT("mo3");
|
||||
RollingWindow.WindowSize = 3;
|
||||
RollingWindow.SolveSampleCount = 3;
|
||||
RollingWindow.bWindowFilled = true;
|
||||
RollingWindow.CurrentAverageTotalTimeMs = 2180;
|
||||
RollingWindow.CurrentAverageRawSolveTimeMs = 2050;
|
||||
RollingWindow.BestAverageTotalTimeMs = 2090;
|
||||
RollingWindow.BestAverageRawSolveTimeMs = 1980;
|
||||
|
||||
FHyperTwistTrainingTimingTrendSummary TimingTrendSummary;
|
||||
TimingTrendSummary.UserId = Session.UserId;
|
||||
TimingTrendSummary.DeckId = Session.DeckId;
|
||||
TimingTrendSummary.ReferenceUtc = Summary.LastCompletedAtUtc;
|
||||
TimingTrendSummary.SolveCount = 12;
|
||||
TimingTrendSummary.LastSolveTotalTimeMs = 2140;
|
||||
TimingTrendSummary.LastSolveRawSolveTimeMs = 2010;
|
||||
TimingTrendSummary.BestSingleTotalTimeMs = 1890;
|
||||
TimingTrendSummary.BestSingleRawSolveTimeMs = 1710;
|
||||
TimingTrendSummary.WorstSingleTotalTimeMs = 2780;
|
||||
TimingTrendSummary.WorstSingleRawSolveTimeMs = 2510;
|
||||
TimingTrendSummary.MeanTotalTimeMs = 2295;
|
||||
TimingTrendSummary.MeanRawSolveTimeMs = 2148;
|
||||
TimingTrendSummary.LastSolveAtUtc = Summary.LastCompletedAtUtc;
|
||||
TimingTrendSummary.RollingWindows.Add(RollingWindow);
|
||||
|
||||
FHyperTwistTrainingPersonalBestSummary PersonalBestSummary;
|
||||
PersonalBestSummary.UserId = Session.UserId;
|
||||
PersonalBestSummary.DeckId = Session.DeckId;
|
||||
PersonalBestSummary.ReferenceUtc = Summary.LastCompletedAtUtc;
|
||||
PersonalBestSummary.BestSingleSessionId = Session.TrainingSessionId;
|
||||
PersonalBestSummary.BestSingleTotalTimeMs = 1890;
|
||||
PersonalBestSummary.BestSingleRawSolveTimeMs = 1710;
|
||||
PersonalBestSummary.BestAverageSessionId = Session.TrainingSessionId;
|
||||
PersonalBestSummary.BestSessionAverageTotalTimeMs = 2050;
|
||||
PersonalBestSummary.BestSessionAverageRawSolveTimeMs = 1930;
|
||||
PersonalBestSummary.LastPersonalBestAtUtc = Summary.LastCompletedAtUtc;
|
||||
PersonalBestSummary.bHasTimedPersonalBest = true;
|
||||
|
||||
FHyperTwistTrainingDeckScopedStats DeckScopedStats;
|
||||
DeckScopedStats.UserId = Session.UserId;
|
||||
DeckScopedStats.DeckId = Session.DeckId;
|
||||
DeckScopedStats.ReferenceUtc = Summary.LastCompletedAtUtc;
|
||||
DeckScopedStats.SessionCount = 4;
|
||||
DeckScopedStats.CompletedRunCount = 3;
|
||||
DeckScopedStats.TrackedCaseCount = 21;
|
||||
DeckScopedStats.AttemptCount = 26;
|
||||
DeckScopedStats.SolveCount = 20;
|
||||
DeckScopedStats.SuccessCount = 17;
|
||||
DeckScopedStats.FailureCount = 5;
|
||||
DeckScopedStats.TimeoutCount = 2;
|
||||
DeckScopedStats.DNFCount = 1;
|
||||
DeckScopedStats.AverageTotalTimeMs = 2330;
|
||||
DeckScopedStats.AverageRawSolveTimeMs = 2140;
|
||||
DeckScopedStats.RecognitionAttemptCount = 7;
|
||||
DeckScopedStats.PrimaryDeliveryMode = Session.Mode;
|
||||
|
||||
FHyperTwistTrainingCaseScopedStats CaseScopedStats;
|
||||
CaseScopedStats.UserId = Session.UserId;
|
||||
CaseScopedStats.DeckId = Session.DeckId;
|
||||
CaseScopedStats.CaseId = Session.CurrentCaseId;
|
||||
CaseScopedStats.ReferenceUtc = Summary.LastCompletedAtUtc;
|
||||
CaseScopedStats.AttemptCount = 3;
|
||||
CaseScopedStats.SolveCount = 3;
|
||||
CaseScopedStats.SuccessCount = 2;
|
||||
CaseScopedStats.FailureCount = 1;
|
||||
CaseScopedStats.AverageTotalTimeMs = 2410;
|
||||
CaseScopedStats.AverageRawSolveTimeMs = 2260;
|
||||
CaseScopedStats.RecognitionAttemptCount = 1;
|
||||
CaseScopedStats.PrimaryDeliveryMode = Session.Mode;
|
||||
CaseScopedStats.LastAttemptAtUtc = Summary.LastCompletedAtUtc;
|
||||
CaseScopedStats.LastSuccessAtUtc = Summary.LastCompletedAtUtc;
|
||||
|
||||
FHyperTwistTrainingLiveTimerState LiveTimerState;
|
||||
LiveTimerState.TrainingSessionId = Session.TrainingSessionId;
|
||||
LiveTimerState.UserId = Session.UserId;
|
||||
LiveTimerState.DeckId = Session.DeckId;
|
||||
LiveTimerState.CaseId = Session.CurrentCaseId;
|
||||
LiveTimerState.LastUpdatedAtUtc = Summary.LastCompletedAtUtc;
|
||||
LiveTimerState.Phase = EHyperTwistTrainingLiveTimerPhase::Solving;
|
||||
LiveTimerState.SolveElapsedMs = 4120;
|
||||
FHyperTwistTrainingSplitCapture InspectionSplit;
|
||||
InspectionSplit.PhaseId = TEXT("inspection");
|
||||
InspectionSplit.Label = TEXT("Inspection");
|
||||
InspectionSplit.TimestampMs = 1200;
|
||||
LiveTimerState.SplitCaptures.Add(InspectionSplit);
|
||||
FHyperTwistTrainingSplitCapture ExecutionSplit;
|
||||
ExecutionSplit.PhaseId = TEXT("execution");
|
||||
ExecutionSplit.Label = TEXT("Execution");
|
||||
ExecutionSplit.TimestampMs = 2780;
|
||||
LiveTimerState.SplitCaptures.Add(ExecutionSplit);
|
||||
|
||||
const FHyperTwistTrainingAnalyticsReportBundle ReportBundle =
|
||||
UHyperTwistTrainingAnalyticsLibrary::BuildActiveTrainingAnalyticsReportBundle(
|
||||
Summary,
|
||||
DeckHistorySummary,
|
||||
TimingTrendSummary,
|
||||
PersonalBestSummary,
|
||||
DeckScopedStats,
|
||||
CaseScopedStats,
|
||||
LiveTimerState,
|
||||
FHyperTwistReplayReviewAnalytics(),
|
||||
FHyperTwistRecognitionReplaySummary()
|
||||
);
|
||||
|
||||
TestTrue(TEXT("The retained echarts-backed report bundle must stay structurally valid."), ReportBundle.IsStructurallyValid());
|
||||
TestEqual(
|
||||
TEXT("The primary report id must remain the bounded session-outcome owner."),
|
||||
ReportBundle.PrimaryReportId,
|
||||
TEXT("analytics/session-outcome-balance")
|
||||
);
|
||||
TestEqual(TEXT("All six bounded report contracts must still be emitted when their inputs are valid."), ReportBundle.Reports.Num(), 6);
|
||||
|
||||
const FHyperTwistTrainingSessionDomainBundle SessionDomainBundle =
|
||||
UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
|
||||
Session,
|
||||
Summary,
|
||||
FHyperTwistTrainingAttempt(),
|
||||
FHyperTwistTrainingTemplateLaunchProvenance(),
|
||||
FHyperTwistTrainingCoachPanelState(),
|
||||
FHyperTwistTrainingMethodDrillRunState(),
|
||||
ReportBundle
|
||||
);
|
||||
const FHyperTwistTrainingAnalyticsPanelContractBundle PanelBundle =
|
||||
UHyperTwistTrainingAnalyticsLibrary::BuildAnalyticsPanelContractBundle(
|
||||
SessionDomainBundle,
|
||||
ReportBundle
|
||||
);
|
||||
|
||||
TestTrue(TEXT("Panel bundle must be structurally valid when report inputs are valid."), PanelBundle.IsStructurallyValid());
|
||||
TestEqual(
|
||||
TEXT("Primary panel ids must be stable first-party names over the existing report ids."),
|
||||
PanelBundle.PrimaryPanelId,
|
||||
TEXT("panel/training-session-overview")
|
||||
);
|
||||
TestEqual(TEXT("Panel contracts must stay one-to-one with the retained report contracts."), PanelBundle.Panels.Num(), 6);
|
||||
|
||||
const FHyperTwistTrainingAnalyticsPanelContract* SessionOverviewPanel =
|
||||
HyperTwistCubeDeskBound2SessionAnalyticsTestInternal::FindPanelById(
|
||||
PanelBundle,
|
||||
TEXT("panel/training-session-overview")
|
||||
);
|
||||
TestNotNull(TEXT("Session overview panel contract must be discoverable by its stable panel id."), SessionOverviewPanel);
|
||||
if (SessionOverviewPanel != nullptr)
|
||||
{
|
||||
TestEqual(
|
||||
TEXT("Session overview panel contract must preserve the original report id."),
|
||||
SessionOverviewPanel->ReportId,
|
||||
TEXT("analytics/session-outcome-balance")
|
||||
);
|
||||
TestTrue(TEXT("Session overview panel must be marked as primary."), SessionOverviewPanel->bPrimaryPanel);
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingAnalyticsPanelContract* LiveSplitPanel =
|
||||
HyperTwistCubeDeskBound2SessionAnalyticsTestInternal::FindPanelById(
|
||||
PanelBundle,
|
||||
TEXT("panel/live-split-ladder")
|
||||
);
|
||||
TestNotNull(TEXT("Live split panel contract must be discoverable by its stable panel id."), LiveSplitPanel);
|
||||
if (LiveSplitPanel != nullptr)
|
||||
{
|
||||
TestEqual(
|
||||
TEXT("Live split panel contract must preserve the retained live split report id."),
|
||||
LiveSplitPanel->ReportId,
|
||||
TEXT("analytics/live-split-ladder")
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Reference in a new issue