diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index f9b0ab2..202f5cd 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -7815,3 +7815,512 @@ UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionDispatchS return Surface; } + +FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface +UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionExecutionImpactSurface( + const FHyperTwistTrainingRunState& RunState, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeLaunchExecutionContextSurface& LaunchExecutionContextSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface& OptionDispatchSurface +) +{ + FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface Surface; + Surface.SelectorId = SelectorId; + Surface.SelectorLabel = OptionMenuSurface.SelectorLabel; + Surface.BindingLabel = OptionMenuSurface.BindingLabel; + Surface.Headline = TEXT("Generated Mode Selector Option Execution Impact"); + Surface.bHasSelector = OptionMenuSurface.bHasSelector; + + if (!SelectorId.IsEmpty()) + { + Surface.Headline = OptionMenuSurface.SelectorLabel.IsEmpty() + ? FString::Printf( + TEXT("Generated Mode Selector Option Execution Impact: %s"), + *SelectorId + ) + : FString::Printf( + TEXT("Generated Mode Selector Option Execution Impact: %s"), + *OptionMenuSurface.SelectorLabel + ); + } + + if (SelectorId.IsEmpty()) + { + Surface.StatusLine = + TEXT("Generated-mode selector option execution impact requires a selector id from the active selector roster."); + Surface.ExecutionImpactSummaryLine = TEXT("Execution impact: n/a"); + Surface.CurrentSelectionExecutionImpactLine = TEXT("Current selection: n/a"); + Surface.DetailLine = OptionDispatchSurface.StatusLine.IsEmpty() + ? TEXT("Dispatch: n/a") + : FString::Printf(TEXT("Dispatch: %s"), *OptionDispatchSurface.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 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 BuildUnresolvedSelectorBlockedReason = [&DescribeSelector, &RuntimeSurface]( + const FHyperTwistTrainingImportedRuntimeSelectionState& SelectionState, + int32& OutUnresolvedSelectorCount + ) -> FString + { + OutUnresolvedSelectorCount = 0; + FString FirstUnresolvedSelectorLabel; + + for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors) + { + if (!Selector.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingImportedRuntimeSelectorChoice* MatchingChoice = + SelectionState.SelectorChoices.FindByPredicate( + [&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == Selector.SelectorId + && Candidate.IsStructurallyValid(); + } + ); + if (MatchingChoice != nullptr) + { + continue; + } + + ++OutUnresolvedSelectorCount; + if (FirstUnresolvedSelectorLabel.IsEmpty()) + { + FirstUnresolvedSelectorLabel = DescribeSelector(Selector); + } + } + + return OutUnresolvedSelectorCount > 0 + ? FString::Printf( + TEXT("Generated-mode execution is blocked because selector %s is unresolved."), + FirstUnresolvedSelectorLabel.IsEmpty() ? TEXT("n/a") : *FirstUnresolvedSelectorLabel + ) + : FString(); + }; + + const auto DoSelectorChoiceSetsMatch = [&RuntimeSurface]( + const TArray& LeftChoices, + const TArray& RightChoices + ) -> bool + { + for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors) + { + if (!Selector.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingImportedRuntimeSelectorChoice* LeftChoice = + LeftChoices.FindByPredicate( + [&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == Selector.SelectorId + && Candidate.IsStructurallyValid(); + } + ); + const FHyperTwistTrainingImportedRuntimeSelectorChoice* RightChoice = + RightChoices.FindByPredicate( + [&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == Selector.SelectorId + && Candidate.IsStructurallyValid(); + } + ); + if (LeftChoice == nullptr || RightChoice == nullptr) + { + return false; + } + if (LeftChoice->SelectedValue != RightChoice->SelectedValue) + { + return false; + } + } + + return true; + }; + + const auto DoesLaunchRequestMatchLaunchConfig = [&DoSelectorChoiceSetsMatch]( + const FHyperTwistTrainingImportedGeneratedModeLaunchRequest& LaunchRequest, + const FHyperTwistTrainingImportedGeneratedModeLaunchConfig& ExpectedLaunchConfig + ) -> bool + { + return LaunchRequest.IsStructurallyValid() + && LaunchRequest.LaunchConfig.DeckId == ExpectedLaunchConfig.DeckId + && LaunchRequest.LaunchConfig.SurfaceId == ExpectedLaunchConfig.SurfaceId + && LaunchRequest.LaunchConfig.RuntimeModeId.Equals( + ExpectedLaunchConfig.RuntimeModeId, + ESearchCase::IgnoreCase + ) + && DoSelectorChoiceSetsMatch( + LaunchRequest.LaunchConfig.SelectorChoices, + ExpectedLaunchConfig.SelectorChoices + ); + }; + + const auto DescribeUnchangedExecutionImpact = [&LaunchExecutionContextSurface]() -> FString + { + if (LaunchExecutionContextSurface.bHasExecutionTarget + && LaunchExecutionContextSurface.bCanExecute) + { + return LaunchExecutionContextSurface.bWillRefreshLaunchRequestOnExecute + ? TEXT("Execution impact: execute posture is unchanged and still refreshes the stored request.") + : TEXT("Execution impact: execute posture is unchanged and still uses the stored request."); + } + if (LaunchExecutionContextSurface.bHasExecutionTarget) + { + return TEXT("Execution impact: execute posture is unchanged and remains blocked."); + } + if (LaunchExecutionContextSurface.bHasLatestLaunchRequest) + { + return TEXT("Execution impact: execute posture is unchanged and no execution target is currently staged."); + } + return TEXT("Execution impact: execute posture is unchanged and no stored request or execution target is currently staged."); + }; + + if (!RunState.IsStructurallyValid() || !bHasGeneratedModeSurface) + { + Surface.StatusLine = OptionMenuSurface.StatusLine.IsEmpty() + ? TEXT("Generated-mode selector option execution impact is not currently available.") + : OptionMenuSurface.StatusLine; + Surface.ExecutionImpactSummaryLine = TEXT("Execution impact: n/a"); + Surface.CurrentSelectionExecutionImpactLine = OptionMenuSurface.bHasUnresolvedSelectionDetail + ? OptionMenuSurface.UnresolvedSelectionLine + : (OptionMenuSurface.bHasSelection + ? TEXT("Current selection remains cached, but no active generated-mode execute context is available.") + : TEXT("Current selection: unresolved")); + Surface.DetailLine = FString::Printf( + TEXT("Dispatch: %s | Execute: %s"), + OptionDispatchSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *OptionDispatchSurface.StatusLine, + LaunchExecutionContextSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *LaunchExecutionContextSurface.StatusLine + ); + return Surface; + } + + for (const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchEntrySurface& DispatchEntry : + OptionDispatchSurface.OptionEntries) + { + FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactEntrySurface Entry; + Entry.OptionId = DispatchEntry.OptionId; + Entry.Label = DispatchEntry.Label; + Entry.OptionValue = DispatchEntry.OptionValue; + Entry.bMatchesCurrentSelection = DispatchEntry.bMatchesCurrentSelection; + + if (DispatchEntry.DispatchKind + == EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::DirectCaseLaunch + && DispatchEntry.bCanDispatch) + { + Entry.ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::ImmediateOwnedCaseStart; + Entry.ImpactLine = TEXT("Execution impact: starts a direct owned case immediately."); + Entry.DetailLine = DispatchEntry.DetailLine.IsEmpty() + ? TEXT("Direct option launch bypasses generated-mode execute for this interaction.") + : DispatchEntry.DetailLine; + Entry.bStartsOwnedCaseImmediately = true; + Entry.bCanApplyImpact = true; + ++Surface.ImmediateOwnedCaseStartOptionCount; + } + else if (DispatchEntry.DispatchKind + == EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::ApplyGeneratedChoice + && DispatchEntry.bCanDispatch && MatchingSelector != nullptr) + { + 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)); + } + + int32 UnresolvedSelectorCount = 0; + Entry.BlockedReasonLine = BuildUnresolvedSelectorBlockedReason( + HypotheticalSelectionState, + UnresolvedSelectorCount + ); + + if (!Entry.BlockedReasonLine.IsEmpty()) + { + Entry.ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::ExecuteBlocked; + Entry.ImpactLine = + TEXT("Execution impact: applies the selector choice, but execute remains blocked."); + Entry.DetailLine = FString::Printf( + TEXT("Choice apply: %s | %d selector%s still need resolution before generated-mode execute."), + DispatchEntry.DetailLine.IsEmpty() ? TEXT("n/a") : *DispatchEntry.DetailLine, + UnresolvedSelectorCount, + UnresolvedSelectorCount == 1 ? TEXT("") : TEXT("s") + ); + Entry.bLeavesExecuteBlocked = true; + Entry.bCanApplyImpact = true; + ++Surface.BlockedExecuteOptionCount; + } + else + { + FHyperTwistTrainingImportedGeneratedModeLaunchConfig HypotheticalLaunchConfig; + if (!UHyperTwistTrainingCatalogLibrary::TryBuildImportedGeneratedModeLaunchConfig( + RunState.ActiveDeck, + HypotheticalSelectionState, + RunState.Session, + HypotheticalLaunchConfig)) + { + Entry.ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::ExecuteBlocked; + Entry.ImpactLine = + TEXT("Execution impact: applies the selector choice, but execute remains blocked."); + Entry.BlockedReasonLine = + TEXT("Generated-mode execution could not derive a structurally valid launch request from the resulting selector state."); + Entry.DetailLine = FString::Printf( + TEXT("Choice apply: %s | The resulting selector state is not executable yet."), + DispatchEntry.DetailLine.IsEmpty() ? TEXT("n/a") : *DispatchEntry.DetailLine + ); + Entry.bLeavesExecuteBlocked = true; + Entry.bCanApplyImpact = true; + ++Surface.BlockedExecuteOptionCount; + } + else + { + const bool bWouldUseStoredRequest = DoesLaunchRequestMatchLaunchConfig( + RunState.ImportedGeneratedModeLaunchRequest, + HypotheticalLaunchConfig + ); + + if (DispatchEntry.bMatchesCurrentSelection) + { + Entry.ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::ExecutePostureUnchanged; + Entry.ImpactLine = DescribeUnchangedExecutionImpact(); + Entry.BlockedReasonLine = LaunchExecutionContextSurface.bHasBlockedExecution + ? LaunchExecutionContextSurface.BlockedReasonLine + : FString(); + Entry.DetailLine = LaunchExecutionContextSurface.StatusLine.IsEmpty() + ? TEXT("Current generated-mode execute posture remains unchanged.") + : FString::Printf( + TEXT("Current generated-mode execute posture: %s"), + *LaunchExecutionContextSurface.StatusLine + ); + Entry.bLeavesExecutePostureUnchanged = true; + Entry.bExecutesFromStoredRequest = + LaunchExecutionContextSurface.bHasExecutionTarget + && LaunchExecutionContextSurface.bCanExecute + && !LaunchExecutionContextSurface.bWillRefreshLaunchRequestOnExecute; + Entry.bRequiresLaunchRequestRefreshBeforeExecute = + LaunchExecutionContextSurface.bWillRefreshLaunchRequestOnExecute; + Entry.bLeavesExecuteBlocked = + LaunchExecutionContextSurface.bHasExecutionTarget + && !LaunchExecutionContextSurface.bCanExecute; + Entry.bCanApplyImpact = true; + ++Surface.UnchangedExecutePostureOptionCount; + } + else if (bWouldUseStoredRequest) + { + Entry.ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::ExecuteFromStoredRequest; + Entry.ImpactLine = TEXT("Execution impact: generated-mode execute can use the stored request after this choice."); + Entry.DetailLine = FString::Printf( + TEXT("Choice apply: %s | The resulting selector state realigns with the current stored request."), + DispatchEntry.DetailLine.IsEmpty() ? TEXT("n/a") : *DispatchEntry.DetailLine + ); + Entry.bExecutesFromStoredRequest = true; + Entry.bCanApplyImpact = true; + ++Surface.ExecuteFromStoredRequestOptionCount; + } + else + { + Entry.ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::LaunchRequestRefreshRequired; + Entry.ImpactLine = TEXT("Execution impact: generated-mode execute will require launch-request refresh after this choice."); + Entry.DetailLine = FString::Printf( + TEXT("Choice apply: %s | The resulting selector state no longer matches the current stored request."), + DispatchEntry.DetailLine.IsEmpty() ? TEXT("n/a") : *DispatchEntry.DetailLine + ); + Entry.bRequiresLaunchRequestRefreshBeforeExecute = true; + Entry.bCanApplyImpact = true; + ++Surface.RefreshRequiredOptionCount; + } + } + } + } + else + { + Entry.ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::Unavailable; + Entry.ImpactLine = TEXT("Execution impact: unavailable."); + Entry.DetailLine = TEXT("Execution-impact preview is unavailable because this selector option cannot dispatch from the active generated-mode surface."); + Entry.bCanApplyImpact = false; + ++Surface.UnavailableOptionCount; + } + + if (Entry.bMatchesCurrentSelection) + { + Surface.bCurrentSelectionStartsOwnedCaseImmediately = Entry.bStartsOwnedCaseImmediately; + Surface.bCurrentSelectionLeavesExecutePostureUnchanged = + Entry.bLeavesExecutePostureUnchanged; + Surface.bCurrentSelectionRequiresLaunchRequestRefreshBeforeExecute = + Entry.bRequiresLaunchRequestRefreshBeforeExecute; + Surface.bCurrentSelectionLeavesExecuteBlocked = Entry.bLeavesExecuteBlocked; + Surface.CurrentSelectionExecutionImpactLine = Entry.bLeavesExecuteBlocked + && !Entry.BlockedReasonLine.IsEmpty() + ? FString::Printf(TEXT("%s %s"), *Entry.ImpactLine, *Entry.BlockedReasonLine) + : Entry.ImpactLine; + } + + Surface.OptionEntries.Add(MoveTemp(Entry)); + } + + Surface.bHasExecutionImpactOptions = Surface.OptionEntries.Num() > 0; + Surface.bHasImmediateOwnedCaseStartOptions = + Surface.ImmediateOwnedCaseStartOptionCount > 0; + Surface.bHasStoredRequestExecutionOptions = + Surface.ExecuteFromStoredRequestOptionCount > 0; + Surface.bHasRefreshRequiredOptions = Surface.RefreshRequiredOptionCount > 0; + Surface.bHasBlockedExecuteOptions = Surface.BlockedExecuteOptionCount > 0; + + Surface.ExecutionImpactSummaryLine = Surface.bHasExecutionImpactOptions + ? FString::Printf( + TEXT("Execution impact: %d direct-start | %d stored-request | %d unchanged | %d refresh-required | %d blocked | %d unavailable"), + Surface.ImmediateOwnedCaseStartOptionCount, + Surface.ExecuteFromStoredRequestOptionCount, + Surface.UnchangedExecutePostureOptionCount, + Surface.RefreshRequiredOptionCount, + Surface.BlockedExecuteOptionCount, + Surface.UnavailableOptionCount + ) + : TEXT("Execution impact: no imported options"); + + if (Surface.CurrentSelectionExecutionImpactLine.IsEmpty()) + { + if (OptionMenuSurface.bHasUnresolvedSelectionDetail) + { + Surface.CurrentSelectionExecutionImpactLine = OptionMenuSurface.UnresolvedSelectionLine; + } + else if (OptionMenuSurface.bHasSelection) + { + Surface.CurrentSelectionExecutionImpactLine = + TEXT("Current selection has no additional execution-impact detail."); + } + else if (OptionMenuSurface.bHasSelector) + { + Surface.CurrentSelectionExecutionImpactLine = TEXT("Current selection: unresolved"); + } + else + { + Surface.CurrentSelectionExecutionImpactLine = TEXT("Current selection: n/a"); + } + } + + if (OptionMenuSurface.bHasSelector && Surface.bHasExecutionImpactOptions) + { + Surface.StatusLine = FString::Printf( + TEXT("Generated-mode selector option execution impact exposes %d direct-start option%s, %d stored-request option%s, %d unchanged execute option%s, %d refresh-required option%s, %d blocked option%s, and %d unavailable option%s."), + Surface.ImmediateOwnedCaseStartOptionCount, + Surface.ImmediateOwnedCaseStartOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.ExecuteFromStoredRequestOptionCount, + Surface.ExecuteFromStoredRequestOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.UnchangedExecutePostureOptionCount, + Surface.UnchangedExecutePostureOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.RefreshRequiredOptionCount, + Surface.RefreshRequiredOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.BlockedExecuteOptionCount, + Surface.BlockedExecuteOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.UnavailableOptionCount, + Surface.UnavailableOptionCount == 1 ? TEXT("") : TEXT("s") + ); + } + else if (OptionMenuSurface.bHasSelector) + { + Surface.StatusLine = + TEXT("Generated-mode selector option execution impact is active, but this selector does not expose imported options."); + } + else + { + Surface.StatusLine = OptionMenuSurface.StatusLine.IsEmpty() + ? TEXT("Generated-mode selector option execution impact 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 | Execute: %s | Selector: %s | Binding: %s"), + OptionDispatchSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *OptionDispatchSurface.StatusLine, + LaunchExecutionContextSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *LaunchExecutionContextSurface.StatusLine, + *SelectorSummary, + *BindingSummary + ); + + return Surface; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index 6f7f0f8..fd1bf36 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -750,6 +750,27 @@ UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionDisp ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface +UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionExecutionImpactSurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeLaunchExecutionContextSurface + LaunchExecutionContextSurface = + GetDisplayedCoachGeneratedModeLaunchExecutionContextSurface(); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface = + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface OptionDispatchSurface = + GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(SelectorId); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionExecutionImpactSurface( + CachedRunState, + SelectorId, + LaunchExecutionContextSurface, + OptionMenuSurface, + OptionDispatchSurface + ); +} + 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 2c4286c..d9004aa 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp @@ -731,6 +731,27 @@ AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionDis ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface +AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionExecutionImpactSurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeLaunchExecutionContextSurface + LaunchExecutionContextSurface = + GetDisplayedCoachGeneratedModeLaunchExecutionContextSurface(); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface = + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId); + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface OptionDispatchSurface = + GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(SelectorId); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionExecutionImpactSurface( + CachedRunState, + SelectorId, + LaunchExecutionContextSurface, + OptionMenuSurface, + OptionDispatchSurface + ); +} + 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 83207b3..dff5ce5 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -713,4 +713,15 @@ public: const FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface& OptionActionabilitySurface ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface + DeriveCoachGeneratedModeSelectorOptionExecutionImpactSurface( + const FHyperTwistTrainingRunState& RunState, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeLaunchExecutionContextSurface& + LaunchExecutionContextSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface& OptionDispatchSurface + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h index 1f687dd..6be2463 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h @@ -268,6 +268,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface + GetDisplayedCoachGeneratedModeSelectorOptionExecutionImpactSurface(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 3d1ac24..bf03ed2 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h @@ -249,6 +249,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface + GetDisplayedCoachGeneratedModeSelectorOptionExecutionImpactSurface(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 b92e54d..d69c0aa 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -80,6 +80,18 @@ enum class EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind : uint8 Unavailable UMETA(DisplayName = "Unavailable") }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind : uint8 +{ + None UMETA(DisplayName = "None"), + ImmediateOwnedCaseStart UMETA(DisplayName = "Immediate Owned Case Start"), + ExecuteFromStoredRequest UMETA(DisplayName = "Execute From Stored Request"), + ExecutePostureUnchanged UMETA(DisplayName = "Execute Posture Unchanged"), + LaunchRequestRefreshRequired UMETA(DisplayName = "Launch-Request Refresh Required"), + ExecuteBlocked UMETA(DisplayName = "Execute Blocked"), + Unavailable UMETA(DisplayName = "Unavailable") +}; + UENUM(BlueprintType) enum class EHyperTwistTrainingPromptKind : uint8 { @@ -8903,3 +8915,153 @@ struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface || bHasDispatchOptions; } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactEntrySurface +{ + 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") + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind ExecutionImpactKind = + EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::None; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ImpactLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BlockedReasonLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMatchesCurrentSelection = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bStartsOwnedCaseImmediately = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bExecutesFromStoredRequest = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bLeavesExecutePostureUnchanged = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresLaunchRequestRefreshBeforeExecute = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bLeavesExecuteBlocked = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanApplyImpact = false; + + bool IsStructurallyValid() const + { + return !OptionId.IsEmpty() + || !Label.IsEmpty() + || !ImpactLine.IsEmpty() + || ExecutionImpactKind + != EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind::None + || bCanApplyImpact; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface +{ + 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 ExecutionImpactSummaryLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CurrentSelectionExecutionImpactLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray + OptionEntries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ImmediateOwnedCaseStartOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExecuteFromStoredRequestOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 UnchangedExecutePostureOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RefreshRequiredOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BlockedExecuteOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 UnavailableOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasSelector = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasExecutionImpactOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasImmediateOwnedCaseStartOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasStoredRequestExecutionOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRefreshRequiredOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasBlockedExecuteOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionStartsOwnedCaseImmediately = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionLeavesExecutePostureUnchanged = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionRequiresLaunchRequestRefreshBeforeExecute = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionLeavesExecuteBlocked = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || !SelectorId.IsEmpty() + || OptionEntries.Num() > 0 + || bHasSelector + || bHasExecutionImpactOptions; + } +}; diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 9acbd56..867fb78 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 interaction / dispatch surface over the already-cached selector/runtime state, so gameplay/Blueprint callers can render whether choosing an option should launch a direct owned case, apply a generated-mode selector choice, or stay unavailable without reaching into raw runtime action-path rules directly - - the current next bounded packet is to expose one thin generated-mode selector option execution-impact / launch-request-refresh surface on those same two consumer surfaces so gameplay/Blueprint callers can render whether a choice would start an owned case immediately, leave generated-mode execute posture unchanged, or require launch-request refresh before execute without reading raw launch-request freshness or blocked-reason rules directly + - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds one thin generated-mode selector option execution-impact / launch-request-refresh surface over the already-cached selector/runtime state, so gameplay/Blueprint callers can render whether a choice would start a direct owned case immediately, leave generated-mode execute posture unchanged, realign with the stored request, require launch-request refresh before execute, or still leave execute blocked without reading raw launch-request freshness or blocked-reason rules directly + - the current next bounded packet is to expose one thin generated-mode selector option request-alignment / execute-preview surface on those same two consumer surfaces so gameplay/Blueprint callers can render hypothetical selector summary, stored-request alignment, and unresolved-selector preview for a chosen option without diffing cached selector choices or launch-request payloads 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_EXECUTION_IMPACT_SURFACE_PACKET_2026-05-08.md b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_EXECUTION_IMPACT_SURFACE_PACKET_2026-05-08.md new file mode 100644 index 0000000..583b857 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_EXECUTION_IMPACT_SURFACE_PACKET_2026-05-08.md @@ -0,0 +1,104 @@ +# HyperTwist Phase 4 gameplay generated-mode selector option execution-impact / launch-request-refresh 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 interaction / dispatch surface by exposing one parameterized option execution-impact surface for a chosen selector id. + +The open tasks were: + +- stop forcing gameplay / Blueprint callers to reconstruct launch-request freshness and blocked-execute rules just to explain what choosing an imported option would do to generated-mode execute posture +- expose whether an option would start a direct owned case immediately, keep generated-mode execute posture unchanged, realign with the stored request, require a fresh launch request before execute, or still leave execute blocked +- keep the surface derived from already-cached gameplay consumer run, selector, and request state plus the already-landed launch-execution, option-detail, and option-dispatch surfaces instead of widening into backend logic + +It is not: + +- a new selector-dispatch packet +- a new launch-request creation packet +- a generated-mode backend behavior change +- a broader gameplay UI composition pass + +## Scope + +Bounded lane: + +- add first-party generated-mode selector option execution-impact surfaces in shared training types +- add a coach-library helper that derives execution-impact posture from the cached run state plus the already-landed launch-execution, option-detail, and option-dispatch surfaces +- expose that parameterized execution-impact surface on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor` +- keep the derivation presentation-only and rooted in the already-owned selector-choice / launch-request / execute rules + +Out of scope: + +- changing selector-choice apply behavior +- changing direct owned-case option-launch behavior +- changing generated-mode execute or launch-request persistence 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 launch / execution context +- those same consumers already exposed truthful chosen-selector option detail +- those same consumers already exposed truthful chosen-selector option dispatch posture + +But one thin consumer seam was still open: + +- callers could tell how an option dispatches +- but they still had to know raw launch-request freshness and blocked-execute rules to explain what that option would do to generated-mode execute posture + +That meant: + +- dispatch posture was thin and reusable +- but execute-impact posture still leaked runtime freshness/blocking rules into callers + +So the next bounded move was: + +- keep the already-landed generated-mode execute context, option-detail, and option-dispatch surfaces unchanged +- and expose one narrow option execution-impact / launch-request-refresh surface above those already-cached surfaces + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h` + - added `EHyperTwistTrainingCoachGeneratedModeOptionExecutionImpactKind` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactEntrySurface` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionExecutionImpactSurface` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp` + - added `DeriveCoachGeneratedModeSelectorOptionExecutionImpactSurface(...)` + - execution-impact posture is derived from the cached run state plus the already-landed launch-execution, option-detail, and option-dispatch surfaces +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionExecutionImpactSurface(const FString& SelectorId)` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionExecutionImpactSurface(const FString& SelectorId)` + +Surface behavior: + +- reports whether each imported option starts a direct owned case immediately, realigns with the stored request, leaves current execute posture unchanged, requires launch-request refresh before execute, or still leaves execute blocked +- exposes blocked-reason preview for options that still leave generated-mode execute unresolved +- reports how the current cached selection would affect generated-mode execute right now +- keeps the surface presentation-only and compatible with the already-landed generated-mode launch / execution, option-detail, and option-dispatch 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 request-alignment / execute-preview surface so callers can render hypothetical selector summary, stored-request alignment, and unresolved-selector preview for a chosen option without diffing cached selector choices or launch-request payloads directly.