Expose gameplay generated-mode selector option dispatch surface

This commit is contained in:
axiomlogicnexus 2026-05-08 02:48:42 +02:00
parent 046ff8ee62
commit 3521d4ca4d
9 changed files with 518 additions and 2 deletions

View file

@ -7583,3 +7583,235 @@ UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionActionabi
return Surface;
}
FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface
UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionDispatchSurface(
const FString& SelectorId,
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface,
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface&
OptionActionabilitySurface
)
{
FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface Surface;
Surface.SelectorId = SelectorId;
Surface.SelectorLabel = OptionMenuSurface.SelectorLabel;
Surface.BindingLabel = OptionMenuSurface.BindingLabel;
Surface.Headline = TEXT("Generated Mode Selector Option Dispatch");
Surface.bHasSelector = OptionMenuSurface.bHasSelector;
if (!SelectorId.IsEmpty())
{
Surface.Headline = OptionMenuSurface.SelectorLabel.IsEmpty()
? FString::Printf(TEXT("Generated Mode Selector Option Dispatch: %s"), *SelectorId)
: FString::Printf(
TEXT("Generated Mode Selector Option Dispatch: %s"),
*OptionMenuSurface.SelectorLabel
);
}
if (SelectorId.IsEmpty())
{
Surface.StatusLine =
TEXT("Generated-mode selector option dispatch requires a selector id from the active selector roster.");
Surface.DispatchSummaryLine = TEXT("Dispatch: n/a");
Surface.CurrentSelectionDispatchLine = TEXT("Current selection: n/a");
Surface.DetailLine = OptionActionabilitySurface.StatusLine.IsEmpty()
? TEXT("Coverage: n/a")
: FString::Printf(TEXT("Coverage: %s"), *OptionActionabilitySurface.StatusLine);
return Surface;
}
for (const FHyperTwistTrainingCoachGeneratedModeSelectorOptionEntrySurface& OptionEntry :
OptionMenuSurface.OptionEntries)
{
FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchEntrySurface Entry;
Entry.OptionId = OptionEntry.OptionId;
Entry.Label = OptionEntry.Label;
Entry.OptionValue = OptionEntry.OptionValue;
Entry.bMatchesCurrentSelection = OptionEntry.bMatchesCurrentSelection;
Entry.GeneratedChoiceSelectedValue = OptionEntry.OptionValue.IsEmpty()
? OptionEntry.OptionId
: OptionEntry.OptionValue;
Entry.GeneratedChoiceDisplayValue = OptionEntry.Label.IsEmpty()
? Entry.GeneratedChoiceSelectedValue
: OptionEntry.Label;
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilityEntrySurface*
MatchingActionabilityEntry =
OptionActionabilitySurface.OptionEntries.FindByPredicate(
[&OptionEntry](
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilityEntrySurface&
Candidate
)
{
return Candidate.OptionId == OptionEntry.OptionId;
}
);
const bool bHasCoveredCase =
MatchingActionabilityEntry != nullptr && MatchingActionabilityEntry->bHasCoveredCase;
if (bHasCoveredCase)
{
Entry.DispatchKind =
EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::DirectCaseLaunch;
Entry.DirectLaunchOptionId = OptionEntry.OptionId;
Entry.DispatchLine = TEXT("Dispatch: launch direct owned case");
Entry.DetailLine = MatchingActionabilityEntry->MatchedCaseLabel.IsEmpty()
? TEXT("Launch path: ApplyActiveImportedRuntimeSelectorOption")
: FString::Printf(
TEXT("Launch path: ApplyActiveImportedRuntimeSelectorOption | Case: %s"),
*MatchingActionabilityEntry->MatchedCaseLabel
);
Entry.bCanDispatch = true;
++Surface.DirectLaunchOptionCount;
}
else if (OptionMenuSurface.bHasSelector)
{
Entry.DispatchKind =
EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::ApplyGeneratedChoice;
Entry.DispatchLine = TEXT("Dispatch: apply generated-mode selector choice");
Entry.DetailLine = FString::Printf(
TEXT("Choice path: ApplyActiveImportedGeneratedModeSelectorChoice | Value: %s"),
Entry.GeneratedChoiceSelectedValue.IsEmpty()
? TEXT("n/a")
: *Entry.GeneratedChoiceSelectedValue
);
Entry.bCanDispatch = !Entry.GeneratedChoiceSelectedValue.IsEmpty();
++Surface.GeneratedChoiceOptionCount;
}
else
{
Entry.DispatchKind = EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::Unavailable;
Entry.DispatchLine = TEXT("Dispatch: unavailable");
Entry.DetailLine = TEXT("Option dispatch is unavailable because the selector is not active.");
Entry.bCanDispatch = false;
++Surface.UnavailableOptionCount;
}
if (Entry.bMatchesCurrentSelection)
{
Surface.bCurrentSelectionDispatchesDirectLaunch =
Entry.DispatchKind
== EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::DirectCaseLaunch;
Surface.bCurrentSelectionDispatchesGeneratedChoice =
Entry.DispatchKind
== EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::ApplyGeneratedChoice;
}
if (!Entry.bCanDispatch
&& Entry.DispatchKind
!= EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::Unavailable)
{
++Surface.UnavailableOptionCount;
}
Surface.OptionEntries.Add(MoveTemp(Entry));
}
Surface.bHasDispatchOptions = Surface.OptionEntries.Num() > 0;
Surface.bHasDirectLaunchOptions = Surface.DirectLaunchOptionCount > 0;
Surface.bHasGeneratedChoiceOptions = Surface.GeneratedChoiceOptionCount > 0;
if (Surface.bHasDispatchOptions)
{
Surface.DispatchSummaryLine = FString::Printf(
TEXT("Dispatch: %d direct-launch | %d generated-choice | %d unavailable"),
Surface.DirectLaunchOptionCount,
Surface.GeneratedChoiceOptionCount,
Surface.UnavailableOptionCount
);
}
else
{
Surface.DispatchSummaryLine = TEXT("Dispatch: no imported options");
}
if (Surface.bCurrentSelectionDispatchesDirectLaunch)
{
Surface.CurrentSelectionDispatchLine =
TEXT("Current selection dispatches as direct owned-case launch.");
}
else if (Surface.bCurrentSelectionDispatchesGeneratedChoice)
{
Surface.CurrentSelectionDispatchLine =
TEXT("Current selection dispatches as generated-mode selector choice apply.");
}
else if (OptionMenuSurface.bHasUnresolvedSelectionDetail)
{
Surface.CurrentSelectionDispatchLine = OptionMenuSurface.UnresolvedSelectionLine.IsEmpty()
? TEXT("Current selection does not currently map to an imported option.")
: OptionMenuSurface.UnresolvedSelectionLine;
}
else if (OptionMenuSurface.bHasSelection)
{
Surface.CurrentSelectionDispatchLine =
TEXT("Current selection has no additional dispatch detail.");
}
else if (OptionMenuSurface.bHasSelector)
{
Surface.CurrentSelectionDispatchLine = TEXT("Current selection: unresolved");
}
else
{
Surface.CurrentSelectionDispatchLine = TEXT("Current selection: n/a");
}
if (OptionMenuSurface.bHasSelector && Surface.DirectLaunchOptionCount > 0
&& Surface.GeneratedChoiceOptionCount == 0 && Surface.UnavailableOptionCount == 0)
{
Surface.StatusLine = FString::Printf(
TEXT("Generated-mode selector option dispatch exposes %d direct-launch option%s and no fallback choice applies."),
Surface.DirectLaunchOptionCount,
Surface.DirectLaunchOptionCount == 1 ? TEXT("") : TEXT("s")
);
}
else if (OptionMenuSurface.bHasSelector && Surface.GeneratedChoiceOptionCount > 0
&& Surface.DirectLaunchOptionCount == 0 && Surface.UnavailableOptionCount == 0)
{
Surface.StatusLine = FString::Printf(
TEXT("Generated-mode selector option dispatch exposes %d generated-choice option%s and no direct-launch cases."),
Surface.GeneratedChoiceOptionCount,
Surface.GeneratedChoiceOptionCount == 1 ? TEXT("") : TEXT("s")
);
}
else if (OptionMenuSurface.bHasSelector && Surface.bHasDispatchOptions)
{
Surface.StatusLine = FString::Printf(
TEXT("Generated-mode selector option dispatch exposes %d direct-launch option%s, %d generated-choice option%s, and %d unavailable option%s."),
Surface.DirectLaunchOptionCount,
Surface.DirectLaunchOptionCount == 1 ? TEXT("") : TEXT("s"),
Surface.GeneratedChoiceOptionCount,
Surface.GeneratedChoiceOptionCount == 1 ? TEXT("") : TEXT("s"),
Surface.UnavailableOptionCount,
Surface.UnavailableOptionCount == 1 ? TEXT("") : TEXT("s")
);
}
else if (OptionMenuSurface.bHasSelector)
{
Surface.StatusLine =
TEXT("Generated-mode selector option dispatch is active, but this selector does not expose imported options.");
}
else
{
Surface.StatusLine = OptionMenuSurface.StatusLine.IsEmpty()
? TEXT("Generated-mode selector option dispatch 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("Coverage: %s | Selector: %s | Binding: %s"),
OptionActionabilitySurface.StatusLine.IsEmpty()
? TEXT("n/a")
: *OptionActionabilitySurface.StatusLine,
*SelectorSummary,
*BindingSummary
);
return Surface;
}

View file

@ -733,6 +733,23 @@ UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionActi
);
}
FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface
UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(
const FString& SelectorId
) const
{
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface =
GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId);
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface
OptionActionabilitySurface =
GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(SelectorId);
return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionDispatchSurface(
SelectorId,
OptionMenuSurface,
OptionActionabilitySurface
);
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachRecommendedRun(
const int32 MaxCases,
const FString& StartActionSourceLabel

View file

@ -714,6 +714,23 @@ AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionAct
);
}
FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface
AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(
const FString& SelectorId
) const
{
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface =
GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId);
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface
OptionActionabilitySurface =
GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(SelectorId);
return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionDispatchSurface(
SelectorId,
OptionMenuSurface,
OptionActionabilitySurface
);
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachRecommendedTrainingRun(
const int32 MaxCases
)

View file

@ -704,4 +704,13 @@ public:
const FString& SelectorId,
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface
DeriveCoachGeneratedModeSelectorOptionDispatchSurface(
const FString& SelectorId,
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface,
const FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface&
OptionActionabilitySurface
);
};

View file

@ -264,6 +264,10 @@ public:
FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface
GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(const FString& SelectorId) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface
GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(const FString& SelectorId) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartCoachRecommendedRun(
int32 MaxCases,

View file

@ -245,6 +245,10 @@ public:
FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface
GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(const FString& SelectorId) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface
GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(const FString& SelectorId) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartCoachRecommendedTrainingRun(int32 MaxCases);

View file

@ -71,6 +71,15 @@ enum class EHyperTwistTrainingCoachEligibilityKind : uint8
NoEligibleAction UMETA(DisplayName = "No Eligible Action")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind : uint8
{
None UMETA(DisplayName = "None"),
DirectCaseLaunch UMETA(DisplayName = "Direct Case Launch"),
ApplyGeneratedChoice UMETA(DisplayName = "Apply Generated Choice"),
Unavailable UMETA(DisplayName = "Unavailable")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingPromptKind : uint8
{
@ -8776,3 +8785,121 @@ struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface
|| bHasCaseCoverage;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchEntrySurface
{
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")
EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind DispatchKind =
EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DispatchLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DetailLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DirectLaunchOptionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString GeneratedChoiceSelectedValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString GeneratedChoiceDisplayValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bMatchesCurrentSelection = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCanDispatch = false;
bool IsStructurallyValid() const
{
return !OptionId.IsEmpty()
|| !Label.IsEmpty()
|| !DispatchLine.IsEmpty()
|| DispatchKind != EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind::None
|| bCanDispatch;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface
{
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 DispatchSummaryLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CurrentSelectionDispatchLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DetailLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchEntrySurface> OptionEntries;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DirectLaunchOptionCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 GeneratedChoiceOptionCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 UnavailableOptionCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasSelector = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasDispatchOptions = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasDirectLaunchOptions = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasGeneratedChoiceOptions = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCurrentSelectionDispatchesDirectLaunch = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCurrentSelectionDispatchesGeneratedChoice = false;
bool IsStructurallyValid() const
{
return !StatusLine.IsEmpty()
|| !SelectorId.IsEmpty()
|| OptionEntries.Num() > 0
|| bHasSelector
|| bHasDispatchOptions;
}
};

View file

@ -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 actionability / case-coverage surface over the already-cached selector/runtime state, 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 next bounded packet is to expose one thin generated-mode selector option interaction / dispatch surface on those same two consumer surfaces 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 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 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

View file

@ -0,0 +1,106 @@
# HyperTwist Phase 4 gameplay generated-mode selector option interaction / dispatch 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 case-coverage surface by exposing one parameterized option interaction / dispatch surface for a chosen selector id.
The open tasks were:
- stop forcing gameplay / Blueprint callers to reconstruct runtime action-path rules just to decide whether choosing an imported option should launch a direct owned case or apply a generated-mode selector choice
- expose exact dispatch posture and dispatch arguments for each imported option
- keep the surface derived from already-cached gameplay consumer selector/runtime state plus the already-landed option-detail and case-coverage surfaces instead of widening into new backend behavior
It is not:
- a new selector-application behavior packet
- a generated-mode execution packet
- a launch-request refresh / execute-impact packet
- a broader gameplay UI composition pass
## Scope
Bounded lane:
- add first-party generated-mode selector option dispatch surfaces in shared training types
- add a coach-library helper that derives dispatch posture from the requested selector id plus the already-landed option-detail and case-coverage surfaces
- expose that parameterized dispatch surface on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor`
- keep the derivation presentation-only and rooted in the already-owned runtime action paths
Out of scope:
- changing direct option-launch behavior
- changing generated-mode selector-choice apply behavior
- changing launch-request freshness rules
- widening into execute-impact posture 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
- those same consumers already exposed truthful chosen-selector option detail
- those same consumers already exposed truthful chosen-selector option case coverage
But one thin consumer seam was still open:
- callers could tell whether an option still mapped to an owned case
- but they still had to know raw runtime branching rules to decide whether interacting with that option should launch a direct case, apply a generated-mode selector choice, or stay unavailable
That meant:
- case coverage was thin and reusable
- but actual option dispatch posture still leaked runtime action-path rules into callers
So the next bounded move was:
- keep the already-landed generated-mode context, roster, option-detail, and option-coverage surfaces unchanged
- and expose one narrow option interaction / dispatch surface above those already-cached surfaces
## What landed
Primary code changes:
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h`
- added `EHyperTwistTrainingCoachGeneratedModeOptionDispatchKind`
- added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchEntrySurface`
- added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionDispatchSurface`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp`
- added `DeriveCoachGeneratedModeSelectorOptionDispatchSurface(...)`
- dispatch posture is derived from the requested selector id plus the already-landed option-detail and option-coverage surfaces
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp`
- added `GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(const FString& SelectorId)`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp`
- added `GetDisplayedCoachGeneratedModeSelectorOptionDispatchSurface(const FString& SelectorId)`
Surface behavior:
- reports whether each imported option dispatches as direct owned-case launch, generated-mode selector-choice apply, or unavailable
- exposes exact dispatch arguments for direct launch and generated-choice apply flows
- reports how the current cached selection would dispatch right now
- keeps the surface presentation-only and compatible with the already-landed generated-mode option-detail and case-coverage 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 execution-impact / launch-request-refresh surface so callers can render whether choosing an option 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.