diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..b4eadc7 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,10 @@ +# HyperTwist AI Instructions + +- Treat `UnrealHyperTwist/` as the live project root and `UnrealHyperTwist/Source/UnrealHyperTwist/` as the primary owned code surface. +- Prefer Visual Studio MCP semantic tools when available: + - use solution/project inspection instead of manual tree walks + - use symbol/navigation tools instead of grep for C# and Unreal code + - use document open/read/write tools for files already inside the loaded solution +- Only `UnrealHyperTwist/Plugins/UnrealMCPChong/` is a maintained in-repo plugin fork. Leave the other plugin folders alone unless the task explicitly targets them. +- Do not treat `mirrors/`, `zippedreposource/`, or `docs/refs/` as default edit surfaces. +- If Rust code appears inside dependencies or mirrors, use the nearest `Cargo.toml`; do not invent Rust project markers at repo root. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..86c2a55 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Repository Guidelines + +## Project Structure & Module Organization +`UnrealHyperTwist/` is the live application root. Treat `UnrealHyperTwist/Source/UnrealHyperTwist/` as the primary first-party code surface and `UnrealHyperTwist/Config/` plus `Content/` as the active project shell. `UnrealHyperTwist/Plugins/UnrealMCPChong/` is the only maintained in-repo plugin fork; the other plugin folders are external copies and should stay untouched unless a plugin task explicitly names them. `docs/` contains working documentation, while `docs/refs/` is reference-only and may contain sensitive ops material. `mirrors/`, `zippedreposource/`, and the archived content folders at repo root are custody/reference storage, not the default edit target. + +## Build, Test, and Development Commands +Open `UnrealHyperTwist/UnrealHyperTwist.uproject` or `UnrealHyperTwist/UnrealHyperTwist.sln` in Visual Studio 2022 / Unreal Engine 5.7 for normal work. Use `git -C C:\HyperTwist status --short` before and after changes. The bootstrap CI check is defined in `.woodpecker/hypertwist-bootstrap.yml` and validates the presence of the Unreal project, source module root, and docs tree. + +## Agent Instructions +When Visual Studio MCP tools are available, prefer semantic solution/project/document tools over raw filesystem grep for C# or Unreal symbol work. Default the working scope to `UnrealHyperTwist/` and tracked repo docs before exploring mirrors or archived source material. Do not modify `mirrors/`, `zippedreposource/`, or `docs/refs/` unless the task is explicitly about provenance, intake, or operations. If you encounter Rust content under mirrored dependencies, anchor analysis on the nearest `Cargo.toml`; do not fabricate Rust workspace files for this repo. diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index ca51908..080ef42 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -1,5 +1,6 @@ #include "HyperTwistBootstrap/HyperTwistContractLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingLibrary.h" #include "JsonObjectConverter.h" namespace HyperTwistContractLibraryInternal @@ -294,6 +295,51 @@ FHyperTwistVisionServiceHealth UHyperTwistContractLibrary::MakeMockVisionService return Health; } +FHyperTwistTrainingRunState UHyperTwistContractLibrary::MakeSampleTrainingRunState() +{ + const FHyperTwistContentPack ContentPack = UHyperTwistTrainingLibrary::MakeSampleClassicContentPack(); + if (!ContentPack.IsStructurallyValid() || ContentPack.Tracks.Num() == 0 || ContentPack.Tracks[0].Decks.Num() == 0) + { + return FHyperTwistTrainingRunState(); + } + + return UHyperTwistTrainingLibrary::StartTrainingRun( + ContentPack.Tracks[0].Decks[0], + TEXT("local-user"), + TEXT("training_session_01"), + EHyperTwistTrainingDeliveryMode::Timer + ); +} + +FHyperTwistTrainingRunStepResult UHyperTwistContractLibrary::MakeSampleTrainingRunStepResult() +{ + const FHyperTwistTrainingRunState RunState = MakeSampleTrainingRunState(); + if (!RunState.IsStructurallyValid()) + { + return FHyperTwistTrainingRunStepResult(); + } + + FHyperTwistTrainingAttempt Attempt; + Attempt.AttemptId = TEXT("attempt_01"); + Attempt.TrainingSessionId = RunState.Session.TrainingSessionId; + Attempt.CaseId = RunState.Session.CurrentCaseId; + Attempt.Result = EHyperTwistTrainingAttemptResult::Success; + Attempt.TotalTimeMs = 2214; + Attempt.ExecutionTimeMs = 2214; + Attempt.RecognitionTimeMs = 0; + Attempt.Mistakes = 0; + Attempt.ReplayId = RunState.ReplayPacket.ReplayId; + Attempt.CompletedAtUtc = TEXT("2026-04-25T12:02:14Z"); + + return UHyperTwistTrainingLibrary::ResolveTrainingRunAttempt(RunState, Attempt, Attempt.TotalTimeMs); +} + +FHyperTwistTrainingSessionSummary UHyperTwistContractLibrary::MakeSampleTrainingRunSummary() +{ + const FHyperTwistTrainingRunStepResult StepResult = MakeSampleTrainingRunStepResult(); + return StepResult.SessionSummary; +} + bool UHyperTwistContractLibrary::IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State) { return State.IsStructurallyValid(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp index 2323f25..3eeca40 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp @@ -5,6 +5,19 @@ namespace HyperTwistTrainingLibraryInternal { + EHyperTwistCaptureMode ToCaptureMode(EHyperTwistTrainingDeliveryMode Mode) + { + switch (Mode) + { + case EHyperTwistTrainingDeliveryMode::RecognitionAssisted: + return EHyperTwistCaptureMode::Recognition; + case EHyperTwistTrainingDeliveryMode::SmartDevice: + return EHyperTwistCaptureMode::SmartDevice; + default: + return EHyperTwistCaptureMode::Runtime; + } + } + bool IsModeAllowed( const FHyperTwistTrainingCase& TrainingCase, EHyperTwistTrainingDeliveryMode Mode @@ -29,6 +42,65 @@ namespace HyperTwistTrainingLibraryInternal return nullptr; } + const FHyperTwistTrainingCase* FindCaseById( + const FHyperTwistTrainingDeck& Deck, + const FString& CaseId + ) + { + for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases) + { + if (TrainingCase.CaseId == CaseId) + { + return &TrainingCase; + } + } + + return nullptr; + } + + bool ContainsCaseId(const TArray& CaseIds, const FString& CaseId) + { + return CaseIds.ContainsByPredicate([&CaseId](const FString& Value) + { + return Value == CaseId; + }); + } + + const FHyperTwistTrainingCase* FindFirstCompatibleCase( + const FHyperTwistTrainingDeck& Deck, + EHyperTwistTrainingDeliveryMode Mode + ) + { + for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases) + { + if (TrainingCase.IsStructurallyValid() && IsModeAllowed(TrainingCase, Mode)) + { + return &TrainingCase; + } + } + + return nullptr; + } + + FHyperTwistTrainingDeck FilterDeckToCaseIds( + const FHyperTwistTrainingDeck& Deck, + const TArray& AllowedCaseIds + ) + { + FHyperTwistTrainingDeck FilteredDeck = Deck; + FilteredDeck.Cases.Reset(); + + for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases) + { + if (ContainsCaseId(AllowedCaseIds, TrainingCase.CaseId)) + { + FilteredDeck.Cases.Add(TrainingCase); + } + } + + return FilteredDeck; + } + float GetBaseDifficultyWeight(const FHyperTwistTrainingCase& TrainingCase) { return TrainingCase.Difficulty > 0.0f ? TrainingCase.Difficulty : 1.0f; @@ -39,6 +111,75 @@ namespace HyperTwistTrainingLibraryInternal return FMath::Max(1.0f, GetBaseDifficultyWeight(TrainingCase) * 10.0f); } + FHyperTwistPuzzleDefinitionRef MakeDefinitionForCase(const FHyperTwistTrainingCase& TrainingCase) + { + FHyperTwistPuzzleDefinitionRef Definition; + Definition.PuzzleId = TrainingCase.PuzzleId; + Definition.DefinitionVersion = TEXT("2026.04"); + + if (TrainingCase.PuzzleId.StartsWith(TEXT("hypercube/"))) + { + Definition.PuzzleFamily = EHyperTwistPuzzleFamily::Hypercube; + Definition.NotationProfile = TEXT("hyper-4d"); + Definition.SizeVector = {2, 2, 2, 2}; + Definition.Dimension = 4; + return Definition; + } + + Definition.PuzzleFamily = EHyperTwistPuzzleFamily::ClassicCube; + Definition.NotationProfile = TEXT("classic-wca"); + Definition.SizeVector = {3, 3, 3}; + Definition.Dimension = 3; + + const FString Prefix = TEXT("cube/"); + if (TrainingCase.PuzzleId.StartsWith(Prefix)) + { + const FString SizePart = TrainingCase.PuzzleId.RightChop(Prefix.Len()); + TArray Tokens; + SizePart.ParseIntoArray(Tokens, TEXT("x"), true); + TArray ParsedSizes; + for (const FString& Token : Tokens) + { + const int32 ParsedValue = FCString::Atoi(*Token); + if (ParsedValue > 0) + { + ParsedSizes.Add(ParsedValue); + } + } + + if (ParsedSizes.Num() > 0) + { + Definition.SizeVector = ParsedSizes; + Definition.Dimension = ParsedSizes.Num(); + } + } + + return Definition; + } + + FHyperTwistReplayPacket MakeReplayPacketForRun( + const FHyperTwistTrainingSession& Session, + const FHyperTwistTrainingCase& TrainingCase + ) + { + FHyperTwistReplayPacket Packet; + Packet.ReplayId = FString::Printf(TEXT("replay_%s"), *Session.TrainingSessionId); + Packet.PuzzleDefinition = MakeDefinitionForCase(TrainingCase); + Packet.CaptureMode = ToCaptureMode(Session.Mode); + Packet.SessionId = Session.TrainingSessionId; + Packet.StartedAtUtc = Session.StartedAtUtc; + Packet.EndedAtUtc = Session.EndedAtUtc; + + FHyperTwistReplayEvent TimerStartEvent; + TimerStartEvent.EventId = TEXT("evt_timer_start"); + TimerStartEvent.Sequence = 1; + TimerStartEvent.TimeMs = 0; + TimerStartEvent.EventType = EHyperTwistReplayEventType::TimerStart; + + Packet = UHyperTwistReplayLibrary::AppendReplayEvent(Packet, TimerStartEvent); + return Packet; + } + float ComputeSelectionScore( const FHyperTwistTrainingCase& TrainingCase, const FHyperTwistProgressionMemory* Memory, @@ -193,6 +334,66 @@ FHyperTwistTrainingSession UHyperTwistTrainingLibrary::StartTrainingSession( return UpdatedSession; } +FHyperTwistTrainingRunState UHyperTwistTrainingLibrary::StartTrainingRun( + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& SessionId, + EHyperTwistTrainingDeliveryMode Mode +) +{ + FHyperTwistTrainingRunState RunState; + if (!Deck.IsStructurallyValid()) + { + return RunState; + } + + RunState.ActiveDeck = Deck; + RunState.Session = StartTrainingSession(Deck, UserId, SessionId, Mode); + if (!RunState.Session.IsStructurallyValid()) + { + return RunState; + } + + for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases) + { + if (!TrainingCase.IsStructurallyValid()) + { + continue; + } + + RunState.RemainingCaseIds.Add(TrainingCase.CaseId); + RunState.Memories.Add(MakeInitialProgressionMemory(RunState.Session, TrainingCase)); + } + + const FHyperTwistTrainingCase* InitialCase = HyperTwistTrainingLibraryInternal::FindFirstCompatibleCase(Deck, Mode); + if (InitialCase != nullptr) + { + FHyperTwistTrainingDeck InitialDeck = Deck; + InitialDeck.Cases.Reset(); + InitialDeck.Cases.Add(*InitialCase); + RunState.CurrentSelection = SelectNextTrainingCase(InitialDeck, RunState.Memories, Mode, FString()); + } + + if (RunState.CurrentSelection.IsStructurallyValid()) + { + RunState.Session = AdvanceTrainingSession( + RunState.Session, + EHyperTwistTrainingSessionState::Active, + RunState.CurrentSelection.TrainingCase.CaseId + ); + RunState.ReplayPacket = HyperTwistTrainingLibraryInternal::MakeReplayPacketForRun( + RunState.Session, + RunState.CurrentSelection.TrainingCase + ); + if (RunState.ReplayPacket.Events.Num() > 0) + { + RunState.LastReplayEvent = RunState.ReplayPacket.Events.Last(); + } + } + + return RunState; +} + bool UHyperTwistTrainingLibrary::CanTransitionTrainingSession( EHyperTwistTrainingSessionState FromState, EHyperTwistTrainingSessionState ToState @@ -654,3 +855,147 @@ FHyperTwistTrainingAttemptResolution UHyperTwistTrainingLibrary::ResolveTraining Resolution.NextCaseSelection = AdvanceResult.NextCaseSelection; return Resolution; } + +FHyperTwistTrainingRunStepResult UHyperTwistTrainingLibrary::ResolveTrainingRunAttempt( + const FHyperTwistTrainingRunState& RunState, + const FHyperTwistTrainingAttempt& Attempt, + int32 TimeMs +) +{ + FHyperTwistTrainingRunStepResult StepResult; + StepResult.UpdatedRunState = RunState; + + if (!RunState.IsStructurallyValid() || !Attempt.IsStructurallyValid()) + { + return StepResult; + } + + const FHyperTwistTrainingCase* TrainingCase = HyperTwistTrainingLibraryInternal::FindCaseById( + RunState.ActiveDeck, + Attempt.CaseId + ); + if (TrainingCase == nullptr || !TrainingCase->IsStructurallyValid()) + { + return StepResult; + } + + FHyperTwistTrainingAttempt NormalizedAttempt = Attempt; + if (NormalizedAttempt.ReplayId.IsEmpty() && !RunState.ReplayPacket.ReplayId.IsEmpty()) + { + NormalizedAttempt.ReplayId = RunState.ReplayPacket.ReplayId; + } + + StepResult.AttemptResolution = ResolveTrainingAttempt( + RunState.Session, + RunState.ActiveDeck, + *TrainingCase, + RunState.Memories, + NormalizedAttempt, + TimeMs + ); + if (!StepResult.AttemptResolution.IsStructurallyValid()) + { + return StepResult; + } + + StepResult.UpdatedRunState.Session = StepResult.AttemptResolution.UpdatedSession; + StepResult.UpdatedRunState.CurrentSelection = StepResult.AttemptResolution.NextCaseSelection; + StepResult.UpdatedRunState.Attempts.Add(NormalizedAttempt); + StepResult.UpdatedRunState.ReplayPacket = AppendTrainingAttemptToReplay( + RunState.ReplayPacket, + RunState.Session, + NormalizedAttempt, + TimeMs + ); + if (StepResult.UpdatedRunState.ReplayPacket.Events.Num() > 0) + { + StepResult.UpdatedRunState.LastReplayEvent = StepResult.UpdatedRunState.ReplayPacket.Events.Last(); + } + else + { + StepResult.UpdatedRunState.LastReplayEvent = StepResult.AttemptResolution.ReplayEvent; + } + + bool bUpdatedExistingMemory = false; + for (FHyperTwistProgressionMemory& Memory : StepResult.UpdatedRunState.Memories) + { + if (Memory.CaseId == StepResult.AttemptResolution.UpdatedProgressionMemory.CaseId) + { + Memory = StepResult.AttemptResolution.UpdatedProgressionMemory; + bUpdatedExistingMemory = true; + break; + } + } + + if (!bUpdatedExistingMemory) + { + StepResult.UpdatedRunState.Memories.Add(StepResult.AttemptResolution.UpdatedProgressionMemory); + } + + StepResult.UpdatedRunState.RemainingCaseIds = RunState.RemainingCaseIds; + if (NormalizedAttempt.Result == EHyperTwistTrainingAttemptResult::Success) + { + StepResult.UpdatedRunState.RemainingCaseIds.Remove(NormalizedAttempt.CaseId); + } + + if (StepResult.UpdatedRunState.RemainingCaseIds.Num() == 0) + { + StepResult.UpdatedRunState.Session = CompleteTrainingSession( + StepResult.UpdatedRunState.Session, + false + ); + StepResult.UpdatedRunState.ReplayPacket.EndedAtUtc = !NormalizedAttempt.CompletedAtUtc.IsEmpty() + ? NormalizedAttempt.CompletedAtUtc + : StepResult.UpdatedRunState.Session.EndedAtUtc; + + FHyperTwistReplayEvent SolveEndEvent; + SolveEndEvent.EventType = EHyperTwistReplayEventType::SolveEnd; + SolveEndEvent.TimeMs = FMath::Max(TimeMs, NormalizedAttempt.TotalTimeMs); + StepResult.UpdatedRunState.ReplayPacket = UHyperTwistReplayLibrary::AppendReplayEvent( + StepResult.UpdatedRunState.ReplayPacket, + SolveEndEvent + ); + if (StepResult.UpdatedRunState.ReplayPacket.Events.Num() > 0) + { + StepResult.UpdatedRunState.LastReplayEvent = StepResult.UpdatedRunState.ReplayPacket.Events.Last(); + } + StepResult.UpdatedRunState.CurrentSelection = FHyperTwistTrainingCaseSelection(); + } + else + { + const FHyperTwistTrainingDeck RemainingDeck = HyperTwistTrainingLibraryInternal::FilterDeckToCaseIds( + RunState.ActiveDeck, + StepResult.UpdatedRunState.RemainingCaseIds + ); + const FHyperTwistTrainingCaseSelection RemainingSelection = SelectNextTrainingCase( + RemainingDeck, + StepResult.UpdatedRunState.Memories, + RunState.Session.Mode, + NormalizedAttempt.CaseId + ); + + if (RemainingSelection.IsStructurallyValid()) + { + StepResult.UpdatedRunState.CurrentSelection = RemainingSelection; + StepResult.UpdatedRunState.Session.CurrentCaseId = RemainingSelection.TrainingCase.CaseId; + StepResult.UpdatedRunState.Session.SessionState = EHyperTwistTrainingSessionState::Active; + } + else + { + StepResult.UpdatedRunState.Session = CompleteTrainingSession( + StepResult.UpdatedRunState.Session, + false + ); + StepResult.UpdatedRunState.CurrentSelection = FHyperTwistTrainingCaseSelection(); + StepResult.UpdatedRunState.ReplayPacket.EndedAtUtc = !NormalizedAttempt.CompletedAtUtc.IsEmpty() + ? NormalizedAttempt.CompletedAtUtc + : StepResult.UpdatedRunState.Session.EndedAtUtc; + } + } + + StepResult.SessionSummary = DeriveTrainingSessionSummary( + StepResult.UpdatedRunState.Session, + StepResult.UpdatedRunState.Attempts + ); + return StepResult; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp new file mode 100644 index 0000000..7d7d97a --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -0,0 +1,117 @@ +#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" + +#include "HyperTwistTraining/HyperTwistTrainingLibrary.h" + +bool UHyperTwistTrainingSubsystem::HasActiveRun() const +{ + return bHasActiveRun && ActiveRunState.IsStructurallyValid(); +} + +FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::GetActiveRunState() const +{ + return ActiveRunState; +} + +FHyperTwistTrainingSessionSummary UHyperTwistTrainingSubsystem::GetActiveRunSummary() const +{ + return ActiveRunSummary; +} + +FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromDeck( + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& SessionId, + EHyperTwistTrainingDeliveryMode Mode +) +{ + ActiveRunState = UHyperTwistTrainingLibrary::StartTrainingRun(Deck, UserId, SessionId, Mode); + bHasActiveRun = ActiveRunState.IsStructurallyValid(); + RefreshSummary(); + return ActiveRunState; +} + +FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartSampleClassicTrainingRun( + const FString& UserId, + const FString& SessionId, + EHyperTwistTrainingDeliveryMode Mode +) +{ + const FHyperTwistContentPack ContentPack = UHyperTwistTrainingLibrary::MakeSampleClassicContentPack(); + if (!ContentPack.IsStructurallyValid() || ContentPack.Tracks.Num() == 0 || ContentPack.Tracks[0].Decks.Num() == 0) + { + ClearActiveRun(); + return ActiveRunState; + } + + return StartTrainingRunFromDeck(ContentPack.Tracks[0].Decks[0], UserId, SessionId, Mode); +} + +FHyperTwistTrainingRunStepResult UHyperTwistTrainingSubsystem::SubmitTrainingAttempt( + const FHyperTwistTrainingAttempt& Attempt, + int32 TimeMs +) +{ + FHyperTwistTrainingRunStepResult StepResult; + StepResult.UpdatedRunState = ActiveRunState; + + if (!HasActiveRun()) + { + return StepResult; + } + + StepResult = UHyperTwistTrainingLibrary::ResolveTrainingRunAttempt( + ActiveRunState, + Attempt, + TimeMs + ); + if (!StepResult.IsStructurallyValid()) + { + return StepResult; + } + + ActiveRunState = StepResult.UpdatedRunState; + bHasActiveRun = ActiveRunState.IsStructurallyValid(); + ActiveRunSummary = StepResult.SessionSummary; + return StepResult; +} + +FHyperTwistTrainingSession UHyperTwistTrainingSubsystem::CompleteActiveRun(bool bAbortSession) +{ + if (!HasActiveRun()) + { + return FHyperTwistTrainingSession(); + } + + ActiveRunState.Session = UHyperTwistTrainingLibrary::CompleteTrainingSession(ActiveRunState.Session, bAbortSession); + ActiveRunState.CurrentSelection = FHyperTwistTrainingCaseSelection(); + ActiveRunState.RemainingCaseIds.Reset(); + + if (ActiveRunState.ReplayPacket.ReplayId.Len() > 0) + { + ActiveRunState.ReplayPacket.EndedAtUtc = ActiveRunState.Session.EndedAtUtc; + } + + RefreshSummary(); + return ActiveRunState.Session; +} + +void UHyperTwistTrainingSubsystem::ClearActiveRun() +{ + ActiveRunState = FHyperTwistTrainingRunState(); + ActiveRunSummary = FHyperTwistTrainingSessionSummary(); + bHasActiveRun = false; +} + +void UHyperTwistTrainingSubsystem::RefreshSummary() +{ + if (!ActiveRunState.Session.IsStructurallyValid()) + { + ActiveRunSummary = FHyperTwistTrainingSessionSummary(); + return; + } + + ActiveRunSummary = UHyperTwistTrainingLibrary::DeriveTrainingSessionSummary( + ActiveRunState.Session, + ActiveRunState.Attempts + ); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h index 70c8526..012176f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h @@ -5,6 +5,7 @@ #include "HyperTwistCore/HyperTwistCoreTypes.h" #include "HyperTwistReplay/HyperTwistReplayTypes.h" #include "HyperTwistRecognition/HyperTwistRecognitionTypes.h" +#include "HyperTwistTraining/HyperTwistTrainingTypes.h" #include "HyperTwistContractLibrary.generated.h" UCLASS() @@ -52,6 +53,15 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Recognition") static FHyperTwistVisionServiceHealth MakeMockVisionServiceHealth(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + static FHyperTwistTrainingRunState MakeSampleTrainingRunState(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + static FHyperTwistTrainingRunStepResult MakeSampleTrainingRunStepResult(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + static FHyperTwistTrainingSessionSummary MakeSampleTrainingRunSummary(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Validation") static bool IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h index da2c026..98a605c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h @@ -35,6 +35,14 @@ public: EHyperTwistTrainingDeliveryMode Mode ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + static FHyperTwistTrainingRunState StartTrainingRun( + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& SessionId, + EHyperTwistTrainingDeliveryMode Mode + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") static bool CanTransitionTrainingSession( EHyperTwistTrainingSessionState FromState, @@ -120,4 +128,11 @@ public: const FHyperTwistTrainingAttempt& Attempt, int32 TimeMs ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + static FHyperTwistTrainingRunStepResult ResolveTrainingRunAttempt( + const FHyperTwistTrainingRunState& RunState, + const FHyperTwistTrainingAttempt& Attempt, + int32 TimeMs + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h new file mode 100644 index 0000000..947959f --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -0,0 +1,61 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "HyperTwistTraining/HyperTwistTrainingTypes.h" +#include "HyperTwistTrainingSubsystem.generated.h" + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistTrainingSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + bool HasActiveRun() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + FHyperTwistTrainingRunState GetActiveRunState() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + FHyperTwistTrainingSessionSummary GetActiveRunSummary() const; + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") + FHyperTwistTrainingRunState StartTrainingRunFromDeck( + const FHyperTwistTrainingDeck& Deck, + const FString& UserId, + const FString& SessionId, + EHyperTwistTrainingDeliveryMode Mode + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") + FHyperTwistTrainingRunState StartSampleClassicTrainingRun( + const FString& UserId, + const FString& SessionId, + EHyperTwistTrainingDeliveryMode Mode + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") + FHyperTwistTrainingRunStepResult SubmitTrainingAttempt( + const FHyperTwistTrainingAttempt& Attempt, + int32 TimeMs + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") + FHyperTwistTrainingSession CompleteActiveRun(bool bAbortSession); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") + void ClearActiveRun(); + +private: + void RefreshSummary(); + + UPROPERTY() + FHyperTwistTrainingRunState ActiveRunState; + + UPROPERTY() + FHyperTwistTrainingSessionSummary ActiveRunSummary; + + UPROPERTY() + bool bHasActiveRun = false; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 1a98c5a..bad9d11 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "HyperTwistCore/HyperTwistCoreTypes.h" +#include "HyperTwistReplay/HyperTwistReplayTypes.h" #include "HyperTwistTrainingTypes.generated.h" UENUM(BlueprintType) @@ -483,3 +484,60 @@ struct FHyperTwistTrainingAttemptResolution && UpdatedProgressionMemory.CaseId.Len() > 0; } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingRunState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingDeck ActiveDeck; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingSession Session; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingCaseSelection CurrentSelection; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray RemainingCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Memories; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Attempts; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistReplayPacket ReplayPacket; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistReplayEvent LastReplayEvent; + + bool IsStructurallyValid() const + { + return ActiveDeck.IsStructurallyValid() + && Session.IsStructurallyValid() + && (!ReplayPacket.ReplayId.IsEmpty() ? ReplayPacket.IsStructurallyValid() : true); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingRunStepResult +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingRunState UpdatedRunState; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingAttemptResolution AttemptResolution; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingSessionSummary SessionSummary; + + bool IsStructurallyValid() const + { + return UpdatedRunState.IsStructurallyValid(); + } +};