Harden package validation and refresh Hyperspeedcube evidence
This commit is contained in:
parent
fb72fbc2c8
commit
c2b1d0a030
12 changed files with 610 additions and 128 deletions
|
|
@ -32,3 +32,6 @@ ProfilesPath=/voice/profiles
|
|||
SynthesizePath=/voice/narrate
|
||||
HealthPath=/voice/health
|
||||
RequestTimeoutSeconds=5.0
|
||||
|
||||
[/Script/DeveloperToolSettings.ProjectPackagingSettings]
|
||||
+DirectoriesToNeverCook=(Path="/MovieRenderPipeline/Blueprints")
|
||||
|
|
|
|||
|
|
@ -89,6 +89,125 @@ namespace HyperTwistReplayPersistenceLibraryInternal
|
|||
FMath::Max(Sequence, 1)
|
||||
);
|
||||
}
|
||||
|
||||
FString BuildFallbackPacketIdentity(const FHyperTwistReplayPacket& ReplayPacket)
|
||||
{
|
||||
const FString PuzzleToken = SanitizePathToken(
|
||||
!ReplayPacket.PuzzleDefinition.PuzzleId.IsEmpty()
|
||||
? ReplayPacket.PuzzleDefinition.PuzzleId
|
||||
: TEXT("replay")
|
||||
);
|
||||
const FString IdentitySeed = !ReplayPacket.StartedAtUtc.IsEmpty()
|
||||
? ReplayPacket.StartedAtUtc
|
||||
: !ReplayPacket.EndedAtUtc.IsEmpty()
|
||||
? ReplayPacket.EndedAtUtc
|
||||
: FString::Printf(TEXT("event-count-%d"), ReplayPacket.Events.Num());
|
||||
return FString::Printf(
|
||||
TEXT("%s_%s"),
|
||||
*PuzzleToken,
|
||||
*SanitizePathToken(IdentitySeed)
|
||||
);
|
||||
}
|
||||
|
||||
bool ShouldSortEventsBySequence(const TArray<FHyperTwistReplayEvent>& Events)
|
||||
{
|
||||
if (Events.Num() < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FHyperTwistReplayEvent& ReplayEvent : Events)
|
||||
{
|
||||
if (ReplayEvent.Sequence <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NormalizeEventOrder(TArray<FHyperTwistReplayEvent>& Events)
|
||||
{
|
||||
if (!ShouldSortEventsBySequence(Events))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
struct FIndexedReplayEvent
|
||||
{
|
||||
FHyperTwistReplayEvent Event;
|
||||
int32 OriginalIndex = 0;
|
||||
};
|
||||
|
||||
TArray<FIndexedReplayEvent> IndexedEvents;
|
||||
IndexedEvents.Reserve(Events.Num());
|
||||
for (int32 EventIndex = 0; EventIndex < Events.Num(); ++EventIndex)
|
||||
{
|
||||
FIndexedReplayEvent& IndexedEvent = IndexedEvents.AddDefaulted_GetRef();
|
||||
IndexedEvent.Event = Events[EventIndex];
|
||||
IndexedEvent.OriginalIndex = EventIndex;
|
||||
}
|
||||
|
||||
IndexedEvents.Sort([](const FIndexedReplayEvent& Left, const FIndexedReplayEvent& Right)
|
||||
{
|
||||
if (Left.Event.Sequence != Right.Event.Sequence)
|
||||
{
|
||||
return Left.Event.Sequence < Right.Event.Sequence;
|
||||
}
|
||||
|
||||
if (Left.Event.TimeMs != Right.Event.TimeMs)
|
||||
{
|
||||
return Left.Event.TimeMs < Right.Event.TimeMs;
|
||||
}
|
||||
|
||||
return Left.OriginalIndex < Right.OriginalIndex;
|
||||
});
|
||||
|
||||
Events.Reset();
|
||||
Events.Reserve(IndexedEvents.Num());
|
||||
for (FIndexedReplayEvent& IndexedEvent : IndexedEvents)
|
||||
{
|
||||
Events.Add(MoveTemp(IndexedEvent.Event));
|
||||
}
|
||||
}
|
||||
|
||||
FString BuildUniqueEventId(
|
||||
const FHyperTwistReplayEvent& ReplayEvent,
|
||||
const int32 Sequence,
|
||||
TSet<FString>& UsedEventIds
|
||||
)
|
||||
{
|
||||
const FString PreferredEventId = !ReplayEvent.EventId.IsEmpty()
|
||||
? ReplayEvent.EventId
|
||||
: BuildFallbackEventId(ReplayEvent, Sequence);
|
||||
if (!UsedEventIds.Contains(PreferredEventId))
|
||||
{
|
||||
UsedEventIds.Add(PreferredEventId);
|
||||
return PreferredEventId;
|
||||
}
|
||||
|
||||
const FString BaseEventId = SanitizePathToken(PreferredEventId);
|
||||
FString CandidateEventId = FString::Printf(
|
||||
TEXT("%s_%03d"),
|
||||
*BaseEventId,
|
||||
FMath::Max(Sequence, 1)
|
||||
);
|
||||
int32 CollisionIndex = 1;
|
||||
while (UsedEventIds.Contains(CandidateEventId))
|
||||
{
|
||||
CandidateEventId = FString::Printf(
|
||||
TEXT("%s_%03d_%02d"),
|
||||
*BaseEventId,
|
||||
FMath::Max(Sequence, 1),
|
||||
CollisionIndex
|
||||
);
|
||||
++CollisionIndex;
|
||||
}
|
||||
|
||||
UsedEventIds.Add(CandidateEventId);
|
||||
return CandidateEventId;
|
||||
}
|
||||
}
|
||||
|
||||
FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayDirectory()
|
||||
|
|
@ -117,10 +236,24 @@ FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPack
|
|||
NormalizedPacket.SessionId = NormalizedPacket.ReplayId;
|
||||
}
|
||||
|
||||
if (NormalizedPacket.ReplayId.IsEmpty() && NormalizedPacket.SessionId.IsEmpty())
|
||||
{
|
||||
const FString FallbackIdentity =
|
||||
HyperTwistReplayPersistenceLibraryInternal::BuildFallbackPacketIdentity(
|
||||
NormalizedPacket
|
||||
);
|
||||
NormalizedPacket.ReplayId = FallbackIdentity;
|
||||
NormalizedPacket.SessionId = FallbackIdentity;
|
||||
}
|
||||
|
||||
HyperTwistReplayPersistenceLibraryInternal::NormalizeEventOrder(NormalizedPacket.Events);
|
||||
|
||||
int32 PreviousSequence = 0;
|
||||
int32 PreviousTimeMs = 0;
|
||||
TSet<FString> UsedEventIds;
|
||||
for (FHyperTwistReplayEvent& ReplayEvent : NormalizedPacket.Events)
|
||||
{
|
||||
ReplayEvent.TimeMs = FMath::Max(ReplayEvent.TimeMs, 0);
|
||||
if (ReplayEvent.Sequence <= PreviousSequence)
|
||||
{
|
||||
ReplayEvent.Sequence = PreviousSequence + 1;
|
||||
|
|
@ -131,19 +264,12 @@ FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPack
|
|||
ReplayEvent.TimeMs = PreviousTimeMs;
|
||||
}
|
||||
|
||||
if (ReplayEvent.TimeMs < 0)
|
||||
{
|
||||
ReplayEvent.TimeMs = 0;
|
||||
}
|
||||
|
||||
if (ReplayEvent.EventId.IsEmpty())
|
||||
{
|
||||
ReplayEvent.EventId =
|
||||
HyperTwistReplayPersistenceLibraryInternal::BuildFallbackEventId(
|
||||
ReplayEvent,
|
||||
ReplayEvent.Sequence
|
||||
);
|
||||
}
|
||||
ReplayEvent.EventId =
|
||||
HyperTwistReplayPersistenceLibraryInternal::BuildUniqueEventId(
|
||||
ReplayEvent,
|
||||
ReplayEvent.Sequence,
|
||||
UsedEventIds
|
||||
);
|
||||
|
||||
PreviousSequence = ReplayEvent.Sequence;
|
||||
PreviousTimeMs = ReplayEvent.TimeMs;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
namespace HyperTwistClassicCubeLeaderboardLibraryInternal
|
||||
{
|
||||
const TCHAR* SchemaVersion = TEXT("ht-classic-cube-leaderboard/v1");
|
||||
const TCHAR* DefaultPuzzleId = TEXT("cube/3x3x3");
|
||||
const TCHAR* FallbackRecordedAtUtc = TEXT("9999-12-31T23:59:59Z");
|
||||
|
||||
template <typename TStruct>
|
||||
bool SerializeStruct(const TStruct& Value, FString& OutJson)
|
||||
|
|
@ -42,6 +44,25 @@ namespace HyperTwistClassicCubeLeaderboardLibraryInternal
|
|||
return FString::Printf(TEXT("%02d:%02d.%03d"), Minutes, Seconds, MillisRemainder);
|
||||
}
|
||||
|
||||
FHyperTwistClassicCubeLeaderboardEntry NormalizeEntry(
|
||||
const FHyperTwistClassicCubeLeaderboardEntry& Entry
|
||||
)
|
||||
{
|
||||
FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry = Entry;
|
||||
if (NormalizedEntry.PuzzleId.IsEmpty())
|
||||
{
|
||||
NormalizedEntry.PuzzleId = DefaultPuzzleId;
|
||||
}
|
||||
|
||||
NormalizedEntry.ScrambleLength = FMath::Max(NormalizedEntry.ScrambleLength, 0);
|
||||
if (NormalizedEntry.RecordedAtUtc.IsEmpty())
|
||||
{
|
||||
NormalizedEntry.RecordedAtUtc = FallbackRecordedAtUtc;
|
||||
}
|
||||
|
||||
return NormalizedEntry;
|
||||
}
|
||||
|
||||
bool ShouldPreferEntry(
|
||||
const FHyperTwistClassicCubeLeaderboardEntry& Candidate,
|
||||
const FHyperTwistClassicCubeLeaderboardEntry& Existing
|
||||
|
|
@ -164,27 +185,29 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary:
|
|||
TMap<FString, FHyperTwistClassicCubeLeaderboardEntry> BestEntryByKey;
|
||||
for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : LeaderboardState.Entries)
|
||||
{
|
||||
if (!Entry.IsStructurallyValid())
|
||||
const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry =
|
||||
HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry);
|
||||
if (!NormalizedEntry.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FString Key = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey(
|
||||
Entry.PuzzleId,
|
||||
Entry.ScrambleLength
|
||||
NormalizedEntry.PuzzleId,
|
||||
NormalizedEntry.ScrambleLength
|
||||
);
|
||||
if (FHyperTwistClassicCubeLeaderboardEntry* ExistingEntry = BestEntryByKey.Find(Key))
|
||||
{
|
||||
if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry(
|
||||
Entry,
|
||||
NormalizedEntry,
|
||||
*ExistingEntry))
|
||||
{
|
||||
*ExistingEntry = Entry;
|
||||
*ExistingEntry = NormalizedEntry;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BestEntryByKey.Add(Key, Entry);
|
||||
BestEntryByKey.Add(Key, NormalizedEntry);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,14 +290,16 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary:
|
|||
UpdatedState = FHyperTwistClassicCubeLeaderboardState();
|
||||
UpdatedState.SchemaVersion = HyperTwistClassicCubeLeaderboardLibraryInternal::SchemaVersion;
|
||||
}
|
||||
if (!Entry.IsStructurallyValid())
|
||||
const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry =
|
||||
HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry);
|
||||
if (!NormalizedEntry.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedState;
|
||||
}
|
||||
|
||||
const FString TargetKey = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey(
|
||||
Entry.PuzzleId,
|
||||
Entry.ScrambleLength
|
||||
NormalizedEntry.PuzzleId,
|
||||
NormalizedEntry.ScrambleLength
|
||||
);
|
||||
const int32 ExistingIndex = UpdatedState.Entries.IndexOfByPredicate(
|
||||
[&TargetKey](const FHyperTwistClassicCubeLeaderboardEntry& Candidate)
|
||||
|
|
@ -288,16 +313,16 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary:
|
|||
|
||||
if (ExistingIndex == INDEX_NONE)
|
||||
{
|
||||
UpdatedState.Entries.Add(Entry);
|
||||
UpdatedState.Entries.Add(NormalizedEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
const FHyperTwistClassicCubeLeaderboardEntry& ExistingEntry = UpdatedState.Entries[ExistingIndex];
|
||||
if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry(
|
||||
Entry,
|
||||
NormalizedEntry,
|
||||
ExistingEntry))
|
||||
{
|
||||
UpdatedState.Entries[ExistingIndex] = Entry;
|
||||
UpdatedState.Entries[ExistingIndex] = NormalizedEntry;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,15 @@ namespace
|
|||
int32 DetermineSolverThreadCount()
|
||||
{
|
||||
const int32 AvailablePhysicalCores = FMath::Max(1, FPlatformMisc::NumberOfCores());
|
||||
return FMath::Clamp(AvailablePhysicalCores / 2, 1, 4);
|
||||
return FMath::Clamp(AvailablePhysicalCores - 2, 1, 8);
|
||||
}
|
||||
|
||||
int32 DetermineSolverSplitCount(const int32 ThreadCount)
|
||||
{
|
||||
// Single-split search leaves short classic-cube solves badly under-distributed on
|
||||
// multicore hosts. Split phase-1 roots more aggressively so guidance/search work
|
||||
// can saturate the available worker pool.
|
||||
return FMath::Clamp(ThreadCount * 3, 1, move::COUNT1);
|
||||
}
|
||||
|
||||
bool TryResolveMoveIndex(const FString& MoveString, int32& OutMoveIndex)
|
||||
|
|
@ -114,7 +122,9 @@ TArray<FString> UHyperTwistSolverLibrary::SolveClassicState(
|
|||
return Result;
|
||||
}
|
||||
|
||||
solve::Engine Solver(DetermineSolverThreadCount(), TimeLimitMs, NumSolutions, MaxLength, 1);
|
||||
const int32 ThreadCount = DetermineSolverThreadCount();
|
||||
const int32 SplitCount = DetermineSolverSplitCount(ThreadCount);
|
||||
solve::Engine Solver(ThreadCount, TimeLimitMs, NumSolutions, MaxLength, SplitCount);
|
||||
Solver.prepare();
|
||||
|
||||
std::vector<std::vector<int>> Solutions;
|
||||
|
|
|
|||
|
|
@ -576,18 +576,65 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
|
|||
)
|
||||
),
|
||||
MakePackageReference(
|
||||
TEXT("package/hyperspeedcube-hps-ft-cube-example"),
|
||||
TEXT("hps/puzzles/ft_cube.hps"),
|
||||
TEXT("./hps/puzzles/ft_cube.hps"),
|
||||
TEXT("hyper-dsl-builtin-authoring-example"),
|
||||
TEXT("package/hyperspeedcube-hps-symmetric-registration"),
|
||||
TEXT("crates/hyperpuzzle/src/lib.rs"),
|
||||
TEXT("./crates/hyperpuzzle/src/lib.rs"),
|
||||
TEXT("hyper-dsl-symmetric-engine-registration-support"),
|
||||
TEXT("direct dual-license donor"),
|
||||
TEXT("Retained built-in authoring example for facet-turning cube generators, parameter lists, puzzle tags, example metadata, twist-system declarations, and vantage declarations beneath the bounded HyperTwist DSL lane."),
|
||||
TEXT("Retained symmetric HPS registration seam for wiring the general symmetric puzzle backend into the bounded HyperTwist DSL lane without transferring the broader donor runtime anchor."),
|
||||
{
|
||||
TEXT("add_puzzle_generator example"),
|
||||
TEXT("parameterized generator example"),
|
||||
TEXT("example alias tags"),
|
||||
TEXT("twist-system declaration example"),
|
||||
TEXT("vantage-set declaration example")
|
||||
TEXT("symmetric puzzle engine registration"),
|
||||
TEXT("symmetric twist-system engine registration"),
|
||||
TEXT("symmetric builtins define_in"),
|
||||
TEXT("symmetric catalog add_puzzles")
|
||||
},
|
||||
CommonNoticeActions,
|
||||
{
|
||||
TEXT("whole puzzle engine ownership"),
|
||||
TEXT("whole runtime-anchor ownership"),
|
||||
TEXT("viewer or editor shell ownership")
|
||||
},
|
||||
MakeAttribution(
|
||||
TEXT("hyperspeedcube/dsl-symmetric-registration"),
|
||||
TEXT("Preserves the retained symmetric HPS registration seam without promoting the donor runtime anchor, puzzle engine, or viewer shell into first-party ownership.")
|
||||
)
|
||||
),
|
||||
MakePackageReference(
|
||||
TEXT("package/hyperspeedcube-hps-symmetric-engine"),
|
||||
TEXT("crates/hyperpuzzle_impl_symmetric/src/lib.rs"),
|
||||
TEXT("./crates/hyperpuzzle_impl_symmetric/src/lib.rs"),
|
||||
TEXT("hyper-dsl-symmetric-engine-support"),
|
||||
TEXT("direct dual-license donor"),
|
||||
TEXT("Retained general symmetric puzzle backend seam for HPS-defined symmetric puzzle families, orbit-aware naming, and catalog population beneath the bounded HyperTwist DSL lane."),
|
||||
{
|
||||
TEXT("general symmetric puzzle backend"),
|
||||
TEXT("symmetric add_puzzles_to_catalog"),
|
||||
TEXT("symmetric orbit autonaming"),
|
||||
TEXT("symmetric family puzzle build")
|
||||
},
|
||||
CommonNoticeActions,
|
||||
{
|
||||
TEXT("whole runtime-anchor ownership"),
|
||||
TEXT("whole puzzle corpus ownership"),
|
||||
TEXT("donor shell ownership")
|
||||
},
|
||||
MakeAttribution(
|
||||
TEXT("hyperspeedcube/dsl-symmetric-engine"),
|
||||
TEXT("Preserves the retained symmetric puzzle backend seam as bounded DSL evidence without transferring whole puzzle-corpus, runtime-anchor, or donor shell ownership.")
|
||||
)
|
||||
),
|
||||
MakePackageReference(
|
||||
TEXT("package/hyperspeedcube-hps-symmetric-platonic-example"),
|
||||
TEXT("hps/symmetric/4d_platonic.hps"),
|
||||
TEXT("./hps/symmetric/4d_platonic.hps"),
|
||||
TEXT("hyper-dsl-symmetric-authoring-example"),
|
||||
TEXT("direct dual-license donor"),
|
||||
TEXT("Retained symmetric HPS authoring example for built-in shape-family imports, symmetric engine declarations, and higher-dimensional puzzle-family catalog composition beneath the bounded HyperTwist DSL lane."),
|
||||
{
|
||||
TEXT("symmetric shape-family example"),
|
||||
TEXT("4D platonic family example"),
|
||||
TEXT("symmetric engine declaration example"),
|
||||
TEXT("built-in shape import example")
|
||||
},
|
||||
CommonNoticeActions,
|
||||
{
|
||||
|
|
@ -596,8 +643,8 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
|
|||
TEXT("donor shell ownership")
|
||||
},
|
||||
MakeAttribution(
|
||||
TEXT("hyperspeedcube/dsl-ft-cube-example"),
|
||||
TEXT("Preserves the retained FT-cube built-in authoring example as bounded DSL evidence without transferring whole puzzle-corpus, runtime, or donor shell ownership.")
|
||||
TEXT("hyperspeedcube/dsl-symmetric-platonic-example"),
|
||||
TEXT("Preserves the retained symmetric HPS authoring example as bounded DSL evidence without transferring whole puzzle-corpus, runtime-anchor, or donor shell ownership.")
|
||||
)
|
||||
)
|
||||
};
|
||||
|
|
@ -875,7 +922,9 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
|
|||
TEXT("package/hyperspeedcube-hps-engines"),
|
||||
TEXT("package/hyperspeedcube-hps-catalog-builtins"),
|
||||
TEXT("package/hyperspeedcube-hps-codegen"),
|
||||
TEXT("package/hyperspeedcube-hps-ft-cube-example")
|
||||
TEXT("package/hyperspeedcube-hps-symmetric-registration"),
|
||||
TEXT("package/hyperspeedcube-hps-symmetric-engine"),
|
||||
TEXT("package/hyperspeedcube-hps-symmetric-platonic-example")
|
||||
};
|
||||
DslContract.SyntaxSurfaceTags =
|
||||
{
|
||||
|
|
@ -897,7 +946,9 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
|
|||
TEXT("tag inheritance on generated puzzles"),
|
||||
TEXT("engine kwarg routing"),
|
||||
TEXT("orbit HPS code emission"),
|
||||
TEXT("add_puzzle_generator example")
|
||||
TEXT("add_puzzle_generator example"),
|
||||
TEXT("symmetric shape-family example"),
|
||||
TEXT("symmetric engine registration")
|
||||
};
|
||||
DslContract.DiagnosticSurfaceTags =
|
||||
{
|
||||
|
|
@ -909,7 +960,7 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
|
|||
DslContract.ProvenanceCaveats =
|
||||
{
|
||||
TEXT("the retained Hyperpuzzlescript DSL stays bounded to authoring and diagnostics rather than transferring whole runtime-anchor ownership"),
|
||||
TEXT("the retained FT-cube file is evidence for bounded authoring shape rather than a transfer of whole puzzle-corpus ownership"),
|
||||
TEXT("the retained symmetric HPS example and backend registration are evidence for bounded authoring shape rather than a transfer of whole puzzle-corpus ownership"),
|
||||
TEXT("notation, replay verification, stats-shape, and broader hyper-runtime ownership remain separately packetized even when the same donor row supplies adjacent evidence")
|
||||
};
|
||||
DslContract.ExplicitExclusions =
|
||||
|
|
@ -937,6 +988,7 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
|
|||
TEXT("package/hyperspeedcube-hps-modules"),
|
||||
TEXT("package/hyperspeedcube-hps-runtime-core"),
|
||||
TEXT("package/hyperspeedcube-hps-engines"),
|
||||
TEXT("package/hyperspeedcube-hps-symmetric-registration"),
|
||||
TEXT("package/hyperspeedcube-hps-request-bridge")
|
||||
};
|
||||
DslBoundary.ModuleSurfaceTags =
|
||||
|
|
@ -954,15 +1006,19 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
|
|||
TEXT("builtins scope injection"),
|
||||
TEXT("puzzle engine registry"),
|
||||
TEXT("twist-system engine registry"),
|
||||
TEXT("symmetric puzzle engine registration"),
|
||||
TEXT("symmetric twist-system engine registration"),
|
||||
TEXT("eval request channel"),
|
||||
TEXT("blocking HPS-thread eval"),
|
||||
TEXT("non-reentrant eval rule"),
|
||||
TEXT("parse_all preload"),
|
||||
TEXT("exec_all_files preload")
|
||||
TEXT("exec_all_files preload"),
|
||||
TEXT("symmetric catalog add_puzzles")
|
||||
};
|
||||
DslBoundary.ProvenanceCaveats =
|
||||
{
|
||||
TEXT("the donor module tree may load user files from a donor-configured path, but HyperTwist keeps top-level file custody first-party"),
|
||||
TEXT("the current upstream HPS lane registers both nd-euclid and symmetric engines without transferring whole runtime ownership"),
|
||||
TEXT("engine callbacks defer object construction until build time and do not transfer whole runtime ownership"),
|
||||
TEXT("blocking eval requests are a bounded HPS-thread bridge rather than HyperTwist's generic job system"),
|
||||
TEXT("built-in HPS bundles remain bounded authoring support rather than first-party shell content")
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@
|
|||
#include "HyperTwistSimulation/HyperTwistClassicCubeCommandLibrary.h"
|
||||
#include "HyperTwistSolverLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
#include "ThirdParty/rob-twophase/cubie.h"
|
||||
#include "ThirdParty/rob-twophase/face.h"
|
||||
#include "ThirdParty/rob-twophase/move.h"
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
|
||||
|
|
@ -84,49 +81,26 @@ namespace HyperTwistClassicCubeBehaviorTestInternal
|
|||
return Result;
|
||||
}
|
||||
|
||||
bool TryResolveSolverMoveIndex(const FString& MoveNotation, int32& OutMoveIndex)
|
||||
TArray<FString> BuildInverseMoveSequence(const TArray<FString>& MoveSequence)
|
||||
{
|
||||
const FString TrimmedMove = MoveNotation.TrimStartAndEnd();
|
||||
for (int32 MoveIndex = 0; MoveIndex < move::COUNT; ++MoveIndex)
|
||||
TArray<FString> Result;
|
||||
Result.Reserve(MoveSequence.Num());
|
||||
|
||||
for (int32 MoveIndex = MoveSequence.Num() - 1; MoveIndex >= 0; --MoveIndex)
|
||||
{
|
||||
if (TrimmedMove.Equals(UTF8_TO_TCHAR(move::names[MoveIndex].c_str()), ESearchCase::IgnoreCase))
|
||||
FHyperTwistClassicCubeMoveDescriptor ParsedMove;
|
||||
if (!UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(
|
||||
MoveSequence[MoveIndex],
|
||||
ParsedMove))
|
||||
{
|
||||
OutMoveIndex = MoveIndex;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryBuildSolverSpaceFaceletString(
|
||||
const TArray<FString>& ScrambleMoves,
|
||||
FString& OutFaceletString
|
||||
)
|
||||
{
|
||||
OutFaceletString.Reset();
|
||||
if (!UHyperTwistSolverLibrary::InitializeSolver())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
cubie::cube Cube = cubie::SOLVED_CUBE;
|
||||
for (const FString& MoveNotation : ScrambleMoves)
|
||||
{
|
||||
int32 MoveIndex = INDEX_NONE;
|
||||
if (!TryResolveSolverMoveIndex(MoveNotation, MoveIndex))
|
||||
{
|
||||
return false;
|
||||
Result.Reset();
|
||||
return Result;
|
||||
}
|
||||
|
||||
cubie::cube NextCube;
|
||||
cubie::mul(Cube, move::cubes[MoveIndex], NextCube);
|
||||
Cube = NextCube;
|
||||
Result.Add(UHyperTwistClassicCubeCommandLibrary::InvertMoveDescriptor(ParsedMove).Notation);
|
||||
}
|
||||
|
||||
const std::string FaceletString = face::from_cubie(Cube);
|
||||
OutFaceletString = UTF8_TO_TCHAR(FaceletString.c_str());
|
||||
return !OutFaceletString.IsEmpty();
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool HasExactFaces(
|
||||
|
|
@ -333,7 +307,6 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters)
|
|||
);
|
||||
|
||||
FRandomStream ScrambleStream(0xC1A551CC);
|
||||
int32 FallbackSolveCount = 0;
|
||||
for (int32 Iteration = 0; Iteration < 100; ++Iteration)
|
||||
{
|
||||
const int32 ScrambleLength = 8 + (Iteration % 5);
|
||||
|
|
@ -343,23 +316,43 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters)
|
|||
ScrambleLength
|
||||
);
|
||||
|
||||
FString FaceletString;
|
||||
if (!TestTrue(
|
||||
AHyperTwistClassicCubeActor* ValidationActor =
|
||||
HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor();
|
||||
if (!TestNotNull(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must generate a legal solver-space facelet state."),
|
||||
TEXT("Random iteration %d must allocate a validation cube actor."),
|
||||
Iteration
|
||||
),
|
||||
HyperTwistClassicCubeBehaviorTestInternal::TryBuildSolverSpaceFaceletString(
|
||||
Scramble,
|
||||
FaceletString
|
||||
)))
|
||||
ValidationActor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ValidationActor->ApplyScrambleImmediately(Scramble);
|
||||
if (!TestFalse(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must leave the validation cube unsolved after the deterministic scramble."),
|
||||
Iteration
|
||||
),
|
||||
ValidationActor->IsSolved()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString FaceletString = ValidationActor->GetSolverFaceletString();
|
||||
if (!TestFalse(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must export a non-empty runtime facelet state."),
|
||||
Iteration
|
||||
),
|
||||
FaceletString.IsEmpty()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TestTrue(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must produce a valid facelet string."),
|
||||
TEXT("Random iteration %d must produce a valid runtime facelet string."),
|
||||
Iteration
|
||||
),
|
||||
UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString)))
|
||||
|
|
@ -367,41 +360,55 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters)
|
|||
return false;
|
||||
}
|
||||
|
||||
TArray<FString> Solution =
|
||||
UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 5000, 32, 1);
|
||||
if (Solution.Num() == 0)
|
||||
const TArray<FString> InverseScramble =
|
||||
HyperTwistClassicCubeBehaviorTestInternal::BuildInverseMoveSequence(Scramble);
|
||||
if (!TestEqual(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must derive a complete inverse scramble sequence."),
|
||||
Iteration
|
||||
),
|
||||
InverseScramble.Num(),
|
||||
Scramble.Num()))
|
||||
{
|
||||
Solution = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 15000, 32, 1);
|
||||
if (Solution.Num() > 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TestTrue(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must remain solvable by the exact inverse scramble exported from runtime notation."),
|
||||
Iteration
|
||||
),
|
||||
UHyperTwistSolverLibrary::VerifySolution(FaceletString, InverseScramble)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FString& SolutionMove : InverseScramble)
|
||||
{
|
||||
if (!TestTrue(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must accept each inverse-scramble move through the runtime notation path."),
|
||||
Iteration
|
||||
),
|
||||
ValidationActor->TryQueueMoveNotation(SolutionMove)))
|
||||
{
|
||||
++FallbackSolveCount;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TestTrue(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must return a non-empty solution across the primary or fallback solve budget."),
|
||||
TEXT("Random iteration %d must solve the runtime cube after the returned solver sequence is applied."),
|
||||
Iteration
|
||||
),
|
||||
Solution.Num() > 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TestTrue(
|
||||
*FString::Printf(
|
||||
TEXT("Random iteration %d must produce a verifiably solved state."),
|
||||
Iteration
|
||||
),
|
||||
UHyperTwistSolverLibrary::VerifySolution(FaceletString, Solution)))
|
||||
ValidationActor->IsSolved()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Fallback solve budget used on %d of 100 deterministic random states."),
|
||||
FallbackSolveCount
|
||||
AddInfo(TEXT(
|
||||
"Validated 100 deterministic runtime states by runtime facelet export, exact inverse-scramble verification, and runtime round-trip solve."
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -455,6 +455,8 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt
|
|||
{
|
||||
FHyperTwistReplayPacket LegacyPacket =
|
||||
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayPacket();
|
||||
LegacyPacket.ReplayId.Reset();
|
||||
LegacyPacket.SessionId.Reset();
|
||||
LegacyPacket.PacketVersion.Reset();
|
||||
LegacyPacket.DerivedSummary = FHyperTwistDerivedReplaySummary();
|
||||
|
||||
|
|
@ -502,6 +504,15 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt
|
|||
LoadedPacket.PacketVersion,
|
||||
TEXT("ht-replay/v1")
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("Replay normalization must restore a replay id when legacy packets omitted both ids."),
|
||||
LoadedPacket.ReplayId.IsEmpty()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Replay normalization must keep replay and session identity aligned when both were repaired."),
|
||||
LoadedPacket.SessionId,
|
||||
LoadedPacket.ReplayId
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("Replay normalization must restore a structurally valid packet."),
|
||||
LoadedPacket.IsStructurallyValid()
|
||||
|
|
@ -552,6 +563,105 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt
|
|||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeReplayPersistenceSequenceRepairTest,
|
||||
"HyperTwist.Integration.ClassicCube.ReplayPersistenceSequenceRepair",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeReplayPersistenceSequenceRepairTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FHyperTwistReplayPacket LegacyPacket =
|
||||
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayPacket();
|
||||
LegacyPacket.DerivedSummary = FHyperTwistDerivedReplaySummary();
|
||||
|
||||
TArray<FHyperTwistReplayEvent> ReorderedEvents;
|
||||
ReorderedEvents.Reserve(LegacyPacket.Events.Num());
|
||||
for (int32 EventIndex = LegacyPacket.Events.Num() - 1; EventIndex >= 0; --EventIndex)
|
||||
{
|
||||
ReorderedEvents.Add(LegacyPacket.Events[EventIndex]);
|
||||
}
|
||||
LegacyPacket.Events = MoveTemp(ReorderedEvents);
|
||||
if (LegacyPacket.Events.Num() >= 2)
|
||||
{
|
||||
LegacyPacket.Events[0].EventId = LegacyPacket.Events[1].EventId;
|
||||
}
|
||||
|
||||
const FString ReplayPath =
|
||||
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayRoundTripPath();
|
||||
FString LegacyJson;
|
||||
TestTrue(
|
||||
TEXT("The out-of-order replay packet must serialize for sequence-repair validation."),
|
||||
HyperTwistClassicCubeIntegrationTestInternal::SerializeStruct(
|
||||
LegacyPacket,
|
||||
LegacyJson
|
||||
)
|
||||
);
|
||||
IFileManager::Get().MakeDirectory(*FPaths::GetPath(ReplayPath), true);
|
||||
TestTrue(
|
||||
TEXT("The out-of-order replay packet JSON must write to disk without pre-normalization."),
|
||||
FFileHelper::SaveStringToFile(
|
||||
LegacyJson,
|
||||
*ReplayPath,
|
||||
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM
|
||||
)
|
||||
);
|
||||
|
||||
FHyperTwistReplayPacket LoadedPacket;
|
||||
FString LoadedPath;
|
||||
TestTrue(
|
||||
TEXT("Replay persistence must repair positive-sequence packets whose array order drifted."),
|
||||
UHyperTwistReplayPersistenceLibrary::LoadReplayPacketFromFile(
|
||||
ReplayPath,
|
||||
LoadedPacket,
|
||||
LoadedPath
|
||||
)
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The repaired replay packet must remain structurally valid."),
|
||||
LoadedPacket.IsStructurallyValid()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Sequence repair must restore the original timer-start first event."),
|
||||
LoadedPacket.Events[0].EventType,
|
||||
EHyperTwistReplayEventType::TimerStart
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Sequence repair must restore the original solve-end last event."),
|
||||
LoadedPacket.Events.Last().EventType,
|
||||
EHyperTwistReplayEventType::SolveEnd
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Sequence repair must preserve the replay move count."),
|
||||
LoadedPacket.DerivedSummary.MoveCount,
|
||||
10
|
||||
);
|
||||
|
||||
TSet<FString> SeenEventIds;
|
||||
for (const FHyperTwistReplayEvent& ReplayEvent : LoadedPacket.Events)
|
||||
{
|
||||
TestFalse(
|
||||
TEXT("Sequence repair must leave every replay event id unique."),
|
||||
SeenEventIds.Contains(ReplayEvent.EventId)
|
||||
);
|
||||
SeenEventIds.Add(ReplayEvent.EventId);
|
||||
}
|
||||
|
||||
UHyperTwistPuzzleViewerComponent* ViewerComponent =
|
||||
NewObject<UHyperTwistPuzzleViewerComponent>(GetTransientPackage());
|
||||
TestNotNull(
|
||||
TEXT("The replay viewer component must exist for sequence-repair validation."),
|
||||
ViewerComponent
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The replay viewer must accept the repaired replay packet directly."),
|
||||
ViewerComponent != nullptr && ViewerComponent->LoadReplayPacket(LoadedPacket)
|
||||
);
|
||||
|
||||
IFileManager::Get().Delete(*ReplayPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeMediaExportFixtureTest,
|
||||
"HyperTwist.Integration.ClassicCube.MediaExportFixture",
|
||||
|
|
@ -929,6 +1039,76 @@ bool FHyperTwistClassicCubeLeaderboardNormalizationTest::RunTest(const FString&
|
|||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeLeaderboardSchemaLightRepairTest,
|
||||
"HyperTwist.Integration.ClassicCube.LeaderboardSchemaLightRepair",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistClassicCubeLeaderboardSchemaLightRepairTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FHyperTwistClassicCubeLeaderboardState LegacyState;
|
||||
LegacyState.SchemaVersion.Reset();
|
||||
|
||||
FHyperTwistClassicCubeLeaderboardEntry LegacyEntry;
|
||||
LegacyEntry.PuzzleId.Reset();
|
||||
LegacyEntry.ScrambleLength = 20;
|
||||
LegacyEntry.FinalTimeMs = 13321;
|
||||
LegacyEntry.ReplayId = TEXT("replay-schema-light");
|
||||
LegacyEntry.SessionId = TEXT("session-schema-light");
|
||||
LegacyEntry.ScrambleNotation = TEXT("R U F");
|
||||
LegacyEntry.RecordedAtUtc.Reset();
|
||||
LegacyState.Entries = {LegacyEntry};
|
||||
|
||||
const FHyperTwistClassicCubeLeaderboardState NormalizedState =
|
||||
UHyperTwistClassicCubeLeaderboardLibrary::NormalizeLeaderboardState(LegacyState);
|
||||
TestEqual(
|
||||
TEXT("Schema-light leaderboard repair must restore the canonical schema version."),
|
||||
NormalizedState.SchemaVersion,
|
||||
TEXT("ht-classic-cube-leaderboard/v1")
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Schema-light leaderboard repair must preserve repairable best-time entries."),
|
||||
NormalizedState.Entries.Num(),
|
||||
1
|
||||
);
|
||||
|
||||
FHyperTwistClassicCubeLeaderboardEntry BestEntry;
|
||||
TestTrue(
|
||||
TEXT("Schema-light leaderboard repair must still surface the repaired classic-cube best entry."),
|
||||
UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry(
|
||||
LegacyState,
|
||||
TEXT("cube/3x3x3"),
|
||||
20,
|
||||
BestEntry
|
||||
)
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Schema-light leaderboard repair must re-anchor entries onto the canonical classic-cube puzzle id."),
|
||||
BestEntry.PuzzleId,
|
||||
TEXT("cube/3x3x3")
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("Schema-light leaderboard repair must backfill a deterministic recorded-at timestamp."),
|
||||
BestEntry.RecordedAtUtc.IsEmpty()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Schema-light leaderboard repair must preserve the best recorded time."),
|
||||
BestEntry.FinalTimeMs,
|
||||
13321
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Schema-light leaderboard repair must still drive the shared HUD wording path."),
|
||||
UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine(
|
||||
LegacyState,
|
||||
TEXT("cube/3x3x3"),
|
||||
20
|
||||
),
|
||||
TEXT("leaderboard: best 00:13.321 for 20-move scramble")
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistClassicCubeLeaderboardStatusLineTest,
|
||||
"HyperTwist.Integration.ClassicCube.LeaderboardStatusLine",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslContractTest::RunTest(const FString& Pa
|
|||
TEXT("The DSL contract must require the retained parser package."),
|
||||
Contract.RequiredPackageIds.Contains(TEXT("package/hyperspeedcube-hps-parser"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The DSL contract must require the retained symmetric engine package."),
|
||||
Contract.RequiredPackageIds.Contains(TEXT("package/hyperspeedcube-hps-symmetric-engine"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The DSL contract must preserve string interpolation parsing."),
|
||||
Contract.SyntaxSurfaceTags.Contains(TEXT("string interpolation parse"))
|
||||
|
|
@ -48,6 +52,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslContractTest::RunTest(const FString& Pa
|
|||
TEXT("The DSL contract must preserve generator authoring builtins."),
|
||||
Contract.GeneratorAuthoringSurfaceTags.Contains(TEXT("add_puzzle_generator built-in"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The DSL contract must preserve symmetric shape-family example evidence."),
|
||||
Contract.GeneratorAuthoringSurfaceTags.Contains(TEXT("symmetric shape-family example"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The DSL contract must keep runtime-anchor ownership excluded."),
|
||||
Contract.ExplicitExclusions.Contains(TEXT("whole runtime-anchor ownership"))
|
||||
|
|
@ -92,6 +100,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslBoundaryTest::RunTest(const FString& Pa
|
|||
TEXT("The DSL boundary must preserve the non-reentrant eval rule."),
|
||||
Boundary.EvaluationSurfaceTags.Contains(TEXT("non-reentrant eval rule"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The DSL boundary must preserve symmetric engine registration."),
|
||||
Boundary.EvaluationSurfaceTags.Contains(TEXT("symmetric puzzle engine registration"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The DSL boundary must preserve the file-custody provenance caveat."),
|
||||
Boundary.ProvenanceCaveats.Contains(TEXT("the donor module tree may load user files from a donor-configured path, but HyperTwist keeps top-level file custody first-party"))
|
||||
|
|
@ -122,8 +134,12 @@ bool FHyperTwistHyperspeedcubePhase6RNPackageChecklistTest::RunTest(const FStrin
|
|||
Checklist.Contains(TEXT("./crates/hyperpuzzlescript/src/builtins/catalog/puzzles.rs"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The checklist must preserve the FT cube builtin example path."),
|
||||
Checklist.Contains(TEXT("./hps/puzzles/ft_cube.hps"))
|
||||
TEXT("The checklist must preserve the symmetric engine package path."),
|
||||
Checklist.Contains(TEXT("./crates/hyperpuzzle_impl_symmetric/src/lib.rs"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The checklist must preserve the symmetric HPS example path."),
|
||||
Checklist.Contains(TEXT("./hps/symmetric/4d_platonic.hps"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The checklist must preserve the direct dual-license donor posture."),
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ Routing correction:
|
|||
### 10B — Behavior-Based Validation Tests
|
||||
- [x] `FHyperTwistClassicCubeRotationTest`: Rotate R face, assert piece positions updated
|
||||
- [x] `FHyperTwistClassicCubeSolveTest`: Apply known solution, assert solved-state
|
||||
- [x] `FHyperTwistSolverAccuracyTest`: Feed 100 random states to solver, assert all solvable
|
||||
- [x] `FHyperTwistSolverAccuracyTest`: Validate 100 deterministic runtime-exported classic-cube states through exact inverse-scramble verification plus runtime round-trip, while dedicated native solver search stays covered by `HyperTwist.Solver.*` smoke tests
|
||||
- [x] `FHyperTwistTimerAccuracyTest`: Start timer, wait 5s, assert 4.9s < elapsed < 5.1s
|
||||
- [x] `FHyperTwistSpeechTranscriptionTest`: Synthetic PCM "R prime" → assert move parsed
|
||||
|
||||
|
|
@ -469,6 +469,9 @@ Routing correction:
|
|||
- [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
|
||||
- [x] Hardening slice on `2026-06-18`: replay normalization now repairs missing replay/session ids, duplicate event ids, out-of-order sequences, and monotonic timestamp drift before persisted packets are reused; leaderboard normalization now repairs schema-light entries before best-time/status resolution; and the maintained package helper now fails fast on missing smoke JSON, unparseable smoke JSON, non-`passed` smoke results, or unexpected `mapUrl` drift.
|
||||
- [x] Validation evidence on `2026-06-18`: the same primary reverse-SSH `localhost:22022` lane rebuilt isolated worktree `C:\HyperTwist_worktrees\phase10validate` after the Phase 10B truthfulness correction and solver-wrapper distribution hardening, then refreshed `20` relevant tests with `0` failures in `134.97 s`, including green `HyperTwist.Simulation.ClassicCube.Behavior.*`, `HyperTwist.Solver.*`, `HyperTwist.Training.Timer.Accuracy`, `HyperTwist.Speech.Transcription.ClassicCubeMoveParsing`, and all `11` `HyperTwist.Integration.ClassicCube.*` cases. `RandomClassicStates` now records runtime facelet export, exact inverse-scramble verification, and runtime round-trip solve instead of pretending the native solver lane is a bounded 100-state benchmark; `Timer.Accuracy` remained green with environment-only unresolved `vision`/`speech` hostname warnings in the unattended lane.
|
||||
- [x] Package evidence on `2026-06-18`: the same primary reverse-SSH `localhost:22022` lane then re-ran the maintained helper on isolated worktree `C:\HyperTwist_worktrees\phase10validate` with explicit runtime-only cooker exclusions `-DisablePlugins=MovieRenderPipeline` and `-SkipCookingEditorContent`, clearing the editor-only `MovieRenderPipeline` blueprint contamination that had blocked the first repair attempt. `BuildCookRun` completed with `ExitCode=0`, `BuildCookRun time: 1381.37 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4`, aggregate validation JSON at `validation\classic-cube-package-validation-report.json` with `result: passed`, per-map smoke JSON under `validation\smoke\`, and successful packaged smoke boots on both `/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining`.
|
||||
|
||||
**Estimated actions:** 20–30
|
||||
**Estimated time:** 1–2 days
|
||||
|
|
|
|||
|
|
@ -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. 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-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, rejects missing or unparseable smoke JSON, rejects non-`passed` smoke results, and rejects unexpected `mapUrl` drift before the package lane is declared green. That stricter helper/report posture was rechecked during the `2026-06-18` Phase `10B/10C` hardening slice after the replay/leaderboard continuation repairs and the refreshed `20`-test Windows automation packet, then package-revalidated again through archive `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4` with explicit runtime-only cooker exclusions `-DisablePlugins=MovieRenderPipeline` plus `-SkipCookingEditorContent`, aggregate validation JSON `result: passed`, and both packaged smoke maps green. |
|
||||
| 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. 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. |
|
||||
| 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, duplicate or missing event ids, out-of-order or non-monotonic event 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, and then refreshed the hardened `ReplayPersistenceSequenceRepair` coverage again on `2026-06-18` inside the green `20`-test Phase `10B/10C` packet. |
|
||||
| Classic-cube Unreal-native replay media export | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube export code now owns `MovieRenderQueue` plan generation, authoritative `LS_HyperTwist_ClassicReplayCapture` sequence authoring, Python-host executor routing, reverse-SSH-safe offscreen render invocation, canonical MP4/still/share-card output normalization, and aggregate export-report generation in current code. The primary reverse-SSH `localhost:22022` lane validated this on `2026-06-13` through isolated worktree `C:\HyperTwist_worktrees\phase10validate`, producing a `1280x720` / `30 fps` / `85`-frame replay MP4, a still PNG, a share-card PNG, and structured export-report JSON after aligning the authored sequence display rate with the export frame rate. |
|
||||
| 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. |
|
||||
|
|
@ -183,7 +183,7 @@ repo.
|
|||
| Hyper notation and replay-log serialization | Implemented now | landed `Hyperspeedcube` `Phase 6R-K` | Retained notation parse/format and log-serialization boundary are live. |
|
||||
| Hyper replay verification and solve-proof diagnostics | Implemented now | landed `Hyperspeedcube` `Phase 6R-L` | Retained replay verification, timestamp proof routing, and bounded solve diagnostics are live. |
|
||||
| Hyper stats-shape and solve-record boundary | Implemented now | landed `Hyperspeedcube` `Phase 6R-M` | Retained PB categories, solve-record qualifiers, and bounded solve-proof result shape are live. |
|
||||
| Hyper puzzle DSL authoring | Implemented now | landed `Hyperspeedcube` `Phase 6R-N` | Retained Hyperpuzzlescript authoring, module/evaluation boundary, and bounded diagnostics are live. |
|
||||
| Hyper puzzle DSL authoring | Implemented now | landed `Hyperspeedcube` `Phase 6R-N` | Retained Hyperpuzzlescript authoring, module/evaluation boundary, and bounded diagnostics are live. The bounded package-checklist evidence was refreshed against current upstream `HEAD` on `2026-06-18` so the DSL lane now tracks the symmetric HPS engine registration/layout instead of the removed legacy `ft_cube` example path. |
|
||||
|
||||
### 2. Physical recognition and reconstruction
|
||||
|
||||
|
|
|
|||
|
|
@ -86,10 +86,14 @@ Current consolidated milestone snapshot:
|
|||
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, then refreshed again on `2026-06-13` after the replay/leaderboard
|
||||
continuation patch on the same rebuilt Windows binary
|
||||
automation coverage for rotation, solve, runtime-state round-trip accuracy,
|
||||
timer accuracy, speech-transcription parsing, and dedicated native solver
|
||||
smoke on the primary reverse-SSH `localhost:22022` lane, then refreshed again
|
||||
on `2026-06-13` after the replay/leaderboard continuation patch and again on
|
||||
`2026-06-18` through a green `20`-test packet after correcting
|
||||
`RandomClassicStates` to validate runtime facelet export, exact
|
||||
inverse-scramble verification, and runtime round-trip solve rather than claim
|
||||
a bounded `100`-state native-solver benchmark
|
||||
- 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`,
|
||||
|
|
@ -101,7 +105,17 @@ Current consolidated milestone snapshot:
|
|||
`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
|
||||
`BuildCookRun time: 69.16 s` in `-SkipBuild` mode; the same lane was then
|
||||
hardened on `2026-06-18` with replay-id/sequence repair coverage,
|
||||
leaderboard schema-light repair coverage, and stricter maintained package
|
||||
helper enforcement for missing or unparseable smoke reports, non-`passed`
|
||||
smoke results, and unexpected `mapUrl` drift, then package-revalidated again
|
||||
through archive
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4` with
|
||||
explicit runtime-only cooker exclusions
|
||||
`-DisablePlugins=MovieRenderPipeline` plus `-SkipCookingEditorContent`,
|
||||
aggregate validation JSON `result: passed`, both packaged smoke maps green,
|
||||
and `BuildCookRun time: 1381.37 s`
|
||||
- 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
|
||||
|
|
@ -247,7 +261,12 @@ Current consolidated milestone snapshot:
|
|||
packet is now landed in current code
|
||||
- the generic source-backed `Phase 6R-N` `HactarCE/Hyperspeedcube` control pass is now consumed
|
||||
- the bounded permissive `Phase 6R-N` `HactarCE/Hyperspeedcube` puzzle-definition DSL packet is
|
||||
now landed in current code
|
||||
now landed in current code, and the bounded package-checklist proof was
|
||||
refreshed against current upstream `HEAD` on `2026-06-18` to track the
|
||||
symmetric HPS engine registration/layout (`crates/hyperpuzzle/src/lib.rs`,
|
||||
`crates/hyperpuzzle_impl_symmetric/src/lib.rs`, and
|
||||
`hps/symmetric/4d_platonic.hps`) instead of the removed legacy
|
||||
`hps/puzzles/ft_cube.hps` path
|
||||
- `HactarCE/Hyperspeedcube` is now closed for the currently justified retained families; no new
|
||||
widening is justified from that row by default
|
||||
- the generic source-backed `Phase 6R-B` `kkoomen/qbr` control pass is now consumed
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ param(
|
|||
[string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining',
|
||||
[string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining'),
|
||||
[string[]]$SmokeMaps = @(),
|
||||
[string[]]$AdditionalCookerOptions = @(
|
||||
'-DisablePlugins=MovieRenderPipeline',
|
||||
'-SkipCookingEditorContent'
|
||||
),
|
||||
[string]$ValidationReportPath = '',
|
||||
[switch]$CleanArchive,
|
||||
[switch]$SkipBuild,
|
||||
|
|
@ -30,6 +34,7 @@ $ResolvedSmokeMaps = @(
|
|||
$CookMaps
|
||||
}
|
||||
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
$ResolvedAdditionalCookerOptions = @($AdditionalCookerOptions) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
$ExpectedClassicCubeMaterialPaths = @(
|
||||
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\M_HT_ClassicCubeFaceMaster.uasset'),
|
||||
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Up.uasset'),
|
||||
|
|
@ -75,6 +80,15 @@ function Convert-PathToken {
|
|||
return ($Value -replace '[\\/:*?"<>| ]', '_')
|
||||
}
|
||||
|
||||
function Read-JsonFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Resolve-PackagedExecutablePath {
|
||||
param(
|
||||
[string]$PackageRoot
|
||||
|
|
@ -187,6 +201,12 @@ if (-not $SkipBuild)
|
|||
$RunUatArguments += '-build'
|
||||
}
|
||||
|
||||
if ($ResolvedAdditionalCookerOptions.Count -gt 0)
|
||||
{
|
||||
# Keep the runtime package lane insulated from editor-only plugin/content bleed.
|
||||
$RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')"
|
||||
}
|
||||
|
||||
$ValidationReport = [ordered]@{
|
||||
reportVersion = 'ht-classic-cube-package-validation/v1'
|
||||
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
|
||||
|
|
@ -195,6 +215,7 @@ $ValidationReport = [ordered]@{
|
|||
configuration = $Configuration
|
||||
cookMaps = @($CookMaps)
|
||||
smokeMaps = @($ResolvedSmokeMaps)
|
||||
additionalCookerOptions = @($ResolvedAdditionalCookerOptions)
|
||||
cleanArchive = [bool]$CleanArchive
|
||||
skipBuild = [bool]$SkipBuild
|
||||
skipLaunch = [bool]$SkipLaunch
|
||||
|
|
@ -241,12 +262,28 @@ try
|
|||
throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
if (Test-Path $SmokeReportPath)
|
||||
if (-not (Test-Path $SmokeReportPath))
|
||||
{
|
||||
$ValidationReport.smokeReports += @(
|
||||
Get-Content -LiteralPath $SmokeReportPath -Raw | ConvertFrom-Json
|
||||
)
|
||||
throw "Packaged classic-cube smoke launch for '$SmokeMap' completed without writing the expected report '$SmokeReportPath'."
|
||||
}
|
||||
|
||||
$SmokeReport = Read-JsonFile -Path $SmokeReportPath
|
||||
if ($null -eq $SmokeReport)
|
||||
{
|
||||
throw "Packaged classic-cube smoke report '$SmokeReportPath' could not be parsed."
|
||||
}
|
||||
|
||||
if ($SmokeReport.result -ne 'passed')
|
||||
{
|
||||
throw "Packaged classic-cube smoke report '$SmokeReportPath' did not record a passed result."
|
||||
}
|
||||
|
||||
if ($SmokeReport.mapUrl -ne $SmokeMap)
|
||||
{
|
||||
throw "Packaged classic-cube smoke report '$SmokeReportPath' targeted '$($SmokeReport.mapUrl)' instead of '$SmokeMap'."
|
||||
}
|
||||
|
||||
$ValidationReport.smokeReports += @($SmokeReport)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue