Harden classic cube validation and package reporting
This commit is contained in:
parent
10b66a37c3
commit
56cd8e7802
14 changed files with 649 additions and 53 deletions
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
#include "HAL/FileManager.h"
|
||||
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
|
||||
#include "HyperTwistReplay/HyperTwistReplayLibrary.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
namespace HyperTwistReplayPersistenceLibraryInternal
|
||||
{
|
||||
const TCHAR* ReplayPacketVersion = TEXT("ht-replay/v1");
|
||||
|
||||
FString SanitizePathToken(const FString& Value)
|
||||
{
|
||||
FString Sanitized = Value;
|
||||
|
|
@ -33,6 +36,59 @@ namespace HyperTwistReplayPersistenceLibraryInternal
|
|||
{
|
||||
return FPaths::ConvertRelativePathToFull(Path);
|
||||
}
|
||||
|
||||
FString GetEventTypeToken(const EHyperTwistReplayEventType EventType)
|
||||
{
|
||||
switch (EventType)
|
||||
{
|
||||
case EHyperTwistReplayEventType::InspectionStart:
|
||||
return TEXT("inspection_start");
|
||||
case EHyperTwistReplayEventType::InspectionEnd:
|
||||
return TEXT("inspection_end");
|
||||
case EHyperTwistReplayEventType::TimerStart:
|
||||
return TEXT("timer_start");
|
||||
case EHyperTwistReplayEventType::Move:
|
||||
return TEXT("move");
|
||||
case EHyperTwistReplayEventType::Rotation:
|
||||
return TEXT("rotation");
|
||||
case EHyperTwistReplayEventType::StateSnapshot:
|
||||
return TEXT("state_snapshot");
|
||||
case EHyperTwistReplayEventType::PhaseMark:
|
||||
return TEXT("phase_mark");
|
||||
case EHyperTwistReplayEventType::DeviceSync:
|
||||
return TEXT("device_sync");
|
||||
case EHyperTwistReplayEventType::RecognitionPreview:
|
||||
return TEXT("recognition_preview");
|
||||
case EHyperTwistReplayEventType::RecognitionScan:
|
||||
return TEXT("recognition_scan");
|
||||
case EHyperTwistReplayEventType::RecognitionCommit:
|
||||
return TEXT("recognition_commit");
|
||||
case EHyperTwistReplayEventType::RecognitionCorrection:
|
||||
return TEXT("recognition_correction");
|
||||
case EHyperTwistReplayEventType::Pause:
|
||||
return TEXT("pause");
|
||||
case EHyperTwistReplayEventType::Resume:
|
||||
return TEXT("resume");
|
||||
case EHyperTwistReplayEventType::Annotation:
|
||||
return TEXT("annotation");
|
||||
case EHyperTwistReplayEventType::SolveEnd:
|
||||
return TEXT("solve_end");
|
||||
default:
|
||||
return TEXT("event");
|
||||
}
|
||||
}
|
||||
|
||||
FString BuildFallbackEventId(
|
||||
const FHyperTwistReplayEvent& ReplayEvent,
|
||||
const int32 Sequence
|
||||
)
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("%s_%03d"),
|
||||
*GetEventTypeToken(ReplayEvent.EventType),
|
||||
FMath::Max(Sequence, 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayDirectory()
|
||||
|
|
@ -40,6 +96,65 @@ FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayDirectory()
|
|||
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HyperTwist"), TEXT("Replays"));
|
||||
}
|
||||
|
||||
FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPacket(
|
||||
const FHyperTwistReplayPacket& ReplayPacket
|
||||
)
|
||||
{
|
||||
FHyperTwistReplayPacket NormalizedPacket = ReplayPacket;
|
||||
if (NormalizedPacket.PacketVersion.IsEmpty())
|
||||
{
|
||||
NormalizedPacket.PacketVersion =
|
||||
HyperTwistReplayPersistenceLibraryInternal::ReplayPacketVersion;
|
||||
}
|
||||
|
||||
if (NormalizedPacket.ReplayId.IsEmpty() && !NormalizedPacket.SessionId.IsEmpty())
|
||||
{
|
||||
NormalizedPacket.ReplayId = NormalizedPacket.SessionId;
|
||||
}
|
||||
|
||||
if (NormalizedPacket.SessionId.IsEmpty() && !NormalizedPacket.ReplayId.IsEmpty())
|
||||
{
|
||||
NormalizedPacket.SessionId = NormalizedPacket.ReplayId;
|
||||
}
|
||||
|
||||
int32 PreviousSequence = 0;
|
||||
int32 PreviousTimeMs = 0;
|
||||
for (FHyperTwistReplayEvent& ReplayEvent : NormalizedPacket.Events)
|
||||
{
|
||||
if (ReplayEvent.Sequence <= PreviousSequence)
|
||||
{
|
||||
ReplayEvent.Sequence = PreviousSequence + 1;
|
||||
}
|
||||
|
||||
if (ReplayEvent.TimeMs < PreviousTimeMs)
|
||||
{
|
||||
ReplayEvent.TimeMs = PreviousTimeMs;
|
||||
}
|
||||
|
||||
if (ReplayEvent.TimeMs < 0)
|
||||
{
|
||||
ReplayEvent.TimeMs = 0;
|
||||
}
|
||||
|
||||
if (ReplayEvent.EventId.IsEmpty())
|
||||
{
|
||||
ReplayEvent.EventId =
|
||||
HyperTwistReplayPersistenceLibraryInternal::BuildFallbackEventId(
|
||||
ReplayEvent,
|
||||
ReplayEvent.Sequence
|
||||
);
|
||||
}
|
||||
|
||||
PreviousSequence = ReplayEvent.Sequence;
|
||||
PreviousTimeMs = ReplayEvent.TimeMs;
|
||||
}
|
||||
|
||||
NormalizedPacket.DerivedSummary = NormalizedPacket.Events.Num() > 0
|
||||
? UHyperTwistReplayLibrary::DeriveReplaySummary(NormalizedPacket)
|
||||
: FHyperTwistDerivedReplaySummary();
|
||||
return NormalizedPacket;
|
||||
}
|
||||
|
||||
FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayPath(
|
||||
const FHyperTwistReplayPacket& ReplayPacket
|
||||
)
|
||||
|
|
@ -80,13 +195,15 @@ bool UHyperTwistReplayPersistenceLibrary::SaveReplayPacketToFile(
|
|||
FString& OutResolvedPath
|
||||
)
|
||||
{
|
||||
OutResolvedPath = ResolveReplayPacketPath(ReplayPacket, ReplayPath);
|
||||
if (!ReplayPacket.IsStructurallyValid() || OutResolvedPath.IsEmpty())
|
||||
const FHyperTwistReplayPacket NormalizedPacket = NormalizeReplayPacket(ReplayPacket);
|
||||
OutResolvedPath = ResolveReplayPacketPath(NormalizedPacket, ReplayPath);
|
||||
if (!NormalizedPacket.IsStructurallyValid() || OutResolvedPath.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString Json = UHyperTwistContractLibrary::SerializeReplayPacketToJson(ReplayPacket);
|
||||
const FString Json =
|
||||
UHyperTwistContractLibrary::SerializeReplayPacketToJson(NormalizedPacket);
|
||||
if (Json.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
|
|
@ -121,6 +238,12 @@ bool UHyperTwistReplayPersistenceLibrary::LoadReplayPacketFromFile(
|
|||
return false;
|
||||
}
|
||||
|
||||
return UHyperTwistContractLibrary::DeserializeReplayPacketFromJson(Json, OutReplayPacket)
|
||||
&& OutReplayPacket.IsStructurallyValid();
|
||||
FHyperTwistReplayPacket LoadedPacket;
|
||||
if (!UHyperTwistContractLibrary::DeserializeReplayPacketFromJson(Json, LoadedPacket))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutReplayPacket = NormalizeReplayPacket(LoadedPacket);
|
||||
return OutReplayPacket.IsStructurallyValid();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -891,25 +891,12 @@ void AHyperTwistClassicCubeGameMode::RefreshLocalLeaderboardLine()
|
|||
CurrentScrambleLength = ActiveReplayMetadata.ScrambleLength;
|
||||
}
|
||||
|
||||
FHyperTwistClassicCubeLeaderboardEntry BestEntry;
|
||||
if (UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry(
|
||||
LeaderboardLineOverride =
|
||||
UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine(
|
||||
LocalLeaderboardState,
|
||||
ResolveActivePuzzleId(),
|
||||
CurrentScrambleLength,
|
||||
BestEntry))
|
||||
{
|
||||
LeaderboardLineOverride = FString::Printf(
|
||||
TEXT("leaderboard: best %s for %d-move scramble"),
|
||||
*HyperTwistClassicCubeGameModeInternal::FormatMilliseconds(BestEntry.FinalTimeMs),
|
||||
BestEntry.ScrambleLength
|
||||
CurrentScrambleLength
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
LeaderboardLineOverride = FString::Printf(
|
||||
TEXT("leaderboard: no local best yet for %d-move scramble"),
|
||||
CurrentScrambleLength
|
||||
);
|
||||
}
|
||||
|
||||
void AHyperTwistClassicCubeGameMode::RecordSolvedAttemptToLocalLeaderboard()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@ namespace HyperTwistClassicCubeLeaderboardLibraryInternal
|
|||
return FString::Printf(TEXT("%s|%d"), *PuzzleId, ScrambleLength);
|
||||
}
|
||||
|
||||
FString FormatMilliseconds(const int32 Milliseconds)
|
||||
{
|
||||
const int32 SafeMilliseconds = FMath::Max(Milliseconds, 0);
|
||||
const int32 Minutes = SafeMilliseconds / 60000;
|
||||
const int32 Seconds = (SafeMilliseconds / 1000) % 60;
|
||||
const int32 MillisRemainder = SafeMilliseconds % 1000;
|
||||
return FString::Printf(TEXT("%02d:%02d.%03d"), Minutes, Seconds, MillisRemainder);
|
||||
}
|
||||
|
||||
bool ShouldPreferEntry(
|
||||
const FHyperTwistClassicCubeLeaderboardEntry& Candidate,
|
||||
const FHyperTwistClassicCubeLeaderboardEntry& Existing
|
||||
|
|
@ -125,6 +134,13 @@ FString UHyperTwistClassicCubeLeaderboardLibrary::GetDefaultLeaderboardPath()
|
|||
);
|
||||
}
|
||||
|
||||
FString UHyperTwistClassicCubeLeaderboardLibrary::FormatBestTimeMs(const int32 FinalTimeMs)
|
||||
{
|
||||
return HyperTwistClassicCubeLeaderboardLibraryInternal::FormatMilliseconds(
|
||||
FinalTimeMs
|
||||
);
|
||||
}
|
||||
|
||||
FString UHyperTwistClassicCubeLeaderboardLibrary::ResolveLeaderboardPath(
|
||||
const FString& LeaderboardPath
|
||||
)
|
||||
|
|
@ -326,3 +342,27 @@ bool UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry(
|
|||
|
||||
return bFoundBestEntry && OutEntry.IsStructurallyValid();
|
||||
}
|
||||
|
||||
FString UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine(
|
||||
const FHyperTwistClassicCubeLeaderboardState& LeaderboardState,
|
||||
const FString& PuzzleId,
|
||||
const int32 ScrambleLength
|
||||
)
|
||||
{
|
||||
const int32 SafeScrambleLength = FMath::Max(ScrambleLength, 0);
|
||||
FHyperTwistClassicCubeLeaderboardEntry BestEntry;
|
||||
if (!PuzzleId.IsEmpty()
|
||||
&& FindBestEntry(LeaderboardState, PuzzleId, SafeScrambleLength, BestEntry))
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("leaderboard: best %s for %d-move scramble"),
|
||||
*FormatBestTimeMs(BestEntry.FinalTimeMs),
|
||||
BestEntry.ScrambleLength
|
||||
);
|
||||
}
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("leaderboard: no local best yet for %d-move scramble"),
|
||||
SafeScrambleLength
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h"
|
||||
|
||||
#include "HyperTwistReplay/HyperTwistReplayLibrary.h"
|
||||
#include "HyperTwistReplay/HyperTwistReplayPersistenceLibrary.h"
|
||||
#include "HyperTwistSimulation/HyperTwistViewerLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingClassicCubingLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSpatialLibrary.h"
|
||||
|
|
@ -617,7 +618,9 @@ void UHyperTwistPuzzleViewerComponent::LoadMoveList(const TArray<FHyperTwistView
|
|||
|
||||
bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayPacket& InReplayPacket)
|
||||
{
|
||||
if (!InReplayPacket.IsStructurallyValid())
|
||||
const FHyperTwistReplayPacket NormalizedPacket =
|
||||
UHyperTwistReplayPersistenceLibrary::NormalizeReplayPacket(InReplayPacket);
|
||||
if (!NormalizedPacket.IsStructurallyValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -626,20 +629,20 @@ bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayP
|
|||
const FHyperTwistViewerPositionSnapshot PreviousPositionSnapshot = PositionSnapshot;
|
||||
const FHyperTwistViewerDirectionState PreviousDirectionState = DirectionState;
|
||||
|
||||
ReplayPacket = InReplayPacket;
|
||||
ReplaySummary = UHyperTwistReplayLibrary::DeriveReplaySummary(InReplayPacket);
|
||||
ReplayPacket = NormalizedPacket;
|
||||
ReplaySummary = ReplayPacket.DerivedSummary;
|
||||
bReplayPacketLoaded = true;
|
||||
MoveList = HyperTwistPuzzleViewerComponentInternal::BuildReplayMoveList(InReplayPacket);
|
||||
MoveList = HyperTwistPuzzleViewerComponentInternal::BuildReplayMoveList(ReplayPacket);
|
||||
HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState();
|
||||
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
|
||||
|
||||
FHyperTwistSimulationSceneContext ReplaySceneContext;
|
||||
if (HyperTwistPuzzleViewerComponentInternal::TryBuildSceneContextFromReplay(InReplayPacket, ReplaySceneContext))
|
||||
if (HyperTwistPuzzleViewerComponentInternal::TryBuildSceneContextFromReplay(ReplayPacket, ReplaySceneContext))
|
||||
{
|
||||
SceneContext = ReplaySceneContext;
|
||||
}
|
||||
else if (!SceneContext.IsStructurallyValid()
|
||||
|| SceneContext.PuzzleState.Definition.PuzzleId != InReplayPacket.PuzzleDefinition.PuzzleId)
|
||||
|| SceneContext.PuzzleState.Definition.PuzzleId != ReplayPacket.PuzzleDefinition.PuzzleId)
|
||||
{
|
||||
SceneContext = FHyperTwistSimulationSceneContext();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ public:
|
|||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay|Persistence")
|
||||
static FString GetDefaultReplayDirectory();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay|Persistence")
|
||||
static FHyperTwistReplayPacket NormalizeReplayPacket(
|
||||
const FHyperTwistReplayPacket& ReplayPacket
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay|Persistence")
|
||||
static FString GetDefaultReplayPath(const FHyperTwistReplayPacket& ReplayPacket);
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ public:
|
|||
UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|Leaderboard")
|
||||
static FString GetDefaultLeaderboardPath();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|Leaderboard")
|
||||
static FString FormatBestTimeMs(int32 FinalTimeMs);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|Leaderboard")
|
||||
static FString ResolveLeaderboardPath(const FString& LeaderboardPath);
|
||||
|
||||
|
|
@ -113,4 +116,11 @@ public:
|
|||
int32 ScrambleLength,
|
||||
FHyperTwistClassicCubeLeaderboardEntry& OutEntry
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|Leaderboard")
|
||||
static FString BuildLeaderboardStatusLine(
|
||||
const FHyperTwistClassicCubeLeaderboardState& LeaderboardState,
|
||||
const FString& PuzzleId,
|
||||
int32 ScrambleLength
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Guid.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
|
|
@ -434,6 +435,113 @@ bool FHyperTwistClassicCubeReplayViewerLoadTest::RunTest(const FString& Paramete
|
|||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeReplayPersistenceNormalizationTest,
|
||||
"HyperTwist.Integration.ClassicCube.ReplayPersistenceNormalization",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FHyperTwistReplayPacket LegacyPacket =
|
||||
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayPacket();
|
||||
LegacyPacket.PacketVersion.Reset();
|
||||
LegacyPacket.DerivedSummary = FHyperTwistDerivedReplaySummary();
|
||||
|
||||
for (int32 EventIndex = 0; EventIndex < LegacyPacket.Events.Num(); ++EventIndex)
|
||||
{
|
||||
FHyperTwistReplayEvent& ReplayEvent = LegacyPacket.Events[EventIndex];
|
||||
ReplayEvent.EventId.Reset();
|
||||
ReplayEvent.Sequence = 0;
|
||||
ReplayEvent.TimeMs = FMath::Max(LegacyPacket.Events.Num() - EventIndex - 1, 0);
|
||||
}
|
||||
|
||||
FString LegacyJson;
|
||||
TestTrue(
|
||||
TEXT("The legacy replay packet must serialize for normalization checks."),
|
||||
HyperTwistClassicCubeIntegrationTestInternal::SerializeStruct(
|
||||
LegacyPacket,
|
||||
LegacyJson
|
||||
)
|
||||
);
|
||||
|
||||
const FString ReplayPath =
|
||||
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayRoundTripPath();
|
||||
IFileManager::Get().MakeDirectory(*FPaths::GetPath(ReplayPath), true);
|
||||
TestTrue(
|
||||
TEXT("The legacy replay packet JSON must write to disk."),
|
||||
FFileHelper::SaveStringToFile(
|
||||
LegacyJson,
|
||||
*ReplayPath,
|
||||
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM
|
||||
)
|
||||
);
|
||||
|
||||
FHyperTwistReplayPacket LoadedPacket;
|
||||
FString LoadedPath;
|
||||
TestTrue(
|
||||
TEXT("The replay persistence library must normalize and load schema-light replay packets."),
|
||||
UHyperTwistReplayPersistenceLibrary::LoadReplayPacketFromFile(
|
||||
ReplayPath,
|
||||
LoadedPacket,
|
||||
LoadedPath
|
||||
)
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Replay normalization must restore the canonical packet version."),
|
||||
LoadedPacket.PacketVersion,
|
||||
TEXT("ht-replay/v1")
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Replay normalization must restore a structurally valid packet."),
|
||||
LoadedPacket.IsStructurallyValid()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Replay normalization must rebuild the derived move count."),
|
||||
LoadedPacket.DerivedSummary.MoveCount,
|
||||
10
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Replay normalization must rebuild the completed result."),
|
||||
LoadedPacket.DerivedSummary.Result,
|
||||
TEXT("completed")
|
||||
);
|
||||
|
||||
int32 PreviousSequence = 0;
|
||||
int32 PreviousTimeMs = -1;
|
||||
for (const FHyperTwistReplayEvent& ReplayEvent : LoadedPacket.Events)
|
||||
{
|
||||
TestFalse(
|
||||
TEXT("Replay normalization must populate every event id."),
|
||||
ReplayEvent.EventId.IsEmpty()
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Replay normalization must restore strictly increasing event sequence numbers."),
|
||||
ReplayEvent.Sequence > PreviousSequence
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Replay normalization must restore monotonic event times."),
|
||||
ReplayEvent.TimeMs >= PreviousTimeMs
|
||||
);
|
||||
PreviousSequence = ReplayEvent.Sequence;
|
||||
PreviousTimeMs = ReplayEvent.TimeMs;
|
||||
}
|
||||
|
||||
UHyperTwistPuzzleViewerComponent* ViewerComponent =
|
||||
NewObject<UHyperTwistPuzzleViewerComponent>(GetTransientPackage());
|
||||
TestNotNull(
|
||||
TEXT("The replay viewer component must exist for normalized replay loading."),
|
||||
ViewerComponent
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The viewer must accept the normalized replay packet directly."),
|
||||
ViewerComponent != nullptr && ViewerComponent->LoadReplayPacket(LegacyPacket)
|
||||
);
|
||||
|
||||
IFileManager::Get().Delete(*ReplayPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeLeaderboardPersistenceTest,
|
||||
"HyperTwist.Integration.ClassicCube.LeaderboardPersistence",
|
||||
|
|
@ -616,6 +724,54 @@ bool FHyperTwistClassicCubeLeaderboardNormalizationTest::RunTest(const FString&
|
|||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeLeaderboardStatusLineTest,
|
||||
"HyperTwist.Integration.ClassicCube.LeaderboardStatusLine",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeLeaderboardStatusLineTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FHyperTwistClassicCubeLeaderboardState LegacyState;
|
||||
LegacyState.SchemaVersion.Reset();
|
||||
|
||||
FHyperTwistClassicCubeLeaderboardEntry SlowerEntry;
|
||||
SlowerEntry.PuzzleId = TEXT("cube/3x3x3");
|
||||
SlowerEntry.ScrambleLength = 20;
|
||||
SlowerEntry.FinalTimeMs = 15321;
|
||||
SlowerEntry.ReplayId = TEXT("replay-slower");
|
||||
SlowerEntry.SessionId = TEXT("session-slower");
|
||||
SlowerEntry.RecordedAtUtc = TEXT("2026-06-12T10:10:00Z");
|
||||
|
||||
FHyperTwistClassicCubeLeaderboardEntry FasterEntry = SlowerEntry;
|
||||
FasterEntry.FinalTimeMs = 14321;
|
||||
FasterEntry.ReplayId = TEXT("replay-faster");
|
||||
FasterEntry.SessionId = TEXT("session-faster");
|
||||
FasterEntry.RecordedAtUtc = TEXT("2026-06-12T10:05:00Z");
|
||||
|
||||
LegacyState.Entries = {SlowerEntry, FasterEntry};
|
||||
|
||||
TestEqual(
|
||||
TEXT("The leaderboard status line must normalize duplicate buckets before choosing the best entry."),
|
||||
UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine(
|
||||
LegacyState,
|
||||
TEXT("cube/3x3x3"),
|
||||
20
|
||||
),
|
||||
TEXT("leaderboard: best 00:14.321 for 20-move scramble")
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The leaderboard status line must surface empty buckets explicitly."),
|
||||
UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine(
|
||||
LegacyState,
|
||||
TEXT("cube/3x3x3"),
|
||||
25
|
||||
),
|
||||
TEXT("leaderboard: no local best yet for 25-move scramble")
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeRuntimeReplaySmokeTest,
|
||||
"HyperTwist.Integration.ClassicCube.RuntimeReplaySmoke",
|
||||
|
|
|
|||
|
|
@ -409,6 +409,7 @@ Closure read:
|
|||
- [x] Export to `.json` replay file
|
||||
- [x] Load replay, reconstruct animation
|
||||
- [x] Legacy-safe replay packet consumers now preserve current behavior without depending on hand-curated local state ordering
|
||||
- [x] Replay save, load, and viewer-import paths now repair schema-light packet version, ids, monotonic event ordering, monotonic timestamps, and derived replay summary during normalization
|
||||
|
||||
### 9B — Media Export
|
||||
- [ ] Use UE `MovieRenderQueue` as the primary video-export route; treat `remotion-dev/remotion` only as restrictive-custody reference context unless a later clean-room/specification pass explicitly reopens it
|
||||
|
|
@ -423,6 +424,7 @@ Routing correction:
|
|||
- [x] Store best times per puzzle type + scramble length
|
||||
- [x] Display in HUD
|
||||
- [x] Normalize schema-light or duplicate local JSON buckets before resolving the displayed best time
|
||||
- [x] Centralize the displayed leaderboard status line so legacy-bucket repair and HUD best-time wording resolve through the same deterministic path
|
||||
|
||||
**Estimated actions:** 40–60
|
||||
**Estimated time:** 2–3 days
|
||||
|
|
@ -450,6 +452,7 @@ Routing correction:
|
|||
- [x] The packaged validation helper now smoke-boots every cooked classic-cube training map by default instead of depending on a remembered second manual launch
|
||||
- [x] Validation evidence on `2026-06-13`: primary reverse-SSH `localhost:22022` lane, isolated Windows worktree `C:\HyperTwist_worktrees\phase10validate`, commandlet-safe `UnrealMCP` and `UnrealMCPChong` guards landed for unattended cook, `BuildCookRun` `ExitCode=0` with `BuildCookRun time: 164.59 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged`, packaged smoke boot on `L_HyperTwist_ClassicTraining`, and explicit packaged smoke boot on `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining`
|
||||
- [x] Repair validation evidence on `2026-06-13`: the maintained helper itself was re-run on the same primary reverse-SSH `localhost:22022` lane against isolated worktree `C:\HyperTwist_worktrees\phase10validate`, wrote archive output to `C:\HyperTwist_worktrees\phase10validate_packaged_repair`, completed `BuildCookRun` with `ExitCode=0` and `BuildCookRun time: 116.89 s`, then default-smoke-launched both `/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining` without a manual second launch step
|
||||
- [x] Continuation evidence on `2026-06-13`: after a prior aborted broad tar sync corrupted remote `HyperTwistWhisperCppPhase6RESpeechTranscriptContractTest.cpp`, the primary reverse-SSH `localhost:22022` lane hash-matched the changed slice files back into `C:\HyperTwist_worktrees\phase10validate`, completed the canonical `Build.bat` pass with `Result: Succeeded` and UnrealBuildTool `Total execution time: 2029.78 seconds`, passed all `7` `HyperTwist.Integration.ClassicCube` tests including `ReplayPersistenceNormalization`, `LeaderboardNormalization`, and `LeaderboardStatusLine`, refreshed `Phase 10B` coverage through green `Rotation`, `Solve`, `RandomClassicStates`, `Timer.Accuracy`, and `ClassicCubeMoveParsing` commandlet passes, and revalidated the maintained package helper in `-SkipBuild` mode with `BuildCookRun time: 69.16 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c`, aggregate JSON at `validation\classic-cube-package-validation-report.json`, per-map smoke JSON under `validation\smoke\`, and successful smoke boots on both classic-cube training maps
|
||||
|
||||
**Estimated actions:** 20–30
|
||||
**Estimated time:** 1–2 days
|
||||
|
|
|
|||
|
|
@ -366,6 +366,57 @@ Operational rules reinforced by this proof:
|
|||
first, validate both direct profile resolution and real training-run
|
||||
definition propagation on the same Windows Unreal lane
|
||||
|
||||
## Addendum - 2026-06-13 (replay, leaderboard, and structured package-report proof)
|
||||
|
||||
Live follow-up on `2026-06-13` established these additional facts:
|
||||
|
||||
- the primary `localhost:22022` lane remained healthy for the current
|
||||
replay/leaderboard/package continuation
|
||||
- the validated route again used the isolated Windows tree
|
||||
`C:\HyperTwist_worktrees\phase10validate`
|
||||
- a prior aborted broad tar sync had left remote
|
||||
`HyperTwistWhisperCppPhase6RESpeechTranscriptContractTest.cpp` corrupted in
|
||||
that worktree; targeted re-sync plus hash parity across the changed slice
|
||||
files repaired the lane before validation continued
|
||||
- the canonical `Build.bat` pass then succeeded there with `Result: Succeeded`
|
||||
and UnrealBuildTool `Total execution time: 2029.78 seconds`
|
||||
- a fresh `UnrealEditor-Cmd` integration pass then found `7` tests under
|
||||
`HyperTwist.Integration.ClassicCube` and passed all `7`, including
|
||||
`ReplayPersistenceNormalization`, `LeaderboardNormalization`, and
|
||||
`LeaderboardStatusLine`
|
||||
- fresh focused `Phase 10B` commandlet passes on the same rebuilt binary also
|
||||
passed:
|
||||
- `HyperTwist.Simulation.ClassicCube.Behavior.Rotation`
|
||||
- `HyperTwist.Simulation.ClassicCube.Behavior.Solve`
|
||||
- `HyperTwist.Solver.Accuracy.RandomClassicStates`
|
||||
- `HyperTwist.Training.Timer.Accuracy`
|
||||
- `HyperTwist.Speech.Transcription.ClassicCubeMoveParsing`
|
||||
- the timer-accuracy pass closed green but surfaced unrelated warning noise
|
||||
from missing local `vision` and `speech` health hosts, so that warning
|
||||
context should be recorded rather than misclassified as a test failure
|
||||
- the maintained package helper was then revalidated in `-SkipBuild` mode
|
||||
against the already-present packaged-game receipt, wrote archive output to
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c`,
|
||||
completed `BuildCookRun` with `ExitCode=0` and `BuildCookRun time: 69.16 s`,
|
||||
wrote aggregate JSON to
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c\validation\classic-cube-package-validation-report.json`,
|
||||
wrote per-map smoke JSON beneath
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c\validation\smoke\`,
|
||||
and smoke-launched both training maps successfully
|
||||
|
||||
Operational rules reinforced by this proof:
|
||||
|
||||
- when a broad reverse-sync is interrupted or aborted, hash-check or explicitly
|
||||
re-sync the changed files before treating later remote compiler failures as
|
||||
true source regressions
|
||||
- when validating package-helper archive/report behavior and the packaged-game
|
||||
receipt already exists, prefer a fresh `-SkipBuild` package proof over an
|
||||
unrelated full game rebuild
|
||||
- when one `UnrealEditor-Cmd` `ExecCmds` string attempts to queue multiple
|
||||
disjoint `Automation RunTests` filters, inspect the exported report JSON or
|
||||
rerun the filters explicitly; the first filter may be the only one reflected
|
||||
in the report
|
||||
|
||||
## Addendum - 2026-06-03 (stale-listener recovery)
|
||||
|
||||
Live follow-up on `2026-06-03` established this additional operational rule:
|
||||
|
|
|
|||
|
|
@ -177,6 +177,25 @@ including `RunStateActivationResolution`, with
|
|||
earlier activation-catalog checkpoint into current active-run and
|
||||
`RuntimeModeId` fallback proof on the same primary reverse-SSH lane.
|
||||
|
||||
Further `2026-06-13` continuation proof on that same primary lane recovered
|
||||
the classic-cube replay/leaderboard/package slice after a corrupted reverse
|
||||
sync incident: a prior aborted broad tar sync left remote
|
||||
`HyperTwistWhisperCppPhase6RESpeechTranscriptContractTest.cpp` damaged inside
|
||||
`C:\HyperTwist_worktrees\phase10validate`, a targeted hash-matched re-sync
|
||||
repaired the affected files, the canonical `Build.bat` pass then completed with
|
||||
`Result: Succeeded` and UnrealBuildTool `Total execution time: 2029.78
|
||||
seconds`, and fresh `UnrealEditor-Cmd` automation passed all `7`
|
||||
`HyperTwist.Integration.ClassicCube` tests plus refreshed `Phase 10B`
|
||||
validation for `Rotation`, `Solve`, `RandomClassicStates`, `Timer.Accuracy`,
|
||||
and `ClassicCubeMoveParsing`. The same continuation then revalidated the
|
||||
maintained package helper in `-SkipBuild` mode against the existing packaged
|
||||
receipt, finishing `BuildCookRun` in `69.16 s`, writing aggregate JSON to
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c\validation\classic-cube-package-validation-report.json`,
|
||||
writing per-map smoke JSON beneath `...\validation\smoke\`, and successfully
|
||||
smoke-booting both classic-cube training maps. This extends the doctrine from
|
||||
generic package proof into current structured validation-report proof on the
|
||||
same primary reverse-SSH lane.
|
||||
|
||||
Operational reading:
|
||||
|
||||
- use `localhost:22022` as the primary reverse-SSH lane
|
||||
|
|
@ -195,6 +214,16 @@ Operational reading:
|
|||
before rerunning package validation; otherwise the global UBT mutex and
|
||||
AutomationTool log files can block a healthy source state from being
|
||||
revalidated
|
||||
- when a broad reverse-sync is interrupted or aborted, do not trust the remote
|
||||
validation worktree blindly; hash-check or explicitly re-sync the changed
|
||||
files before classifying later compiler failures as a real source regression
|
||||
- when validating package-helper archive/report behavior and the packaged-game
|
||||
receipt already exists, `-SkipBuild` is the preferred proof path over an
|
||||
unrelated full game rebuild
|
||||
- when one `UnrealEditor-Cmd` `ExecCmds` string tries to queue multiple
|
||||
disjoint `Automation RunTests` filters, inspect the exported report JSON or
|
||||
fall back to explicit per-filter invocations; a green exit alone is not
|
||||
sufficient proof that every intended filter actually executed
|
||||
- when an editor-facing plugin still enters `UnrealEditor-Cmd` during cook,
|
||||
guard toolbar, Slate, and local bridge startup paths for
|
||||
commandlet/unattended execution instead of assuming interactive editor-only
|
||||
|
|
|
|||
|
|
@ -161,12 +161,12 @@ repo.
|
|||
| Feature | Status | Primary authority | Notes |
|
||||
|---|---|---|---|
|
||||
| Native puzzle-state runtime | Implemented now | first-party runtime + landed donor packets | Core product identity. |
|
||||
| Classic-cube playable runtime loop | Implemented now | first-party current code + landed timer/training substrate | First-party `AHyperTwistClassicCubeActor`, `AHyperTwistClassicCubeGameMode`, `AHyperTwistClassicCubePlayerController`, `AHyperTwistClassicCubeOrbitPawn`, and `UHyperTwistClassicCubeHUDWidget` now own the bounded classic-cube scramble/play/timer/solve loop with left-click, right-click, touch, middle-mouse drag orbit, scroll-wheel zoom, GUI or keyboard fresh-attempt restart, solved-state submission, active-cube orbit focus, solver hint glow, follow-along guidance, hold-to-talk voice commands, voice-profile cycling, and coach narration in code. Dedicated `L_HyperTwist_ClassicTraining` and `L_HyperTwist_FollowAlongTraining` maps plus the authored classic-cube material family are now source-controlled through `scripts/Invoke-HyperTwistClassicCubeMapAuthoring.ps1`, `scripts/hypertwist_author_classic_cube_training_maps.py`, `scripts/Invoke-HyperTwistClassicCubePackage.ps1`, and `scripts/Launch-HyperTwistClassicCubePackage.ps1`. That route was first package-proven on `2026-06-12` through the fallback reverse-SSH `localhost:22023` lane and then package-proven again on `2026-06-13` through the primary reverse-SSH `localhost:22022` lane on isolated worktree `C:\HyperTwist_worktrees\phase10validate`, including packaged smoke boots on both training maps. |
|
||||
| Classic-cube playable runtime loop | Implemented now | first-party current code + landed timer/training substrate | First-party `AHyperTwistClassicCubeActor`, `AHyperTwistClassicCubeGameMode`, `AHyperTwistClassicCubePlayerController`, `AHyperTwistClassicCubeOrbitPawn`, and `UHyperTwistClassicCubeHUDWidget` now own the bounded classic-cube scramble/play/timer/solve loop with left-click, right-click, touch, middle-mouse drag orbit, scroll-wheel zoom, GUI or keyboard fresh-attempt restart, solved-state submission, active-cube orbit focus, solver hint glow, follow-along guidance, hold-to-talk voice commands, voice-profile cycling, and coach narration in code. Dedicated `L_HyperTwist_ClassicTraining` and `L_HyperTwist_FollowAlongTraining` maps plus the authored classic-cube material family are now source-controlled through `scripts/Invoke-HyperTwistClassicCubeMapAuthoring.ps1`, `scripts/hypertwist_author_classic_cube_training_maps.py`, `scripts/Invoke-HyperTwistClassicCubePackage.ps1`, and `scripts/Launch-HyperTwistClassicCubePackage.ps1`. That route was first package-proven on `2026-06-12` through the fallback reverse-SSH `localhost:22023` lane and then package-proven again on `2026-06-13` through the primary reverse-SSH `localhost:22022` lane on isolated worktree `C:\HyperTwist_worktrees\phase10validate`, including packaged smoke boots on both training maps. The maintained package helper now also emits aggregate validation JSON plus per-map smoke JSON under the archived `validation\` tree, and that report path was revalidated on `2026-06-13` through archive `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c`. |
|
||||
| Classic-cubing semantic/runtime adapter | Implemented now | landed `cubing/cubing.js` packet | Live adapter family. |
|
||||
| Classic-cubing semantic, bridge, and `MPL`-boundary reference grounding | Implemented now | `cubing/cubing.js` retained boundary-sensitive lane + first-party current code | Current live `Classic Cubing Semantics and Runtime` reference side includes seven rewritten first-party contract/reference targets grounded in `cubing/cubing.js`: semantics, geometry, viewer adapter, device boundary, search contract, Melinda bridge, and explicit `MPL` compliance-boundary notes. This does not displace the landed `Phase 4R-A` first-party owner lane, the separate `cubing/twisty.js` replay shell lane, the separate `cubing/alg.js` parser/AST lane, or the explicit practical `MPL` path and notice-retention boundary. |
|
||||
| Seeded competition scramble workflow and lightweight scramble-operator shell adjuncts | Deep-source grounded retained | `cubing/cubing.js` retained lane + `cubing/mark3` / `cubing/scramble.cubing.net` successor evaluation | Source-backed successor surfaces sharpen competition-spec workflow and operator-shell expectations above the retained scramble and visualization seams, but they do not displace `cubing/cubing.js` or `cubing/twisty.js`; `scramble-display` remains comparison-only. |
|
||||
| Replay shell and timeline | Implemented now | landed `cubing/twisty.js` bounded packets | First-party `HyperTwistSimulation` now owns the bounded replay-player shell, cursor/timeline transport, adapter/bootstrap, and local visualization or fallback presentation contract grounded in `cubing/twisty.js`; classic-cubing semantics remain with `cubing/cubing.js`, parser and AST ownership remain with `cubing/alg.js`, and broader browser support ownership stays with the landed browser lanes. |
|
||||
| Classic-cube runtime replay capture, JSON persistence, and playback reconstruction | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube runtime records move and state-snapshot replay events with timestamps during live solves, persists replay packets as `.json` through `UHyperTwistReplayPersistenceLibrary`, restores replay metadata into `AHyperTwistClassicCubeGameMode`, and reconstructs saved move streams for local playback in current code. The primary reverse-SSH `localhost:22022` Windows validation lane passed `ReplayPersistenceRoundTrip`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-12`. Current replay-adjacent local consumers are also now hardened so schema-light or duplicate local state does not silently distort the retained best-attempt view. |
|
||||
| Classic-cube runtime replay capture, JSON persistence, and playback reconstruction | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube runtime records move and state-snapshot replay events with timestamps during live solves, persists replay packets as `.json` through `UHyperTwistReplayPersistenceLibrary`, restores replay metadata into `AHyperTwistClassicCubeGameMode`, and reconstructs saved move streams for local playback in current code. Replay save, load, and viewer-import paths now normalize schema-light packets by repairing packet version, replay/session ids, event ids, monotonic sequences, monotonic timestamps, and derived replay summary before the packet is reused. The primary reverse-SSH `localhost:22022` Windows validation lane then refreshed the full `HyperTwist.Integration.ClassicCube` suite on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-13`, passing `ReplayPersistenceRoundTrip`, `ReplayPersistenceNormalization`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` alongside the related leaderboard integration cases. |
|
||||
| Media-export, embedded-playback, and replay-explainer reference grounding | Implemented now | `remotion-dev/remotion` retained restrictive-custody lane + first-party current code | Current live `Media Export and Replay Explainers` reference side includes five rewritten first-party contract/reference targets grounded in `remotion-dev/remotion`: embedded playback, render orchestration, media parser, explainer-studio preview/output registration, and explicit commercial-license/package-split compliance-boundary notes. This does not displace the landed `Phase 4R-F` first-party owner lane, the broader browser/spatial/media adjunct family, or the explicit package-split commercial boundary. Current routing correction: preserve the already-landed first-party outputs, but treat future donor-backed widening from `remotion-dev/remotion` as restrictive-custody and prefer Unreal-native export for fresh shipping work. |
|
||||
| Browser-viewer, compact-editor, export, and docs-boundary grounding | Implemented now | `google/model-viewer` retained permissive lane + first-party current code | Current live `Browser Viewer and Asset QA` reference side includes five rewritten first-party contract/reference targets grounded in the root `google/model-viewer` lane: viewer embed, compact editor/inspection, snippet/export, renderer comparison/fidelity, and docs/demo separation. The subordinate `space-opera`, `modelviewer.dev`, and `render-fidelity-tools` source attributions are absorbed here as support-only contributors rather than separate live owner lanes. This does not displace the landed `Phase 3R-D` first-party owner lane, the separate `KhronosGroup/glTF-Sample-Viewer` standards-aware QA lane, or the already explicit `google/model-viewer/packages/shared-assets` boundary-sensitive fixture lane. |
|
||||
| Standards-aware asset-validation and statistics grounding | Implemented now | `KhronosGroup/glTF-Sample-Viewer` retained permissive support lane + first-party current code | Current live `Browser Viewer and Asset QA` reference side includes one rewritten first-party target grounded in `KhronosGroup/glTF-Sample-Viewer`: standards-aware asset validation and statistics. This does not displace the landed `Phase 3R-D` first-party browser viewer owner, the separate root `google/model-viewer` viewer/reference lane, or the already explicit shared-assets boundary-sensitive fixture lane. |
|
||||
|
|
@ -249,7 +249,7 @@ repo.
|
|||
| Feature | Status | Primary authority | Notes |
|
||||
|---|---|---|---|
|
||||
| Publication/leaderboard projection | Implemented now | landed `kash/cubedesk` `Bound 4` | Live bounded slice. |
|
||||
| Classic-cube local leaderboard stub | Implemented now | first-party current code + landed timer/training substrate | Current classic-cube runtime persists best local solve times by puzzle id and scramble-length bucket to JSON, carries replay/session identity forward on winning entries, repairs schema-light or duplicate local leaderboard buckets before resolving the displayed best entry, and projects the best current-bucket line into the in-game HUD without requiring a backend. The primary reverse-SSH `localhost:22022` Windows validation lane passed `LeaderboardPersistence` on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-12`. |
|
||||
| Classic-cube local leaderboard stub | Implemented now | first-party current code + landed timer/training substrate | Current classic-cube runtime persists best local solve times by puzzle id and scramble-length bucket to JSON, carries replay/session identity forward on winning entries, repairs schema-light or duplicate local leaderboard buckets before resolving the displayed best entry, routes the user-facing HUD best-time wording through a centralized deterministic status-line formatter, and projects the best current-bucket line into the in-game HUD without requiring a backend. The primary reverse-SSH `localhost:22022` Windows validation lane refreshed the corresponding leaderboard coverage on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-13`, passing `LeaderboardPersistence`, `LeaderboardNormalization`, and `LeaderboardStatusLine`. |
|
||||
| Entitlement gating | Implemented now | landed `kash/cubedesk` `Bound 4` | Live bounded slice. |
|
||||
| Local social challenge bundle | Implemented now | landed `kash/cubedesk` `Bound 5` | Live bounded slice. |
|
||||
| Broader admin/report lane | Deep-source grounded retained | repo-local justification required | `Bound 6` is deferred and not currently justified for implementation. |
|
||||
|
|
|
|||
|
|
@ -68,17 +68,20 @@ Current consolidated milestone snapshot:
|
|||
targeted automation, and live Windows validation proof
|
||||
- classic-cube `Phase 9A` replay recording is now closed through first-party
|
||||
runtime capture, `.json` replay persistence, local playback reconstruction,
|
||||
and live Windows integration proof on isolated worktree
|
||||
schema-light replay normalization across save/load/viewer import, and live
|
||||
Windows integration proof on isolated worktree
|
||||
`C:\HyperTwist_worktrees\phase10validate`
|
||||
- classic-cube `Phase 9C` local leaderboard stub is now closed through
|
||||
first-party JSON persistence, scramble-length bucket best-time retention, HUD
|
||||
display, legacy-state normalization for schema-light or duplicate local
|
||||
buckets, and live Windows integration proof on isolated worktree
|
||||
buckets, deterministic status-line resolution, and live Windows integration
|
||||
proof on isolated worktree
|
||||
`C:\HyperTwist_worktrees\phase10validate`
|
||||
- classic-cube `Phase 10B` behavior validation is now closed through Windows
|
||||
automation coverage for rotation, solve, solver accuracy, timer accuracy, and
|
||||
speech-transcription parsing on the primary reverse-SSH `localhost:22022`
|
||||
lane
|
||||
lane, then refreshed again on `2026-06-13` after the replay/leaderboard
|
||||
continuation patch on the same rebuilt Windows binary
|
||||
- classic-cube `Phase 10C` integration validation is now closed through the
|
||||
same primary reverse-SSH `localhost:22022` lane, packaged
|
||||
`BuildCookRun`/archive proof on `C:\HyperTwist_worktrees\phase10validate`,
|
||||
|
|
@ -87,7 +90,10 @@ Current consolidated milestone snapshot:
|
|||
across the full cooked classic-cube training map set; that helper default was
|
||||
revalidated on `2026-06-13` through archive
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_repair` with
|
||||
`BuildCookRun time: 116.89 s`
|
||||
`BuildCookRun time: 116.89 s`, then revalidated again through
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c` with
|
||||
aggregate validation JSON plus per-map smoke JSON in `validation\` and
|
||||
`BuildCookRun time: 69.16 s` in `-SkipBuild` mode
|
||||
- the next best deliberate widening move is the now-opened `Phase 6C`
|
||||
clean-room `Magic120Cell` / `MagicCube5D` runtime widening lane, starting
|
||||
from the first-party higher-dimensional activation catalog now that the owned
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ param(
|
|||
[string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining',
|
||||
[string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining'),
|
||||
[string[]]$SmokeMaps = @(),
|
||||
[string]$ValidationReportPath = '',
|
||||
[switch]$CleanArchive,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipLaunch
|
||||
|
|
@ -39,6 +40,54 @@ $ExpectedClassicCubeMaterialPaths = @(
|
|||
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Right.uasset'),
|
||||
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Internal.uasset')
|
||||
)
|
||||
$ValidationRootPath = Join-Path $ArchiveDirectory 'validation'
|
||||
$SmokeReportDirectory = Join-Path $ValidationRootPath 'smoke'
|
||||
|
||||
function Write-Utf8JsonFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
$ParentPath = Split-Path -Parent $Path
|
||||
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
|
||||
{
|
||||
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
|
||||
}
|
||||
|
||||
$Json = $Value | ConvertTo-Json -Depth 10
|
||||
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
|
||||
}
|
||||
|
||||
function Convert-PathToken {
|
||||
param(
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value))
|
||||
{
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
return ($Value -replace '[\\/:*?"<>| ]', '_')
|
||||
}
|
||||
|
||||
function Resolve-PackagedExecutablePath {
|
||||
param(
|
||||
[string]$PackageRoot
|
||||
)
|
||||
|
||||
$CandidateExecutablePaths = @(
|
||||
(Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
|
||||
(Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
|
||||
(Join-Path $PackageRoot 'UnrealHyperTwist.exe')
|
||||
)
|
||||
|
||||
return $CandidateExecutablePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
}
|
||||
|
||||
function Convert-GameMapPathToContentPath {
|
||||
param(
|
||||
|
|
@ -108,6 +157,13 @@ if ($CleanArchive -and (Test-Path $ArchiveDirectory))
|
|||
|
||||
New-Item -ItemType Directory -Force -Path $UnrealBuildToolSavedPath | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $ArchiveDirectory | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $ValidationRootPath | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $SmokeReportDirectory | Out-Null
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ValidationReportPath))
|
||||
{
|
||||
$ValidationReportPath = Join-Path $ValidationRootPath 'classic-cube-package-validation-report.json'
|
||||
}
|
||||
|
||||
$RunUatArguments = @(
|
||||
'BuildCookRun',
|
||||
|
|
@ -131,28 +187,76 @@ if (-not $SkipBuild)
|
|||
$RunUatArguments += '-build'
|
||||
}
|
||||
|
||||
Write-Host "Packaging HyperTwist classic-cube validation lane to '$ArchiveDirectory'..."
|
||||
& $RunUatPath @RunUatArguments
|
||||
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
throw "RunUAT packaging failed with exit code $LASTEXITCODE."
|
||||
$ValidationReport = [ordered]@{
|
||||
reportVersion = 'ht-classic-cube-package-validation/v1'
|
||||
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
|
||||
projectRoot = $ProjectRoot
|
||||
archiveDirectory = $ArchiveDirectory
|
||||
configuration = $Configuration
|
||||
cookMaps = @($CookMaps)
|
||||
smokeMaps = @($ResolvedSmokeMaps)
|
||||
cleanArchive = [bool]$CleanArchive
|
||||
skipBuild = [bool]$SkipBuild
|
||||
skipLaunch = [bool]$SkipLaunch
|
||||
result = 'failed'
|
||||
packagedExecutablePath = $null
|
||||
smokeReports = @()
|
||||
error = $null
|
||||
}
|
||||
|
||||
if (-not $SkipLaunch)
|
||||
try
|
||||
{
|
||||
if (-not (Test-Path $LaunchScriptPath))
|
||||
Write-Host "Packaging HyperTwist classic-cube validation lane to '$ArchiveDirectory'..."
|
||||
& $RunUatPath @RunUatArguments
|
||||
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
throw "Launch script was not found at '$LaunchScriptPath'."
|
||||
throw "RunUAT packaging failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
foreach ($SmokeMap in $ResolvedSmokeMaps)
|
||||
$PackagedExecutablePath = Resolve-PackagedExecutablePath -PackageRoot $ArchiveDirectory
|
||||
if ($null -eq $PackagedExecutablePath)
|
||||
{
|
||||
Write-Host "Smoke validating packaged classic-cube map '$SmokeMap'..."
|
||||
& $LaunchScriptPath -PackageRoot $ArchiveDirectory -MapUrl $SmokeMap
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
throw "No packaged UnrealHyperTwist executable was found beneath '$ArchiveDirectory' after packaging."
|
||||
}
|
||||
|
||||
$ValidationReport.packagedExecutablePath = $PackagedExecutablePath
|
||||
|
||||
if (-not $SkipLaunch)
|
||||
{
|
||||
if (-not (Test-Path $LaunchScriptPath))
|
||||
{
|
||||
throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
|
||||
throw "Launch script was not found at '$LaunchScriptPath'."
|
||||
}
|
||||
|
||||
foreach ($SmokeMap in $ResolvedSmokeMaps)
|
||||
{
|
||||
$SmokeReportPath = Join-Path $SmokeReportDirectory (
|
||||
'{0}.json' -f (Convert-PathToken -Value $SmokeMap)
|
||||
)
|
||||
Write-Host "Smoke validating packaged classic-cube map '$SmokeMap'..."
|
||||
& $LaunchScriptPath -PackageRoot $ArchiveDirectory -MapUrl $SmokeMap -ReportPath $SmokeReportPath
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
if (Test-Path $SmokeReportPath)
|
||||
{
|
||||
$ValidationReport.smokeReports += @(
|
||||
Get-Content -LiteralPath $SmokeReportPath -Raw | ConvertFrom-Json
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ValidationReport.result = 'passed'
|
||||
}
|
||||
catch
|
||||
{
|
||||
$ValidationReport.error = $_.Exception.Message
|
||||
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
|
||||
throw
|
||||
}
|
||||
|
||||
Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
|
||||
|
|
|
|||
|
|
@ -4,11 +4,31 @@ param(
|
|||
[int]$SmokeSeconds = 10,
|
||||
[int]$ResX = 1600,
|
||||
[int]$ResY = 900,
|
||||
[string]$ReportPath = '',
|
||||
[switch]$KeepRunning
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-Utf8JsonFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
$ParentPath = Split-Path -Parent $Path
|
||||
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
|
||||
{
|
||||
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
|
||||
}
|
||||
|
||||
$Json = $Value | ConvertTo-Json -Depth 8
|
||||
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
|
||||
}
|
||||
|
||||
$CandidateExecutablePaths = @(
|
||||
(Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
|
||||
(Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
|
||||
|
|
@ -30,18 +50,77 @@ $ArgumentList = @(
|
|||
)
|
||||
|
||||
Write-Host "Launching packaged classic-cube validation lane from '$ExecutablePath'..."
|
||||
$Process = Start-Process -FilePath $ExecutablePath -ArgumentList $ArgumentList -PassThru
|
||||
Start-Sleep -Seconds $SmokeSeconds
|
||||
$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
|
||||
$Process = $null
|
||||
$Report = [ordered]@{
|
||||
reportVersion = 'ht-classic-cube-package-smoke/v1'
|
||||
generatedAtUtc = $GeneratedAtUtc
|
||||
packageRoot = $ResolvedPackageRoot
|
||||
executablePath = $ExecutablePath
|
||||
mapUrl = $MapUrl
|
||||
smokeSeconds = $SmokeSeconds
|
||||
resolution = [ordered]@{
|
||||
width = $ResX
|
||||
height = $ResY
|
||||
}
|
||||
keepRunning = [bool]$KeepRunning
|
||||
result = 'failed'
|
||||
processId = $null
|
||||
processStopped = $false
|
||||
exitCode = $null
|
||||
error = $null
|
||||
}
|
||||
|
||||
$Process.Refresh()
|
||||
if ($Process.HasExited)
|
||||
try
|
||||
{
|
||||
throw "Packaged classic-cube executable exited early with code $($Process.ExitCode)."
|
||||
$Process = Start-Process -FilePath $ExecutablePath -ArgumentList $ArgumentList -PassThru
|
||||
Start-Sleep -Seconds $SmokeSeconds
|
||||
|
||||
$Process.Refresh()
|
||||
$Report.processId = $Process.Id
|
||||
if ($Process.HasExited)
|
||||
{
|
||||
$Report.exitCode = $Process.ExitCode
|
||||
throw "Packaged classic-cube executable exited early with code $($Process.ExitCode)."
|
||||
}
|
||||
|
||||
$Report.result = 'passed'
|
||||
}
|
||||
catch
|
||||
{
|
||||
$Report.error = $_.Exception.Message
|
||||
if ($null -ne $Process)
|
||||
{
|
||||
$Process.Refresh()
|
||||
if ($Process.HasExited)
|
||||
{
|
||||
$Report.exitCode = $Process.ExitCode
|
||||
}
|
||||
elseif (-not $KeepRunning)
|
||||
{
|
||||
Stop-Process -Id $Process.Id -Force
|
||||
$Report.processStopped = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ReportPath))
|
||||
{
|
||||
Write-Utf8JsonFile -Path $ReportPath -Value $Report
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
|
||||
Write-Host "Packaged classic-cube smoke launch succeeded (PID $($Process.Id))."
|
||||
if (-not $KeepRunning)
|
||||
{
|
||||
Stop-Process -Id $Process.Id -Force
|
||||
$Report.processStopped = $true
|
||||
Write-Host 'Stopped packaged classic-cube smoke process after successful launch validation.'
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ReportPath))
|
||||
{
|
||||
Write-Utf8JsonFile -Path $ReportPath -Value $Report
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue