diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index ddb6173..612b309 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -20,6 +20,80 @@ namespace HyperTwistContractLibraryInternal { return FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutValue, 0, 0); } + + FHyperTwistTrainingSourceAttribution MakeMethodDrillCleanRoomAttribution() + { + FHyperTwistTrainingSourceAttribution Attribution; + Attribution.SourceMode = EHyperTwistTrainingSourceMode::CleanRoom; + Attribution.SourceRepo = TEXT("onionhoney/roux-trainers"); + Attribution.SourceLicense = TEXT("GPLv3 clean-room behavioral handoff"); + Attribution.SourceNotes = TEXT("First-party HyperTwist method-stage drill sample built from scrubbed clean-room guidance."); + return Attribution; + } + + FHyperTwistTrainingDeck MakeSampleMethodDrillDeck() + { + const FHyperTwistTrainingSourceAttribution Attribution = MakeMethodDrillCleanRoomAttribution(); + + FHyperTwistTrainingCase RecognitionCaseA; + RecognitionCaseA.CaseId = TEXT("roux-block-case-01"); + RecognitionCaseA.PuzzleId = TEXT("cube/3x3x3"); + RecognitionCaseA.PromptKind = EHyperTwistTrainingPromptKind::Recognition; + RecognitionCaseA.PromptLabel = TEXT("Left Block Recognition A"); + RecognitionCaseA.CanonicalStateRef = TEXT("cube/3x3x3:roux:first-block:left:a"); + RecognitionCaseA.CanonicalNotation = TEXT("R U R'"); + RecognitionCaseA.ScrambleNotation = TEXT("U R U' L' U"); + RecognitionCaseA.SubsetId = TEXT("first-block"); + RecognitionCaseA.Difficulty = 0.45f; + RecognitionCaseA.Tags = { + TEXT("classic"), + TEXT("roux"), + TEXT("block-building"), + TEXT("subset:first-block"), + TEXT("stage-recognition") + }; + RecognitionCaseA.AllowedDeliveryModes = { + EHyperTwistTrainingDeliveryMode::VirtualCube, + EHyperTwistTrainingDeliveryMode::RecognitionAssisted, + EHyperTwistTrainingDeliveryMode::CoachReviewed + }; + RecognitionCaseA.TimeTargetMs = 0; + RecognitionCaseA.SourceAttribution = Attribution; + + FHyperTwistTrainingCase RecognitionCaseB = RecognitionCaseA; + RecognitionCaseB.CaseId = TEXT("roux-block-case-02"); + RecognitionCaseB.PromptLabel = TEXT("Left Block Recognition B"); + RecognitionCaseB.CanonicalStateRef = TEXT("cube/3x3x3:roux:first-block:left:b"); + RecognitionCaseB.CanonicalNotation = TEXT("r U' r'"); + RecognitionCaseB.ScrambleNotation = TEXT("R U2 R' F U'"); + RecognitionCaseB.Difficulty = 0.62f; + + FHyperTwistTrainingDeck DrillDeck; + DrillDeck.DeckId = TEXT("roux-first-block-recognition"); + DrillDeck.Title = TEXT("First Block Recognition"); + DrillDeck.DeliveryModes = { + EHyperTwistTrainingDeliveryMode::VirtualCube, + EHyperTwistTrainingDeliveryMode::RecognitionAssisted, + EHyperTwistTrainingDeliveryMode::CoachReviewed + }; + DrillDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Scripted; + DrillDeck.bUsesRealScrambles = false; + DrillDeck.bSupportsAlgorithmReveal = true; + DrillDeck.bSupportsVirtualCube = true; + DrillDeck.bSupportsSmartCube = false; + DrillDeck.Cases = {RecognitionCaseA, RecognitionCaseB}; + DrillDeck.DifficultyBand = TEXT("stage"); + DrillDeck.Tags = { + TEXT("classic"), + TEXT("roux"), + TEXT("block-building"), + TEXT("subset:first-block"), + TEXT("method-drill") + }; + DrillDeck.SubsetId = TEXT("first-block"); + DrillDeck.SourceAttribution = Attribution; + return DrillDeck; + } } FHyperTwistPuzzleDefinitionRef UHyperTwistContractLibrary::MakeSampleClassicPuzzleDefinition() @@ -329,6 +403,96 @@ FHyperTwistTrainingRunState UHyperTwistContractLibrary::MakeSampleTrainingRunSta ); } +FHyperTwistTrainingMethodDrillDefinition UHyperTwistContractLibrary::MakeSampleMethodDrillDefinition() +{ + FHyperTwistTrainingMethodDrillDefinition DrillDefinition = + UHyperTwistTrainingLibrary::BuildMethodDrillDefinitionFromDeck( + HyperTwistContractLibraryInternal::MakeSampleMethodDrillDeck(), + EHyperTwistTrainingDrillModeKind::Recognition, + EHyperTwistTrainingDrillVisibilityPolicy::MaskedState, + true, + EHyperTwistTrainingDrillEntrySource::Catalog + ); + DrillDefinition.Title = TEXT("Roux First Block Recognition Drill"); + DrillDefinition.StageId = TEXT("first-block"); + DrillDefinition.StageLabel = TEXT("First Block"); + return DrillDefinition; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistContractLibrary::MakeSampleMethodDrillRunState() +{ + const FHyperTwistTrainingMethodDrillDefinition DrillDefinition = MakeSampleMethodDrillDefinition(); + if (!DrillDefinition.IsStructurallyValid()) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + const FHyperTwistTrainingMethodDrillRunState ReadyRun = UHyperTwistTrainingLibrary::StartMethodDrillRun( + DrillDefinition, + TEXT("local-user"), + TEXT("method_drill_run_01") + ); + + FHyperTwistTrainingMethodDrillRunState ActiveRun = UHyperTwistTrainingLibrary::StartNextMethodDrillCase(ReadyRun); + if (!ActiveRun.IsStructurallyValid()) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + ActiveRun.StartedAtUtc = TEXT("2026-04-25T12:12:00Z"); + ActiveRun.LastUpdatedAtUtc = TEXT("2026-04-25T12:12:05Z"); + return ActiveRun; +} + +FHyperTwistTrainingMethodDrillSummary UHyperTwistContractLibrary::MakeSampleMethodDrillSummary() +{ + return UHyperTwistTrainingLibrary::DeriveMethodDrillSummary(MakeSampleMethodDrillRunState()); +} + +FHyperTwistTrainingMethodDrillFavoriteEntry UHyperTwistContractLibrary::MakeSampleMethodDrillFavoriteEntry() +{ + return UHyperTwistTrainingRepositoryLibrary::MakeMethodDrillFavoriteEntry( + MakeSampleMethodDrillDefinition(), + TEXT("local-user"), + TEXT("roux-block-case-01"), + TEXT("2026-04-25T12:11:00Z") + ); +} + +FHyperTwistTrainingMethodDrillBatch UHyperTwistContractLibrary::MakeSampleMethodDrillBatch() +{ + FHyperTwistTrainingMethodDrillRunState RunState = MakeSampleMethodDrillRunState(); + if (!RunState.IsStructurallyValid()) + { + return FHyperTwistTrainingMethodDrillBatch(); + } + + RunState = UHyperTwistTrainingLibrary::RevealMethodDrillCase(RunState); + + FHyperTwistTrainingMethodDrillResponseRecord ResponseRecord; + ResponseRecord.ResponseId = TEXT("method_drill_response_01"); + ResponseRecord.DrillRunId = RunState.RunId; + ResponseRecord.CaseId = RunState.CurrentCase.CaseId; + ResponseRecord.PresentedAtUtc = RunState.StartedAtUtc; + ResponseRecord.RespondedAtUtc = TEXT("2026-04-25T12:13:00Z"); + ResponseRecord.DrillMode = RunState.DrillDefinition.DrillMode; + ResponseRecord.VisibilityPolicy = RunState.DrillDefinition.VisibilityPolicy; + ResponseRecord.bAnswerWasRevealed = true; + ResponseRecord.bMarkedCorrect = true; + ResponseRecord.bWasRepeat = false; + ResponseRecord.ElapsedMs = 1850; + ResponseRecord.ResponseText = TEXT("identified left block anchor and follow-up pair"); + RunState.Responses.Add(ResponseRecord); + RunState.LastUpdatedAtUtc = ResponseRecord.RespondedAtUtc; + + return UHyperTwistTrainingLibrary::BuildMethodDrillBatch( + RunState, + TEXT("method_drill_batch_01"), + TEXT("Pinned Roux Recognition Queue"), + true + ); +} + FHyperTwistTrainingRunStepResult UHyperTwistContractLibrary::MakeSampleTrainingRunStepResult() { const FHyperTwistTrainingRunState RunState = MakeSampleTrainingRunState(); @@ -434,6 +598,24 @@ FHyperTwistTrainingRepositoryState UHyperTwistContractLibrary::MakeSampleTrainin ); } + const FHyperTwistTrainingMethodDrillFavoriteEntry FavoriteEntry = MakeSampleMethodDrillFavoriteEntry(); + if (FavoriteEntry.IsStructurallyValid()) + { + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertMethodDrillFavorite( + RepositoryState, + FavoriteEntry + ); + } + + const FHyperTwistTrainingMethodDrillBatch DrillBatch = MakeSampleMethodDrillBatch(); + if (DrillBatch.IsStructurallyValid()) + { + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertMethodDrillBatch( + RepositoryState, + DrillBatch + ); + } + return RepositoryState; } @@ -468,6 +650,22 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistContractLibrary::MakeSam } RepositoryState.StoredSessionTemplates.Add(FHyperTwistTrainingSessionTemplate()); + const FHyperTwistTrainingMethodDrillFavoriteEntry FavoriteEntry = MakeSampleMethodDrillFavoriteEntry(); + if (FavoriteEntry.IsStructurallyValid()) + { + RepositoryState.StoredMethodDrillFavorites.Add(FavoriteEntry); + RepositoryState.StoredMethodDrillFavorites.Add(FavoriteEntry); + } + RepositoryState.StoredMethodDrillFavorites.Add(FHyperTwistTrainingMethodDrillFavoriteEntry()); + + const FHyperTwistTrainingMethodDrillBatch DrillBatch = MakeSampleMethodDrillBatch(); + if (DrillBatch.IsStructurallyValid()) + { + RepositoryState.StoredMethodDrillBatches.Add(DrillBatch); + RepositoryState.StoredMethodDrillBatches.Add(DrillBatch); + } + RepositoryState.StoredMethodDrillBatches.Add(FHyperTwistTrainingMethodDrillBatch()); + return UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(RepositoryState); } @@ -845,6 +1043,34 @@ FString UHyperTwistContractLibrary::SerializeContentPackToJson(const FHyperTwist return HyperTwistContractLibraryInternal::SerializeStructToJson(ContentPack); } +FString UHyperTwistContractLibrary::SerializeMethodDrillDefinitionToJson( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition +) +{ + return HyperTwistContractLibraryInternal::SerializeStructToJson(DrillDefinition); +} + +FString UHyperTwistContractLibrary::SerializeMethodDrillRunStateToJson( + const FHyperTwistTrainingMethodDrillRunState& RunState +) +{ + return HyperTwistContractLibraryInternal::SerializeStructToJson(RunState); +} + +FString UHyperTwistContractLibrary::SerializeMethodDrillFavoriteEntryToJson( + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry +) +{ + return HyperTwistContractLibraryInternal::SerializeStructToJson(FavoriteEntry); +} + +FString UHyperTwistContractLibrary::SerializeMethodDrillBatchToJson( + const FHyperTwistTrainingMethodDrillBatch& DrillBatch +) +{ + return HyperTwistContractLibraryInternal::SerializeStructToJson(DrillBatch); +} + FString UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(const FHyperTwistTrainingRepositoryState& RepositoryState) { return HyperTwistContractLibraryInternal::SerializeStructToJson(RepositoryState); @@ -872,6 +1098,38 @@ bool UHyperTwistContractLibrary::DeserializeContentPackFromJson(const FString& J return HyperTwistContractLibraryInternal::DeserializeStructFromJson(Json, OutContentPack); } +bool UHyperTwistContractLibrary::DeserializeMethodDrillDefinitionFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillDefinition& OutDrillDefinition +) +{ + return HyperTwistContractLibraryInternal::DeserializeStructFromJson(Json, OutDrillDefinition); +} + +bool UHyperTwistContractLibrary::DeserializeMethodDrillRunStateFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillRunState& OutRunState +) +{ + return HyperTwistContractLibraryInternal::DeserializeStructFromJson(Json, OutRunState); +} + +bool UHyperTwistContractLibrary::DeserializeMethodDrillFavoriteEntryFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillFavoriteEntry& OutFavoriteEntry +) +{ + return HyperTwistContractLibraryInternal::DeserializeStructFromJson(Json, OutFavoriteEntry); +} + +bool UHyperTwistContractLibrary::DeserializeMethodDrillBatchFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillBatch& OutDrillBatch +) +{ + return HyperTwistContractLibraryInternal::DeserializeStructFromJson(Json, OutDrillBatch); +} + bool UHyperTwistContractLibrary::DeserializeTrainingRepositoryStateFromJson(const FString& Json, FHyperTwistTrainingRepositoryState& OutRepositoryState) { return HyperTwistContractLibraryInternal::DeserializeStructFromJson(Json, OutRepositoryState); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp index d64e1a6..a7f4d98 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp @@ -1,5 +1,6 @@ #include "HyperTwistTraining/HyperTwistTrainingLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h" #include "HyperTwistReplay/HyperTwistReplayLibrary.h" #include "JsonObjectConverter.h" @@ -471,6 +472,171 @@ namespace HyperTwistTrainingLibraryInternal FJsonObjectConverter::UStructToJsonObjectString(Payload, PayloadJson); return PayloadJson; } + + FString ResolveDrillTimestamp(const FString& Value) + { + return Value.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : Value; + } + + FString DrillModeToToken(EHyperTwistTrainingDrillModeKind DrillMode) + { + switch (DrillMode) + { + case EHyperTwistTrainingDrillModeKind::Recognition: + return TEXT("recognition"); + case EHyperTwistTrainingDrillModeKind::Recall: + return TEXT("recall"); + case EHyperTwistTrainingDrillModeKind::Execution: + return TEXT("execution"); + case EHyperTwistTrainingDrillModeKind::Reconstruction: + return TEXT("reconstruction"); + default: + return TEXT("drill"); + } + } + + FString DrillVisibilityToToken(EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy) + { + switch (VisibilityPolicy) + { + case EHyperTwistTrainingDrillVisibilityPolicy::FullInformation: + return TEXT("full"); + case EHyperTwistTrainingDrillVisibilityPolicy::PromptOnly: + return TEXT("prompt-only"); + case EHyperTwistTrainingDrillVisibilityPolicy::MaskedState: + return TEXT("masked"); + case EHyperTwistTrainingDrillVisibilityPolicy::PartialState: + return TEXT("partial"); + default: + return TEXT("visibility"); + } + } + + TArray BuildDrillCaseIds(const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition) + { + TArray CaseIds; + if (!DrillDefinition.SourceDeck.IsStructurallyValid()) + { + return CaseIds; + } + + if (DrillDefinition.FocusCaseIds.Num() > 0) + { + for (const FString& CaseId : DrillDefinition.FocusCaseIds) + { + const FHyperTwistTrainingCase* MatchingCase = FindCaseById(DrillDefinition.SourceDeck, CaseId); + if (MatchingCase != nullptr && MatchingCase->IsStructurallyValid()) + { + CaseIds.AddUnique(CaseId); + } + } + } + + if (CaseIds.Num() == 0) + { + for (const FHyperTwistTrainingCase& TrainingCase : DrillDefinition.SourceDeck.Cases) + { + if (TrainingCase.IsStructurallyValid()) + { + CaseIds.AddUnique(TrainingCase.CaseId); + } + } + } + + return CaseIds; + } + + const FHyperTwistTrainingCase* FindDrillCaseById( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& CaseId + ) + { + return FindCaseById(DrillDefinition.SourceDeck, CaseId); + } + + void NormalizeDrillQueueAgainstDefinition( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + TArray& PendingCaseIds, + TArray& CompletedCaseIds, + FString& CurrentCaseId + ) + { + const TArray AllowedCaseIds = BuildDrillCaseIds(DrillDefinition); + TSet AllowedCaseSet(AllowedCaseIds); + + TArray NormalizedPending; + for (const FString& CaseId : PendingCaseIds) + { + if (AllowedCaseSet.Contains(CaseId)) + { + NormalizedPending.AddUnique(CaseId); + } + } + + TArray NormalizedCompleted; + for (const FString& CaseId : CompletedCaseIds) + { + if (AllowedCaseSet.Contains(CaseId)) + { + NormalizedCompleted.AddUnique(CaseId); + } + } + + PendingCaseIds = MoveTemp(NormalizedPending); + CompletedCaseIds = MoveTemp(NormalizedCompleted); + + if (!CurrentCaseId.IsEmpty()) + { + if (!AllowedCaseSet.Contains(CurrentCaseId)) + { + CurrentCaseId.Reset(); + } + else + { + CompletedCaseIds.Remove(CurrentCaseId); + } + } + } + + FHyperTwistTrainingMethodDrillSummary BuildDrillSummaryInternal( + const FHyperTwistTrainingMethodDrillRunState& RunState + ) + { + FHyperTwistTrainingMethodDrillSummary Summary; + if (!RunState.IsStructurallyValid()) + { + return Summary; + } + + TSet KnownCaseIds; + for (const FString& CaseId : RunState.PendingCaseIds) + { + KnownCaseIds.Add(CaseId); + } + for (const FString& CaseId : RunState.CompletedCaseIds) + { + KnownCaseIds.Add(CaseId); + } + if (!RunState.CurrentCase.CaseId.IsEmpty()) + { + KnownCaseIds.Add(RunState.CurrentCase.CaseId); + } + + Summary.RunId = RunState.RunId; + Summary.DrillId = RunState.DrillDefinition.DrillId; + Summary.UserId = RunState.UserId; + Summary.LifecycleState = RunState.LifecycleState; + Summary.TotalCaseCount = KnownCaseIds.Num(); + Summary.PendingCaseCount = RunState.PendingCaseIds.Num(); + Summary.CompletedCaseCount = RunState.CompletedCaseIds.Num(); + Summary.ResponseCount = RunState.Responses.Num(); + Summary.RevealCount = RunState.RevealCount; + Summary.RepeatCount = RunState.RepeatCount; + Summary.bAnswerHiddenUntilReveal = RunState.DrillDefinition.bAnswerHiddenUntilReveal; + Summary.CurrentCaseId = RunState.CurrentCase.CaseId; + Summary.LastUpdatedAtUtc = RunState.LastUpdatedAtUtc; + return Summary; + } } FHyperTwistContentPack UHyperTwistTrainingLibrary::MakeSampleClassicContentPack() @@ -1449,3 +1615,351 @@ FHyperTwistTrainingRunStepResult UHyperTwistTrainingLibrary::ResolveTrainingRunA ); return StepResult; } + +FHyperTwistTrainingMethodDrillDefinition UHyperTwistTrainingLibrary::BuildMethodDrillDefinitionFromDeck( + const FHyperTwistTrainingDeck& Deck, + EHyperTwistTrainingDrillModeKind DrillMode, + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy, + bool bAnswerHiddenUntilReveal, + EHyperTwistTrainingDrillEntrySource EntrySource +) +{ + FHyperTwistTrainingMethodDrillDefinition DrillDefinition; + if (!Deck.IsStructurallyValid()) + { + return DrillDefinition; + } + + DrillDefinition.MethodDescriptor = UHyperTwistTrainingCatalogLibrary::DescribeTrainingDeckMethod(Deck); + if (!DrillDefinition.MethodDescriptor.IsStructurallyValid()) + { + return DrillDefinition; + } + + DrillDefinition.SourceDeckId = Deck.DeckId; + DrillDefinition.SourceDeck = Deck; + DrillDefinition.FocusCaseIds = HyperTwistTrainingLibraryInternal::BuildDrillCaseIds(DrillDefinition); + DrillDefinition.DrillMode = DrillMode; + DrillDefinition.VisibilityPolicy = VisibilityPolicy; + DrillDefinition.EntrySource = EntrySource; + DrillDefinition.bAnswerHiddenUntilReveal = bAnswerHiddenUntilReveal; + DrillDefinition.bAllowImmediateRepeat = true; + DrillDefinition.SourceAttribution = Deck.SourceAttribution; + DrillDefinition.StageId = !Deck.SubsetId.IsEmpty() + ? Deck.SubsetId.ToLower() + : DrillDefinition.MethodDescriptor.SegmentId; + DrillDefinition.StageLabel = !Deck.SubsetId.IsEmpty() + ? Deck.SubsetId + : DrillDefinition.MethodDescriptor.MethodLabel; + DrillDefinition.DrillId = FString::Printf( + TEXT("%s/drill/%s/%s"), + *Deck.DeckId, + *HyperTwistTrainingLibraryInternal::DrillModeToToken(DrillMode), + *HyperTwistTrainingLibraryInternal::DrillVisibilityToToken(VisibilityPolicy) + ); + DrillDefinition.Title = FString::Printf(TEXT("%s Drill"), *Deck.Title); + DrillDefinition.Tags = Deck.Tags; + DrillDefinition.Tags.AddUnique(TEXT("method-drill")); + DrillDefinition.Tags.AddUnique(FString::Printf( + TEXT("drill-mode:%s"), + *HyperTwistTrainingLibraryInternal::DrillModeToToken(DrillMode) + )); + DrillDefinition.Tags.AddUnique(FString::Printf( + TEXT("visibility:%s"), + *HyperTwistTrainingLibraryInternal::DrillVisibilityToToken(VisibilityPolicy) + )); + DrillDefinition.Tags.AddUnique(FString::Printf( + TEXT("method-segment:%s"), + *DrillDefinition.MethodDescriptor.SegmentId.ToLower() + )); + return DrillDefinition; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingLibrary::StartMethodDrillRun( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& RunId +) +{ + FHyperTwistTrainingMethodDrillRunState RunState; + if (!DrillDefinition.IsStructurallyValid()) + { + return RunState; + } + + RunState.DrillDefinition = DrillDefinition; + RunState.RunId = RunId.IsEmpty() + ? FString::Printf(TEXT("method_drill_%s"), *DrillDefinition.DrillId) + : RunId; + RunState.UserId = UserId.IsEmpty() ? TEXT("local-user") : UserId; + RunState.StartedAtUtc = FDateTime::UtcNow().ToIso8601(); + RunState.LastUpdatedAtUtc = RunState.StartedAtUtc; + RunState.LifecycleState = EHyperTwistTrainingDrillLifecycleState::Ready; + RunState.PendingCaseIds = HyperTwistTrainingLibraryInternal::BuildDrillCaseIds(DrillDefinition); + if (RunState.PendingCaseIds.Num() == 0) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + return RunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingLibrary::ResumeMethodDrillRunFromBatch( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch, + const FString& RunId +) +{ + FHyperTwistTrainingMethodDrillRunState RunState; + if (!DrillDefinition.IsStructurallyValid() + || !DrillBatch.IsStructurallyValid() + || DrillBatch.DrillId != DrillDefinition.DrillId + || DrillBatch.SourceDeckId != DrillDefinition.SourceDeckId) + { + return RunState; + } + + RunState.DrillDefinition = DrillDefinition; + RunState.RunId = RunId.IsEmpty() + ? FString::Printf(TEXT("method_drill_resume_%s"), *DrillBatch.BatchId) + : RunId; + RunState.UserId = DrillBatch.UserId; + RunState.SourceBatchId = DrillBatch.BatchId; + RunState.LifecycleState = DrillBatch.SavedLifecycleState; + RunState.StartedAtUtc = HyperTwistTrainingLibraryInternal::ResolveDrillTimestamp(DrillBatch.CreatedAtUtc); + RunState.LastUpdatedAtUtc = HyperTwistTrainingLibraryInternal::ResolveDrillTimestamp(DrillBatch.LastUpdatedAtUtc); + RunState.PendingCaseIds = DrillBatch.PendingCaseIds; + RunState.CompletedCaseIds = DrillBatch.CompletedCaseIds; + RunState.Responses = DrillBatch.Responses; + RunState.RevealCount = DrillBatch.RevealCount; + RunState.RepeatCount = DrillBatch.RepeatCount; + RunState.bAnswerVisible = RunState.LifecycleState == EHyperTwistTrainingDrillLifecycleState::Revealed; + + FString CurrentCaseId = DrillBatch.CurrentCaseId; + HyperTwistTrainingLibraryInternal::NormalizeDrillQueueAgainstDefinition( + RunState.DrillDefinition, + RunState.PendingCaseIds, + RunState.CompletedCaseIds, + CurrentCaseId + ); + + if (!CurrentCaseId.IsEmpty()) + { + const FHyperTwistTrainingCase* CurrentCase = HyperTwistTrainingLibraryInternal::FindDrillCaseById( + RunState.DrillDefinition, + CurrentCaseId + ); + if (CurrentCase != nullptr && CurrentCase->IsStructurallyValid()) + { + RunState.CurrentCase = *CurrentCase; + } + } + + if ((RunState.LifecycleState == EHyperTwistTrainingDrillLifecycleState::Active + || RunState.LifecycleState == EHyperTwistTrainingDrillLifecycleState::Revealed) + && !RunState.CurrentCase.IsStructurallyValid()) + { + RunState.LifecycleState = EHyperTwistTrainingDrillLifecycleState::Ready; + RunState.bAnswerVisible = false; + } + + if (RunState.LifecycleState == EHyperTwistTrainingDrillLifecycleState::Completed + || RunState.LifecycleState == EHyperTwistTrainingDrillLifecycleState::Aborted) + { + RunState.EndedAtUtc = RunState.LastUpdatedAtUtc; + RunState.CurrentCase = FHyperTwistTrainingCase(); + RunState.bAnswerVisible = false; + } + + return RunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingLibrary::StartNextMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState +) +{ + FHyperTwistTrainingMethodDrillRunState UpdatedRunState = RunState; + if (!UpdatedRunState.IsStructurallyValid() + || UpdatedRunState.LifecycleState != EHyperTwistTrainingDrillLifecycleState::Ready) + { + return UpdatedRunState; + } + + FString CurrentCaseId = UpdatedRunState.CurrentCase.CaseId; + HyperTwistTrainingLibraryInternal::NormalizeDrillQueueAgainstDefinition( + UpdatedRunState.DrillDefinition, + UpdatedRunState.PendingCaseIds, + UpdatedRunState.CompletedCaseIds, + CurrentCaseId + ); + + if (UpdatedRunState.PendingCaseIds.Num() == 0) + { + return FinishMethodDrillRun(UpdatedRunState, false); + } + + const FHyperTwistTrainingCase* NextCase = HyperTwistTrainingLibraryInternal::FindDrillCaseById( + UpdatedRunState.DrillDefinition, + UpdatedRunState.PendingCaseIds[0] + ); + if (NextCase == nullptr || !NextCase->IsStructurallyValid()) + { + UpdatedRunState.PendingCaseIds.RemoveAt(0); + return StartNextMethodDrillCase(UpdatedRunState); + } + + UpdatedRunState.CurrentCase = *NextCase; + UpdatedRunState.LifecycleState = EHyperTwistTrainingDrillLifecycleState::Active; + UpdatedRunState.bAnswerVisible = !UpdatedRunState.DrillDefinition.bAnswerHiddenUntilReveal; + UpdatedRunState.LastUpdatedAtUtc = FDateTime::UtcNow().ToIso8601(); + return UpdatedRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingLibrary::RevealMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState +) +{ + FHyperTwistTrainingMethodDrillRunState UpdatedRunState = RunState; + if (!UpdatedRunState.IsStructurallyValid() + || UpdatedRunState.LifecycleState != EHyperTwistTrainingDrillLifecycleState::Active + || !UpdatedRunState.CurrentCase.IsStructurallyValid()) + { + return UpdatedRunState; + } + + if (!UpdatedRunState.bAnswerVisible) + { + UpdatedRunState.RevealCount += 1; + } + + UpdatedRunState.bAnswerVisible = true; + UpdatedRunState.LifecycleState = EHyperTwistTrainingDrillLifecycleState::Revealed; + UpdatedRunState.LastUpdatedAtUtc = FDateTime::UtcNow().ToIso8601(); + return UpdatedRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingLibrary::RepeatMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState +) +{ + FHyperTwistTrainingMethodDrillRunState UpdatedRunState = RunState; + if (!UpdatedRunState.IsStructurallyValid() + || (UpdatedRunState.LifecycleState != EHyperTwistTrainingDrillLifecycleState::Active + && UpdatedRunState.LifecycleState != EHyperTwistTrainingDrillLifecycleState::Revealed) + || !UpdatedRunState.CurrentCase.IsStructurallyValid() + || !UpdatedRunState.DrillDefinition.bAllowImmediateRepeat) + { + return UpdatedRunState; + } + + UpdatedRunState.RepeatCount += 1; + UpdatedRunState.bAnswerVisible = !UpdatedRunState.DrillDefinition.bAnswerHiddenUntilReveal; + UpdatedRunState.LifecycleState = EHyperTwistTrainingDrillLifecycleState::Active; + UpdatedRunState.LastUpdatedAtUtc = FDateTime::UtcNow().ToIso8601(); + return UpdatedRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingLibrary::AdvanceMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState +) +{ + FHyperTwistTrainingMethodDrillRunState UpdatedRunState = RunState; + if (!UpdatedRunState.IsStructurallyValid() + || (UpdatedRunState.LifecycleState != EHyperTwistTrainingDrillLifecycleState::Active + && UpdatedRunState.LifecycleState != EHyperTwistTrainingDrillLifecycleState::Revealed) + || !UpdatedRunState.CurrentCase.IsStructurallyValid()) + { + return UpdatedRunState; + } + + const FString CompletedCaseId = UpdatedRunState.CurrentCase.CaseId; + UpdatedRunState.PendingCaseIds.Remove(CompletedCaseId); + UpdatedRunState.CompletedCaseIds.AddUnique(CompletedCaseId); + UpdatedRunState.CurrentCase = FHyperTwistTrainingCase(); + UpdatedRunState.bAnswerVisible = false; + UpdatedRunState.LastUpdatedAtUtc = FDateTime::UtcNow().ToIso8601(); + + FString CurrentCaseId; + HyperTwistTrainingLibraryInternal::NormalizeDrillQueueAgainstDefinition( + UpdatedRunState.DrillDefinition, + UpdatedRunState.PendingCaseIds, + UpdatedRunState.CompletedCaseIds, + CurrentCaseId + ); + + UpdatedRunState.LifecycleState = UpdatedRunState.PendingCaseIds.Num() > 0 + ? EHyperTwistTrainingDrillLifecycleState::Ready + : EHyperTwistTrainingDrillLifecycleState::Completed; + + if (UpdatedRunState.LifecycleState == EHyperTwistTrainingDrillLifecycleState::Completed) + { + UpdatedRunState.EndedAtUtc = UpdatedRunState.LastUpdatedAtUtc; + } + + return UpdatedRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingLibrary::FinishMethodDrillRun( + const FHyperTwistTrainingMethodDrillRunState& RunState, + bool bAbortRun +) +{ + FHyperTwistTrainingMethodDrillRunState UpdatedRunState = RunState; + if (!UpdatedRunState.IsStructurallyValid()) + { + return UpdatedRunState; + } + + UpdatedRunState.LifecycleState = bAbortRun + ? EHyperTwistTrainingDrillLifecycleState::Aborted + : EHyperTwistTrainingDrillLifecycleState::Completed; + UpdatedRunState.EndedAtUtc = FDateTime::UtcNow().ToIso8601(); + UpdatedRunState.LastUpdatedAtUtc = UpdatedRunState.EndedAtUtc; + UpdatedRunState.CurrentCase = FHyperTwistTrainingCase(); + UpdatedRunState.bAnswerVisible = false; + return UpdatedRunState; +} + +FHyperTwistTrainingMethodDrillSummary UHyperTwistTrainingLibrary::DeriveMethodDrillSummary( + const FHyperTwistTrainingMethodDrillRunState& RunState +) +{ + return HyperTwistTrainingLibraryInternal::BuildDrillSummaryInternal(RunState); +} + +FHyperTwistTrainingMethodDrillBatch UHyperTwistTrainingLibrary::BuildMethodDrillBatch( + const FHyperTwistTrainingMethodDrillRunState& RunState, + const FString& BatchId, + const FString& Title, + bool bPinnedFavoritesOnly +) +{ + FHyperTwistTrainingMethodDrillBatch DrillBatch; + if (!RunState.IsStructurallyValid()) + { + return DrillBatch; + } + + DrillBatch.UserId = RunState.UserId; + DrillBatch.BatchId = BatchId.IsEmpty() + ? FString::Printf(TEXT("method_drill_batch_%s"), *RunState.RunId) + : BatchId; + DrillBatch.Title = Title.IsEmpty() ? RunState.DrillDefinition.Title : Title; + DrillBatch.DrillId = RunState.DrillDefinition.DrillId; + DrillBatch.SourceDeckId = RunState.DrillDefinition.SourceDeckId; + DrillBatch.MethodDescriptor = RunState.DrillDefinition.MethodDescriptor; + DrillBatch.DrillMode = RunState.DrillDefinition.DrillMode; + DrillBatch.VisibilityPolicy = RunState.DrillDefinition.VisibilityPolicy; + DrillBatch.bAnswerHiddenUntilReveal = RunState.DrillDefinition.bAnswerHiddenUntilReveal; + DrillBatch.SavedLifecycleState = RunState.LifecycleState; + DrillBatch.PendingCaseIds = RunState.PendingCaseIds; + DrillBatch.CompletedCaseIds = RunState.CompletedCaseIds; + DrillBatch.CurrentCaseId = RunState.CurrentCase.CaseId; + DrillBatch.Responses = RunState.Responses; + DrillBatch.RevealCount = RunState.RevealCount; + DrillBatch.RepeatCount = RunState.RepeatCount; + DrillBatch.CreatedAtUtc = HyperTwistTrainingLibraryInternal::ResolveDrillTimestamp(RunState.StartedAtUtc); + DrillBatch.LastUpdatedAtUtc = HyperTwistTrainingLibraryInternal::ResolveDrillTimestamp(RunState.LastUpdatedAtUtc); + DrillBatch.bPinnedFavoritesOnly = bPinnedFavoritesOnly; + return DrillBatch; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index 85be793..52da1a6 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -282,6 +282,54 @@ namespace HyperTwistTrainingRepositoryLibraryInternal return UserId + CaseKeySeparator + TemplateId; } + FString MakeMethodDrillFavoriteKey( + const FString& UserId, + const FString& DrillId, + const FString& CaseId + ) + { + return UserId + CaseKeySeparator + DrillId + CaseKeySeparator + CaseId; + } + + FString MakeMethodDrillBatchKey(const FString& UserId, const FString& BatchId) + { + return UserId + CaseKeySeparator + BatchId; + } + + TArray DeduplicateCaseIds(const TArray& CaseIds) + { + TArray UniqueCaseIds; + for (const FString& CaseId : CaseIds) + { + if (!CaseId.IsEmpty()) + { + UniqueCaseIds.AddUnique(CaseId); + } + } + + return UniqueCaseIds; + } + + TArray DeduplicateMethodDrillResponses( + const TArray& Responses + ) + { + TArray UniqueResponses; + TSet SeenResponseIds; + for (const FHyperTwistTrainingMethodDrillResponseRecord& Response : Responses) + { + if (!Response.IsStructurallyValid() || SeenResponseIds.Contains(Response.ResponseId)) + { + continue; + } + + SeenResponseIds.Add(Response.ResponseId); + UniqueResponses.Add(Response); + } + + return UniqueResponses; + } + bool SplitCaseKey(const FString& CaseKey, FString& OutUserId, FString& OutDeckId, FString& OutCaseId) { TArray Parts; @@ -308,6 +356,45 @@ namespace HyperTwistTrainingRepositoryLibraryInternal } } + FHyperTwistTrainingMethodDrillFavoriteEntry NormalizeMethodDrillFavoriteEntry( + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry + ) + { + FHyperTwistTrainingMethodDrillFavoriteEntry NormalizedFavorite = FavoriteEntry; + NormalizedFavorite.Tags = DeduplicateTemplateCaseIds(FavoriteEntry.Tags); + NormalizedFavorite.Tags.AddUnique(TEXT("favorite")); + if (NormalizedFavorite.AddedAtUtc.IsEmpty()) + { + NormalizedFavorite.AddedAtUtc = FDateTime::UtcNow().ToIso8601(); + } + + return NormalizedFavorite; + } + + FHyperTwistTrainingMethodDrillBatch NormalizeMethodDrillBatch( + const FHyperTwistTrainingMethodDrillBatch& DrillBatch + ) + { + FHyperTwistTrainingMethodDrillBatch NormalizedBatch = DrillBatch; + NormalizedBatch.PendingCaseIds = DeduplicateCaseIds(DrillBatch.PendingCaseIds); + NormalizedBatch.CompletedCaseIds = DeduplicateCaseIds(DrillBatch.CompletedCaseIds); + if (!NormalizedBatch.CurrentCaseId.IsEmpty()) + { + NormalizedBatch.CompletedCaseIds.Remove(NormalizedBatch.CurrentCaseId); + } + NormalizedBatch.Responses = DeduplicateMethodDrillResponses(DrillBatch.Responses); + if (NormalizedBatch.CreatedAtUtc.IsEmpty()) + { + NormalizedBatch.CreatedAtUtc = FDateTime::UtcNow().ToIso8601(); + } + if (NormalizedBatch.LastUpdatedAtUtc.IsEmpty()) + { + NormalizedBatch.LastUpdatedAtUtc = NormalizedBatch.CreatedAtUtc; + } + + return NormalizedBatch; + } + FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory( const FString& UserId, const FString& DeckId, @@ -1369,6 +1456,273 @@ FHyperTwistTrainingRunRecord UHyperTwistTrainingRepositoryLibrary::BuildRunRecor return RunRecord; } +FHyperTwistTrainingMethodDrillFavoriteEntry UHyperTwistTrainingRepositoryLibrary::MakeMethodDrillFavoriteEntry( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& CaseId, + const FString& AddedAtUtc +) +{ + FHyperTwistTrainingMethodDrillFavoriteEntry FavoriteEntry; + if (!DrillDefinition.IsStructurallyValid() || UserId.IsEmpty() || CaseId.IsEmpty()) + { + return FavoriteEntry; + } + + const FHyperTwistTrainingCase* MatchingCase = DrillDefinition.SourceDeck.Cases.FindByPredicate( + [&CaseId](const FHyperTwistTrainingCase& TrainingCase) + { + return TrainingCase.CaseId == CaseId; + } + ); + if (MatchingCase == nullptr || !MatchingCase->IsStructurallyValid()) + { + return FavoriteEntry; + } + + FavoriteEntry.UserId = UserId; + FavoriteEntry.DrillId = DrillDefinition.DrillId; + FavoriteEntry.CaseId = CaseId; + FavoriteEntry.SourceDeckId = DrillDefinition.SourceDeckId; + FavoriteEntry.MethodDescriptor = DrillDefinition.MethodDescriptor; + FavoriteEntry.AddedAtUtc = AddedAtUtc.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : AddedAtUtc; + FavoriteEntry.Tags = DrillDefinition.Tags; + for (const FString& Tag : MatchingCase->Tags) + { + FavoriteEntry.Tags.AddUnique(Tag); + } + FavoriteEntry.Tags.AddUnique(TEXT("favorite")); + return FavoriteEntry; +} + +TArray UHyperTwistTrainingRepositoryLibrary::ListMethodDrillFavorites( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& MethodSegmentId +) +{ + TArray Favorites; + for (const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry : RepositoryState.StoredMethodDrillFavorites) + { + if (!FavoriteEntry.IsStructurallyValid()) + { + continue; + } + + const bool bMatchesUser = UserId.IsEmpty() || FavoriteEntry.UserId == UserId; + const bool bMatchesSegment = MethodSegmentId.IsEmpty() + || FavoriteEntry.MethodDescriptor.SegmentId == MethodSegmentId; + if (bMatchesUser && bMatchesSegment) + { + Favorites.Add(FavoriteEntry); + } + } + + Favorites.Sort([]( + const FHyperTwistTrainingMethodDrillFavoriteEntry& Left, + const FHyperTwistTrainingMethodDrillFavoriteEntry& Right + ) + { + const int32 AddedAtCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.AddedAtUtc, + Right.AddedAtUtc + ); + if (AddedAtCompare != 0) + { + return AddedAtCompare > 0; + } + if (Left.DrillId != Right.DrillId) + { + return Left.DrillId < Right.DrillId; + } + return Left.CaseId < Right.CaseId; + }); + + return Favorites; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertMethodDrillFavorite( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + if (!FavoriteEntry.IsStructurallyValid()) + { + return UpdatedRepositoryState; + } + + const FHyperTwistTrainingMethodDrillFavoriteEntry NormalizedFavorite = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeMethodDrillFavoriteEntry(FavoriteEntry); + FHyperTwistTrainingMethodDrillFavoriteEntry* ExistingFavorite = + UpdatedRepositoryState.StoredMethodDrillFavorites.FindByPredicate( + [&NormalizedFavorite](const FHyperTwistTrainingMethodDrillFavoriteEntry& Candidate) + { + return Candidate.UserId == NormalizedFavorite.UserId + && Candidate.DrillId == NormalizedFavorite.DrillId + && Candidate.CaseId == NormalizedFavorite.CaseId; + } + ); + + if (ExistingFavorite != nullptr) + { + *ExistingFavorite = NormalizedFavorite; + } + else + { + UpdatedRepositoryState.StoredMethodDrillFavorites.Add(NormalizedFavorite); + } + + return UpdatedRepositoryState; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RemoveMethodDrillFavorite( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& DrillId, + const FString& CaseId +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + if (UserId.IsEmpty() || DrillId.IsEmpty() || CaseId.IsEmpty()) + { + return UpdatedRepositoryState; + } + + UpdatedRepositoryState.StoredMethodDrillFavorites.RemoveAll( + [&UserId, &DrillId, &CaseId](const FHyperTwistTrainingMethodDrillFavoriteEntry& Candidate) + { + return Candidate.UserId == UserId + && Candidate.DrillId == DrillId + && Candidate.CaseId == CaseId; + } + ); + return UpdatedRepositoryState; +} + +TArray UHyperTwistTrainingRepositoryLibrary::ListMethodDrillBatches( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& MethodSegmentId +) +{ + TArray Batches; + for (const FHyperTwistTrainingMethodDrillBatch& DrillBatch : RepositoryState.StoredMethodDrillBatches) + { + if (!DrillBatch.IsStructurallyValid()) + { + continue; + } + + const bool bMatchesUser = UserId.IsEmpty() || DrillBatch.UserId == UserId; + const bool bMatchesSegment = MethodSegmentId.IsEmpty() + || DrillBatch.MethodDescriptor.SegmentId == MethodSegmentId; + if (bMatchesUser && bMatchesSegment) + { + Batches.Add(DrillBatch); + } + } + + Batches.Sort([](const FHyperTwistTrainingMethodDrillBatch& Left, const FHyperTwistTrainingMethodDrillBatch& Right) + { + const int32 UpdatedAtCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.LastUpdatedAtUtc, + Right.LastUpdatedAtUtc + ); + if (UpdatedAtCompare != 0) + { + return UpdatedAtCompare > 0; + } + return Left.BatchId < Right.BatchId; + }); + + return Batches; +} + +bool UHyperTwistTrainingRepositoryLibrary::TryGetMethodDrillBatch( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& BatchId, + FHyperTwistTrainingMethodDrillBatch& OutBatch +) +{ + OutBatch = FHyperTwistTrainingMethodDrillBatch(); + if (UserId.IsEmpty() || BatchId.IsEmpty()) + { + return false; + } + + const FHyperTwistTrainingMethodDrillBatch* MatchingBatch = + RepositoryState.StoredMethodDrillBatches.FindByPredicate( + [&UserId, &BatchId](const FHyperTwistTrainingMethodDrillBatch& Candidate) + { + return Candidate.UserId == UserId && Candidate.BatchId == BatchId; + } + ); + if (MatchingBatch == nullptr) + { + return false; + } + + OutBatch = *MatchingBatch; + return OutBatch.IsStructurallyValid(); +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertMethodDrillBatch( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + if (!DrillBatch.IsStructurallyValid()) + { + return UpdatedRepositoryState; + } + + const FHyperTwistTrainingMethodDrillBatch NormalizedBatch = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeMethodDrillBatch(DrillBatch); + FHyperTwistTrainingMethodDrillBatch* ExistingBatch = + UpdatedRepositoryState.StoredMethodDrillBatches.FindByPredicate( + [&NormalizedBatch](const FHyperTwistTrainingMethodDrillBatch& Candidate) + { + return Candidate.UserId == NormalizedBatch.UserId + && Candidate.BatchId == NormalizedBatch.BatchId; + } + ); + + if (ExistingBatch != nullptr) + { + *ExistingBatch = NormalizedBatch; + } + else + { + UpdatedRepositoryState.StoredMethodDrillBatches.Add(NormalizedBatch); + } + + return UpdatedRepositoryState; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RemoveMethodDrillBatch( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& BatchId +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + if (UserId.IsEmpty() || BatchId.IsEmpty()) + { + return UpdatedRepositoryState; + } + + UpdatedRepositoryState.StoredMethodDrillBatches.RemoveAll( + [&UserId, &BatchId](const FHyperTwistTrainingMethodDrillBatch& Candidate) + { + return Candidate.UserId == UserId && Candidate.BatchId == BatchId; + } + ); + return UpdatedRepositoryState; +} + FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertRunRecord( const FHyperTwistTrainingRepositoryState& RepositoryState, const FHyperTwistTrainingRunRecord& RunRecord @@ -2176,6 +2530,53 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar SeenStoredTemplateKeys.Add(TemplateKey); } + Report.TotalMethodDrillFavoriteCount = RepositoryState.StoredMethodDrillFavorites.Num(); + TSet SeenMethodDrillFavoriteKeys; + for (const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry : RepositoryState.StoredMethodDrillFavorites) + { + if (!FavoriteEntry.IsStructurallyValid()) + { + Report.InvalidMethodDrillFavoriteCount += 1; + continue; + } + + const FString FavoriteKey = HyperTwistTrainingRepositoryLibraryInternal::MakeMethodDrillFavoriteKey( + FavoriteEntry.UserId, + FavoriteEntry.DrillId, + FavoriteEntry.CaseId + ); + if (SeenMethodDrillFavoriteKeys.Contains(FavoriteKey)) + { + Report.DuplicateMethodDrillFavoriteCount += 1; + continue; + } + + SeenMethodDrillFavoriteKeys.Add(FavoriteKey); + } + + Report.TotalMethodDrillBatchCount = RepositoryState.StoredMethodDrillBatches.Num(); + TSet SeenMethodDrillBatchKeys; + for (const FHyperTwistTrainingMethodDrillBatch& DrillBatch : RepositoryState.StoredMethodDrillBatches) + { + if (!DrillBatch.IsStructurallyValid()) + { + Report.InvalidMethodDrillBatchCount += 1; + continue; + } + + const FString BatchKey = HyperTwistTrainingRepositoryLibraryInternal::MakeMethodDrillBatchKey( + DrillBatch.UserId, + DrillBatch.BatchId + ); + if (SeenMethodDrillBatchKeys.Contains(BatchKey)) + { + Report.DuplicateMethodDrillBatchCount += 1; + continue; + } + + SeenMethodDrillBatchKeys.Add(BatchKey); + } + if (Report.InvalidRunRecordCount > 0) { HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( @@ -2235,6 +2636,34 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar TEXT("normalize-and-deduplicate-stored-session-templates") ); } + if (Report.InvalidMethodDrillFavoriteCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("drop-invalid-method-drill-favorites") + ); + } + if (Report.DuplicateMethodDrillFavoriteCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("normalize-and-deduplicate-method-drill-favorites") + ); + } + if (Report.InvalidMethodDrillBatchCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("drop-invalid-method-drill-batches") + ); + } + if (Report.DuplicateMethodDrillBatchCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("normalize-and-deduplicate-method-drill-batches") + ); + } return Report; } @@ -2375,6 +2804,120 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR return Left.TemplateId < Right.TemplateId; }); + TMap MethodDrillFavoriteIndexByKey; + for (const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry : RepositoryState.StoredMethodDrillFavorites) + { + if (!FavoriteEntry.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingMethodDrillFavoriteEntry NormalizedFavorite = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeMethodDrillFavoriteEntry(FavoriteEntry); + if (!NormalizedFavorite.IsStructurallyValid()) + { + continue; + } + + const FString FavoriteKey = HyperTwistTrainingRepositoryLibraryInternal::MakeMethodDrillFavoriteKey( + NormalizedFavorite.UserId, + NormalizedFavorite.DrillId, + NormalizedFavorite.CaseId + ); + if (const int32* ExistingIndex = MethodDrillFavoriteIndexByKey.Find(FavoriteKey)) + { + RepairedState.StoredMethodDrillFavorites[*ExistingIndex] = NormalizedFavorite; + } + else + { + MethodDrillFavoriteIndexByKey.Add( + FavoriteKey, + RepairedState.StoredMethodDrillFavorites.Add(NormalizedFavorite) + ); + } + } + + RepairedState.StoredMethodDrillFavorites.Sort([]( + const FHyperTwistTrainingMethodDrillFavoriteEntry& Left, + const FHyperTwistTrainingMethodDrillFavoriteEntry& Right + ) + { + if (Left.UserId != Right.UserId) + { + return Left.UserId < Right.UserId; + } + + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.AddedAtUtc, + Right.AddedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + if (Left.DrillId != Right.DrillId) + { + return Left.DrillId < Right.DrillId; + } + + return Left.CaseId < Right.CaseId; + }); + + TMap MethodDrillBatchIndexByKey; + for (const FHyperTwistTrainingMethodDrillBatch& DrillBatch : RepositoryState.StoredMethodDrillBatches) + { + if (!DrillBatch.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingMethodDrillBatch NormalizedBatch = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeMethodDrillBatch(DrillBatch); + if (!NormalizedBatch.IsStructurallyValid()) + { + continue; + } + + const FString BatchKey = HyperTwistTrainingRepositoryLibraryInternal::MakeMethodDrillBatchKey( + NormalizedBatch.UserId, + NormalizedBatch.BatchId + ); + if (const int32* ExistingIndex = MethodDrillBatchIndexByKey.Find(BatchKey)) + { + RepairedState.StoredMethodDrillBatches[*ExistingIndex] = NormalizedBatch; + } + else + { + MethodDrillBatchIndexByKey.Add( + BatchKey, + RepairedState.StoredMethodDrillBatches.Add(NormalizedBatch) + ); + } + } + + RepairedState.StoredMethodDrillBatches.Sort([]( + const FHyperTwistTrainingMethodDrillBatch& Left, + const FHyperTwistTrainingMethodDrillBatch& Right + ) + { + if (Left.UserId != Right.UserId) + { + return Left.UserId < Right.UserId; + } + + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.LastUpdatedAtUtc, + Right.LastUpdatedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + return Left.BatchId < Right.BatchId; + }); + return RepairedState; } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index 1a2f62f..aab95f5 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -38,6 +38,16 @@ bool UHyperTwistTrainingRuntimeLibrary::HasActiveTrainingRun(UObject* WorldConte return false; } +bool UHyperTwistTrainingRuntimeLibrary::HasActiveMethodDrillRun(UObject* WorldContextObject) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->HasActiveMethodDrillRun(); + } + + return false; +} + FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRunState(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -48,6 +58,18 @@ FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::GetActiveTraining return FHyperTwistTrainingRunState(); } +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::GetActiveMethodDrillRunState( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveMethodDrillRunState(); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + FHyperTwistTrainingSessionSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRunSummary(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -58,6 +80,18 @@ FHyperTwistTrainingSessionSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTr return FHyperTwistTrainingSessionSummary(); } +FHyperTwistTrainingMethodDrillSummary UHyperTwistTrainingRuntimeLibrary::GetActiveMethodDrillSummary( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveMethodDrillSummary(); + } + + return FHyperTwistTrainingMethodDrillSummary(); +} + FHyperTwistTrainingRepositoryState UHyperTwistTrainingRuntimeLibrary::GetTrainingRepositoryState(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -204,6 +238,34 @@ TArray UHyperTwistTrainingRuntimeLibrary::Ge return TArray(); } +TArray UHyperTwistTrainingRuntimeLibrary::GetMethodDrillFavoritesForUser( + UObject* WorldContextObject, + const FString& UserId, + const FString& MethodSegmentId +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetMethodDrillFavoritesForUser(UserId, MethodSegmentId); + } + + return TArray(); +} + +TArray UHyperTwistTrainingRuntimeLibrary::GetMethodDrillBatchesForUser( + UObject* WorldContextObject, + const FString& UserId, + const FString& MethodSegmentId +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetMethodDrillBatchesForUser(UserId, MethodSegmentId); + } + + return TArray(); +} + TArray UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCaseRecommendations(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -335,6 +397,77 @@ bool UHyperTwistTrainingRuntimeLibrary::SaveTrainingSessionTemplate( return false; } +bool UHyperTwistTrainingRuntimeLibrary::SaveMethodDrillFavorite( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->SaveMethodDrillFavorite(FavoriteEntry); + } + + return false; +} + +bool UHyperTwistTrainingRuntimeLibrary::RemoveMethodDrillFavorite( + UObject* WorldContextObject, + const FString& UserId, + const FString& DrillId, + const FString& CaseId +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->RemoveMethodDrillFavorite(UserId, DrillId, CaseId); + } + + return false; +} + +bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredMethodDrillBatch( + UObject* WorldContextObject, + const FString& UserId, + const FString& BatchId, + FHyperTwistTrainingMethodDrillBatch& OutBatch +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->TryGetStoredMethodDrillBatch(UserId, BatchId, OutBatch); + } + + OutBatch = FHyperTwistTrainingMethodDrillBatch(); + return false; +} + +bool UHyperTwistTrainingRuntimeLibrary::SaveMethodDrillBatch( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->SaveMethodDrillBatch(DrillBatch); + } + + return false; +} + +bool UHyperTwistTrainingRuntimeLibrary::RemoveMethodDrillBatch( + UObject* WorldContextObject, + const FString& UserId, + const FString& BatchId +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->RemoveMethodDrillBatch(UserId, BatchId); + } + + return false; +} + bool UHyperTwistTrainingRuntimeLibrary::RemoveTrainingSessionTemplate( UObject* WorldContextObject, const FString& UserId, @@ -548,6 +681,122 @@ FHyperTwistTrainingRunStepResult UHyperTwistTrainingRuntimeLibrary::SubmitTraini return FHyperTwistTrainingRunStepResult(); } +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::StartMethodDrillRun( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& RunId +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->StartMethodDrillRun(DrillDefinition, UserId, RunId); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::StartMethodDrillRunFromDeck( + UObject* WorldContextObject, + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& RunId, + EHyperTwistTrainingDrillModeKind DrillMode, + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy, + bool bAnswerHiddenUntilReveal +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->StartMethodDrillRunFromDeck( + Deck, + UserId, + RunId, + DrillMode, + VisibilityPolicy, + bAnswerHiddenUntilReveal + ); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::ResumeMethodDrillRunFromBatch( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch, + const FString& RunId +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->ResumeMethodDrillRunFromBatch(DrillDefinition, DrillBatch, RunId); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::StartNextActiveMethodDrillCase( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->StartNextActiveMethodDrillCase(); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::RevealActiveMethodDrillCase( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->RevealActiveMethodDrillCase(); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::RepeatActiveMethodDrillCase( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->RepeatActiveMethodDrillCase(); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::AdvanceActiveMethodDrillCase( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->AdvanceActiveMethodDrillCase(); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingRuntimeLibrary::FinishActiveMethodDrillRun( + UObject* WorldContextObject, + bool bAbortRun +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->FinishActiveMethodDrillRun(bAbortRun); + } + + return FHyperTwistTrainingMethodDrillRunState(); +} + FHyperTwistTrainingSession UHyperTwistTrainingRuntimeLibrary::CompleteActiveTrainingRun( UObject* WorldContextObject, bool bAbortSession @@ -568,3 +817,11 @@ void UHyperTwistTrainingRuntimeLibrary::ClearActiveTrainingRun(UObject* WorldCon TrainingSubsystem->ClearActiveRun(); } } + +void UHyperTwistTrainingRuntimeLibrary::ClearActiveMethodDrillRun(UObject* WorldContextObject) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + TrainingSubsystem->ClearActiveMethodDrillRun(); + } +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 5d3d1c2..946f5e6 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -10,16 +10,31 @@ bool UHyperTwistTrainingSubsystem::HasActiveRun() const return bHasActiveRun && ActiveRunState.IsStructurallyValid(); } +bool UHyperTwistTrainingSubsystem::HasActiveMethodDrillRun() const +{ + return bHasActiveMethodDrillRun && ActiveMethodDrillRunState.IsStructurallyValid(); +} + FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::GetActiveRunState() const { return ActiveRunState; } +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::GetActiveMethodDrillRunState() const +{ + return ActiveMethodDrillRunState; +} + FHyperTwistTrainingSessionSummary UHyperTwistTrainingSubsystem::GetActiveRunSummary() const { return ActiveRunSummary; } +FHyperTwistTrainingMethodDrillSummary UHyperTwistTrainingSubsystem::GetActiveMethodDrillSummary() const +{ + return ActiveMethodDrillSummary; +} + FHyperTwistTrainingRepositoryState UHyperTwistTrainingSubsystem::GetTrainingRepositoryState() const { return TrainingRepositoryState; @@ -93,6 +108,30 @@ TArray UHyperTwistTrainingSubsystem::GetTrai return BuildTrainingSessionTemplatesForUser(UserId, ReferenceUtc); } +TArray UHyperTwistTrainingSubsystem::GetMethodDrillFavoritesForUser( + const FString& UserId, + const FString& MethodSegmentId +) const +{ + return UHyperTwistTrainingRepositoryLibrary::ListMethodDrillFavorites( + TrainingRepositoryState, + UserId, + MethodSegmentId + ); +} + +TArray UHyperTwistTrainingSubsystem::GetMethodDrillBatchesForUser( + const FString& UserId, + const FString& MethodSegmentId +) const +{ + return UHyperTwistTrainingRepositoryLibrary::ListMethodDrillBatches( + TrainingRepositoryState, + UserId, + MethodSegmentId + ); +} + TArray UHyperTwistTrainingSubsystem::GetActiveCaseRecommendations() const { return ActiveCaseRecommendations; @@ -201,6 +240,94 @@ bool UHyperTwistTrainingSubsystem::SaveTrainingSessionTemplate( return true; } +bool UHyperTwistTrainingSubsystem::SaveMethodDrillFavorite( + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry +) +{ + if (!FavoriteEntry.IsStructurallyValid()) + { + return false; + } + + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertMethodDrillFavorite( + TrainingRepositoryState, + FavoriteEntry + ); + RefreshRepositoryViews(); + return true; +} + +bool UHyperTwistTrainingSubsystem::RemoveMethodDrillFavorite( + const FString& UserId, + const FString& DrillId, + const FString& CaseId +) +{ + if (UserId.IsEmpty() || DrillId.IsEmpty() || CaseId.IsEmpty()) + { + return false; + } + + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::RemoveMethodDrillFavorite( + TrainingRepositoryState, + UserId, + DrillId, + CaseId + ); + RefreshRepositoryViews(); + return true; +} + +bool UHyperTwistTrainingSubsystem::TryGetStoredMethodDrillBatch( + const FString& UserId, + const FString& BatchId, + FHyperTwistTrainingMethodDrillBatch& OutBatch +) const +{ + return UHyperTwistTrainingRepositoryLibrary::TryGetMethodDrillBatch( + TrainingRepositoryState, + UserId, + BatchId, + OutBatch + ); +} + +bool UHyperTwistTrainingSubsystem::SaveMethodDrillBatch( + const FHyperTwistTrainingMethodDrillBatch& DrillBatch +) +{ + if (!DrillBatch.IsStructurallyValid()) + { + return false; + } + + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertMethodDrillBatch( + TrainingRepositoryState, + DrillBatch + ); + RefreshRepositoryViews(); + return true; +} + +bool UHyperTwistTrainingSubsystem::RemoveMethodDrillBatch( + const FString& UserId, + const FString& BatchId +) +{ + if (UserId.IsEmpty() || BatchId.IsEmpty()) + { + return false; + } + + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::RemoveMethodDrillBatch( + TrainingRepositoryState, + UserId, + BatchId + ); + RefreshRepositoryViews(); + return true; +} + bool UHyperTwistTrainingSubsystem::RemoveTrainingSessionTemplate( const FString& UserId, const FString& TemplateId @@ -502,6 +629,118 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartSampleClassicTrai return StartTrainingRunFromDeck(DefaultDeck, UserId, SessionId, Mode); } +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::StartMethodDrillRun( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& RunId +) +{ + ActiveMethodDrillRunState = UHyperTwistTrainingLibrary::StartMethodDrillRun(DrillDefinition, UserId, RunId); + bHasActiveMethodDrillRun = ActiveMethodDrillRunState.IsStructurallyValid(); + RefreshMethodDrillSummary(); + return ActiveMethodDrillRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::StartMethodDrillRunFromDeck( + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& RunId, + EHyperTwistTrainingDrillModeKind DrillMode, + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy, + bool bAnswerHiddenUntilReveal +) +{ + const FHyperTwistTrainingMethodDrillDefinition DrillDefinition = + UHyperTwistTrainingLibrary::BuildMethodDrillDefinitionFromDeck( + Deck, + DrillMode, + VisibilityPolicy, + bAnswerHiddenUntilReveal, + EHyperTwistTrainingDrillEntrySource::Catalog + ); + return StartMethodDrillRun(DrillDefinition, UserId, RunId); +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::ResumeMethodDrillRunFromBatch( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch, + const FString& RunId +) +{ + ActiveMethodDrillRunState = UHyperTwistTrainingLibrary::ResumeMethodDrillRunFromBatch( + DrillDefinition, + DrillBatch, + RunId + ); + bHasActiveMethodDrillRun = ActiveMethodDrillRunState.IsStructurallyValid(); + RefreshMethodDrillSummary(); + return ActiveMethodDrillRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::StartNextActiveMethodDrillCase() +{ + if (!HasActiveMethodDrillRun()) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + ActiveMethodDrillRunState = UHyperTwistTrainingLibrary::StartNextMethodDrillCase(ActiveMethodDrillRunState); + bHasActiveMethodDrillRun = ActiveMethodDrillRunState.IsStructurallyValid(); + RefreshMethodDrillSummary(); + return ActiveMethodDrillRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::RevealActiveMethodDrillCase() +{ + if (!HasActiveMethodDrillRun()) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + ActiveMethodDrillRunState = UHyperTwistTrainingLibrary::RevealMethodDrillCase(ActiveMethodDrillRunState); + RefreshMethodDrillSummary(); + return ActiveMethodDrillRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::RepeatActiveMethodDrillCase() +{ + if (!HasActiveMethodDrillRun()) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + ActiveMethodDrillRunState = UHyperTwistTrainingLibrary::RepeatMethodDrillCase(ActiveMethodDrillRunState); + RefreshMethodDrillSummary(); + return ActiveMethodDrillRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::AdvanceActiveMethodDrillCase() +{ + if (!HasActiveMethodDrillRun()) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + ActiveMethodDrillRunState = UHyperTwistTrainingLibrary::AdvanceMethodDrillCase(ActiveMethodDrillRunState); + RefreshMethodDrillSummary(); + return ActiveMethodDrillRunState; +} + +FHyperTwistTrainingMethodDrillRunState UHyperTwistTrainingSubsystem::FinishActiveMethodDrillRun(bool bAbortRun) +{ + if (!HasActiveMethodDrillRun()) + { + return FHyperTwistTrainingMethodDrillRunState(); + } + + ActiveMethodDrillRunState = UHyperTwistTrainingLibrary::FinishMethodDrillRun( + ActiveMethodDrillRunState, + bAbortRun + ); + RefreshMethodDrillSummary(); + return ActiveMethodDrillRunState; +} + FHyperTwistTrainingRunStepResult UHyperTwistTrainingSubsystem::SubmitTrainingAttempt( const FHyperTwistTrainingAttempt& Attempt, int32 TimeMs @@ -595,6 +834,13 @@ void UHyperTwistTrainingSubsystem::ClearActiveRun() RefreshRepositoryViews(); } +void UHyperTwistTrainingSubsystem::ClearActiveMethodDrillRun() +{ + ActiveMethodDrillRunState = FHyperTwistTrainingMethodDrillRunState(); + ActiveMethodDrillSummary = FHyperTwistTrainingMethodDrillSummary(); + bHasActiveMethodDrillRun = false; +} + void UHyperTwistTrainingSubsystem::RefreshSummary() { if (!ActiveRunState.Session.IsStructurallyValid()) @@ -609,6 +855,19 @@ void UHyperTwistTrainingSubsystem::RefreshSummary() ); } +void UHyperTwistTrainingSubsystem::RefreshMethodDrillSummary() +{ + if (!ActiveMethodDrillRunState.IsStructurallyValid()) + { + ActiveMethodDrillSummary = FHyperTwistTrainingMethodDrillSummary(); + return; + } + + ActiveMethodDrillSummary = UHyperTwistTrainingLibrary::DeriveMethodDrillSummary( + ActiveMethodDrillRunState + ); +} + void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() { ActiveRepositoryIntegrityReport = UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport( diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h index 6d1ae47..46ac3ee 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h @@ -65,6 +65,21 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") static FHyperTwistTrainingRunState MakeSampleTrainingRunState(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillDefinition MakeSampleMethodDrillDefinition(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState MakeSampleMethodDrillRunState(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillSummary MakeSampleMethodDrillSummary(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillFavoriteEntry MakeSampleMethodDrillFavoriteEntry(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillBatch MakeSampleMethodDrillBatch(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") static FHyperTwistTrainingRunStepResult MakeSampleTrainingRunStepResult(); @@ -161,6 +176,26 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Serialization") static FString SerializeContentPackToJson(const FHyperTwistContentPack& ContentPack); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Serialization") + static FString SerializeMethodDrillDefinitionToJson( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Serialization") + static FString SerializeMethodDrillRunStateToJson( + const FHyperTwistTrainingMethodDrillRunState& RunState + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Serialization") + static FString SerializeMethodDrillFavoriteEntryToJson( + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Serialization") + static FString SerializeMethodDrillBatchToJson( + const FHyperTwistTrainingMethodDrillBatch& DrillBatch + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Serialization") static FString SerializeTrainingRepositoryStateToJson(const FHyperTwistTrainingRepositoryState& RepositoryState); @@ -176,6 +211,30 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Serialization") static bool DeserializeContentPackFromJson(const FString& Json, FHyperTwistContentPack& OutContentPack); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Serialization") + static bool DeserializeMethodDrillDefinitionFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillDefinition& OutDrillDefinition + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Serialization") + static bool DeserializeMethodDrillRunStateFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillRunState& OutRunState + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Serialization") + static bool DeserializeMethodDrillFavoriteEntryFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillFavoriteEntry& OutFavoriteEntry + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Serialization") + static bool DeserializeMethodDrillBatchFromJson( + const FString& Json, + FHyperTwistTrainingMethodDrillBatch& OutDrillBatch + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Serialization") static bool DeserializeTrainingRepositoryStateFromJson(const FString& Json, FHyperTwistTrainingRepositoryState& OutRepositoryState); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h index f181dd1..b0b43c7 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h @@ -146,4 +146,66 @@ public: const FHyperTwistTrainingAttempt& Attempt, int32 TimeMs ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillDefinition BuildMethodDrillDefinitionFromDeck( + const FHyperTwistTrainingDeck& Deck, + EHyperTwistTrainingDrillModeKind DrillMode, + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy, + bool bAnswerHiddenUntilReveal, + EHyperTwistTrainingDrillEntrySource EntrySource + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState StartMethodDrillRun( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& RunId + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState ResumeMethodDrillRunFromBatch( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch, + const FString& RunId + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState StartNextMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState RevealMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState RepeatMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState AdvanceMethodDrillCase( + const FHyperTwistTrainingMethodDrillRunState& RunState + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillRunState FinishMethodDrillRun( + const FHyperTwistTrainingMethodDrillRunState& RunState, + bool bAbortRun + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillSummary DeriveMethodDrillSummary( + const FHyperTwistTrainingMethodDrillRunState& RunState + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillBatch BuildMethodDrillBatch( + const FHyperTwistTrainingMethodDrillRunState& RunState, + const FString& BatchId, + const FString& Title, + bool bPinnedFavoritesOnly + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h index 95b0b0d..6b754ee 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h @@ -40,6 +40,63 @@ public: const FHyperTwistTrainingRunState& RunState ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static FHyperTwistTrainingMethodDrillFavoriteEntry MakeMethodDrillFavoriteEntry( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& CaseId, + const FString& AddedAtUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static TArray ListMethodDrillFavorites( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& MethodSegmentId + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static FHyperTwistTrainingRepositoryState UpsertMethodDrillFavorite( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static FHyperTwistTrainingRepositoryState RemoveMethodDrillFavorite( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& DrillId, + const FString& CaseId + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static TArray ListMethodDrillBatches( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& MethodSegmentId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Drill") + static bool TryGetMethodDrillBatch( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& BatchId, + FHyperTwistTrainingMethodDrillBatch& OutBatch + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static FHyperTwistTrainingRepositoryState UpsertMethodDrillBatch( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static FHyperTwistTrainingRepositoryState RemoveMethodDrillBatch( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& BatchId + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository") static FHyperTwistTrainingRepositoryState UpsertRunRecord( const FHyperTwistTrainingRepositoryState& RepositoryState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 601362c..a36f362 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -19,12 +19,21 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static bool HasActiveTrainingRun(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static bool HasActiveMethodDrillRun(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRunState GetActiveTrainingRunState(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState GetActiveMethodDrillRunState(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingSessionSummary GetActiveTrainingRunSummary(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillSummary GetActiveMethodDrillSummary(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRepositoryState GetTrainingRepositoryState(UObject* WorldContextObject); @@ -73,6 +82,20 @@ public: const FString& ReferenceUtc ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static TArray GetMethodDrillFavoritesForUser( + UObject* WorldContextObject, + const FString& UserId, + const FString& MethodSegmentId + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static TArray GetMethodDrillBatchesForUser( + UObject* WorldContextObject, + const FString& UserId, + const FString& MethodSegmentId + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static TArray GetActiveTrainingCaseRecommendations(UObject* WorldContextObject); @@ -130,6 +153,41 @@ public: const FHyperTwistTrainingSessionTemplate& SessionTemplate ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static bool SaveMethodDrillFavorite( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static bool RemoveMethodDrillFavorite( + UObject* WorldContextObject, + const FString& UserId, + const FString& DrillId, + const FString& CaseId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static bool TryGetStoredMethodDrillBatch( + UObject* WorldContextObject, + const FString& UserId, + const FString& BatchId, + FHyperTwistTrainingMethodDrillBatch& OutBatch + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static bool SaveMethodDrillBatch( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static bool RemoveMethodDrillBatch( + UObject* WorldContextObject, + const FString& UserId, + const FString& BatchId + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static bool RemoveTrainingSessionTemplate( UObject* WorldContextObject, @@ -234,6 +292,54 @@ public: int32 TimeMs ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState StartMethodDrillRun( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& RunId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState StartMethodDrillRunFromDeck( + UObject* WorldContextObject, + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& RunId, + EHyperTwistTrainingDrillModeKind DrillMode, + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy, + bool bAnswerHiddenUntilReveal + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState ResumeMethodDrillRunFromBatch( + UObject* WorldContextObject, + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch, + const FString& RunId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState StartNextActiveMethodDrillCase(UObject* WorldContextObject); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState RevealActiveMethodDrillCase(UObject* WorldContextObject); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState RepeatActiveMethodDrillCase(UObject* WorldContextObject); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState AdvanceActiveMethodDrillCase(UObject* WorldContextObject); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillRunState FinishActiveMethodDrillRun( + UObject* WorldContextObject, + bool bAbortRun + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static void ClearActiveMethodDrillRun(UObject* WorldContextObject); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingSession CompleteActiveTrainingRun( UObject* WorldContextObject, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 0aadfab..9bbd2e9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -14,12 +14,21 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") bool HasActiveRun() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + bool HasActiveMethodDrillRun() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingRunState GetActiveRunState() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState GetActiveMethodDrillRunState() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingSessionSummary GetActiveRunSummary() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillSummary GetActiveMethodDrillSummary() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingRepositoryState GetTrainingRepositoryState() const; @@ -65,6 +74,18 @@ public: const FString& ReferenceUtc ) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + TArray GetMethodDrillFavoritesForUser( + const FString& UserId, + const FString& MethodSegmentId + ) const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + TArray GetMethodDrillBatchesForUser( + const FString& UserId, + const FString& MethodSegmentId + ) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") TArray GetActiveCaseRecommendations() const; @@ -114,6 +135,32 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") bool SaveTrainingSessionTemplate(const FHyperTwistTrainingSessionTemplate& SessionTemplate); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + bool SaveMethodDrillFavorite(const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + bool RemoveMethodDrillFavorite( + const FString& UserId, + const FString& DrillId, + const FString& CaseId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + bool TryGetStoredMethodDrillBatch( + const FString& UserId, + const FString& BatchId, + FHyperTwistTrainingMethodDrillBatch& OutBatch + ) const; + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + bool SaveMethodDrillBatch(const FHyperTwistTrainingMethodDrillBatch& DrillBatch); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + bool RemoveMethodDrillBatch( + const FString& UserId, + const FString& BatchId + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") bool RemoveTrainingSessionTemplate( const FString& UserId, @@ -190,6 +237,48 @@ public: EHyperTwistTrainingDeliveryMode Mode ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState StartMethodDrillRun( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FString& UserId, + const FString& RunId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState StartMethodDrillRunFromDeck( + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& RunId, + EHyperTwistTrainingDrillModeKind DrillMode, + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy, + bool bAnswerHiddenUntilReveal + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState ResumeMethodDrillRunFromBatch( + const FHyperTwistTrainingMethodDrillDefinition& DrillDefinition, + const FHyperTwistTrainingMethodDrillBatch& DrillBatch, + const FString& RunId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState StartNextActiveMethodDrillCase(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState RevealActiveMethodDrillCase(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState RepeatActiveMethodDrillCase(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState AdvanceActiveMethodDrillCase(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillRunState FinishActiveMethodDrillRun(bool bAbortRun); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") + void ClearActiveMethodDrillRun(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRunStepResult SubmitTrainingAttempt( const FHyperTwistTrainingAttempt& Attempt, @@ -204,6 +293,7 @@ public: private: void RefreshSummary(); + void RefreshMethodDrillSummary(); void RefreshRepositoryViews(); TArray BuildTrainingSessionTemplatesForUser( const FString& UserId, @@ -216,6 +306,12 @@ private: UPROPERTY() FHyperTwistTrainingSessionSummary ActiveRunSummary; + UPROPERTY() + FHyperTwistTrainingMethodDrillRunState ActiveMethodDrillRunState; + + UPROPERTY() + FHyperTwistTrainingMethodDrillSummary ActiveMethodDrillSummary; + UPROPERTY() FHyperTwistTrainingRepositoryState TrainingRepositoryState; @@ -269,4 +365,7 @@ private: UPROPERTY() bool bHasActiveRun = false; + + UPROPERTY() + bool bHasActiveMethodDrillRun = false; }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index fddd4ca..413fc2e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -102,6 +102,43 @@ enum class EHyperTwistTrainingSourceMode : uint8 Benchmark UMETA(DisplayName = "Benchmark") }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingDrillModeKind : uint8 +{ + Recognition UMETA(DisplayName = "Recognition"), + Recall UMETA(DisplayName = "Recall"), + Execution UMETA(DisplayName = "Execution"), + Reconstruction UMETA(DisplayName = "Reconstruction") +}; + +UENUM(BlueprintType) +enum class EHyperTwistTrainingDrillVisibilityPolicy : uint8 +{ + FullInformation UMETA(DisplayName = "Full Information"), + PromptOnly UMETA(DisplayName = "Prompt Only"), + MaskedState UMETA(DisplayName = "Masked State"), + PartialState UMETA(DisplayName = "Partial State") +}; + +UENUM(BlueprintType) +enum class EHyperTwistTrainingDrillLifecycleState : uint8 +{ + Ready UMETA(DisplayName = "Ready"), + Active UMETA(DisplayName = "Active"), + Revealed UMETA(DisplayName = "Revealed"), + Completed UMETA(DisplayName = "Completed"), + Aborted UMETA(DisplayName = "Aborted") +}; + +UENUM(BlueprintType) +enum class EHyperTwistTrainingDrillEntrySource : uint8 +{ + Catalog UMETA(DisplayName = "Catalog"), + Favorites UMETA(DisplayName = "Favorites"), + QueuedBatch UMETA(DisplayName = "Queued Batch"), + Generated UMETA(DisplayName = "Generated") +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingSourceAttribution { @@ -1825,6 +1862,345 @@ struct FHyperTwistTrainingMethodSegment } }; +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillDefinition +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DrillId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Title; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StageId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StageLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingMethodDescriptor MethodDescriptor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingDeck SourceDeck; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillModeKind DrillMode = EHyperTwistTrainingDrillModeKind::Recognition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy = + EHyperTwistTrainingDrillVisibilityPolicy::FullInformation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillEntrySource EntrySource = EHyperTwistTrainingDrillEntrySource::Catalog; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAnswerHiddenUntilReveal = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAllowImmediateRepeat = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Tags; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingSourceAttribution SourceAttribution; + + bool IsStructurallyValid() const + { + return !DrillId.IsEmpty() + && !Title.IsEmpty() + && MethodDescriptor.IsStructurallyValid() + && SourceDeck.IsStructurallyValid(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillResponseRecord +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ResponseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DrillRunId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PresentedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RespondedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillModeKind DrillMode = EHyperTwistTrainingDrillModeKind::Recognition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy = + EHyperTwistTrainingDrillVisibilityPolicy::FullInformation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAnswerWasRevealed = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMarkedCorrect = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWasRepeat = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ResponseText; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Notes; + + bool IsStructurallyValid() const + { + return !ResponseId.IsEmpty() && !DrillRunId.IsEmpty() && !CaseId.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillRunState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingMethodDrillDefinition DrillDefinition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RunId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceBatchId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillLifecycleState LifecycleState = EHyperTwistTrainingDrillLifecycleState::Ready; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StartedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EndedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastUpdatedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingCase CurrentCase; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAnswerVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray PendingCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray CompletedCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Responses; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RevealCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RepeatCount = 0; + + bool IsStructurallyValid() const + { + const bool bNeedsCurrentCase = + LifecycleState == EHyperTwistTrainingDrillLifecycleState::Active + || LifecycleState == EHyperTwistTrainingDrillLifecycleState::Revealed; + const bool bHasValidCurrentCase = CurrentCase.CaseId.IsEmpty() + ? !bNeedsCurrentCase + : CurrentCase.IsStructurallyValid(); + return DrillDefinition.IsStructurallyValid() + && !RunId.IsEmpty() + && !UserId.IsEmpty() + && bHasValidCurrentCase; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillSummary +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RunId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DrillId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillLifecycleState LifecycleState = EHyperTwistTrainingDrillLifecycleState::Ready; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PendingCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ResponseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RevealCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RepeatCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAnswerHiddenUntilReveal = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CurrentCaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastUpdatedAtUtc; + + bool IsStructurallyValid() const + { + return !RunId.IsEmpty() && !DrillId.IsEmpty() && !UserId.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillFavoriteEntry +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DrillId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingMethodDescriptor MethodDescriptor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString AddedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Tags; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Note; + + bool IsStructurallyValid() const + { + return !UserId.IsEmpty() + && !DrillId.IsEmpty() + && !CaseId.IsEmpty() + && !SourceDeckId.IsEmpty() + && MethodDescriptor.IsStructurallyValid(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillBatch +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BatchId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Title; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DrillId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingMethodDescriptor MethodDescriptor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillModeKind DrillMode = EHyperTwistTrainingDrillModeKind::Recognition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy = + EHyperTwistTrainingDrillVisibilityPolicy::FullInformation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAnswerHiddenUntilReveal = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillLifecycleState SavedLifecycleState = EHyperTwistTrainingDrillLifecycleState::Ready; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray PendingCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray CompletedCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CurrentCaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Responses; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RevealCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RepeatCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CreatedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastUpdatedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPinnedFavoritesOnly = false; + + bool IsStructurallyValid() const + { + return !UserId.IsEmpty() + && !BatchId.IsEmpty() + && !DrillId.IsEmpty() + && !SourceDeckId.IsEmpty() + && MethodDescriptor.IsStructurallyValid() + && (!CurrentCaseId.IsEmpty() || PendingCaseIds.Num() > 0 || CompletedCaseIds.Num() > 0); + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingLearnerPreset { @@ -2279,6 +2655,12 @@ struct FHyperTwistTrainingRepositoryState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray StoredSessionTemplates; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray StoredMethodDrillFavorites; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray StoredMethodDrillBatches; }; USTRUCT(BlueprintType) @@ -2322,6 +2704,24 @@ struct FHyperTwistTrainingRepositoryIntegrityReport UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 DuplicateStoredSessionTemplateCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalMethodDrillFavoriteCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InvalidMethodDrillFavoriteCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DuplicateMethodDrillFavoriteCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalMethodDrillBatchCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InvalidMethodDrillBatchCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DuplicateMethodDrillBatchCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray RepairActions; @@ -2334,6 +2734,10 @@ struct FHyperTwistTrainingRepositoryIntegrityReport || InvalidReviewPlanCount > 0 || DuplicateReviewPlanCount > 0 || InvalidStoredSessionTemplateCount > 0 - || DuplicateStoredSessionTemplateCount > 0; + || DuplicateStoredSessionTemplateCount > 0 + || InvalidMethodDrillFavoriteCount > 0 + || DuplicateMethodDrillFavoriteCount > 0 + || InvalidMethodDrillBatchCount > 0 + || DuplicateMethodDrillBatchCount > 0; } };