diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index 04dabac..c658cf6 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -7169,3 +7169,241 @@ UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorRosterSurface( return Surface; } + +FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface +UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionMenuSurface( + const FHyperTwistTrainingDeck& ActiveDeck, + const FHyperTwistTrainingImportedRuntimeSelectionState& ImportedRuntimeSelectionState, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface& SelectorRosterSurface +) +{ + FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface Surface; + Surface.SelectorId = SelectorId; + Surface.Headline = TEXT("Generated Mode Selector Options"); + + const FHyperTwistTrainingImportedRuntimeSurface& RuntimeSurface = ActiveDeck.ImportedRuntimeSurface; + const bool bHasGeneratedModeSurface = RuntimeSurface.IsStructurallyValid() + && RuntimeSurface.SurfaceKind.Equals(TEXT("generated-mode"), ESearchCase::IgnoreCase); + + if (SelectorId.IsEmpty()) + { + Surface.StatusLine = + TEXT("Generated-mode selector option detail requires a selector id from the active selector roster."); + Surface.SelectionLine = TEXT("Selection: n/a"); + Surface.AvailabilityLine = TEXT("Options: n/a"); + Surface.DetailLine = SelectorRosterSurface.StatusLine.IsEmpty() + ? TEXT("Roster: n/a") + : FString::Printf(TEXT("Roster: %s"), *SelectorRosterSurface.StatusLine); + return Surface; + } + + const FHyperTwistTrainingCoachGeneratedModeSelectorRosterEntrySurface* MatchingRosterEntry = + SelectorRosterSurface.SelectorEntries.FindByPredicate( + [&SelectorId](const FHyperTwistTrainingCoachGeneratedModeSelectorRosterEntrySurface& Entry) + { + return Entry.SelectorId == SelectorId; + } + ); + const FHyperTwistTrainingImportedRuntimeSelectorChoice* MatchingChoice = + ImportedRuntimeSelectionState.SelectorChoices.FindByPredicate( + [&SelectorId](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == SelectorId && Candidate.IsStructurallyValid(); + } + ); + const FHyperTwistTrainingImportedRuntimeSelector* MatchingSelector = + RuntimeSurface.Selectors.FindByPredicate( + [&SelectorId](const FHyperTwistTrainingImportedRuntimeSelector& Candidate) + { + return Candidate.SelectorId == SelectorId && Candidate.IsStructurallyValid(); + } + ); + + Surface.bHasSelection = MatchingChoice != nullptr; + if (MatchingChoice != nullptr) + { + Surface.SelectedValue = MatchingChoice->SelectedValue; + Surface.SelectedDisplayValue = MatchingChoice->DisplayValue.IsEmpty() + ? MatchingChoice->SelectedValue + : MatchingChoice->DisplayValue; + } + else if (MatchingRosterEntry != nullptr && MatchingRosterEntry->bHasSelection) + { + Surface.bHasSelection = true; + Surface.SelectedValue = MatchingRosterEntry->SelectedValue; + Surface.SelectedDisplayValue = MatchingRosterEntry->SelectedDisplayValue; + } + + if (MatchingSelector != nullptr) + { + Surface.bHasSelector = true; + Surface.SelectorId = MatchingSelector->SelectorId; + Surface.SelectorLabel = MatchingSelector->Label; + Surface.BindingLabel = MatchingSelector->SelectorBinding; + Surface.Headline = MatchingSelector->Label.IsEmpty() + ? FString::Printf(TEXT("Generated Mode Selector Options: %s"), *MatchingSelector->SelectorId) + : FString::Printf(TEXT("Generated Mode Selector Options: %s"), *MatchingSelector->Label); + + for (const FHyperTwistTrainingImportedRuntimeSelectorOption& Option : MatchingSelector->Options) + { + if (!Option.IsStructurallyValid()) + { + continue; + } + + FHyperTwistTrainingCoachGeneratedModeSelectorOptionEntrySurface Entry; + Entry.OptionId = Option.OptionId; + Entry.Label = Option.Label; + Entry.OptionValue = Option.OptionValue; + Entry.DetailLine = Entry.OptionValue.IsEmpty() || Entry.OptionValue == Entry.OptionId + ? FString::Printf(TEXT("Option id: %s"), *Entry.OptionId) + : FString::Printf( + TEXT("Option id: %s | Value: %s"), + *Entry.OptionId, + *Entry.OptionValue + ); + Entry.bMatchesCurrentSelection = Surface.bHasSelection + && (Surface.SelectedValue == Option.OptionId || Surface.SelectedValue == Option.OptionValue); + if (Entry.bMatchesCurrentSelection && !Surface.bSelectionMatchesAvailableOption) + { + Surface.bSelectionMatchesAvailableOption = true; + Surface.MatchedOptionId = Entry.OptionId; + Surface.MatchedOptionLabel = Entry.Label; + Surface.MatchedOptionValue = Entry.OptionValue; + } + + Surface.OptionEntries.Add(MoveTemp(Entry)); + } + } + + Surface.AvailableOptionCount = Surface.OptionEntries.Num(); + Surface.bHasOptions = Surface.AvailableOptionCount > 0; + Surface.bHasUnresolvedSelectionDetail = + Surface.bHasSelection && !Surface.bSelectionMatchesAvailableOption; + + if (Surface.bHasSelection) + { + const FString SelectionSummary = Surface.SelectedDisplayValue.IsEmpty() + ? Surface.SelectedValue + : Surface.SelectedDisplayValue; + Surface.SelectionLine = Surface.bSelectionMatchesAvailableOption + ? FString::Printf( + TEXT("Selection: %s [%s]"), + SelectionSummary.IsEmpty() ? TEXT("n/a") : *SelectionSummary, + Surface.MatchedOptionId.IsEmpty() ? TEXT("n/a") : *Surface.MatchedOptionId + ) + : FString::Printf( + TEXT("Selection: %s (cached)"), + SelectionSummary.IsEmpty() ? TEXT("n/a") : *SelectionSummary + ); + } + else if (Surface.bHasSelector) + { + Surface.SelectionLine = TEXT("Selection: unresolved"); + } + else + { + Surface.SelectionLine = TEXT("Selection: n/a"); + } + + if (Surface.bHasUnresolvedSelectionDetail) + { + const FString SelectionDisplaySummary = Surface.SelectedDisplayValue.IsEmpty() + ? Surface.SelectedValue + : Surface.SelectedDisplayValue; + Surface.UnresolvedSelectionLine = + Surface.SelectedDisplayValue.IsEmpty() || Surface.SelectedDisplayValue == Surface.SelectedValue + ? FString::Printf( + TEXT("Cached selection '%s' is not present in the imported options."), + SelectionDisplaySummary.IsEmpty() ? TEXT("n/a") : *SelectionDisplaySummary + ) + : FString::Printf( + TEXT("Cached selection '%s' stored as '%s' is not present in the imported options."), + SelectionDisplaySummary.IsEmpty() ? TEXT("n/a") : *SelectionDisplaySummary, + Surface.SelectedValue.IsEmpty() ? TEXT("n/a") : *Surface.SelectedValue + ); + } + + if (Surface.bHasOptions) + { + Surface.AvailabilityLine = FString::Printf( + TEXT("Options: %d imported"), + Surface.AvailableOptionCount + ); + } + else if (Surface.bHasSelector) + { + Surface.AvailabilityLine = TEXT("Options: no imported options"); + } + else if (bHasGeneratedModeSurface) + { + Surface.AvailabilityLine = TEXT("Options: selector not found in active generated-mode surface"); + } + else + { + Surface.AvailabilityLine = TEXT("Options: n/a"); + } + + if (Surface.bHasSelector && Surface.bHasOptions && Surface.bSelectionMatchesAvailableOption) + { + Surface.StatusLine = FString::Printf( + TEXT("Generated-mode selector option detail exposes %d imported option%s and the cached selection resolves cleanly."), + Surface.AvailableOptionCount, + Surface.AvailableOptionCount == 1 ? TEXT("") : TEXT("s") + ); + } + else if (Surface.bHasSelector && Surface.bHasOptions && Surface.bHasSelection) + { + Surface.StatusLine = FString::Printf( + TEXT("Generated-mode selector option detail exposes %d imported option%s, but the cached selection no longer matches an imported option."), + Surface.AvailableOptionCount, + Surface.AvailableOptionCount == 1 ? TEXT("") : TEXT("s") + ); + } + else if (Surface.bHasSelector && Surface.bHasOptions) + { + Surface.StatusLine = FString::Printf( + TEXT("Generated-mode selector option detail exposes %d imported option%s and the selector is currently unresolved."), + Surface.AvailableOptionCount, + Surface.AvailableOptionCount == 1 ? TEXT("") : TEXT("s") + ); + } + else if (Surface.bHasSelector) + { + Surface.StatusLine = + TEXT("Generated-mode selector option detail is active, but this selector does not expose imported options."); + } + else if (bHasGeneratedModeSurface) + { + Surface.StatusLine = + TEXT("Generated-mode selector option detail could not find the requested selector in the active generated-mode surface."); + } + else if (SelectorRosterSurface.bHasSelectorRoster || ImportedRuntimeSelectionState.IsStructurallyValid()) + { + Surface.StatusLine = + TEXT("Generated-mode selector option detail is not active, but cached selector state remains."); + } + else + { + Surface.StatusLine = + TEXT("Generated-mode selector option detail is not currently available."); + } + + 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("Roster: %s | Selector: %s | Binding: %s"), + SelectorRosterSurface.StatusLine.IsEmpty() + ? TEXT("n/a") + : *SelectorRosterSurface.StatusLine, + *SelectorSummary, + *BindingSummary + ); + + return Surface; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index 8ea7ac8..2c6a857 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -704,6 +704,21 @@ UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorRosterSurf ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface +UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface SelectorRosterSurface = + GetDisplayedCoachGeneratedModeSelectorRosterSurface(); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionMenuSurface( + CachedRunState.ActiveDeck, + CachedImportedRuntimeSelectionState, + SelectorId, + SelectorRosterSurface + ); +} + 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 e04c6b4..e668c14 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp @@ -685,6 +685,21 @@ AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorRosterSur ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface +AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface SelectorRosterSurface = + GetDisplayedCoachGeneratedModeSelectorRosterSurface(); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionMenuSurface( + CachedRunState.ActiveDeck, + CachedImportedRuntimeSelectionState, + SelectorId, + SelectorRosterSurface + ); +} + 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 c73d26b..c762671 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -687,4 +687,13 @@ public: const FHyperTwistTrainingCoachGeneratedModeSelectorApplicationContextSurface& SelectorApplicationContextSurface ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface + DeriveCoachGeneratedModeSelectorOptionMenuSurface( + const FHyperTwistTrainingDeck& ActiveDeck, + const FHyperTwistTrainingImportedRuntimeSelectionState& ImportedRuntimeSelectionState, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface& SelectorRosterSurface + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h index 1dbfd87..ddaf56b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h @@ -256,6 +256,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface GetDisplayedCoachGeneratedModeSelectorRosterSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(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 90a2217..8fba97e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h @@ -237,6 +237,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface GetDisplayedCoachGeneratedModeSelectorRosterSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(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 4964b5c..6676747 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -8569,3 +8569,110 @@ struct FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface || bHasUnresolvedSelectors; } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionEntrySurface +{ + 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") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMatchesCurrentSelection = false; + + bool IsStructurallyValid() const + { + return !OptionId.IsEmpty() + || !Label.IsEmpty() + || !OptionValue.IsEmpty() + || bMatchesCurrentSelection; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface +{ + 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 SelectionLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UnresolvedSelectionLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString AvailabilityLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray OptionEntries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectedValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectedDisplayValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MatchedOptionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MatchedOptionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MatchedOptionValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AvailableOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasSelector = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasSelection = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasOptions = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSelectionMatchesAvailableOption = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasUnresolvedSelectionDetail = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || !SelectorId.IsEmpty() + || OptionEntries.Num() > 0 + || bHasSelector + || bHasSelection; + } +}; diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 9a7d49f..357d6a0 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 roster / option-availability surface over the active imported runtime surface and cached selection state, so gameplay/Blueprint callers can render active selector labels, bindings, selected values, and unresolved option availability without reading raw imported selector arrays or option payloads directly - - the current next bounded packet is to expose one thin generated-mode selector option-menu / detail surface on those same two consumer surfaces so gameplay/Blueprint callers can render normalized selector-option ids, labels, and unresolved-selection detail for a chosen selector without reading raw imported option payloads directly + - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds one thin parameterized generated-mode selector option-menu / detail surface over the already-cached selector roster/runtime state, so gameplay/Blueprint callers can render normalized selector-option ids, labels, values, and unresolved-selection detail for a chosen selector without reading raw imported option payloads directly + - the current next bounded packet is to expose one thin generated-mode selector option actionability / case-coverage surface on those same two consumer surfaces so gameplay/Blueprint callers can render which imported options still map cleanly to owned cases and which remain selection-only choices without reading raw option-case payloads or runtime probes 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_DETAIL_SURFACE_PACKET_2026-05-08.md b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_DETAIL_SURFACE_PACKET_2026-05-08.md new file mode 100644 index 0000000..dde03a9 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_DETAIL_SURFACE_PACKET_2026-05-08.md @@ -0,0 +1,103 @@ +# HyperTwist Phase 4 gameplay generated-mode selector option-menu / detail 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 roster by exposing one parameterized selector option-menu / detail surface for a chosen selector id. + +The open tasks were: + +- stop forcing gameplay / Blueprint callers to read raw imported selector-option payloads just to render normalized option ids, labels, values, and chosen-selector option menus +- expose when a cached generated-mode selector choice still matches an imported option and when it has drifted out of the imported option list +- keep the surface derived from already-cached gameplay consumer selector/runtime state instead of widening into selector-application behavior or new runtime fetches + +It is not: + +- a new selector-application behavior packet +- a per-option actionability or case-coverage packet +- a generated-mode execution packet +- a broader gameplay UI composition pass + +## Scope + +Bounded lane: + +- add first-party generated-mode selector option-entry and option-menu surfaces in shared training types +- add a coach-library helper that derives the chosen-selector option menu from the active deck imported runtime surface, cached imported selection state, requested selector id, and the already-landed selector roster surface +- expose that parameterized option-menu surface on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor` +- keep the derivation presentation-only and rooted in already-cached gameplay consumer state + +Out of scope: + +- changing selector-choice persistence +- changing generated-mode launch-config derivation rules +- changing selector-option application behavior +- widening into per-option case coverage or actionability yet + +## 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 selector / application context +- those same consumers already exposed truthful selector roster / option-availability summary + +But one thin consumer seam was still open: + +- callers could list active selectors and see whether unresolved selectors still had imported options +- but they still had to read raw option payloads to render the option menu for a chosen selector and to explain whether the cached selection still matched a live imported option + +That meant: + +- selector roster posture was thin and reusable +- but chosen-selector option detail still leaked raw imported option payloads into callers + +So the next bounded move was: + +- keep the already-landed launch / execution, selector / application, and selector roster surfaces unchanged +- and expose one narrow parameterized option-menu / detail surface above that cached selector state + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionEntrySurface` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp` + - added `DeriveCoachGeneratedModeSelectorOptionMenuSurface(...)` + - option detail is derived from the active imported runtime surface, cached imported selection state, requested selector id, and the already-landed selector roster surface +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(const FString& SelectorId)` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(const FString& SelectorId)` + +Surface behavior: + +- reports normalized option ids, labels, and option values for the chosen selector +- reports whether the cached selection still matches one of the current imported options +- reports unresolved-selection detail when cached selection state remains but no longer maps to the imported option list +- keeps the surface presentation-only and compatible with the already-landed generated-mode context and roster 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 actionability / case-coverage surface so callers can render which imported options still map to owned cases and which remain selection-only choices without reading raw option-case payloads or runtime probes directly.