Close cubedesk bound1 drill timer coupling

This commit is contained in:
axiomlogicnexus 2026-05-19 04:12:43 +02:00
parent 39bd04c48d
commit 366a7a8a0e
5 changed files with 437 additions and 2 deletions

View file

@ -17568,7 +17568,11 @@ bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer()
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = UHyperTwistTrainingRuntimeLibrary::GetTrainingSubsystem(this))
{
const bool bStarted = TrainingSubsystem->StartActiveLiveTimer();
bool bStarted = TrainingSubsystem->StartActiveLiveTimerForCurrentDrillCase();
if (!bStarted)
{
bStarted = TrainingSubsystem->StartActiveLiveTimer();
}
RefreshLiveAttemptClock();
UpdateDashboardPresentation();
return bStarted;
@ -17606,7 +17610,17 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitLiveTime
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = UHyperTwistTrainingRuntimeLibrary::GetTrainingSubsystem(this))
{
const FHyperTwistTrainingRunStepResult StepResult = TrainingSubsystem->SubmitActiveLiveTimedAttempt(Result);
FHyperTwistTrainingRunStepResult StepResult;
const FHyperTwistTrainingDrillTimerCompletionResult CompletionResult =
TrainingSubsystem->SubmitAndAdvanceActiveDrillTimerAttempt(Result);
if (CompletionResult.bSubmitSucceeded)
{
StepResult = CompletionResult.SubmitStepResult;
}
else
{
StepResult = TrainingSubsystem->SubmitActiveLiveTimedAttempt(Result);
}
LastStepResult = StepResult;
RefreshTrainingState();
RefreshLiveAttemptClock();

View file

@ -5067,6 +5067,21 @@ bool UHyperTwistTrainingSubsystem::StartActiveLiveTimer()
return HasActiveLiveTimer();
}
bool UHyperTwistTrainingSubsystem::StartActiveLiveTimerForCurrentDrillCase()
{
if (!HasActiveMethodDrillRun()
|| !HasActiveRun()
|| !ActiveMethodDrillRunState.CurrentCase.IsStructurallyValid()
|| !ActiveRunState.Session.IsStructurallyValid()
|| !ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid()
|| ActiveMethodDrillRunState.CurrentCase.CaseId != ActiveRunState.CurrentSelection.TrainingCase.CaseId)
{
return false;
}
return StartActiveLiveTimer();
}
bool UHyperTwistTrainingSubsystem::AdvanceActiveLiveTimerToSolvePhase()
{
RefreshActiveLiveTimerState();
@ -5267,6 +5282,33 @@ FHyperTwistTrainingRunStepResult UHyperTwistTrainingSubsystem::SubmitActiveLiveT
return StepResult;
}
FHyperTwistTrainingDrillTimerCompletionResult UHyperTwistTrainingSubsystem::SubmitAndAdvanceActiveDrillTimerAttempt(
const EHyperTwistTrainingAttemptResult Result
)
{
FHyperTwistTrainingDrillTimerCompletionResult CompletionResult;
if (!HasActiveMethodDrillRun()
|| !HasActiveRun()
|| !ActiveMethodDrillRunState.CurrentCase.IsStructurallyValid()
|| !ActiveRunState.Session.IsStructurallyValid()
|| !ActiveRunState.CurrentSelection.TrainingCase.IsStructurallyValid()
|| ActiveMethodDrillRunState.CurrentCase.CaseId != ActiveRunState.CurrentSelection.TrainingCase.CaseId)
{
return CompletionResult;
}
CompletionResult.SubmitStepResult = SubmitActiveLiveTimedAttempt(Result);
CompletionResult.bSubmitSucceeded = CompletionResult.SubmitStepResult.IsStructurallyValid();
if (CompletionResult.bSubmitSucceeded)
{
CompletionResult.UpdatedDrillRunState = AdvanceActiveMethodDrillCase();
CompletionResult.bAdvanceSucceeded = CompletionResult.UpdatedDrillRunState.IsStructurallyValid();
OnDrillTimerCompleted.Broadcast(CompletionResult);
}
return CompletionResult;
}
FHyperTwistTrainingSession UHyperTwistTrainingSubsystem::CompleteActiveRun(bool bAbortSession)
{
if (!HasActiveRun())

View file

@ -9,12 +9,21 @@
#include "HyperTwistTraining/HyperTwistTrainingTypes.h"
#include "HyperTwistTrainingSubsystem.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FOnHyperTwistTrainingDrillTimerCompleted,
const FHyperTwistTrainingDrillTimerCompletionResult&,
CompletionResult
);
UCLASS(Config = Game, DefaultConfig)
class UNREALHYPERTWIST_API UHyperTwistTrainingSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Training|Drill")
FOnHyperTwistTrainingDrillTimerCompleted OnDrillTimerCompleted;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
bool HasActiveRun() const;
@ -554,6 +563,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
bool StartActiveLiveTimer();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
bool StartActiveLiveTimerForCurrentDrillCase();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
bool AdvanceActiveLiveTimerToSolvePhase();
@ -575,6 +587,11 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
FHyperTwistTrainingRunStepResult SubmitActiveLiveTimedAttempt(EHyperTwistTrainingAttemptResult Result);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
FHyperTwistTrainingDrillTimerCompletionResult SubmitAndAdvanceActiveDrillTimerAttempt(
EHyperTwistTrainingAttemptResult Result
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
FHyperTwistTrainingSession CompleteActiveRun(bool bAbortSession);

View file

@ -3862,6 +3862,29 @@ struct FHyperTwistTrainingMethodDrillRunState
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingDrillTimerCompletionResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bSubmitSucceeded = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bAdvanceSucceeded = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingRunStepResult SubmitStepResult;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingMethodDrillRunState UpdatedDrillRunState;
bool IsStructurallyValid() const
{
return bSubmitSucceeded && SubmitStepResult.IsStructurallyValid();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingMethodDrillSummary
{

View file

@ -0,0 +1,339 @@
// Copyright HyperTwist, Inc. All Rights Reserved.
#include "Misc/AutomationTest.h"
#include "Engine/GameInstance.h"
#include "HyperTwistTraining/HyperTwistTrainingLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "UObject/Package.h"
#include "UObject/UnrealType.h"
#if WITH_AUTOMATION_TESTS
namespace HyperTwistCubeDeskBound1TrainingTimerTestInternal
{
template <typename PropertyType>
PropertyType* FindTypedProperty(UObject* Object, const FName PropertyName)
{
return Object != nullptr
? CastField<PropertyType>(Object->GetClass()->FindPropertyByName(PropertyName))
: nullptr;
}
template <typename StructType>
bool SetStructProperty(UObject* Object, const FName PropertyName, const StructType& Value)
{
if (FStructProperty* Property = FindTypedProperty<FStructProperty>(Object, PropertyName))
{
if (Property->Struct == StructType::StaticStruct())
{
*Property->ContainerPtrToValuePtr<StructType>(Object) = Value;
return true;
}
}
return false;
}
bool SetBoolProperty(UObject* Object, const FName PropertyName, const bool bValue)
{
if (FBoolProperty* Property = FindTypedProperty<FBoolProperty>(Object, PropertyName))
{
Property->SetPropertyValue_InContainer(Object, bValue);
return true;
}
return false;
}
FHyperTwistTrainingDeck FindSampleMethodDrillDeck()
{
const FHyperTwistContentPack SamplePack = UHyperTwistTrainingLibrary::MakeSampleClassicContentPack();
for (const FHyperTwistTrainingTrack& Track : SamplePack.Tracks)
{
for (const FHyperTwistTrainingDeck& Deck : Track.Decks)
{
if (!Deck.IsStructurallyValid())
{
continue;
}
const FHyperTwistTrainingMethodDrillDefinition DrillDefinition =
UHyperTwistTrainingLibrary::BuildMethodDrillDefinitionFromDeck(
Deck,
EHyperTwistTrainingDrillModeKind::Recognition,
EHyperTwistTrainingDrillVisibilityPolicy::PromptOnly,
true,
EHyperTwistTrainingDrillEntrySource::Catalog
);
if (DrillDefinition.IsStructurallyValid())
{
return Deck;
}
}
}
return FHyperTwistTrainingDeck();
}
FHyperTwistTrainingCaseSelection MakeSingleCaseSelection(
const FHyperTwistTrainingDeck& SourceDeck,
const FHyperTwistTrainingCase& TrainingCase,
const TArray<FHyperTwistProgressionMemory>& Memories,
const EHyperTwistTrainingDeliveryMode Mode
)
{
FHyperTwistTrainingDeck SingleCaseDeck = SourceDeck;
SingleCaseDeck.Cases.Reset();
SingleCaseDeck.Cases.Add(TrainingCase);
return UHyperTwistTrainingLibrary::SelectNextTrainingCase(
SingleCaseDeck,
Memories,
Mode,
FString()
);
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistCubeDeskBound1DrillTimerCouplingTest,
"HyperTwist.CleanRoom.CubeDesk.Bound1.DrillTimerCoupling",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistCubeDeskBound1DrillTimerCouplingTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>(GetTransientPackage());
TestNotNull(TEXT("Game instance outer must be created for subsystem construction."), GameInstance);
if (GameInstance == nullptr)
{
return false;
}
UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject<UHyperTwistTrainingSubsystem>(GameInstance);
TestNotNull(TEXT("Training subsystem instance must be created."), TrainingSubsystem);
if (TrainingSubsystem == nullptr)
{
return false;
}
FHyperTwistTrainingDeck Deck = HyperTwistCubeDeskBound1TrainingTimerTestInternal::FindSampleMethodDrillDeck();
TestTrue(TEXT("A structurally valid sample drill deck must be available."), Deck.IsStructurallyValid());
if (!Deck.IsStructurallyValid())
{
return false;
}
Deck.TimingPolicy.bInspectionEnabled = false;
Deck.TimingPolicy.InspectionDurationMs = 0;
FHyperTwistTrainingRunState RunState = UHyperTwistTrainingLibrary::StartTrainingRun(
Deck,
TEXT("bound1-user"),
TEXT("bound1-session"),
EHyperTwistTrainingDeliveryMode::Timer
);
TestTrue(TEXT("Sample deck must start a structurally valid timer run."), RunState.IsStructurallyValid());
if (!RunState.IsStructurallyValid())
{
return false;
}
const FHyperTwistTrainingMethodDrillDefinition DrillDefinition =
UHyperTwistTrainingLibrary::BuildMethodDrillDefinitionFromDeck(
Deck,
EHyperTwistTrainingDrillModeKind::Recognition,
EHyperTwistTrainingDrillVisibilityPolicy::PromptOnly,
true,
EHyperTwistTrainingDrillEntrySource::Catalog
);
TestTrue(TEXT("Sample deck must build a structurally valid drill definition."), DrillDefinition.IsStructurallyValid());
if (!DrillDefinition.IsStructurallyValid())
{
return false;
}
FHyperTwistTrainingMethodDrillRunState DrillRunState = UHyperTwistTrainingLibrary::StartMethodDrillRun(
DrillDefinition,
RunState.Session.UserId,
TEXT("bound1-drill-run")
);
TestTrue(TEXT("Drill run must initialize from the drill definition."), DrillRunState.IsStructurallyValid());
DrillRunState = UHyperTwistTrainingLibrary::StartNextMethodDrillCase(DrillRunState);
TestTrue(TEXT("Drill run must advance into an active current case."), DrillRunState.CurrentCase.IsStructurallyValid());
if (!DrillRunState.CurrentCase.IsStructurallyValid())
{
return false;
}
const FHyperTwistTrainingCaseSelection MatchingSelection =
HyperTwistCubeDeskBound1TrainingTimerTestInternal::MakeSingleCaseSelection(
Deck,
DrillRunState.CurrentCase,
RunState.Memories,
EHyperTwistTrainingDeliveryMode::Timer
);
TestTrue(TEXT("Timer run must be able to select the drill current case."), MatchingSelection.IsStructurallyValid());
if (!MatchingSelection.IsStructurallyValid())
{
return false;
}
RunState.CurrentSelection = MatchingSelection;
RunState.Session.CurrentCaseId = MatchingSelection.TrainingCase.CaseId;
TestTrue(
TEXT("Test setup must write the active run state into the subsystem."),
HyperTwistCubeDeskBound1TrainingTimerTestInternal::SetStructProperty(
TrainingSubsystem,
TEXT("ActiveRunState"),
RunState
)
);
TestTrue(
TEXT("Test setup must mark the subsystem run state as active."),
HyperTwistCubeDeskBound1TrainingTimerTestInternal::SetBoolProperty(
TrainingSubsystem,
TEXT("bHasActiveRun"),
true
)
);
TestTrue(
TEXT("Test setup must write the active drill run state into the subsystem."),
HyperTwistCubeDeskBound1TrainingTimerTestInternal::SetStructProperty(
TrainingSubsystem,
TEXT("ActiveMethodDrillRunState"),
DrillRunState
)
);
TestTrue(
TEXT("Test setup must mark the subsystem drill run as active."),
HyperTwistCubeDeskBound1TrainingTimerTestInternal::SetBoolProperty(
TrainingSubsystem,
TEXT("bHasActiveMethodDrillRun"),
true
)
);
TestTrue(
TEXT("Drill-aware timer start must succeed when the active drill case matches the timer case."),
TrainingSubsystem->StartActiveLiveTimerForCurrentDrillCase()
);
const FHyperTwistTrainingLiveTimerState StartedTimerState = TrainingSubsystem->GetActiveLiveTimerState();
TestTrue(TEXT("Timer state must be active after a successful drill-aware start."), StartedTimerState.IsStructurallyValid());
TestEqual(
TEXT("Drill-aware timer start must bind the timer to the drill case."),
StartedTimerState.CaseId,
DrillRunState.CurrentCase.CaseId
);
TestEqual(
TEXT("Inspection-disabled drill-aware starts must enter the solving phase directly."),
StartedTimerState.Phase,
EHyperTwistTrainingLiveTimerPhase::Solving
);
TestNotNull(
TEXT("Subsystem class must expose the drill timer completion delegate surface."),
TrainingSubsystem->GetClass()->FindPropertyByName(TEXT("OnDrillTimerCompleted"))
);
const FHyperTwistTrainingDrillTimerCompletionResult CompletionResult =
TrainingSubsystem->SubmitAndAdvanceActiveDrillTimerAttempt(EHyperTwistTrainingAttemptResult::Success);
TestTrue(TEXT("Drill-aware timer completion must submit the active timed attempt."), CompletionResult.bSubmitSucceeded);
TestTrue(TEXT("Drill-aware timer completion must return a structurally valid submit step."), CompletionResult.SubmitStepResult.IsStructurallyValid());
TestTrue(TEXT("Drill-aware timer completion must advance the drill after submit."), CompletionResult.bAdvanceSucceeded);
TestTrue(TEXT("Drill-aware timer completion result must stay structurally valid."), CompletionResult.IsStructurallyValid());
TestTrue(
TEXT("Drill advance must retain at least one completed case after submit."),
CompletionResult.UpdatedDrillRunState.CompletedCaseIds.Num() > 0
);
TestEqual(
TEXT("Advancing the drill must record the completed case id."),
CompletionResult.UpdatedDrillRunState.CompletedCaseIds[0],
DrillRunState.CurrentCase.CaseId
);
const FHyperTwistTrainingLiveTimerState CompletedTimerState = TrainingSubsystem->GetActiveLiveTimerState();
TestFalse(TEXT("Successful drill timer completion must clear the active live timer flag."), TrainingSubsystem->HasActiveLiveTimer());
TestEqual(
TEXT("Successful drill timer completion must reset the timer phase to idle."),
CompletedTimerState.Phase,
EHyperTwistTrainingLiveTimerPhase::Idle
);
const FHyperTwistTrainingMethodDrillRunState UpdatedSubsystemDrillState = TrainingSubsystem->GetActiveMethodDrillRunState();
TestEqual(
TEXT("Subsystem drill state must advance to the same completed case list as the returned result."),
UpdatedSubsystemDrillState.CompletedCaseIds.Num(),
CompletionResult.UpdatedDrillRunState.CompletedCaseIds.Num()
);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistCubeDeskBound1DashboardFallbackTest,
"HyperTwist.CleanRoom.CubeDesk.Bound1.DashboardFallback",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistCubeDeskBound1DashboardFallbackTest::RunTest(const FString& Parameters)
{
const FString DashboardSourcePath = FPaths::Combine(
FPaths::ProjectDir(),
TEXT("Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp")
);
const FString SubsystemSourcePath = FPaths::Combine(
FPaths::ProjectDir(),
TEXT("Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp")
);
FString DashboardSource;
TestTrue(
TEXT("Dashboard source must be readable for fallback wiring inspection."),
FFileHelper::LoadFileToString(DashboardSource, *DashboardSourcePath)
);
if (DashboardSource.IsEmpty())
{
return false;
}
FString SubsystemSource;
TestTrue(
TEXT("Subsystem source must be readable for drill completion broadcast inspection."),
FFileHelper::LoadFileToString(SubsystemSource, *SubsystemSourcePath)
);
if (SubsystemSource.IsEmpty())
{
return false;
}
TestTrue(
TEXT("Dashboard timer start must prefer the drill-aware timer entry point."),
DashboardSource.Contains(TEXT("StartActiveLiveTimerForCurrentDrillCase"))
);
TestTrue(
TEXT("Dashboard timer start must retain the generic timer fallback path."),
DashboardSource.Contains(TEXT("bStarted = TrainingSubsystem->StartActiveLiveTimer();"))
);
TestTrue(
TEXT("Dashboard timed submit must prefer the drill-aware submit-and-advance path."),
DashboardSource.Contains(TEXT("SubmitAndAdvanceActiveDrillTimerAttempt(Result)"))
);
TestTrue(
TEXT("Dashboard timed submit must retain the generic timed-attempt fallback path."),
DashboardSource.Contains(TEXT("StepResult = TrainingSubsystem->SubmitActiveLiveTimedAttempt(Result);"))
);
TestTrue(
TEXT("Subsystem drill completion must broadcast the drill timer completion result."),
SubsystemSource.Contains(TEXT("OnDrillTimerCompleted.Broadcast(CompletionResult);"))
);
return true;
}
#endif // WITH_AUTOMATION_TESTS