From 8fff3845975b09f7922e77876a2bc1a535d41820 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Fri, 8 May 2026 04:48:06 +0200 Subject: [PATCH] Expose gameplay selector launch-request preview surface --- .../HyperTwistTrainingCoachLibrary.cpp | 872 ++++++++++++++++++ .../HyperTwistTrainingPanelWidget.cpp | 21 + .../HyperTwistTrainingSessionActor.cpp | 21 + .../HyperTwistTrainingCoachLibrary.h | 11 + .../HyperTwistTrainingPanelWidget.h | 4 + .../HyperTwistTrainingSessionActor.h | 4 + .../HyperTwistTrainingTypes.h | 252 +++++ ...RTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md | 4 +- ...FRESH_PREVIEW_SURFACE_PACKET_2026-05-08.md | 105 +++ 9 files changed, 1292 insertions(+), 2 deletions(-) create mode 100644 docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_LAUNCH_REQUEST_PAYLOAD_REFRESH_PREVIEW_SURFACE_PACKET_2026-05-08.md diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index eda0f98..d9c000d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -9591,3 +9591,875 @@ UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionStoredReq return Surface; } + +FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface +UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface( + const FHyperTwistTrainingRunState& RunState, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface& OptionDispatchSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionRequestAlignmentSurface& + OptionRequestAlignmentSurface +) +{ + FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface Surface; + Surface.SelectorId = SelectorId; + Surface.SelectorLabel = OptionMenuSurface.SelectorLabel; + Surface.BindingLabel = OptionMenuSurface.BindingLabel; + Surface.Headline = TEXT("Generated Mode Selector Option Launch-Request Preview"); + Surface.bHasSelector = OptionMenuSurface.bHasSelector; + + if (!SelectorId.IsEmpty()) + { + Surface.Headline = OptionMenuSurface.SelectorLabel.IsEmpty() + ? FString::Printf( + TEXT("Generated Mode Selector Option Launch-Request Preview: %s"), + *SelectorId + ) + : FString::Printf( + TEXT("Generated Mode Selector Option Launch-Request Preview: %s"), + *OptionMenuSurface.SelectorLabel + ); + } + + if (SelectorId.IsEmpty()) + { + Surface.StatusLine = + TEXT("Generated-mode selector option launch-request preview requires a selector id from the active selector roster."); + Surface.PayloadSummaryLine = TEXT("Payload preview: n/a"); + Surface.StoredRequestReuseSummaryLine = TEXT("Stored request reuse: n/a"); + Surface.RefreshPreviewSummaryLine = TEXT("Refresh preview: n/a"); + Surface.CurrentSelectionPayloadLine = TEXT("Current selection: n/a"); + Surface.DetailLine = OptionRequestAlignmentSurface.StatusLine.IsEmpty() + ? TEXT("Request alignment: n/a") + : FString::Printf( + TEXT("Request alignment: %s"), + *OptionRequestAlignmentSurface.StatusLine + ); + return Surface; + } + + const FHyperTwistTrainingImportedRuntimeSurface& RuntimeSurface = RunState.ActiveDeck.ImportedRuntimeSurface; + const bool bHasGeneratedModeSurface = RuntimeSurface.IsStructurallyValid() + && RuntimeSurface.SurfaceKind.Equals(TEXT("generated-mode"), ESearchCase::IgnoreCase); + const FHyperTwistTrainingImportedRuntimeSelector* MatchingSelector = + RuntimeSurface.Selectors.FindByPredicate( + [&SelectorId](const FHyperTwistTrainingImportedRuntimeSelector& Candidate) + { + return Candidate.SelectorId == SelectorId && Candidate.IsStructurallyValid(); + } + ); + const FString SelectorBinding = MatchingSelector != nullptr + ? MatchingSelector->SelectorBinding + : OptionMenuSurface.BindingLabel; + const FHyperTwistTrainingImportedGeneratedModeLaunchRequest& LaunchRequest = + RunState.ImportedGeneratedModeLaunchRequest; + const bool bHasComparableStoredRequest = LaunchRequest.IsStructurallyValid() + && LaunchRequest.LaunchConfig.DeckId == RunState.ActiveDeck.DeckId + && LaunchRequest.LaunchConfig.SurfaceId == RuntimeSurface.SurfaceId + && LaunchRequest.LaunchConfig.RuntimeModeId.Equals( + RuntimeSurface.RuntimeModeId, + ESearchCase::IgnoreCase + ); + const FHyperTwistTrainingCase* SeedCase = RunState.ActiveDeck.Cases.FindByPredicate( + [](const FHyperTwistTrainingCase& Candidate) + { + return Candidate.IsStructurallyValid(); + } + ); + const FString FallbackPuzzleId = SeedCase != nullptr ? SeedCase->PuzzleId : FString(); + + const auto DescribeDeliveryMode = []( + const EHyperTwistTrainingDeliveryMode DeliveryMode + ) -> FString + { + switch (DeliveryMode) + { + case EHyperTwistTrainingDeliveryMode::Timer: + return TEXT("Timer"); + case EHyperTwistTrainingDeliveryMode::VirtualCube: + return TEXT("Virtual Cube"); + case EHyperTwistTrainingDeliveryMode::SmartDevice: + return TEXT("Smart Device"); + case EHyperTwistTrainingDeliveryMode::RecognitionAssisted: + return TEXT("Recognition Assisted"); + case EHyperTwistTrainingDeliveryMode::CoachReviewed: + return TEXT("Coach Reviewed"); + default: + return TEXT("Unknown"); + } + }; + + const auto DescribeSelector = []( + const FHyperTwistTrainingImportedRuntimeSelector& Selector + ) -> FString + { + if (!Selector.Label.IsEmpty()) + { + return Selector.Label; + } + if (!Selector.SelectorBinding.IsEmpty()) + { + return Selector.SelectorBinding; + } + return Selector.SelectorId; + }; + + const auto FindSelectorChoice = []( + const TArray& Choices, + const FString& InSelectorId + ) -> const FHyperTwistTrainingImportedRuntimeSelectorChoice* + { + return Choices.FindByPredicate( + [&InSelectorId](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == InSelectorId && Candidate.IsStructurallyValid(); + } + ); + }; + + const auto DoChoicesMatch = []( + const FHyperTwistTrainingImportedRuntimeSelectorChoice* LeftChoice, + const FHyperTwistTrainingImportedRuntimeSelectorChoice* RightChoice + ) -> bool + { + if (LeftChoice == nullptr || RightChoice == nullptr) + { + return LeftChoice == nullptr && RightChoice == nullptr; + } + + return LeftChoice->SelectedValue == RightChoice->SelectedValue; + }; + + const auto DoSelectorChoiceSetsMatch = [&RuntimeSurface, &FindSelectorChoice]( + const TArray& LeftChoices, + const TArray& RightChoices + ) -> bool + { + for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors) + { + if (!Selector.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingImportedRuntimeSelectorChoice* LeftChoice = + FindSelectorChoice(LeftChoices, Selector.SelectorId); + const FHyperTwistTrainingImportedRuntimeSelectorChoice* RightChoice = + FindSelectorChoice(RightChoices, Selector.SelectorId); + if (LeftChoice == nullptr || RightChoice == nullptr) + { + return false; + } + if (LeftChoice->SelectedValue != RightChoice->SelectedValue) + { + return false; + } + } + + return true; + }; + + const auto DoesLaunchRequestMatchLaunchConfig = [&DoSelectorChoiceSetsMatch]( + const FHyperTwistTrainingImportedGeneratedModeLaunchRequest& InLaunchRequest, + const FHyperTwistTrainingImportedGeneratedModeLaunchConfig& ExpectedLaunchConfig + ) -> bool + { + return InLaunchRequest.IsStructurallyValid() + && InLaunchRequest.LaunchConfig.DeckId == ExpectedLaunchConfig.DeckId + && InLaunchRequest.LaunchConfig.SurfaceId == ExpectedLaunchConfig.SurfaceId + && InLaunchRequest.LaunchConfig.RuntimeModeId.Equals( + ExpectedLaunchConfig.RuntimeModeId, + ESearchCase::IgnoreCase + ) + && DoSelectorChoiceSetsMatch( + InLaunchRequest.LaunchConfig.SelectorChoices, + ExpectedLaunchConfig.SelectorChoices + ); + }; + + const auto DescribeChoiceValue = []( + const FHyperTwistTrainingImportedRuntimeSelectorChoice* Choice, + const TCHAR* MissingText + ) -> FString + { + if (Choice == nullptr) + { + return MissingText; + } + + if (Choice->DisplayValue.IsEmpty() || Choice->DisplayValue == Choice->SelectedValue) + { + return Choice->SelectedValue.IsEmpty() ? FString(MissingText) : Choice->SelectedValue; + } + + return FString::Printf( + TEXT("%s [%s]"), + *Choice->DisplayValue, + Choice->SelectedValue.IsEmpty() ? TEXT("n/a") : *Choice->SelectedValue + ); + }; + + const auto BuildPayloadIdentityLine = [&DescribeDeliveryMode]( + const FString& DeckId, + const FString& TrainingSessionId, + const EHyperTwistTrainingDeliveryMode DeliveryMode, + const FString& SurfaceId, + const FString& SurfaceLabel, + const FString& RuntimeModeId, + const FString& GeneratorType, + const FString& PuzzleId + ) -> FString + { + const FString DeckSummary = DeckId.IsEmpty() ? TEXT("n/a") : DeckId; + const FString SessionSummary = TrainingSessionId.IsEmpty() ? TEXT("n/a") : TrainingSessionId; + const FString SurfaceSummary = SurfaceLabel.IsEmpty() + ? (SurfaceId.IsEmpty() ? FString(TEXT("n/a")) : SurfaceId) + : FString::Printf( + TEXT("%s [%s]"), + *SurfaceLabel, + SurfaceId.IsEmpty() ? TEXT("n/a") : *SurfaceId + ); + const FString RuntimeModeSummary = RuntimeModeId.IsEmpty() ? TEXT("n/a") : RuntimeModeId; + const FString GeneratorSummary = GeneratorType.IsEmpty() ? TEXT("n/a") : GeneratorType; + const FString PuzzleSummary = PuzzleId.IsEmpty() ? TEXT("n/a") : PuzzleId; + return FString::Printf( + TEXT("Payload: deck=%s | session=%s | delivery=%s | surface=%s | mode=%s | generator=%s | puzzle=%s"), + *DeckSummary, + *SessionSummary, + *DescribeDeliveryMode(DeliveryMode), + *SurfaceSummary, + *RuntimeModeSummary, + *GeneratorSummary, + *PuzzleSummary + ); + }; + + const auto BuildSelectorChoiceEntriesAndSummary = + [&RuntimeSurface, + &SelectorId, + &RunState, + &LaunchRequest, + &bHasComparableStoredRequest, + &DescribeSelector, + &FindSelectorChoice, + &DoChoicesMatch, + &DescribeChoiceValue]( + const TArray& PreviewChoices, + TArray& + OutEntries, + int32& OutResolvedChoiceCount + ) -> FString + { + OutEntries.Reset(); + OutResolvedChoiceCount = 0; + + TSet AddedSelectorIds; + TArray SelectorParts; + + for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors) + { + if (!Selector.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingImportedRuntimeSelectorChoice* PreviewChoice = + FindSelectorChoice(PreviewChoices, Selector.SelectorId); + const FHyperTwistTrainingImportedRuntimeSelectorChoice* CurrentChoice = + FindSelectorChoice( + RunState.ImportedRuntimeSelectionState.SelectorChoices, + Selector.SelectorId + ); + const FHyperTwistTrainingImportedRuntimeSelectorChoice* StoredRequestChoice = + bHasComparableStoredRequest + ? FindSelectorChoice(LaunchRequest.LaunchConfig.SelectorChoices, Selector.SelectorId) + : nullptr; + const FString ChoiceValueSummary = DescribeChoiceValue(PreviewChoice, TEXT("unresolved")); + + FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestSelectorChoiceEntrySurface + ChoiceEntry; + ChoiceEntry.SelectorId = Selector.SelectorId; + ChoiceEntry.SelectorLabel = DescribeSelector(Selector); + ChoiceEntry.BindingLabel = Selector.SelectorBinding; + ChoiceEntry.SelectedValue = PreviewChoice == nullptr ? FString() : PreviewChoice->SelectedValue; + ChoiceEntry.DisplayValue = ChoiceValueSummary; + ChoiceEntry.PayloadLine = FString::Printf( + TEXT("Payload choice: %s=%s"), + *ChoiceEntry.SelectorLabel, + *ChoiceValueSummary + ); + ChoiceEntry.bIsTargetSelector = Selector.SelectorId == SelectorId; + ChoiceEntry.bHasChoice = PreviewChoice != nullptr; + ChoiceEntry.bMatchesCurrentSelectionChoice = DoChoicesMatch(CurrentChoice, PreviewChoice); + ChoiceEntry.bMatchesStoredRequestChoice = + bHasComparableStoredRequest && DoChoicesMatch(StoredRequestChoice, PreviewChoice); + + if (PreviewChoice != nullptr) + { + ++OutResolvedChoiceCount; + } + + SelectorParts.Add(FString::Printf( + TEXT("%s=%s"), + *ChoiceEntry.SelectorLabel, + *ChoiceValueSummary + )); + AddedSelectorIds.Add(Selector.SelectorId); + OutEntries.Add(MoveTemp(ChoiceEntry)); + } + + for (const FHyperTwistTrainingImportedRuntimeSelectorChoice& PreviewChoice : PreviewChoices) + { + if (!PreviewChoice.IsStructurallyValid() + || AddedSelectorIds.Contains(PreviewChoice.SelectorId)) + { + continue; + } + + const FHyperTwistTrainingImportedRuntimeSelectorChoice* CurrentChoice = + FindSelectorChoice( + RunState.ImportedRuntimeSelectionState.SelectorChoices, + PreviewChoice.SelectorId + ); + const FHyperTwistTrainingImportedRuntimeSelectorChoice* StoredRequestChoice = + bHasComparableStoredRequest + ? FindSelectorChoice( + LaunchRequest.LaunchConfig.SelectorChoices, + PreviewChoice.SelectorId + ) + : nullptr; + const FString ChoiceValueSummary = DescribeChoiceValue(&PreviewChoice, TEXT("unresolved")); + + FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestSelectorChoiceEntrySurface + ChoiceEntry; + ChoiceEntry.SelectorId = PreviewChoice.SelectorId; + ChoiceEntry.SelectorLabel = PreviewChoice.SelectorBinding.IsEmpty() + ? PreviewChoice.SelectorId + : PreviewChoice.SelectorBinding; + ChoiceEntry.BindingLabel = PreviewChoice.SelectorBinding; + ChoiceEntry.SelectedValue = PreviewChoice.SelectedValue; + ChoiceEntry.DisplayValue = ChoiceValueSummary; + ChoiceEntry.PayloadLine = FString::Printf( + TEXT("Payload choice: %s=%s"), + *ChoiceEntry.SelectorLabel, + *ChoiceValueSummary + ); + ChoiceEntry.bIsTargetSelector = PreviewChoice.SelectorId == SelectorId; + ChoiceEntry.bHasChoice = true; + ChoiceEntry.bMatchesCurrentSelectionChoice = DoChoicesMatch(CurrentChoice, &PreviewChoice); + ChoiceEntry.bMatchesStoredRequestChoice = + bHasComparableStoredRequest && DoChoicesMatch(StoredRequestChoice, &PreviewChoice); + + ++OutResolvedChoiceCount; + SelectorParts.Add(FString::Printf( + TEXT("%s=%s"), + *ChoiceEntry.SelectorLabel, + *ChoiceValueSummary + )); + OutEntries.Add(MoveTemp(ChoiceEntry)); + } + + return SelectorParts.Num() > 0 + ? FString::Printf( + TEXT("Selector payload: %s"), + *FString::Join(SelectorParts, TEXT(" | ")) + ) + : TEXT("Selector payload: none"); + }; + + const auto BuildFallbackUnresolvedSelectorLine = [&RuntimeSurface, &DescribeSelector, &FindSelectorChoice]( + const FHyperTwistTrainingImportedRuntimeSelectionState& SelectionState, + int32& OutUnresolvedSelectorCount + ) -> FString + { + OutUnresolvedSelectorCount = 0; + TArray UnresolvedLabels; + + for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors) + { + if (!Selector.IsStructurallyValid()) + { + continue; + } + + if (FindSelectorChoice(SelectionState.SelectorChoices, Selector.SelectorId) != nullptr) + { + continue; + } + + ++OutUnresolvedSelectorCount; + UnresolvedLabels.Add(DescribeSelector(Selector)); + } + + if (OutUnresolvedSelectorCount == 0) + { + return TEXT("Unresolved after choice: none"); + } + + const int32 PreviewCount = FMath::Min(3, UnresolvedLabels.Num()); + TArray PreviewLabels; + for (int32 Index = 0; Index < PreviewCount; ++Index) + { + PreviewLabels.Add(UnresolvedLabels[Index]); + } + + return FString::Printf( + TEXT("Unresolved after choice: %s%s"), + *FString::Join(PreviewLabels, TEXT(", ")), + UnresolvedLabels.Num() > PreviewCount + ? *FString::Printf(TEXT(" (+%d more)"), UnresolvedLabels.Num() - PreviewCount) + : TEXT("") + ); + }; + + if (!RunState.IsStructurallyValid() || !bHasGeneratedModeSurface) + { + Surface.StatusLine = OptionMenuSurface.StatusLine.IsEmpty() + ? TEXT("Generated-mode selector option launch-request preview is not currently available.") + : OptionMenuSurface.StatusLine; + Surface.PayloadSummaryLine = TEXT("Payload preview: n/a"); + Surface.StoredRequestReuseSummaryLine = TEXT("Stored request reuse: n/a"); + Surface.RefreshPreviewSummaryLine = TEXT("Refresh preview: n/a"); + Surface.CurrentSelectionPayloadLine = OptionMenuSurface.bHasUnresolvedSelectionDetail + ? OptionMenuSurface.UnresolvedSelectionLine + : (OptionMenuSurface.bHasSelection + ? TEXT("Current selection remains cached, but no active launch-request preview is available.") + : TEXT("Current selection: unresolved")); + Surface.DetailLine = FString::Printf( + TEXT("Dispatch: %s | Request alignment: %s"), + OptionDispatchSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *OptionDispatchSurface.StatusLine, + OptionRequestAlignmentSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *OptionRequestAlignmentSurface.StatusLine + ); + return Surface; + } + + const FString FallbackPayloadIdentityLine = BuildPayloadIdentityLine( + RunState.ActiveDeck.DeckId, + RunState.Session.TrainingSessionId, + RunState.Session.Mode, + RuntimeSurface.SurfaceId, + RuntimeSurface.SurfaceLabel, + RuntimeSurface.RuntimeModeId, + RuntimeSurface.GeneratorType, + FallbackPuzzleId + ); + + int32 PreviewableGeneratedOptionCount = 0; + int32 PreviewablePayloadOptionCount = 0; + + for (const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchEntrySurface& DispatchEntry : + OptionDispatchSurface.OptionEntries) + { + FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewEntrySurface Entry; + Entry.OptionId = DispatchEntry.OptionId; + Entry.Label = DispatchEntry.Label; + Entry.OptionValue = DispatchEntry.OptionValue; + Entry.bMatchesCurrentSelection = DispatchEntry.bMatchesCurrentSelection; + Entry.HypotheticalDeckId = RunState.ActiveDeck.DeckId; + Entry.HypotheticalTrainingSessionId = RunState.Session.TrainingSessionId; + Entry.HypotheticalSurfaceId = RuntimeSurface.SurfaceId; + Entry.HypotheticalSurfaceLabel = RuntimeSurface.SurfaceLabel; + Entry.HypotheticalRuntimeModeId = RuntimeSurface.RuntimeModeId; + Entry.HypotheticalGeneratorType = RuntimeSurface.GeneratorType; + Entry.HypotheticalPuzzleId = FallbackPuzzleId; + Entry.HypotheticalDeliveryMode = RunState.Session.Mode; + + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionRequestAlignmentEntrySurface* + MatchingRequestAlignmentEntry = + OptionRequestAlignmentSurface.OptionEntries.FindByPredicate( + [&DispatchEntry]( + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionRequestAlignmentEntrySurface& + Candidate + ) + { + return Candidate.OptionId == DispatchEntry.OptionId; + } + ); + const FString RequestAlignmentDetail = + MatchingRequestAlignmentEntry == nullptr || MatchingRequestAlignmentEntry->DetailLine.IsEmpty() + ? TEXT("Request alignment detail: n/a") + : MatchingRequestAlignmentEntry->DetailLine; + + if (DispatchEntry.DispatchKind + == EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::DirectCaseLaunch + && DispatchEntry.bCanDispatch) + { + Entry.LaunchRequestPreviewKind = + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind:: + DirectOwnedCaseStart; + Entry.PayloadIdentityLine = + TEXT("Payload: direct owned-case launch bypasses generated-mode launch-request staging."); + Entry.SelectorPayloadLine = + TEXT("Selector payload: direct owned-case launch bypasses generated-mode selector payload staging."); + Entry.StoredRequestReuseLine = + TEXT("Stored request reuse: direct owned-case launch bypasses stored launch-request reuse."); + Entry.RefreshPreviewLine = + TEXT("Refresh preview: direct owned-case launch bypasses generated-mode request refresh."); + Entry.ExecutePreviewLine = + MatchingRequestAlignmentEntry == nullptr + || MatchingRequestAlignmentEntry->ExecutePreviewLine.IsEmpty() + ? TEXT("Execute preview: direct owned-case launch starts immediately.") + : MatchingRequestAlignmentEntry->ExecutePreviewLine; + Entry.DetailLine = MatchingRequestAlignmentEntry == nullptr + ? DispatchEntry.DetailLine + : FString::Printf(TEXT("%s | %s"), *Entry.PayloadIdentityLine, *RequestAlignmentDetail); + Entry.bStartsOwnedCaseImmediately = true; + Entry.bCanPreview = true; + ++Surface.DirectOwnedCaseStartOptionCount; + } + else if (DispatchEntry.DispatchKind + == EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::ApplyGeneratedChoice + && DispatchEntry.bCanDispatch && MatchingSelector != nullptr) + { + ++PreviewableGeneratedOptionCount; + + FHyperTwistTrainingImportedRuntimeSelectionState HypotheticalSelectionState = + RunState.ImportedRuntimeSelectionState; + if (!HypotheticalSelectionState.IsStructurallyValid()) + { + HypotheticalSelectionState.SurfaceId = RuntimeSurface.SurfaceId; + HypotheticalSelectionState.SurfaceKind = RuntimeSurface.SurfaceKind; + } + + const FString SelectedValue = DispatchEntry.GeneratedChoiceSelectedValue.IsEmpty() + ? (DispatchEntry.OptionValue.IsEmpty() + ? DispatchEntry.OptionId + : DispatchEntry.OptionValue) + : DispatchEntry.GeneratedChoiceSelectedValue; + const FString DisplayValue = DispatchEntry.GeneratedChoiceDisplayValue.IsEmpty() + ? (DispatchEntry.Label.IsEmpty() ? SelectedValue : DispatchEntry.Label) + : DispatchEntry.GeneratedChoiceDisplayValue; + + FHyperTwistTrainingImportedRuntimeSelectorChoice* ExistingChoice = + HypotheticalSelectionState.SelectorChoices.FindByPredicate( + [&SelectorId](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == SelectorId; + } + ); + if (ExistingChoice != nullptr) + { + ExistingChoice->SelectorBinding = SelectorBinding; + ExistingChoice->SelectedValue = SelectedValue; + ExistingChoice->DisplayValue = DisplayValue.IsEmpty() ? SelectedValue : DisplayValue; + } + else + { + FHyperTwistTrainingImportedRuntimeSelectorChoice Choice; + Choice.SelectorId = SelectorId; + Choice.SelectorBinding = SelectorBinding; + Choice.SelectedValue = SelectedValue; + Choice.DisplayValue = DisplayValue.IsEmpty() ? SelectedValue : DisplayValue; + HypotheticalSelectionState.SelectorChoices.Add(MoveTemp(Choice)); + } + + Entry.ExecutePreviewLine = + MatchingRequestAlignmentEntry == nullptr + || MatchingRequestAlignmentEntry->ExecutePreviewLine.IsEmpty() + ? TEXT("Execute preview: unavailable.") + : MatchingRequestAlignmentEntry->ExecutePreviewLine; + if (MatchingRequestAlignmentEntry != nullptr) + { + Entry.bWouldLeaveSelectorsUnresolved = + MatchingRequestAlignmentEntry->bWouldLeaveSelectorsUnresolved; + } + + int32 UnresolvedSelectorCount = 0; + FString UnresolvedSelectorsLine = MatchingRequestAlignmentEntry == nullptr + ? BuildFallbackUnresolvedSelectorLine( + HypotheticalSelectionState, + UnresolvedSelectorCount + ) + : MatchingRequestAlignmentEntry->UnresolvedSelectorsLine; + if (MatchingRequestAlignmentEntry != nullptr) + { + UnresolvedSelectorCount = MatchingRequestAlignmentEntry->UnresolvedSelectorCount; + } + + Entry.PayloadIdentityLine = FallbackPayloadIdentityLine; + Entry.SelectorPayloadLine = BuildSelectorChoiceEntriesAndSummary( + HypotheticalSelectionState.SelectorChoices, + Entry.SelectorChoiceEntries, + Entry.SelectorChoiceCount + ); + + if (Entry.bWouldLeaveSelectorsUnresolved) + { + Entry.LaunchRequestPreviewKind = + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind:: + UnresolvedAfterChoice; + Entry.StoredRequestReuseLine = + TEXT("Stored request reuse: no structurally valid launch payload is available while selector coverage remains incomplete."); + Entry.RefreshPreviewLine = + TEXT("Refresh preview: execute cannot stage a launch request until every required selector is resolved."); + Entry.DetailLine = FString::Printf( + TEXT("%s | %s | %s"), + *Entry.SelectorPayloadLine, + *UnresolvedSelectorsLine, + *RequestAlignmentDetail + ); + Entry.bCanPreview = true; + ++Surface.UnresolvedAfterChoiceOptionCount; + } + else + { + FHyperTwistTrainingImportedGeneratedModeLaunchConfig HypotheticalLaunchConfig; + if (!UHyperTwistTrainingCatalogLibrary::TryBuildImportedGeneratedModeLaunchConfig( + RunState.ActiveDeck, + HypotheticalSelectionState, + RunState.Session, + HypotheticalLaunchConfig)) + { + Entry.LaunchRequestPreviewKind = + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind:: + PayloadUnavailable; + Entry.StoredRequestReuseLine = + TEXT("Stored request reuse: no structurally valid launch payload could be derived from the resulting selector state."); + Entry.RefreshPreviewLine = + TEXT("Refresh preview: execute cannot preview request refresh because the resulting selector state is not structurally valid."); + Entry.DetailLine = FString::Printf( + TEXT("%s | %s"), + *Entry.SelectorPayloadLine, + *RequestAlignmentDetail + ); + Entry.bCanPreview = true; + ++Surface.PayloadUnavailableOptionCount; + } + else + { + const bool bWouldReuseStoredRequest = + MatchingRequestAlignmentEntry != nullptr + ? MatchingRequestAlignmentEntry->bWouldUseStoredRequestOnExecute + : DoesLaunchRequestMatchLaunchConfig( + LaunchRequest, + HypotheticalLaunchConfig + ); + const bool bWouldRequireLaunchRequestRefresh = + MatchingRequestAlignmentEntry != nullptr + ? MatchingRequestAlignmentEntry->bWouldRequireLaunchRequestRefreshBeforeExecute + : !bWouldReuseStoredRequest; + + Entry.HypotheticalDeckId = HypotheticalLaunchConfig.DeckId; + Entry.HypotheticalTrainingSessionId = + HypotheticalLaunchConfig.TrainingSessionId; + Entry.HypotheticalSurfaceId = HypotheticalLaunchConfig.SurfaceId; + Entry.HypotheticalSurfaceLabel = HypotheticalLaunchConfig.SurfaceLabel; + Entry.HypotheticalRuntimeModeId = HypotheticalLaunchConfig.RuntimeModeId; + Entry.HypotheticalGeneratorType = HypotheticalLaunchConfig.GeneratorType; + Entry.HypotheticalPuzzleId = HypotheticalLaunchConfig.PuzzleId; + Entry.HypotheticalDeliveryMode = HypotheticalLaunchConfig.DeliveryMode; + Entry.PayloadIdentityLine = BuildPayloadIdentityLine( + HypotheticalLaunchConfig.DeckId, + HypotheticalLaunchConfig.TrainingSessionId, + HypotheticalLaunchConfig.DeliveryMode, + HypotheticalLaunchConfig.SurfaceId, + HypotheticalLaunchConfig.SurfaceLabel, + HypotheticalLaunchConfig.RuntimeModeId, + HypotheticalLaunchConfig.GeneratorType, + HypotheticalLaunchConfig.PuzzleId + ); + Entry.SelectorPayloadLine = BuildSelectorChoiceEntriesAndSummary( + HypotheticalLaunchConfig.SelectorChoices, + Entry.SelectorChoiceEntries, + Entry.SelectorChoiceCount + ); + Entry.bWouldReuseStoredRequest = bWouldReuseStoredRequest; + Entry.bWouldRequireLaunchRequestRefresh = + bWouldRequireLaunchRequestRefresh; + Entry.bHasStructurallyValidLaunchPayload = true; + Entry.bCanPreview = true; + + if (bWouldReuseStoredRequest) + { + Entry.LaunchRequestPreviewKind = + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind:: + StoredRequestReusable; + Entry.StoredRequestReuseLine = + TEXT("Stored request reuse: the current stored request already matches this hypothetical launch payload."); + Entry.RefreshPreviewLine = + TEXT("Refresh preview: execute can use the stored request without rebuilding it."); + ++Surface.StoredRequestReusableOptionCount; + } + else + { + Entry.LaunchRequestPreviewKind = + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind:: + RefreshRequired; + Entry.StoredRequestReuseLine = + !LaunchRequest.IsStructurallyValid() + ? TEXT("Stored request reuse: no stored request is currently persisted for this hypothetical launch payload.") + : (bHasComparableStoredRequest + ? TEXT("Stored request reuse: this hypothetical launch payload diverges from the current stored request.") + : TEXT("Stored request reuse: a stored request exists, but it does not match this generated-mode surface or runtime mode.")); + Entry.RefreshPreviewLine = + !LaunchRequest.IsStructurallyValid() + ? TEXT("Refresh preview: execute would persist a first stored request from this hypothetical launch payload.") + : (bHasComparableStoredRequest + ? TEXT("Refresh preview: execute would refresh the stored request from this hypothetical launch payload.") + : TEXT("Refresh preview: execute would replace the currently persisted stored request with this hypothetical launch payload.")); + ++Surface.RefreshRequiredOptionCount; + } + + ++PreviewablePayloadOptionCount; + Entry.DetailLine = FString::Printf( + TEXT("%s | %s"), + *Entry.PayloadIdentityLine, + *RequestAlignmentDetail + ); + } + } + } + else + { + Entry.LaunchRequestPreviewKind = + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind::Unavailable; + Entry.PayloadIdentityLine = TEXT("Payload: unavailable"); + Entry.SelectorPayloadLine = TEXT("Selector payload: unavailable"); + Entry.StoredRequestReuseLine = TEXT("Stored request reuse: unavailable"); + Entry.RefreshPreviewLine = TEXT("Refresh preview: unavailable"); + Entry.ExecutePreviewLine = TEXT("Execute preview: unavailable."); + Entry.DetailLine = + TEXT("Launch-request preview is unavailable because this selector option cannot dispatch from the active generated-mode surface."); + Entry.bCanPreview = false; + ++Surface.UnavailableOptionCount; + } + + if (Entry.bMatchesCurrentSelection) + { + Surface.bCurrentSelectionWouldReuseStoredRequest = Entry.bWouldReuseStoredRequest; + Surface.bCurrentSelectionWouldRequireLaunchRequestRefresh = + Entry.bWouldRequireLaunchRequestRefresh; + Surface.bCurrentSelectionWouldLeaveSelectorsUnresolved = + Entry.bWouldLeaveSelectorsUnresolved; + Surface.bCurrentSelectionHasStructurallyValidLaunchPayload = + Entry.bHasStructurallyValidLaunchPayload; + Surface.CurrentSelectionPayloadLine = Entry.bStartsOwnedCaseImmediately + ? FString::Printf( + TEXT("%s | %s"), + *Entry.PayloadIdentityLine, + *Entry.ExecutePreviewLine + ) + : FString::Printf( + TEXT("%s | %s | %s"), + *Entry.PayloadIdentityLine, + *Entry.StoredRequestReuseLine, + *Entry.RefreshPreviewLine + ); + } + + Surface.OptionEntries.Add(MoveTemp(Entry)); + } + + Surface.bHasLaunchRequestPreviewOptions = Surface.OptionEntries.Num() > 0; + Surface.bHasStoredRequestReusableOptions = Surface.StoredRequestReusableOptionCount > 0; + Surface.bHasRefreshRequiredOptions = Surface.RefreshRequiredOptionCount > 0; + Surface.bHasUnresolvedAfterChoiceOptions = Surface.UnresolvedAfterChoiceOptionCount > 0; + Surface.bHasPayloadUnavailableOptions = Surface.PayloadUnavailableOptionCount > 0; + + Surface.PayloadSummaryLine = Surface.OptionEntries.Num() > 0 + ? FString::Printf( + TEXT("Payload preview: %d reusable | %d refresh-required | %d unresolved | %d payload-unavailable | %d direct-launch | %d unavailable"), + Surface.StoredRequestReusableOptionCount, + Surface.RefreshRequiredOptionCount, + Surface.UnresolvedAfterChoiceOptionCount, + Surface.PayloadUnavailableOptionCount, + Surface.DirectOwnedCaseStartOptionCount, + Surface.UnavailableOptionCount + ) + : TEXT("Payload preview: no imported options"); + Surface.StoredRequestReuseSummaryLine = PreviewableGeneratedOptionCount > 0 + ? FString::Printf( + TEXT("Stored request reuse: %d option%s can reuse the current stored request; %d would need refresh; %d still have no valid launch payload."), + Surface.StoredRequestReusableOptionCount, + Surface.StoredRequestReusableOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.RefreshRequiredOptionCount, + Surface.UnresolvedAfterChoiceOptionCount + Surface.PayloadUnavailableOptionCount + ) + : TEXT("Stored request reuse: no previewable generated-choice options"); + Surface.RefreshPreviewSummaryLine = PreviewableGeneratedOptionCount > 0 + ? FString::Printf( + TEXT("Refresh preview: %d option%s would refresh the stored request; %d stay reusable without refresh; %d still cannot stage a complete payload."), + Surface.RefreshRequiredOptionCount, + Surface.RefreshRequiredOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.StoredRequestReusableOptionCount, + Surface.UnresolvedAfterChoiceOptionCount + Surface.PayloadUnavailableOptionCount + ) + : TEXT("Refresh preview: no previewable generated-choice options"); + + if (Surface.CurrentSelectionPayloadLine.IsEmpty()) + { + if (OptionMenuSurface.bHasUnresolvedSelectionDetail) + { + Surface.CurrentSelectionPayloadLine = OptionMenuSurface.UnresolvedSelectionLine; + } + else if (OptionMenuSurface.bHasSelection) + { + Surface.CurrentSelectionPayloadLine = + TEXT("Current selection has no additional launch-request payload preview."); + } + else if (OptionMenuSurface.bHasSelector) + { + Surface.CurrentSelectionPayloadLine = TEXT("Current selection: unresolved"); + } + else + { + Surface.CurrentSelectionPayloadLine = TEXT("Current selection: n/a"); + } + } + + if (OptionMenuSurface.bHasSelector && Surface.OptionEntries.Num() > 0) + { + Surface.StatusLine = FString::Printf( + TEXT("Generated-mode selector option launch-request preview exposes %d stored-request-reusable option%s, %d refresh-required option%s, %d unresolved option%s, %d payload-unavailable option%s, %d direct-launch option%s, and %d unavailable option%s."), + Surface.StoredRequestReusableOptionCount, + Surface.StoredRequestReusableOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.RefreshRequiredOptionCount, + Surface.RefreshRequiredOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.UnresolvedAfterChoiceOptionCount, + Surface.UnresolvedAfterChoiceOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.PayloadUnavailableOptionCount, + Surface.PayloadUnavailableOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.DirectOwnedCaseStartOptionCount, + Surface.DirectOwnedCaseStartOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.UnavailableOptionCount, + Surface.UnavailableOptionCount == 1 ? TEXT("") : TEXT("s") + ); + } + else if (OptionMenuSurface.bHasSelector) + { + Surface.StatusLine = + TEXT("Generated-mode selector option launch-request preview is active, but this selector does not expose imported options."); + } + else + { + Surface.StatusLine = OptionMenuSurface.StatusLine.IsEmpty() + ? TEXT("Generated-mode selector option launch-request preview is not currently available.") + : OptionMenuSurface.StatusLine; + } + + const FString SelectorSummary = Surface.SelectorLabel.IsEmpty() + ? (Surface.SelectorId.IsEmpty() ? FString(TEXT("n/a")) : Surface.SelectorId) + : Surface.SelectorLabel; + const FString BindingSummary = Surface.BindingLabel.IsEmpty() + ? FString(TEXT("n/a")) + : Surface.BindingLabel; + Surface.DetailLine = FString::Printf( + TEXT("Dispatch: %s | Request alignment: %s | Payload-ready: %d | Selector: %s | Binding: %s"), + OptionDispatchSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *OptionDispatchSurface.StatusLine, + OptionRequestAlignmentSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *OptionRequestAlignmentSurface.StatusLine, + PreviewablePayloadOptionCount, + *SelectorSummary, + *BindingSummary + ); + + return Surface; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index 3793eaa..697fa84 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -813,6 +813,27 @@ UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionStor ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface +UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface = + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface OptionDispatchSurface = + GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(SelectorId); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionRequestAlignmentSurface + OptionRequestAlignmentSurface = + GetDisplayedCoachGeneratedModeSelectorOptionRequestAlignmentSurface(SelectorId); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface( + CachedRunState, + SelectorId, + OptionMenuSurface, + OptionDispatchSurface, + OptionRequestAlignmentSurface + ); +} + FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachRecommendedRun( const int32 MaxCases, const FString& StartActionSourceLabel diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp index 5c9cf9e..84d4ea8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp @@ -794,6 +794,27 @@ AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionSto ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface +AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface = + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface OptionDispatchSurface = + GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(SelectorId); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionRequestAlignmentSurface + OptionRequestAlignmentSurface = + GetDisplayedCoachGeneratedModeSelectorOptionRequestAlignmentSurface(SelectorId); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface( + CachedRunState, + SelectorId, + OptionMenuSurface, + OptionDispatchSurface, + OptionRequestAlignmentSurface + ); +} + FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachRecommendedTrainingRun( const int32 MaxCases ) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h index 52b76c8..bc22b53 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -746,4 +746,15 @@ public: const FHyperTwistTrainingCoachGeneratedModeSelectorOptionRequestAlignmentSurface& OptionRequestAlignmentSurface ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface + DeriveCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface( + const FHyperTwistTrainingRunState& RunState, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface& OptionDispatchSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionRequestAlignmentSurface& + OptionRequestAlignmentSurface + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h index 6c03b1f..bdae80e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h @@ -280,6 +280,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorOptionStoredRequestDeltaSurface GetDisplayedCoachGeneratedModeSelectorOptionStoredRequestDeltaSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface + GetDisplayedCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachRecommendedRun( int32 MaxCases, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h index f507ba5..60f3be8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h @@ -261,6 +261,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorOptionStoredRequestDeltaSurface GetDisplayedCoachGeneratedModeSelectorOptionStoredRequestDeltaSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface + GetDisplayedCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachRecommendedTrainingRun(int32 MaxCases); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 42e4963..1ffb6e8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -116,6 +116,18 @@ enum class EHyperTwistTrainingCoachGeneratedModeOptionStoredRequestDeltaKind : u Unavailable UMETA(DisplayName = "Unavailable") }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind : uint8 +{ + None UMETA(DisplayName = "None"), + DirectOwnedCaseStart UMETA(DisplayName = "Direct Owned Case Start"), + StoredRequestReusable UMETA(DisplayName = "Stored Request Reusable"), + RefreshRequired UMETA(DisplayName = "Refresh Required"), + UnresolvedAfterChoice UMETA(DisplayName = "Unresolved After Choice"), + PayloadUnavailable UMETA(DisplayName = "Payload Unavailable"), + Unavailable UMETA(DisplayName = "Unavailable") +}; + UENUM(BlueprintType) enum class EHyperTwistTrainingPromptKind : uint8 { @@ -9496,3 +9508,243 @@ struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionStoredRequestDeltaSurf || bHasStoredRequestDeltaOptions; } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestSelectorChoiceEntrySurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectorId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectorLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BindingLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectedValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DisplayValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PayloadLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bIsTargetSelector = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasChoice = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMatchesCurrentSelectionChoice = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMatchesStoredRequestChoice = false; + + bool IsStructurallyValid() const + { + return !SelectorId.IsEmpty() + || !SelectorLabel.IsEmpty() + || !PayloadLine.IsEmpty() + || bHasChoice; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewEntrySurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString OptionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Label; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString OptionValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind LaunchRequestPreviewKind = + EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind::None; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PayloadIdentityLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectorPayloadLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StoredRequestReuseLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RefreshPreviewLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ExecutePreviewLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray + SelectorChoiceEntries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HypotheticalDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HypotheticalTrainingSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HypotheticalSurfaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HypotheticalSurfaceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HypotheticalRuntimeModeId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HypotheticalGeneratorType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HypotheticalPuzzleId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode HypotheticalDeliveryMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SelectorChoiceCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMatchesCurrentSelection = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bStartsOwnedCaseImmediately = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWouldReuseStoredRequest = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWouldRequireLaunchRequestRefresh = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWouldLeaveSelectorsUnresolved = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasStructurallyValidLaunchPayload = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanPreview = false; + + bool IsStructurallyValid() const + { + return !OptionId.IsEmpty() + || !Label.IsEmpty() + || !PayloadIdentityLine.IsEmpty() + || LaunchRequestPreviewKind + != EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind::None + || bCanPreview; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectorId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectorLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BindingLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PayloadSummaryLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StoredRequestReuseSummaryLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RefreshPreviewSummaryLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CurrentSelectionPayloadLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray + OptionEntries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 StoredRequestReusableOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RefreshRequiredOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 UnresolvedAfterChoiceOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PayloadUnavailableOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DirectOwnedCaseStartOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 UnavailableOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasSelector = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasLaunchRequestPreviewOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasStoredRequestReusableOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRefreshRequiredOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasUnresolvedAfterChoiceOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasPayloadUnavailableOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionWouldReuseStoredRequest = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionWouldRequireLaunchRequestRefresh = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionWouldLeaveSelectorsUnresolved = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionHasStructurallyValidLaunchPayload = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || !SelectorId.IsEmpty() + || OptionEntries.Num() > 0 + || bHasSelector + || bHasLaunchRequestPreviewOptions; + } +}; diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 159d3c1..1baf8f9 100644 --- a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md +++ b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md @@ -68,8 +68,8 @@ Current correction call from the source-exposed audit and current execution refr - the imported generated-mode gap is closed; request/config/selection plumbing and the bounded clean-room executor are already landed in first-party code - the latest landed bounded `Phase 4` packet closes the retained-idle comparison/outcome actionability gap at the end of the recognition-assisted coach-to-review continuity lane, and the final retained-surface confirmation pass now leaves that closure lane functionally complete - the retained idle widget surface now keeps retained comparison and decision history inspectable without continuing to advertise live launch or guidance-outcome actions once the coach-to-review loop has already collapsed to truthful closed-loop idle - - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds one thin generated-mode selector option stored-request delta / selector-diff surface over the already-cached selector/runtime state, so gameplay/Blueprint callers can render the exact current-vs-hypothetical-vs-stored selector change set for a chosen option without diffing cached selector choices or launch-request payloads directly - - the current next bounded packet is to expose one thin generated-mode selector option hypothetical launch-request payload / refresh-preview surface on those same two consumer surfaces so gameplay/Blueprint callers can render the concrete selector-choice payload, stored-request reuse posture, and request-refresh preview for a chosen option without rebuilding launch-config payloads directly + - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds one thin generated-mode selector option hypothetical launch-request payload / refresh-preview surface over the already-cached selector/runtime state, so gameplay/Blueprint callers can render the concrete selector-choice payload, stored-request reuse posture, and request-refresh preview for a chosen option without rebuilding launch-config payloads directly + - the current next bounded packet is to expose one thin generated-mode selector option hypothetical launch-request metadata / origin-notes preview surface on those same two consumer surfaces so gameplay/Blueprint callers can render imported origin-path and notes provenance over that hypothetical payload without reading raw launch-config metadata directly - the current execution-discipline rule that broad refactor / monolith-splitting work should not interrupt the active bounded roadmap packet unless structure is actually blocking it ## Current execution-reality references diff --git a/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_LAUNCH_REQUEST_PAYLOAD_REFRESH_PREVIEW_SURFACE_PACKET_2026-05-08.md b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_LAUNCH_REQUEST_PAYLOAD_REFRESH_PREVIEW_SURFACE_PACKET_2026-05-08.md new file mode 100644 index 0000000..5748e0e --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_LAUNCH_REQUEST_PAYLOAD_REFRESH_PREVIEW_SURFACE_PACKET_2026-05-08.md @@ -0,0 +1,105 @@ +# HyperTwist Phase 4 gameplay generated-mode selector option launch-request payload / refresh-preview surface packet + +Created on `2026-05-08` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` gameplay-facing consumer slice + +## Purpose + +This packet closes the next thin gameplay-facing consumer seam above the panel-widget and session-actor generated-mode selector option stored-request delta / selector-diff surface by exposing one parameterized option launch-request payload / refresh-preview surface for a chosen selector id. + +The open tasks were: + +- stop forcing gameplay / Blueprint callers to rebuild hypothetical generated-mode launch-config payloads just to explain what a chosen imported option would stage +- expose the concrete selector-choice payload, stored-request reuse posture, and refresh-on-execute preview for each imported option on the chosen selector +- keep the surface derived from already-cached gameplay consumer run, selector, and request state plus the already-landed option-menu, dispatch, and request-alignment surfaces instead of widening into backend logic + +It is not: + +- a selector-dispatch behavior change +- a stored launch-request persistence change +- a generated-mode execution-policy change +- a broader gameplay UI composition pass + +## Scope + +Bounded lane: + +- add first-party generated-mode selector option launch-request payload / refresh-preview surfaces in shared training types +- add a coach-library helper that derives hypothetical launch payload, selector payload, stored-request reuse posture, and refresh preview from cached run state plus the already-landed option-menu, dispatch, and request-alignment surfaces +- expose that parameterized launch-request preview surface on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor` +- keep the derivation presentation-only and rooted in the already-owned selector-choice / launch-request rules + +Out of scope: + +- changing selector-choice apply behavior +- changing stored launch-request creation, refresh, or execute behavior +- changing direct owned-case launch behavior +- widening into higher-level UI composition or dashboard-only work + +## Why this was the right next packet + +Before this slice: + +- gameplay-facing panel/session consumers already exposed truthful generated-mode option stored-request delta / selector-diff posture +- those same consumers already exposed truthful generated-mode option request-alignment / execute-preview posture + +But one thin consumer seam was still open: + +- callers could tell whether a chosen option stayed aligned with the stored request or required refresh +- but they still had to rebuild the hypothetical launch payload themselves to show the actual selector payload and request-refresh consequences behind that posture + +That meant: + +- alignment and delta posture were thin and reusable +- but concrete launch-request preview still leaked launch-config reconstruction work into callers + +So the next bounded move was: + +- keep the already-landed option-menu, dispatch, request-alignment, and stored-request-delta surfaces unchanged +- and expose one narrow launch-request payload / refresh-preview surface above those already-cached surfaces + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h` + - added `EHyperTwistTrainingCoachGeneratedModeOptionLaunchRequestPreviewKind` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestSelectorChoiceEntrySurface` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewEntrySurface` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp` + - added `DeriveCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface(...)` + - launch-request preview is derived from cached run state plus the already-landed option-menu, dispatch, and request-alignment surfaces +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface(const FString& SelectorId)` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionLaunchRequestPreviewSurface(const FString& SelectorId)` + +Surface behavior: + +- reports the concrete hypothetical payload identity for each imported option on the chosen selector, including deck, session, delivery, surface, runtime mode, generator, and puzzle context +- exposes the hypothetical selector payload row set for each option, including which selector choices match current cached selection and the stored request +- classifies each option as stored-request-reusable, refresh-required, unresolved-after-choice, payload-unavailable, direct-launch, or unavailable +- reports a compact stored-request reuse and refresh preview for the current cached selection on the chosen selector +- keeps the surface presentation-only and compatible with the already-landed generated-mode option-menu, dispatch, request-alignment, and stored-request-delta surfaces + +## Validation + +Validation target: + +- full `Development Editor|Win64` build of `C:\HyperTwist\UnrealHyperTwist\UnrealHyperTwist.sln` + +Result: + +- passed with `0` warnings and `0` errors + +## Follow-on + +The next bounded packet on this same gameplay-facing consumer lane should stay thin and expose one generated-mode selector option hypothetical launch-request metadata / origin-notes preview surface so callers can render imported origin-path and notes provenance over the hypothetical payload without reading raw launch-config metadata directly.