diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 34fae01..c6b6a62 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -10,6 +10,7 @@ #include "Components/VerticalBox.h" #include "Components/VerticalBoxSlot.h" #include "HAL/PlatformTime.h" +#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h" namespace HyperTwistCoachDashboardWidgetInternal { @@ -35,6 +36,27 @@ namespace HyperTwistCoachDashboardWidgetInternal Weak }; + bool ContainsTagIgnoreCase(const TArray& Tags, const FString& ExpectedTag) + { + return Tags.ContainsByPredicate( + [&ExpectedTag](const FString& Candidate) + { + return Candidate.Equals(ExpectedTag, ESearchCase::IgnoreCase); + } + ); + } + + bool IsGeneratedModeLaunchDeck(const FHyperTwistTrainingRunState& RunState) + { + return RunState.ActiveDeck.IsStructurallyValid() + && RunState.ActiveDeck.ImportedRuntimeSurface.IsStructurallyValid() + && RunState.ActiveDeck.ImportedRuntimeSurface.SurfaceKind.Equals( + TEXT("generated-mode"), + ESearchCase::IgnoreCase + ) + && !ContainsTagIgnoreCase(RunState.ActiveDeck.Tags, TEXT("generated-mode:owned-execution")); + } + struct FHandoffHistoryDashboardBias { int32 QueueAcceptedDeltaCount = 0; @@ -3648,7 +3670,26 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitSyntheti bool UHyperTwistCoachDashboardWidget::ContinueCurrentRunFlow() { bool bSucceeded = false; - if (CachedRunState.Session.IsStructurallyValid() && CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()) + if (HyperTwistCoachDashboardWidgetInternal::IsGeneratedModeLaunchDeck(CachedRunState)) + { + FHyperTwistTrainingRunState RunState; + FString BlockedReason; + bSucceeded = UHyperTwistTrainingRuntimeLibrary::TryExecuteActiveImportedGeneratedModeLaunchRequest( + this, + RunState, + BlockedReason + ); + if (bSucceeded) + { + LastStepResult = FHyperTwistTrainingRunStepResult(); + RefreshTrainingState(); + SyncRetainedRunRecap(); + SyncSelectedQueueEntry(); + SyncSelectedAttempt(); + UpdateDashboardPresentation(); + } + } + else if (CachedRunState.Session.IsStructurallyValid() && CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()) { bSucceeded = StartLiveAttemptTimer(); } @@ -10203,9 +10244,25 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() == EHyperTwistTrainingSessionState::Aborted); const bool bHasNextCaseFromLastStep = bHasSessionScopedLastStep && LastStepResult.AttemptResolution.NextCaseSelection.IsStructurallyValid(); + const bool bGeneratedModeLaunchDeck = + HyperTwistCoachDashboardWidgetInternal::IsGeneratedModeLaunchDeck(CachedRunState); + const bool bCanExecuteGeneratedModeLaunch = [this, bGeneratedModeLaunchDeck]() + { + if (!bGeneratedModeLaunchDeck) + { + return false; + } + + FHyperTwistTrainingImportedGeneratedModeLaunchConfig ActiveGeneratedModeLaunchConfig; + return UHyperTwistTrainingRuntimeLibrary::TryGetActiveImportedGeneratedModeLaunchConfig( + this, + ActiveGeneratedModeLaunchConfig + ); + }(); const bool bCanContinueCurrentRun = !bHasLiveAttemptTimer && bHasRunnableCurrentCase - && !bLastStepCompletedRun; + && !bLastStepCompletedRun + && (!bGeneratedModeLaunchDeck || bCanExecuteGeneratedModeLaunch); const bool bCanLaunchCoachRun = !bHasLiveAttemptTimer && ( CachedCoachPanelState.bCanStartQueuedRun @@ -10249,13 +10306,25 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() ); } } + else if (bGeneratedModeLaunchDeck && !bCanExecuteGeneratedModeLaunch) + { + TransitionStatusLine = + TEXT("Transition: generated-mode selectors are still incomplete. Finish the active selector choices before executing the owned generated run."); + } else if (bCanContinueCurrentRun) { const FHyperTwistTrainingCase& TransitionCase = CachedRunState.CurrentSelection.TrainingCase; const FString TransitionPrompt = TransitionCase.PromptLabel.IsEmpty() ? ActiveCaseId : TransitionCase.PromptLabel; - if (bHasNextCaseFromLastStep) + if (bGeneratedModeLaunchDeck) + { + TransitionStatusLine = FString::Printf( + TEXT("Transition: generated-mode request is staged on %s. Execute the owned generated run when ready."), + *TransitionPrompt + ); + } + else if (bHasNextCaseFromLastStep) { TransitionStatusLine = FString::Printf( TEXT("Transition: next case loaded after the last attempt. Continue the run to begin %s."), @@ -10296,11 +10365,13 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() } const FString ContinueRunLabel = bHasLiveAttemptTimer ? (bLiveInspectionPhase ? TEXT("Start Solve") : TEXT("Attempt Active")) - : (bHasNextCaseFromLastStep - ? TEXT("Continue Run") - : (bCanContinueCurrentRun - ? (CachedRunSummary.AttemptCount > 0 ? TEXT("Start Next Attempt") : TEXT("Start First Attempt")) - : TransitionLaunchLabel)); + : (bGeneratedModeLaunchDeck + ? (bCanExecuteGeneratedModeLaunch ? TEXT("Execute Generated Run") : TEXT("Resolve Selectors")) + : (bHasNextCaseFromLastStep + ? TEXT("Continue Run") + : (bCanContinueCurrentRun + ? (CachedRunSummary.AttemptCount > 0 ? TEXT("Start Next Attempt") : TEXT("Start First Attempt")) + : TransitionLaunchLabel))); const bool bHasMethodDrillRun = CachedMethodDrillRunState.IsStructurallyValid(); const bool bHasMethodDrillDiagnosticPacket = CachedMethodDrillDiagnosticPacket.IsStructurallyValid(); const bool bHasMethodDrillFollowUpTemplate = CachedMethodDrillFollowUpTemplate.IsStructurallyValid(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index ef40395..da6ab85 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -157,6 +157,25 @@ FHyperTwistTrainingImportedGeneratedModeLaunchRequest UHyperTwistTrainingRuntime return FHyperTwistTrainingImportedGeneratedModeLaunchRequest(); } +bool UHyperTwistTrainingRuntimeLibrary::TryExecuteActiveImportedGeneratedModeLaunchRequest( + UObject* WorldContextObject, + FHyperTwistTrainingRunState& OutRunState, + FString& OutBlockedReason +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->TryExecuteActiveImportedGeneratedModeLaunchRequest( + OutRunState, + OutBlockedReason + ); + } + + OutRunState = FHyperTwistTrainingRunState(); + OutBlockedReason = TEXT("Training subsystem was unavailable."); + return false; +} + FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::ApplyActiveImportedRuntimeSelectorOption( UObject* WorldContextObject, const FString& SelectorId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index f668d23..5d7e18e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -155,6 +155,258 @@ namespace HyperTwistTrainingSubsystemInternal return SelectorParts.Num() > 0 ? FString::Join(SelectorParts, TEXT(" | ")) : TEXT("no selector choices"); } + FString SanitizeGeneratedModeToken(const FString& Value) + { + FString Sanitized; + Sanitized.Reserve(Value.Len()); + + for (const TCHAR Character : Value) + { + if (FChar::IsAlnum(Character)) + { + Sanitized.AppendChar(FChar::ToLower(Character)); + } + else if (Sanitized.IsEmpty() || Sanitized[Sanitized.Len() - 1] != TEXT('-')) + { + Sanitized.AppendChar(TEXT('-')); + } + } + + while (Sanitized.StartsWith(TEXT("-"))) + { + Sanitized.RightChopInline(1, EAllowShrinking::No); + } + while (Sanitized.EndsWith(TEXT("-"))) + { + Sanitized.LeftChopInline(1, EAllowShrinking::No); + } + + return Sanitized.IsEmpty() ? TEXT("value") : Sanitized; + } + + const FHyperTwistTrainingCase* FindFirstStructurallyValidCase(const FHyperTwistTrainingDeck& Deck) + { + return Deck.Cases.FindByPredicate( + [](const FHyperTwistTrainingCase& Candidate) + { + return Candidate.IsStructurallyValid(); + } + ); + } + + EHyperTwistTrainingPromptKind ResolveGeneratedModePromptKind( + const FHyperTwistTrainingCase& SeedCase, + const FHyperTwistTrainingImportedGeneratedModeLaunchRequest& LaunchRequest + ) + { + const FString RuntimeModeId = LaunchRequest.LaunchConfig.RuntimeModeId.ToLower(); + const FString GeneratorType = LaunchRequest.LaunchConfig.GeneratorType.ToLower(); + if (RuntimeModeId.Contains(TEXT("recognition")) || GeneratorType.Contains(TEXT("recognition"))) + { + return EHyperTwistTrainingPromptKind::Recognition; + } + if (RuntimeModeId.Contains(TEXT("state")) + || GeneratorType.Contains(TEXT("state")) + || RuntimeModeId.Contains(TEXT("scramble")) + || GeneratorType.Contains(TEXT("scramble"))) + { + return EHyperTwistTrainingPromptKind::State; + } + if ((SeedCase.PromptKind == EHyperTwistTrainingPromptKind::Algorithm + || SeedCase.PromptKind == EHyperTwistTrainingPromptKind::Sequence + || SeedCase.PromptKind == EHyperTwistTrainingPromptKind::Memo) + && SeedCase.CanonicalNotation.IsEmpty()) + { + return EHyperTwistTrainingPromptKind::State; + } + + return SeedCase.PromptKind; + } + + FString BuildGeneratedModeExecutionSessionId( + const FHyperTwistTrainingImportedGeneratedModeLaunchRequest& LaunchRequest + ) + { + const FString BaseSessionId = !LaunchRequest.LaunchConfig.TrainingSessionId.IsEmpty() + ? LaunchRequest.LaunchConfig.TrainingSessionId + : TEXT("generated-mode"); + const FString ExecutionToken = FGuid::NewGuid().ToString(EGuidFormats::Digits).ToLower(); + return FString::Printf( + TEXT("%s__generated__%s__%s"), + *BaseSessionId, + *SanitizeGeneratedModeToken(LaunchRequest.RequestId), + *ExecutionToken + ); + } + + FString BuildGeneratedModeCanonicalStateRef( + const FHyperTwistTrainingImportedGeneratedModeLaunchRequest& LaunchRequest + ) + { + const FString SurfaceToken = SanitizeGeneratedModeToken(LaunchRequest.LaunchConfig.SurfaceId); + const FString RuntimeToken = SanitizeGeneratedModeToken(LaunchRequest.LaunchConfig.RuntimeModeId); + const FString RequestToken = SanitizeGeneratedModeToken(LaunchRequest.RequestId); + return FString::Printf( + TEXT("generated-mode/%s/%s/%s"), + *SurfaceToken, + *RuntimeToken, + *RequestToken + ); + } + + bool TryBuildGeneratedModeExecutionDeck( + const FHyperTwistTrainingDeck& SourceDeck, + const FHyperTwistTrainingImportedGeneratedModeLaunchRequest& LaunchRequest, + FHyperTwistTrainingDeck& OutDeck, + FString& OutBlockedReason + ) + { + OutDeck = FHyperTwistTrainingDeck(); + OutBlockedReason.Reset(); + + if (!SourceDeck.IsStructurallyValid()) + { + OutBlockedReason = TEXT("Generated-mode executor requires a structurally valid source deck."); + return false; + } + if (!LaunchRequest.IsStructurallyValid()) + { + OutBlockedReason = TEXT("Generated-mode executor requires a structurally valid launch request."); + return false; + } + + const FHyperTwistTrainingCase* SeedCase = FindFirstStructurallyValidCase(SourceDeck); + if (SeedCase == nullptr) + { + OutBlockedReason = TEXT("Generated-mode executor could not find a structurally valid source case."); + return false; + } + + const FString SelectorSummary = FormatGeneratedModeSelectorSummary(SourceDeck, LaunchRequest); + const FString PromptBaseLabel = !LaunchRequest.LaunchConfig.SurfaceLabel.IsEmpty() + ? LaunchRequest.LaunchConfig.SurfaceLabel + : SourceDeck.Title; + const FString CaseExecutionNote = FString::Printf( + TEXT("Owned generated-mode execution case produced from request %s."), + *LaunchRequest.RequestId + ); + const FString DeckExecutionNote = FString::Printf( + TEXT("Owned generated-mode execution deck produced from request %s."), + *LaunchRequest.RequestId + ); + + FHyperTwistTrainingCase GeneratedCase = *SeedCase; + GeneratedCase.CaseId = FString::Printf( + TEXT("%s/generated/%s"), + *SourceDeck.DeckId, + *SanitizeGeneratedModeToken(LaunchRequest.RequestId) + ); + GeneratedCase.PuzzleId = !LaunchRequest.LaunchConfig.PuzzleId.IsEmpty() + ? LaunchRequest.LaunchConfig.PuzzleId + : SeedCase->PuzzleId; + GeneratedCase.PromptKind = ResolveGeneratedModePromptKind(*SeedCase, LaunchRequest); + GeneratedCase.PromptLabel = !PromptBaseLabel.IsEmpty() && !SelectorSummary.IsEmpty() + ? FString::Printf( + TEXT("%s | %s"), + *PromptBaseLabel, + *SelectorSummary + ) + : PromptBaseLabel; + GeneratedCase.CanonicalStateRef = BuildGeneratedModeCanonicalStateRef(LaunchRequest); + GeneratedCase.CanonicalNotation.Reset(); + GeneratedCase.AlternateNotations.Reset(); + GeneratedCase.ScrambleNotation.Reset(); + GeneratedCase.SubsetId = SourceDeck.SubsetId; + GeneratedCase.Difficulty = SeedCase->Difficulty; + GeneratedCase.Tags = SeedCase->Tags; + GeneratedCase.Tags.AddUnique(TEXT("runtime-surface:generated-mode")); + GeneratedCase.Tags.AddUnique(TEXT("generated-mode:owned-execution")); + GeneratedCase.Tags.AddUnique( + FString::Printf( + TEXT("generated-request:%s"), + *SanitizeGeneratedModeToken(LaunchRequest.RequestId) + ) + ); + GeneratedCase.Tags.AddUnique( + FString::Printf( + TEXT("generated-surface:%s"), + *SanitizeGeneratedModeToken(LaunchRequest.LaunchConfig.SurfaceId) + ) + ); + for (const FHyperTwistTrainingImportedRuntimeSelectorChoice& Choice : LaunchRequest.LaunchConfig.SelectorChoices) + { + if (!Choice.IsStructurallyValid()) + { + continue; + } + + GeneratedCase.Tags.AddUnique( + FString::Printf( + TEXT("generated-selector:%s=%s"), + *SanitizeGeneratedModeToken(Choice.SelectorId), + *SanitizeGeneratedModeToken(Choice.SelectedValue) + ) + ); + } + GeneratedCase.AllowedDeliveryModes = SeedCase->AllowedDeliveryModes; + if (GeneratedCase.AllowedDeliveryModes.Num() == 0) + { + GeneratedCase.AllowedDeliveryModes = SourceDeck.DeliveryModes; + } + GeneratedCase.AllowedDeliveryModes.AddUnique(LaunchRequest.LaunchConfig.DeliveryMode); + GeneratedCase.TimeTargetMs = SeedCase->TimeTargetMs; + GeneratedCase.SourceAttribution = SeedCase->SourceAttribution.IsStructurallyValid() + ? SeedCase->SourceAttribution + : SourceDeck.SourceAttribution; + if (GeneratedCase.SourceAttribution.SourceCaseId.IsEmpty()) + { + GeneratedCase.SourceAttribution.SourceCaseId = SeedCase->CaseId; + } + GeneratedCase.SourceAttribution.SourceNotes = GeneratedCase.SourceAttribution.SourceNotes.IsEmpty() + ? CaseExecutionNote + : FString::Printf( + TEXT("%s | %s"), + *GeneratedCase.SourceAttribution.SourceNotes, + *CaseExecutionNote + ); + + OutDeck = SourceDeck; + OutDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Scripted; + OutDeck.Title = !SourceDeck.Title.IsEmpty() + ? SourceDeck.Title + : (!LaunchRequest.LaunchConfig.SurfaceLabel.IsEmpty() + ? LaunchRequest.LaunchConfig.SurfaceLabel + : TEXT("Generated mode deck")); + OutDeck.bSupportsAlgorithmReveal = + SourceDeck.bSupportsAlgorithmReveal && !GeneratedCase.CanonicalNotation.IsEmpty(); + OutDeck.Cases.Reset(); + OutDeck.Cases.Add(GeneratedCase); + OutDeck.Tags.AddUnique(TEXT("runtime-surface:generated-mode")); + OutDeck.Tags.AddUnique(TEXT("generated-mode:owned-execution")); + OutDeck.Tags.AddUnique( + FString::Printf( + TEXT("generated-request:%s"), + *SanitizeGeneratedModeToken(LaunchRequest.RequestId) + ) + ); + OutDeck.ImportedRuntimeSurface = SourceDeck.ImportedRuntimeSurface; + OutDeck.SourceAttribution.SourceNotes = OutDeck.SourceAttribution.SourceNotes.IsEmpty() + ? DeckExecutionNote + : FString::Printf( + TEXT("%s | %s"), + *OutDeck.SourceAttribution.SourceNotes, + *DeckExecutionNote + ); + + if (!OutDeck.IsStructurallyValid() || !OutDeck.ImportedRuntimeSurface.IsStructurallyValid()) + { + OutBlockedReason = TEXT("Generated-mode executor could not build a structurally valid owned execution deck."); + return false; + } + + return true; + } + TArray ExtractCaseIdsFromDeck(const FHyperTwistTrainingDeck& Deck) { TArray CaseIds; @@ -871,6 +1123,179 @@ FHyperTwistTrainingImportedGeneratedModeLaunchRequest UHyperTwistTrainingSubsyst return ActiveImportedGeneratedModeLaunchRequest; } +bool UHyperTwistTrainingSubsystem::TryExecuteActiveImportedGeneratedModeLaunchRequest( + FHyperTwistTrainingRunState& OutRunState, + FString& OutBlockedReason +) +{ + OutRunState = FHyperTwistTrainingRunState(); + OutBlockedReason.Reset(); + + if (!HasActiveRun() || !ActiveRunState.IsStructurallyValid()) + { + OutBlockedReason = TEXT("Generated-mode execution requires an active training run."); + return false; + } + + FHyperTwistTrainingImportedRuntimeSurface RuntimeSurface; + if (!UHyperTwistTrainingCatalogLibrary::TryGetImportedRuntimeSurfaceFromDeck( + ActiveRunState.ActiveDeck, + RuntimeSurface) + || !RuntimeSurface.IsStructurallyValid() + || !RuntimeSurface.SurfaceKind.Equals(TEXT("generated-mode"), ESearchCase::IgnoreCase)) + { + OutBlockedReason = TEXT("Active run is not a generated-mode imported runtime surface."); + return false; + } + + if (!ActiveRunState.ImportedRuntimeSelectionState.IsStructurallyValid() + || ActiveRunState.ImportedRuntimeSelectionState.SurfaceId != RuntimeSurface.SurfaceId + || !ActiveRunState.ImportedRuntimeSelectionState.SurfaceKind.Equals( + RuntimeSurface.SurfaceKind, + ESearchCase::IgnoreCase)) + { + OutBlockedReason = TEXT("Generated-mode execution requires imported selector state that matches the active runtime surface."); + return false; + } + + FHyperTwistTrainingImportedGeneratedModeLaunchRequest LaunchRequest = + ActiveImportedGeneratedModeLaunchRequest; + bool bNeedsFreshLaunchRequest = !LaunchRequest.IsStructurallyValid(); + if (!bNeedsFreshLaunchRequest + && (LaunchRequest.LaunchConfig.DeckId != ActiveRunState.ActiveDeck.DeckId + || LaunchRequest.LaunchConfig.SurfaceId != RuntimeSurface.SurfaceId + || !LaunchRequest.LaunchConfig.RuntimeModeId.Equals( + RuntimeSurface.RuntimeModeId, + ESearchCase::IgnoreCase))) + { + bNeedsFreshLaunchRequest = true; + } + + for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors) + { + const FHyperTwistTrainingImportedRuntimeSelectorChoice* ActiveChoice = + ActiveRunState.ImportedRuntimeSelectionState.SelectorChoices.FindByPredicate( + [&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == Selector.SelectorId && Candidate.IsStructurallyValid(); + } + ); + if (ActiveChoice == nullptr) + { + OutBlockedReason = FString::Printf( + TEXT("Generated-mode execution is blocked because selector %s is unresolved."), + *Selector.Label + ); + return false; + } + + const FHyperTwistTrainingImportedRuntimeSelectorChoice* RequestChoice = + LaunchRequest.LaunchConfig.SelectorChoices.FindByPredicate( + [&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == Selector.SelectorId && Candidate.IsStructurallyValid(); + } + ); + if (RequestChoice == nullptr || RequestChoice->SelectedValue != ActiveChoice->SelectedValue) + { + bNeedsFreshLaunchRequest = true; + } + } + + if (bNeedsFreshLaunchRequest) + { + LaunchRequest = CreateActiveImportedGeneratedModeLaunchRequest( + TEXT("runtime-imported-generated-mode-executor") + ); + } + if (!LaunchRequest.IsStructurallyValid()) + { + OutBlockedReason = TEXT("Generated-mode execution could not derive a structurally valid launch request."); + return false; + } + if (LaunchRequest.LaunchConfig.DeckId != ActiveRunState.ActiveDeck.DeckId + || LaunchRequest.LaunchConfig.SurfaceId != RuntimeSurface.SurfaceId + || !LaunchRequest.LaunchConfig.RuntimeModeId.Equals( + RuntimeSurface.RuntimeModeId, + ESearchCase::IgnoreCase)) + { + OutBlockedReason = TEXT("Active generated-mode launch request does not match the active runtime surface."); + return false; + } + + for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors) + { + const FHyperTwistTrainingImportedRuntimeSelectorChoice* RequestChoice = + LaunchRequest.LaunchConfig.SelectorChoices.FindByPredicate( + [&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == Selector.SelectorId && Candidate.IsStructurallyValid(); + } + ); + const FHyperTwistTrainingImportedRuntimeSelectorChoice* ActiveChoice = + ActiveRunState.ImportedRuntimeSelectionState.SelectorChoices.FindByPredicate( + [&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate) + { + return Candidate.SelectorId == Selector.SelectorId && Candidate.IsStructurallyValid(); + } + ); + if (RequestChoice == nullptr || ActiveChoice == nullptr) + { + OutBlockedReason = FString::Printf( + TEXT("Generated-mode execution is blocked because selector %s is unresolved."), + *Selector.Label + ); + return false; + } + if (RequestChoice->SelectedValue != ActiveChoice->SelectedValue) + { + OutBlockedReason = FString::Printf( + TEXT("Generated-mode execution is blocked because selector %s no longer matches the active selection state."), + *Selector.Label + ); + return false; + } + } + + const FHyperTwistTrainingRunState SourceRunState = ActiveRunState; + FHyperTwistTrainingDeck GeneratedDeck; + if (!HyperTwistTrainingSubsystemInternal::TryBuildGeneratedModeExecutionDeck( + SourceRunState.ActiveDeck, + LaunchRequest, + GeneratedDeck, + OutBlockedReason)) + { + return false; + } + + const FString ExecutionSessionId = + HyperTwistTrainingSubsystemInternal::BuildGeneratedModeExecutionSessionId(LaunchRequest); + const FHyperTwistTrainingRunState StartedRunState = StartTrainingRunFromDeck( + GeneratedDeck, + SourceRunState.Session.UserId, + ExecutionSessionId, + LaunchRequest.LaunchConfig.DeliveryMode + ); + if (!StartedRunState.IsStructurallyValid()) + { + OutBlockedReason = TEXT("Generated-mode executor could not start a structurally valid owned run."); + return false; + } + + ActiveRunState.ImportedRuntimeSelectionState = SourceRunState.ImportedRuntimeSelectionState; + ActiveImportedGeneratedModeLaunchRequest = LaunchRequest; + ActiveRunState.ImportedGeneratedModeLaunchRequest = LaunchRequest; + RefreshSummary(); + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::RecordRunState( + TrainingRepositoryState, + ActiveRunState + ); + RefreshRepositoryViews(); + + OutRunState = ActiveRunState; + return OutRunState.IsStructurallyValid(); +} + FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::ApplyActiveImportedRuntimeSelectorOption( const FString& SelectorId, const FString& OptionId diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index a2d6203..1e21f57 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -72,6 +72,13 @@ public: const FString& RequestSource ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) + static bool TryExecuteActiveImportedGeneratedModeLaunchRequest( + UObject* WorldContextObject, + FHyperTwistTrainingRunState& OutRunState, + FString& OutBlockedReason + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRunState ApplyActiveImportedRuntimeSelectorOption( UObject* WorldContextObject, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index b43eac3..3517058 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -58,6 +58,12 @@ public: const FString& RequestSource ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") + bool TryExecuteActiveImportedGeneratedModeLaunchRequest( + FHyperTwistTrainingRunState& OutRunState, + FString& OutBlockedReason + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRunState ApplyActiveImportedRuntimeSelectorOption( const FString& SelectorId,