Add HyperTwist AI bootstrap guidance

This commit is contained in:
axiomlogicnexus 2026-04-25 16:10:21 +02:00
parent 262f193e05
commit 3aeaf16f8b
9 changed files with 672 additions and 0 deletions

10
.github/copilot-instructions.md vendored Normal file
View file

@ -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.

10
AGENTS.md Normal file
View file

@ -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.

View file

@ -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();

View file

@ -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<FString>& 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<FString>& 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<FString> Tokens;
SizePart.ParseIntoArray(Tokens, TEXT("x"), true);
TArray<int32> 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;
}

View file

@ -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
);
}

View file

@ -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);

View file

@ -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
);
};

View file

@ -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;
};

View file

@ -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<FString> RemainingCaseIds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistProgressionMemory> Memories;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingAttempt> 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();
}
};