Implement Bound 5 local social challenge bundle

This commit is contained in:
axiomlogicnexus 2026-05-20 03:10:07 +02:00
parent e2a29fc139
commit bc9ad4cd84
7 changed files with 2628 additions and 0 deletions

View file

@ -103,6 +103,31 @@ namespace HyperTwistTrainingPersistenceLibraryInternal
{
ConsiderLatestUtc(LatestUtc, DrillBatch.LastUpdatedAtUtc);
}
for (const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord : RepositoryState.StoredFriendLinks)
{
ConsiderLatestUtc(LatestUtc, FriendLinkRecord.RequestedAtUtc);
ConsiderLatestUtc(LatestUtc, FriendLinkRecord.RespondedAtUtc);
ConsiderLatestUtc(LatestUtc, FriendLinkRecord.LastUpdatedAtUtc);
ConsiderLatestUtc(LatestUtc, FriendLinkRecord.FriendSnapshot.LastPublishedAtUtc);
}
for (const FHyperTwistTrainingNotificationRecord& NotificationRecord : RepositoryState.StoredNotifications)
{
ConsiderLatestUtc(LatestUtc, NotificationRecord.CreatedAtUtc);
ConsiderLatestUtc(LatestUtc, NotificationRecord.ReadAtUtc);
ConsiderLatestUtc(LatestUtc, NotificationRecord.AcknowledgedAtUtc);
ConsiderLatestUtc(LatestUtc, NotificationRecord.ArchivedAtUtc);
}
for (const FHyperTwistTrainingChallengeRecord& ChallengeRecord : RepositoryState.StoredChallenges)
{
ConsiderLatestUtc(LatestUtc, ChallengeRecord.CreatedAtUtc);
ConsiderLatestUtc(LatestUtc, ChallengeRecord.LastUpdatedAtUtc);
ConsiderLatestUtc(LatestUtc, ChallengeRecord.RespondedAtUtc);
ConsiderLatestUtc(LatestUtc, ChallengeRecord.ReadyAtUtc);
ConsiderLatestUtc(LatestUtc, ChallengeRecord.CompletedAtUtc);
ConsiderLatestUtc(LatestUtc, ChallengeRecord.ExpiresAtUtc);
ConsiderLatestUtc(LatestUtc, ChallengeRecord.ChallengerSnapshot.ReferenceEntry.RecordedAtUtc);
ConsiderLatestUtc(LatestUtc, ChallengeRecord.RecipientSnapshot.ReferenceEntry.RecordedAtUtc);
}
for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans)
{
ConsiderLatestUtc(LatestUtc, ActionPlan.CompletedAtUtc);
@ -180,6 +205,31 @@ namespace HyperTwistTrainingPersistenceLibraryInternal
OutUserIds.Add(DrillBatch.UserId);
}
}
for (const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord : RepositoryState.StoredFriendLinks)
{
if (!FriendLinkRecord.UserId.IsEmpty())
{
OutUserIds.Add(FriendLinkRecord.UserId);
}
}
for (const FHyperTwistTrainingNotificationRecord& NotificationRecord : RepositoryState.StoredNotifications)
{
if (!NotificationRecord.RecipientUserId.IsEmpty())
{
OutUserIds.Add(NotificationRecord.RecipientUserId);
}
}
for (const FHyperTwistTrainingChallengeRecord& ChallengeRecord : RepositoryState.StoredChallenges)
{
if (!ChallengeRecord.ChallengerUserId.IsEmpty())
{
OutUserIds.Add(ChallengeRecord.ChallengerUserId);
}
if (!ChallengeRecord.RecipientUserId.IsEmpty())
{
OutUserIds.Add(ChallengeRecord.RecipientUserId);
}
}
for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans)
{
if (!ActionPlan.UserId.IsEmpty())
@ -445,6 +495,9 @@ UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
Verification.ExpectedStoredSessionTemplateCount = ExpectedRepositoryState.StoredSessionTemplates.Num();
Verification.ExpectedMethodDrillFavoriteCount = ExpectedRepositoryState.StoredMethodDrillFavorites.Num();
Verification.ExpectedMethodDrillBatchCount = ExpectedRepositoryState.StoredMethodDrillBatches.Num();
Verification.ExpectedFriendLinkCount = ExpectedRepositoryState.StoredFriendLinks.Num();
Verification.ExpectedNotificationCount = ExpectedRepositoryState.StoredNotifications.Num();
Verification.ExpectedChallengeCount = ExpectedRepositoryState.StoredChallenges.Num();
Verification.ExpectedCoachActionPlanCount = ExpectedRepositoryState.StoredCoachActionPlans.Num();
Verification.ExpectedCoachSessionQueueCount = ExpectedRepositoryState.StoredCoachSessionQueues.Num();
Verification.ExpectedCoachSessionQueueHistoryCount =
@ -494,6 +547,9 @@ UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
Verification.LoadedStoredSessionTemplateCount = LoadedRepositoryState.StoredSessionTemplates.Num();
Verification.LoadedMethodDrillFavoriteCount = LoadedRepositoryState.StoredMethodDrillFavorites.Num();
Verification.LoadedMethodDrillBatchCount = LoadedRepositoryState.StoredMethodDrillBatches.Num();
Verification.LoadedFriendLinkCount = LoadedRepositoryState.StoredFriendLinks.Num();
Verification.LoadedNotificationCount = LoadedRepositoryState.StoredNotifications.Num();
Verification.LoadedChallengeCount = LoadedRepositoryState.StoredChallenges.Num();
Verification.LoadedCoachActionPlanCount = LoadedRepositoryState.StoredCoachActionPlans.Num();
Verification.LoadedCoachSessionQueueCount = LoadedRepositoryState.StoredCoachSessionQueues.Num();
Verification.LoadedCoachSessionQueueHistoryCount =
@ -511,6 +567,9 @@ UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
&& Verification.ExpectedStoredSessionTemplateCount == Verification.LoadedStoredSessionTemplateCount
&& Verification.ExpectedMethodDrillFavoriteCount == Verification.LoadedMethodDrillFavoriteCount
&& Verification.ExpectedMethodDrillBatchCount == Verification.LoadedMethodDrillBatchCount
&& Verification.ExpectedFriendLinkCount == Verification.LoadedFriendLinkCount
&& Verification.ExpectedNotificationCount == Verification.LoadedNotificationCount
&& Verification.ExpectedChallengeCount == Verification.LoadedChallengeCount
&& Verification.ExpectedCoachActionPlanCount == Verification.LoadedCoachActionPlanCount
&& Verification.ExpectedCoachSessionQueueCount == Verification.LoadedCoachSessionQueueCount
&& Verification.ExpectedCoachSessionQueueHistoryCount

View file

@ -3069,6 +3069,11 @@ FHyperTwistTrainingPublicationBundle UHyperTwistTrainingSubsystem::GetActivePubl
return ActivePublicationBundle;
}
FHyperTwistTrainingSocialChallengeBundle UHyperTwistTrainingSubsystem::GetActiveSocialChallengeBundle() const
{
return ActiveSocialChallengeBundle;
}
FHyperTwistTrainingCoachMemorySnapshot UHyperTwistTrainingSubsystem::GetActiveCoachMemorySnapshot() const
{
return ActiveCoachMemorySnapshot;
@ -3233,6 +3238,30 @@ TArray<FHyperTwistTrainingMethodDrillBatch> UHyperTwistTrainingSubsystem::GetRes
);
}
TArray<FHyperTwistTrainingFriendLinkRecord> UHyperTwistTrainingSubsystem::GetFriendLinksForUser(
const FString& UserId
) const
{
return UHyperTwistTrainingRepositoryLibrary::ListFriendLinksForUser(TrainingRepositoryState, UserId);
}
TArray<FHyperTwistTrainingNotificationRecord> UHyperTwistTrainingSubsystem::GetNotificationsForUser(
const FString& UserId
) const
{
return UHyperTwistTrainingRepositoryLibrary::ListNotificationsForUser(
TrainingRepositoryState,
UserId
);
}
TArray<FHyperTwistTrainingChallengeRecord> UHyperTwistTrainingSubsystem::GetChallengesForUser(
const FString& UserId
) const
{
return UHyperTwistTrainingRepositoryLibrary::ListChallengesForUser(TrainingRepositoryState, UserId);
}
FHyperTwistTrainingMethodDrillBatchReadiness UHyperTwistTrainingSubsystem::DeriveMethodDrillBatchReadiness(
const FHyperTwistTrainingMethodDrillBatch& DrillBatch,
const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition
@ -3373,6 +3402,44 @@ bool UHyperTwistTrainingSubsystem::TryGetStoredCoachActionPlan(
);
}
bool UHyperTwistTrainingSubsystem::TryGetStoredFriendLinkRecord(
const FString& UserId,
const FString& FriendUserId,
FHyperTwistTrainingFriendLinkRecord& OutFriendLinkRecord
) const
{
return UHyperTwistTrainingRepositoryLibrary::TryGetFriendLinkRecord(
TrainingRepositoryState,
UserId,
FriendUserId,
OutFriendLinkRecord
);
}
bool UHyperTwistTrainingSubsystem::TryGetStoredNotificationRecord(
const FString& NotificationId,
FHyperTwistTrainingNotificationRecord& OutNotificationRecord
) const
{
return UHyperTwistTrainingRepositoryLibrary::TryGetNotificationRecord(
TrainingRepositoryState,
NotificationId,
OutNotificationRecord
);
}
bool UHyperTwistTrainingSubsystem::TryGetStoredChallengeRecord(
const FString& ChallengeId,
FHyperTwistTrainingChallengeRecord& OutChallengeRecord
) const
{
return UHyperTwistTrainingRepositoryLibrary::TryGetChallengeRecord(
TrainingRepositoryState,
ChallengeId,
OutChallengeRecord
);
}
bool UHyperTwistTrainingSubsystem::TryGetStoredCoachSessionQueueState(
const FString& UserId,
const FString& QueueId,
@ -3419,6 +3486,105 @@ bool UHyperTwistTrainingSubsystem::SaveTrainingSessionTemplate(
return true;
}
bool UHyperTwistTrainingSubsystem::SaveFriendLinkRecord(
const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord
)
{
if (!FriendLinkRecord.IsStructurallyValid())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertFriendLinkRecord(
TrainingRepositoryState,
FriendLinkRecord
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::SaveNotificationRecord(
const FHyperTwistTrainingNotificationRecord& NotificationRecord
)
{
if (!NotificationRecord.IsStructurallyValid())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertNotificationRecord(
TrainingRepositoryState,
NotificationRecord
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::MarkNotificationRead(const FString& NotificationId)
{
if (NotificationId.IsEmpty())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::MarkNotificationRead(
TrainingRepositoryState,
NotificationId,
FDateTime::UtcNow().ToIso8601()
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::AcknowledgeNotification(const FString& NotificationId)
{
if (NotificationId.IsEmpty())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::AcknowledgeNotification(
TrainingRepositoryState,
NotificationId,
FDateTime::UtcNow().ToIso8601()
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::ArchiveNotification(const FString& NotificationId)
{
if (NotificationId.IsEmpty())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::ArchiveNotification(
TrainingRepositoryState,
NotificationId,
FDateTime::UtcNow().ToIso8601()
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::SaveChallengeRecord(
const FHyperTwistTrainingChallengeRecord& ChallengeRecord
)
{
if (!ChallengeRecord.IsStructurallyValid())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertChallengeRecord(
TrainingRepositoryState,
ChallengeRecord
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::SaveMethodDrillFavorite(
const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry
)
@ -6947,6 +7113,7 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveLearnerDeckStateSummary = FHyperTwistTrainingLearnerDeckStateSummary();
ActiveLearnerProfile = FHyperTwistTrainingLearnerProfile();
ActivePublicationBundle = FHyperTwistTrainingPublicationBundle();
ActiveSocialChallengeBundle = FHyperTwistTrainingSocialChallengeBundle();
ActiveCoachMemorySnapshot = FHyperTwistTrainingCoachMemorySnapshot();
ActiveReplayReviewAnalytics = FHyperTwistReplayReviewAnalytics();
ActiveRecognitionReplaySummary = FHyperTwistRecognitionReplaySummary();
@ -7192,6 +7359,12 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
)
: FHyperTwistTrainingPublicationBundle();
}
ActiveSocialChallengeBundle = UHyperTwistTrainingRepositoryLibrary::DeriveSocialChallengeBundle(
TrainingRepositoryState,
RepositoryViewUserId,
ResolvedReferenceUtc,
ActivePublicationBundle.EntitlementGate
);
ActiveReviewFlowStatus = ActiveReviewPlanState.IsStructurallyValid()
? UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus(ActiveReviewPlanState)
: FHyperTwistTrainingReviewFlowStatus();

View file

@ -130,6 +130,113 @@ public:
const FString& TemplateId
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static TArray<FHyperTwistTrainingFriendLinkRecord> ListFriendLinksForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Social")
static bool TryGetFriendLinkRecord(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& FriendUserId,
FHyperTwistTrainingFriendLinkRecord& OutFriendLinkRecord
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingRepositoryState UpsertFriendLinkRecord(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingFriendLinkRecord ResolveFriendLinkRecordState(
const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord,
EHyperTwistTrainingFriendLinkState State,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static TArray<FHyperTwistTrainingNotificationRecord> ListNotificationsForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Social")
static bool TryGetNotificationRecord(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& NotificationId,
FHyperTwistTrainingNotificationRecord& OutNotificationRecord
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingRepositoryState UpsertNotificationRecord(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingNotificationRecord& NotificationRecord
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingRepositoryState MarkNotificationRead(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& NotificationId,
const FString& ReadAtUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingRepositoryState AcknowledgeNotification(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& NotificationId,
const FString& AcknowledgedAtUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingRepositoryState ArchiveNotification(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& NotificationId,
const FString& ArchivedAtUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static TArray<FHyperTwistTrainingChallengeRecord> ListChallengesForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Social")
static bool TryGetChallengeRecord(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& ChallengeId,
FHyperTwistTrainingChallengeRecord& OutChallengeRecord
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingRepositoryState UpsertChallengeRecord(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingChallengeRecord& ChallengeRecord
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingChallengeRecord ResolveChallengeRecordState(
const FHyperTwistTrainingChallengeRecord& ChallengeRecord,
EHyperTwistTrainingChallengeLifecycleState LifecycleState,
EHyperTwistTrainingLocalMatchReadinessState MatchReadinessState,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingChallengeComparisonBundle BuildChallengeComparisonBundle(
const FHyperTwistTrainingChallengeRecord& ChallengeRecord
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Social")
static FHyperTwistTrainingSocialChallengeBundle DeriveSocialChallengeBundle(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& ReferenceUtc,
const FHyperTwistTrainingEntitlementGateState& EntitlementGateState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|SmartDevice")
static FHyperTwistTrainingRepositoryState UpsertSmartDeviceSessionRecord(
const FHyperTwistTrainingRepositoryState& RepositoryState,

View file

@ -161,6 +161,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Publication")
FHyperTwistTrainingPublicationBundle GetActivePublicationBundle() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Social")
FHyperTwistTrainingSocialChallengeBundle GetActiveSocialChallengeBundle() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
FHyperTwistTrainingCoachMemorySnapshot GetActiveCoachMemorySnapshot() const;
@ -257,6 +260,15 @@ public:
const FString& MethodSegmentId
) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Social")
TArray<FHyperTwistTrainingFriendLinkRecord> GetFriendLinksForUser(const FString& UserId) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Social")
TArray<FHyperTwistTrainingNotificationRecord> GetNotificationsForUser(const FString& UserId) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Social")
TArray<FHyperTwistTrainingChallengeRecord> GetChallengesForUser(const FString& UserId) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill")
FHyperTwistTrainingMethodDrillBatchReadiness DeriveMethodDrillBatchReadiness(
const FHyperTwistTrainingMethodDrillBatch& DrillBatch,
@ -321,6 +333,25 @@ public:
FHyperTwistTrainingCoachActionPlan& OutActionPlan
) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool TryGetStoredFriendLinkRecord(
const FString& UserId,
const FString& FriendUserId,
FHyperTwistTrainingFriendLinkRecord& OutFriendLinkRecord
) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool TryGetStoredNotificationRecord(
const FString& NotificationId,
FHyperTwistTrainingNotificationRecord& OutNotificationRecord
) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool TryGetStoredChallengeRecord(
const FString& ChallengeId,
FHyperTwistTrainingChallengeRecord& OutChallengeRecord
) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool TryGetStoredCoachSessionQueueState(
const FString& UserId,
@ -338,6 +369,24 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
bool SaveTrainingSessionTemplate(const FHyperTwistTrainingSessionTemplate& SessionTemplate);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool SaveFriendLinkRecord(const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool SaveNotificationRecord(const FHyperTwistTrainingNotificationRecord& NotificationRecord);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool MarkNotificationRead(const FString& NotificationId);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool AcknowledgeNotification(const FString& NotificationId);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool ArchiveNotification(const FString& NotificationId);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Social")
bool SaveChallengeRecord(const FHyperTwistTrainingChallengeRecord& ChallengeRecord);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
bool SaveMethodDrillFavorite(const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry);
@ -756,6 +805,9 @@ private:
UPROPERTY()
FHyperTwistTrainingPublicationBundle ActivePublicationBundle;
UPROPERTY()
FHyperTwistTrainingSocialChallengeBundle ActiveSocialChallengeBundle;
UPROPERTY()
FHyperTwistTrainingCoachMemorySnapshot ActiveCoachMemorySnapshot;

View file

@ -3515,6 +3515,459 @@ struct FHyperTwistTrainingPublicationBundle
}
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingFriendLinkState : uint8
{
None UMETA(DisplayName = "None"),
PendingOutgoing UMETA(DisplayName = "Pending Outgoing"),
PendingIncoming UMETA(DisplayName = "Pending Incoming"),
Active UMETA(DisplayName = "Active"),
Rejected UMETA(DisplayName = "Rejected")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingNotificationKind : uint8
{
None UMETA(DisplayName = "None"),
FriendRequest UMETA(DisplayName = "Friend Request"),
FriendAccepted UMETA(DisplayName = "Friend Accepted"),
FriendActivity UMETA(DisplayName = "Friend Activity"),
ChallengeInvitation UMETA(DisplayName = "Challenge Invitation"),
ChallengeAccepted UMETA(DisplayName = "Challenge Accepted"),
ChallengeReady UMETA(DisplayName = "Challenge Ready"),
ChallengeExpired UMETA(DisplayName = "Challenge Expired"),
ChallengeCompleted UMETA(DisplayName = "Challenge Completed")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingNotificationVisibilityState : uint8
{
None UMETA(DisplayName = "None"),
Hidden UMETA(DisplayName = "Hidden"),
Inbox UMETA(DisplayName = "Inbox"),
Archived UMETA(DisplayName = "Archived")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingChallengeLifecycleState : uint8
{
None UMETA(DisplayName = "None"),
Pending UMETA(DisplayName = "Pending"),
Accepted UMETA(DisplayName = "Accepted"),
Rejected UMETA(DisplayName = "Rejected"),
Expired UMETA(DisplayName = "Expired"),
Ready UMETA(DisplayName = "Ready"),
Completed UMETA(DisplayName = "Completed")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingLocalMatchReadinessState : uint8
{
None UMETA(DisplayName = "None"),
Seeded UMETA(DisplayName = "Seeded"),
AwaitingAttempts UMETA(DisplayName = "Awaiting Attempts"),
ReadyToCompare UMETA(DisplayName = "Ready To Compare"),
ComparisonLocked UMETA(DisplayName = "Comparison Locked")
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingFriendIdentitySnapshot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FriendUserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PublicationId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Headline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastPublishedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PreferredEventId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Tags;
bool IsStructurallyValid() const
{
return !FriendUserId.IsEmpty() && !DisplayName.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingFriendLinkRecord
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LinkId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingFriendIdentitySnapshot FriendSnapshot;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString RequestedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString RespondedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastUpdatedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingFriendLinkState State = EHyperTwistTrainingFriendLinkState::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Tags;
bool IsStructurallyValid() const
{
return !LinkId.IsEmpty()
&& !UserId.IsEmpty()
&& FriendSnapshot.IsStructurallyValid()
&& State != EHyperTwistTrainingFriendLinkState::None;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingNotificationRecord
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NotificationId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString RecipientUserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActorUserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FriendUserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ChallengeId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CreatedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ReadAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString AcknowledgedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ArchivedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Title;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SummaryLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString VisibilityReason;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingNotificationKind Kind = EHyperTwistTrainingNotificationKind::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingNotificationVisibilityState VisibilityState =
EHyperTwistTrainingNotificationVisibilityState::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRead = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bAcknowledged = false;
bool IsStructurallyValid() const
{
return !NotificationId.IsEmpty()
&& !RecipientUserId.IsEmpty()
&& !CreatedAtUtc.IsEmpty()
&& !Title.IsEmpty()
&& Kind != EHyperTwistTrainingNotificationKind::None
&& VisibilityState != EHyperTwistTrainingNotificationVisibilityState::None;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingChallengeParticipantSnapshot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PublicationId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Headline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingLeaderboardEntry ReferenceEntry;
bool IsStructurallyValid() const
{
return !UserId.IsEmpty() && !DisplayName.IsEmpty() && !DeckId.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingChallengeRecord
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ChallengeId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ChallengerUserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString RecipientUserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CreatedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastUpdatedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString RespondedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ReadyAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CompletedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ExpiresAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Title;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SummaryLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SeedLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingChallengeLifecycleState LifecycleState =
EHyperTwistTrainingChallengeLifecycleState::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingLocalMatchReadinessState MatchReadinessState =
EHyperTwistTrainingLocalMatchReadinessState::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingChallengeParticipantSnapshot ChallengerSnapshot;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingChallengeParticipantSnapshot RecipientSnapshot;
bool IsStructurallyValid() const
{
return !ChallengeId.IsEmpty()
&& !ChallengerUserId.IsEmpty()
&& !RecipientUserId.IsEmpty()
&& !DeckId.IsEmpty()
&& !EventId.IsEmpty()
&& !CreatedAtUtc.IsEmpty()
&& LifecycleState != EHyperTwistTrainingChallengeLifecycleState::None
&& ChallengerSnapshot.IsStructurallyValid()
&& RecipientSnapshot.IsStructurallyValid();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingChallengeComparisonEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventTitle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString MetricLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString MetricPrimaryValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString MetricSecondaryValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString RelativeDateLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ProgramLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCurrentUser = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasComparableEntry = false;
bool IsStructurallyValid() const
{
return !UserId.IsEmpty() && !DisplayName.IsEmpty() && !EventId.IsEmpty() && !MetricLabel.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingChallengeComparisonBundle
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ChallengeId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventTitle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString MetricLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LifecycleLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString MatchReadinessLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingChallengeComparisonEntry> Entries;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bReadyToCompare = false;
bool IsStructurallyValid() const
{
return !ChallengeId.IsEmpty() && !DeckId.IsEmpty() && !EventId.IsEmpty() && Entries.Num() > 0;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingSocialChallengeBundle
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ReferenceUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString GateId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FeatureId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString VisibilityLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bVisible = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bChallengeCreationEnabled = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingFriendLinkRecord> FriendLinks;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingNotificationRecord> Notifications;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingChallengeRecord> Challenges;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingChallengeComparisonBundle> ComparisonBundles;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 PendingFriendCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 VisibleNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 UnreadNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AcknowledgedNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ArchivedNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ActiveChallengeCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ReadyChallengeCount = 0;
bool IsStructurallyValid() const
{
return !UserId.IsEmpty() && !ReferenceUtc.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachMemorySnapshot
{
@ -8031,6 +8484,15 @@ struct FHyperTwistTrainingRepositoryState
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingMethodDrillBatch> StoredMethodDrillBatches;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingFriendLinkRecord> StoredFriendLinks;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingNotificationRecord> StoredNotifications;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingChallengeRecord> StoredChallenges;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachActionPlan> StoredCoachActionPlans;
@ -8112,6 +8574,33 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateMethodDrillBatchCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalFriendLinkCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidFriendLinkCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateFriendLinkCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalChallengeCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidChallengeCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateChallengeCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalCoachActionPlanCount = 0;
@ -8233,6 +8722,12 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
|| DuplicateMethodDrillFavoriteCount > 0
|| InvalidMethodDrillBatchCount > 0
|| DuplicateMethodDrillBatchCount > 0
|| InvalidFriendLinkCount > 0
|| DuplicateFriendLinkCount > 0
|| InvalidNotificationCount > 0
|| DuplicateNotificationCount > 0
|| InvalidChallengeCount > 0
|| DuplicateChallengeCount > 0
|| InvalidCoachActionPlanCount > 0
|| DuplicateCoachActionPlanCount > 0
|| MissingCoachActionPlanSourceSessionCount > 0
@ -8330,6 +8825,24 @@ struct FHyperTwistTrainingRepositoryRoundTripVerification
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 LoadedMethodDrillBatchCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ExpectedFriendLinkCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 LoadedFriendLinkCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ExpectedNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 LoadedNotificationCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ExpectedChallengeCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 LoadedChallengeCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ExpectedCoachActionPlanCount = 0;

View file

@ -0,0 +1,534 @@
// Copyright HyperTwist, Inc. All Rights Reserved.
#include "Misc/AutomationTest.h"
#include "Engine/GameInstance.h"
#include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "UObject/Package.h"
#if WITH_AUTOMATION_TESTS
namespace HyperTwistCubeDeskBound5SocialChallengeTestInternal
{
FHyperTwistTrainingDeck MakeDeck(const FString& DeckId)
{
FHyperTwistTrainingDeck Deck;
Deck.DeckId = DeckId;
Deck.Title = TEXT("Bound5 Social Deck");
Deck.DeliveryModes = {EHyperTwistTrainingDeliveryMode::Timer};
FHyperTwistTrainingCase TrainingCase;
TrainingCase.CaseId = TEXT("bound5-case-1");
TrainingCase.PuzzleId = TEXT("3x3x3x3");
TrainingCase.PromptLabel = TEXT("Bound5 Case");
TrainingCase.AllowedDeliveryModes = {EHyperTwistTrainingDeliveryMode::Timer};
Deck.Cases.Add(TrainingCase);
return Deck;
}
FHyperTwistTrainingEntitlementGateState MakeVisibleEntitledGate()
{
FHyperTwistTrainingEntitlementGateState GateState;
GateState.GateId = TEXT("gate/local-social");
GateState.FeatureId = TEXT("feature/local-social");
GateState.Title = TEXT("Local Social");
GateState.VisibilityLabel = TEXT("published");
GateState.FeatureGovernanceContractId = TEXT("posthog-feature-governance-and-blast-radius");
GateState.EarlyAccessContractId = TEXT("posthog-early-access-rollout");
GateState.LifecycleStageId = TEXT("active");
GateState.EnrollmentModeLabel = TEXT("automatic");
GateState.ApprovedWorkflowTags = {TEXT("feature-governance"), TEXT("local-social")};
GateState.bVisible = true;
GateState.bEntitled = true;
return GateState;
}
FHyperTwistTrainingFriendLinkRecord MakeFriendLinkRecord(
const FString& UserId,
const FString& FriendUserId,
const FString& DisplayName,
const EHyperTwistTrainingFriendLinkState State,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingFriendLinkRecord FriendLinkRecord;
FriendLinkRecord.LinkId = FString::Printf(TEXT("friend-link/%s/%s"), *UserId, *FriendUserId);
FriendLinkRecord.UserId = UserId;
FriendLinkRecord.FriendSnapshot.FriendUserId = FriendUserId;
FriendLinkRecord.FriendSnapshot.DisplayName = DisplayName;
FriendLinkRecord.FriendSnapshot.DeckId = TEXT("deck/hypercube-3333");
FriendLinkRecord.FriendSnapshot.PublicationId =
FString::Printf(TEXT("publication/%s/%s"), *FriendUserId, TEXT("deck/hypercube-3333"));
FriendLinkRecord.FriendSnapshot.LastPublishedAtUtc = ReferenceUtc;
FriendLinkRecord.Tags = {TEXT("local"), TEXT("bound5")};
return UHyperTwistTrainingRepositoryLibrary::ResolveFriendLinkRecordState(
FriendLinkRecord,
State,
ReferenceUtc
);
}
FHyperTwistTrainingNotificationRecord MakeNotificationRecord(
const FString& NotificationId,
const FString& RecipientUserId,
const FString& ActorUserId,
const FString& FriendUserId,
const FString& ChallengeId,
const EHyperTwistTrainingNotificationKind Kind,
const FString& CreatedAtUtc
)
{
FHyperTwistTrainingNotificationRecord NotificationRecord;
NotificationRecord.NotificationId = NotificationId;
NotificationRecord.RecipientUserId = RecipientUserId;
NotificationRecord.ActorUserId = ActorUserId;
NotificationRecord.FriendUserId = FriendUserId;
NotificationRecord.ChallengeId = ChallengeId;
NotificationRecord.DeckId = TEXT("deck/hypercube-3333");
NotificationRecord.EventId = TEXT("event/3x3x3x3/single");
NotificationRecord.CreatedAtUtc = CreatedAtUtc;
NotificationRecord.Title = TEXT("Bound5 notification");
NotificationRecord.SummaryLine = TEXT("Local social notification");
NotificationRecord.Kind = Kind;
NotificationRecord.VisibilityState = EHyperTwistTrainingNotificationVisibilityState::Inbox;
return NotificationRecord;
}
FHyperTwistTrainingLeaderboardEntry MakeLeaderboardEntry(
const FString& EntryId,
const FString& DisplayName,
const FString& PrimaryValue,
const FString& SecondaryValue,
const FString& RecordedAtUtc
)
{
FHyperTwistTrainingLeaderboardEntry Entry;
Entry.EntryId = EntryId;
Entry.EventId = TEXT("event/3x3x3x3/single");
Entry.EventTitle = TEXT("3x3x3 Single");
Entry.MetricLabel = TEXT("Single");
Entry.SolverDisplayName = DisplayName;
Entry.ProgramLabel = TEXT("HSC");
Entry.RecordedAtUtc = RecordedAtUtc;
Entry.RelativeDateLabel = TEXT("today");
Entry.RankLabel = TEXT("#1 local");
Entry.MetricPrimaryValue = PrimaryValue;
Entry.MetricSecondaryValue = SecondaryValue;
Entry.DeckId = TEXT("deck/hypercube-3333");
Entry.bEligible = true;
Entry.bPublished = true;
return Entry;
}
FHyperTwistTrainingChallengeRecord MakeChallengeRecord(
const FString& ChallengeId,
const FString& ChallengerUserId,
const FString& ChallengerDisplayName,
const FString& RecipientUserId,
const FString& RecipientDisplayName,
const EHyperTwistTrainingChallengeLifecycleState LifecycleState,
const EHyperTwistTrainingLocalMatchReadinessState MatchReadinessState,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingChallengeRecord ChallengeRecord;
ChallengeRecord.ChallengeId = ChallengeId;
ChallengeRecord.ChallengerUserId = ChallengerUserId;
ChallengeRecord.RecipientUserId = RecipientUserId;
ChallengeRecord.DeckId = TEXT("deck/hypercube-3333");
ChallengeRecord.EventId = TEXT("event/3x3x3x3/single");
ChallengeRecord.Title = TEXT("Bound5 challenge");
ChallengeRecord.SummaryLine = TEXT("Local comparison challenge");
ChallengeRecord.SeedLabel = TEXT("single");
ChallengeRecord.ChallengerSnapshot.UserId = ChallengerUserId;
ChallengeRecord.ChallengerSnapshot.DisplayName = ChallengerDisplayName;
ChallengeRecord.ChallengerSnapshot.PublicationId =
FString::Printf(TEXT("publication/%s/%s"), *ChallengerUserId, TEXT("deck/hypercube-3333"));
ChallengeRecord.ChallengerSnapshot.DeckId = TEXT("deck/hypercube-3333");
ChallengeRecord.ChallengerSnapshot.Headline = TEXT("Local challenger");
ChallengeRecord.ChallengerSnapshot.ReferenceEntry = MakeLeaderboardEntry(
FString::Printf(TEXT("%s-entry"), *ChallengerUserId),
ChallengerDisplayName,
TEXT("12.34"),
TEXT("12340 ms"),
ReferenceUtc
);
ChallengeRecord.RecipientSnapshot.UserId = RecipientUserId;
ChallengeRecord.RecipientSnapshot.DisplayName = RecipientDisplayName;
ChallengeRecord.RecipientSnapshot.PublicationId =
FString::Printf(TEXT("publication/%s/%s"), *RecipientUserId, TEXT("deck/hypercube-3333"));
ChallengeRecord.RecipientSnapshot.DeckId = TEXT("deck/hypercube-3333");
ChallengeRecord.RecipientSnapshot.Headline = TEXT("Local recipient");
ChallengeRecord.RecipientSnapshot.ReferenceEntry = MakeLeaderboardEntry(
FString::Printf(TEXT("%s-entry"), *RecipientUserId),
RecipientDisplayName,
TEXT("13.21"),
TEXT("13210 ms"),
ReferenceUtc
);
return UHyperTwistTrainingRepositoryLibrary::ResolveChallengeRecordState(
ChallengeRecord,
LifecycleState,
MatchReadinessState,
ReferenceUtc
);
}
const FHyperTwistTrainingChallengeComparisonEntry* FindCurrentUserEntry(
const FHyperTwistTrainingChallengeComparisonBundle& ComparisonBundle
)
{
return ComparisonBundle.Entries.FindByPredicate(
[](const FHyperTwistTrainingChallengeComparisonEntry& Entry)
{
return Entry.bCurrentUser;
}
);
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistCubeDeskBound5FriendLinkLifecycleTest,
"HyperTwist.CleanRoom.CubeDesk.Bound5.FriendLinkLifecycle",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistCubeDeskBound5FriendLinkLifecycleTest::RunTest(const FString& Parameters)
{
FHyperTwistTrainingRepositoryState RepositoryState;
FHyperTwistTrainingFriendLinkRecord FriendLinkRecord =
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeFriendLinkRecord(
TEXT("bound5-user"),
TEXT("friend-user"),
TEXT("Friend User"),
EHyperTwistTrainingFriendLinkState::PendingOutgoing,
TEXT("2026-05-20T10:00:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertFriendLinkRecord(
RepositoryState,
FriendLinkRecord
);
TArray<FHyperTwistTrainingFriendLinkRecord> FriendLinks =
UHyperTwistTrainingRepositoryLibrary::ListFriendLinksForUser(
RepositoryState,
TEXT("bound5-user")
);
TestEqual(TEXT("One pending friend link must be stored."), FriendLinks.Num(), 1);
if (FriendLinks.Num() != 1)
{
return false;
}
TestEqual(
TEXT("The pending friend link must retain the outgoing state."),
FriendLinks[0].State,
EHyperTwistTrainingFriendLinkState::PendingOutgoing
);
FriendLinkRecord = UHyperTwistTrainingRepositoryLibrary::ResolveFriendLinkRecordState(
FriendLinks[0],
EHyperTwistTrainingFriendLinkState::Active,
TEXT("2026-05-20T10:15:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertFriendLinkRecord(
RepositoryState,
FriendLinkRecord
);
FriendLinks = UHyperTwistTrainingRepositoryLibrary::ListFriendLinksForUser(
RepositoryState,
TEXT("bound5-user")
);
TestEqual(TEXT("Friend-link dedupe must retain one active record."), FriendLinks.Num(), 1);
if (FriendLinks.Num() != 1)
{
return false;
}
TestEqual(
TEXT("The active friend link must retain the accepted lifecycle state."),
FriendLinks[0].State,
EHyperTwistTrainingFriendLinkState::Active
);
TestTrue(
TEXT("The active friend link must record a responded-at timestamp."),
!FriendLinks[0].RespondedAtUtc.IsEmpty()
);
const FHyperTwistTrainingRepositoryIntegrityReport IntegrityReport =
UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(RepositoryState);
TestEqual(TEXT("Repository integrity must retain one friend link."), IntegrityReport.TotalFriendLinkCount, 1);
TestEqual(TEXT("Repository integrity must retain zero duplicate friend links."), IntegrityReport.DuplicateFriendLinkCount, 0);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistCubeDeskBound5NotificationVisibilityTest,
"HyperTwist.CleanRoom.CubeDesk.Bound5.NotificationVisibility",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistCubeDeskBound5NotificationVisibilityTest::RunTest(const FString& Parameters)
{
FHyperTwistTrainingRepositoryState RepositoryState;
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertNotificationRecord(
RepositoryState,
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeNotificationRecord(
TEXT("bound5-notification"),
TEXT("bound5-user"),
TEXT("friend-user"),
TEXT("friend-user"),
TEXT(""),
EHyperTwistTrainingNotificationKind::FriendRequest,
TEXT("2026-05-20T11:00:00Z")
)
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::MarkNotificationRead(
RepositoryState,
TEXT("bound5-notification"),
TEXT("2026-05-20T11:01:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::AcknowledgeNotification(
RepositoryState,
TEXT("bound5-notification"),
TEXT("2026-05-20T11:02:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::ArchiveNotification(
RepositoryState,
TEXT("bound5-notification"),
TEXT("2026-05-20T11:03:00Z")
);
const TArray<FHyperTwistTrainingNotificationRecord> Notifications =
UHyperTwistTrainingRepositoryLibrary::ListNotificationsForUser(
RepositoryState,
TEXT("bound5-user")
);
TestEqual(TEXT("Exactly one notification must be retained."), Notifications.Num(), 1);
if (Notifications.Num() != 1)
{
return false;
}
TestTrue(TEXT("The notification must be marked as read."), Notifications[0].bRead);
TestTrue(TEXT("The notification must be marked as acknowledged."), Notifications[0].bAcknowledged);
TestEqual(
TEXT("The notification must move into the archived visibility state."),
Notifications[0].VisibilityState,
EHyperTwistTrainingNotificationVisibilityState::Archived
);
const FHyperTwistTrainingSocialChallengeBundle SocialBundle =
UHyperTwistTrainingRepositoryLibrary::DeriveSocialChallengeBundle(
RepositoryState,
TEXT("bound5-user"),
TEXT("2026-05-20T11:04:00Z"),
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeVisibleEntitledGate()
);
TestEqual(TEXT("Archived notifications must no longer count as inbox-visible."), SocialBundle.VisibleNotificationCount, 0);
TestEqual(TEXT("Archived notifications must not count as unread."), SocialBundle.UnreadNotificationCount, 0);
TestEqual(TEXT("Acknowledged notifications must be counted."), SocialBundle.AcknowledgedNotificationCount, 1);
TestEqual(TEXT("Archived notifications must be counted."), SocialBundle.ArchivedNotificationCount, 1);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistCubeDeskBound5ChallengeLifecycleTest,
"HyperTwist.CleanRoom.CubeDesk.Bound5.ChallengeLifecycle",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistCubeDeskBound5ChallengeLifecycleTest::RunTest(const FString& Parameters)
{
FHyperTwistTrainingRepositoryState RepositoryState;
FHyperTwistTrainingChallengeRecord ChallengeRecord =
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeChallengeRecord(
TEXT("bound5-challenge"),
TEXT("bound5-user"),
TEXT("Bound5 User"),
TEXT("friend-user"),
TEXT("Friend User"),
EHyperTwistTrainingChallengeLifecycleState::Pending,
EHyperTwistTrainingLocalMatchReadinessState::Seeded,
TEXT("2026-05-20T12:00:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertChallengeRecord(
RepositoryState,
ChallengeRecord
);
ChallengeRecord = UHyperTwistTrainingRepositoryLibrary::ResolveChallengeRecordState(
ChallengeRecord,
EHyperTwistTrainingChallengeLifecycleState::Accepted,
EHyperTwistTrainingLocalMatchReadinessState::AwaitingAttempts,
TEXT("2026-05-20T12:05:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertChallengeRecord(
RepositoryState,
ChallengeRecord
);
ChallengeRecord = UHyperTwistTrainingRepositoryLibrary::ResolveChallengeRecordState(
ChallengeRecord,
EHyperTwistTrainingChallengeLifecycleState::Ready,
EHyperTwistTrainingLocalMatchReadinessState::ReadyToCompare,
TEXT("2026-05-20T12:10:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertChallengeRecord(
RepositoryState,
ChallengeRecord
);
const TArray<FHyperTwistTrainingChallengeRecord> Challenges =
UHyperTwistTrainingRepositoryLibrary::ListChallengesForUser(
RepositoryState,
TEXT("bound5-user")
);
TestEqual(TEXT("Exactly one challenge must be retained."), Challenges.Num(), 1);
if (Challenges.Num() != 1)
{
return false;
}
TestEqual(
TEXT("The challenge lifecycle must advance into the ready state."),
Challenges[0].LifecycleState,
EHyperTwistTrainingChallengeLifecycleState::Ready
);
TestEqual(
TEXT("The challenge readiness must advance into the ready-to-compare state."),
Challenges[0].MatchReadinessState,
EHyperTwistTrainingLocalMatchReadinessState::ReadyToCompare
);
TestTrue(TEXT("Accepted challenges must retain a responded-at timestamp."), !Challenges[0].RespondedAtUtc.IsEmpty());
TestTrue(TEXT("Ready challenges must retain a ready-at timestamp."), !Challenges[0].ReadyAtUtc.IsEmpty());
const FHyperTwistTrainingSocialChallengeBundle SocialBundle =
UHyperTwistTrainingRepositoryLibrary::DeriveSocialChallengeBundle(
RepositoryState,
TEXT("bound5-user"),
TEXT("2026-05-20T12:11:00Z"),
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeVisibleEntitledGate()
);
TestEqual(TEXT("The ready challenge must count as active."), SocialBundle.ActiveChallengeCount, 1);
TestEqual(TEXT("The ready challenge must count as ready to compare."), SocialBundle.ReadyChallengeCount, 1);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistCubeDeskBound5ComparisonProjectionTest,
"HyperTwist.CleanRoom.CubeDesk.Bound5.ComparisonProjection",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistCubeDeskBound5ComparisonProjectionTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>(GetTransientPackage());
TestNotNull(TEXT("A game instance must exist for the Bound 5 subsystem test."), GameInstance);
if (GameInstance == nullptr)
{
return false;
}
UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject<UHyperTwistTrainingSubsystem>(GameInstance);
TestNotNull(TEXT("The training subsystem must be created."), TrainingSubsystem);
if (TrainingSubsystem == nullptr)
{
return false;
}
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck(
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeDeck(TEXT("deck/hypercube-3333")),
TEXT("bound5-user"),
TEXT("bound5-social-session"),
EHyperTwistTrainingDeliveryMode::Timer
);
TestTrue(TEXT("A social-challenge repository view must start from a valid run."), RunState.IsStructurallyValid());
if (!RunState.IsStructurallyValid())
{
return false;
}
const bool bSavedFriendLink = TrainingSubsystem->SaveFriendLinkRecord(
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeFriendLinkRecord(
TEXT("bound5-user"),
TEXT("friend-user"),
TEXT("Friend User"),
EHyperTwistTrainingFriendLinkState::Active,
TEXT("2026-05-20T13:00:00Z")
)
);
TestTrue(TEXT("The subsystem must persist local friend-link state."), bSavedFriendLink);
const bool bSavedNotification = TrainingSubsystem->SaveNotificationRecord(
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeNotificationRecord(
TEXT("bound5-challenge-notification"),
TEXT("bound5-user"),
TEXT("friend-user"),
TEXT("friend-user"),
TEXT("bound5-comparison-challenge"),
EHyperTwistTrainingNotificationKind::ChallengeReady,
TEXT("2026-05-20T13:01:00Z")
)
);
TestTrue(TEXT("The subsystem must persist local notification state."), bSavedNotification);
const bool bSavedChallenge = TrainingSubsystem->SaveChallengeRecord(
HyperTwistCubeDeskBound5SocialChallengeTestInternal::MakeChallengeRecord(
TEXT("bound5-comparison-challenge"),
TEXT("bound5-user"),
TEXT("Bound5 User"),
TEXT("friend-user"),
TEXT("Friend User"),
EHyperTwistTrainingChallengeLifecycleState::Ready,
EHyperTwistTrainingLocalMatchReadinessState::ReadyToCompare,
TEXT("2026-05-20T13:02:00Z")
)
);
TestTrue(TEXT("The subsystem must persist local challenge state."), bSavedChallenge);
const FHyperTwistTrainingSocialChallengeBundle SocialBundle =
TrainingSubsystem->GetActiveSocialChallengeBundle();
TestTrue(TEXT("The subsystem must materialize a structurally valid social bundle."), SocialBundle.IsStructurallyValid());
TestEqual(TEXT("The subsystem must surface one stored friend link."), SocialBundle.FriendLinks.Num(), 1);
TestEqual(TEXT("The subsystem must surface one stored notification."), SocialBundle.Notifications.Num(), 1);
TestEqual(TEXT("The subsystem must surface one stored challenge."), SocialBundle.Challenges.Num(), 1);
TestEqual(TEXT("The subsystem must surface one comparison bundle."), SocialBundle.ComparisonBundles.Num(), 1);
if (SocialBundle.ComparisonBundles.Num() != 1)
{
return false;
}
const FHyperTwistTrainingChallengeComparisonBundle& ComparisonBundle = SocialBundle.ComparisonBundles[0];
TestTrue(TEXT("The comparison bundle must resolve as structurally valid."), ComparisonBundle.IsStructurallyValid());
TestEqual(TEXT("The comparison bundle must retain the challenge id."), ComparisonBundle.ChallengeId, TEXT("bound5-comparison-challenge"));
TestEqual(TEXT("The comparison bundle must retain two compared entries."), ComparisonBundle.Entries.Num(), 2);
TestEqual(TEXT("The comparison bundle must retain the landed event title."), ComparisonBundle.EventTitle, TEXT("3x3x3x3 Single"));
TestEqual(TEXT("The comparison bundle must retain the landed metric label."), ComparisonBundle.MetricLabel, TEXT("Time"));
TestTrue(TEXT("The comparison bundle must report ready-to-compare state."), ComparisonBundle.bReadyToCompare);
const FHyperTwistTrainingChallengeComparisonEntry* CurrentUserEntry =
HyperTwistCubeDeskBound5SocialChallengeTestInternal::FindCurrentUserEntry(ComparisonBundle);
TestNotNull(TEXT("One comparison entry must be marked as the current user."), CurrentUserEntry);
if (CurrentUserEntry == nullptr)
{
return false;
}
TestEqual(TEXT("The current-user comparison entry must retain the user id."), CurrentUserEntry->UserId, TEXT("bound5-user"));
TestEqual(TEXT("The current-user comparison entry must retain the local metric value."), CurrentUserEntry->MetricPrimaryValue, TEXT("12.34"));
TestEqual(TEXT("The current-user comparison entry must retain the program badge."), CurrentUserEntry->ProgramLabel, TEXT("HSC"));
const FHyperTwistTrainingRepositoryRoundTripVerification Verification =
UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
TrainingSubsystem->GetTrainingRepositoryState(),
TEXT("Automation/HyperTwistCubeDeskBound5RepositoryRoundTrip.json")
);
TestTrue(TEXT("Repository roundtrip verification must pass for Bound 5 state."), Verification.bDidRoundTripPass);
TestEqual(TEXT("Roundtrip verification must retain one friend link."), Verification.LoadedFriendLinkCount, 1);
TestEqual(TEXT("Roundtrip verification must retain one notification."), Verification.LoadedNotificationCount, 1);
TestEqual(TEXT("Roundtrip verification must retain one challenge."), Verification.LoadedChallengeCount, 1);
return true;
}
#endif