fix: deliver validated HyperTwist desktop alpha

This commit is contained in:
axiomlogicnexus 2026-07-23 08:42:30 +00:00
parent a8471737c8
commit 49422acf8f
85 changed files with 9238 additions and 688 deletions

View file

@ -34,8 +34,8 @@ r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8
r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8
[/Script/WindowsTargetPlatform.WindowsTargetSettings]
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
; Prefer the broadly compatible packaged Alpha path; D3D12 remains available via -d3d12.
DefaultGraphicsRHI=DefaultGraphicsRHI_DX11
-D3D12TargetedShaderFormats=PCD3D_SM5
+D3D12TargetedShaderFormats=PCD3D_SM6
-D3D11TargetedShaderFormats=PCD3D_SM5

View file

@ -7,7 +7,7 @@ ProjectID=11C9116142A58F76A4E7F987AE989538
[/Script/UnrealHyperTwist.HyperTwistHttpVisionClient]
ProviderLabel=local-http-sidecar
ServiceBaseUrl=http://127.0.0.1:8766
ServiceBaseUrl="http://127.0.0.1:8766"
OpenSessionPath=/vision/session/open
SubmitFramePath=/vision/frame
CommitObservationPath=/vision/commit
@ -18,7 +18,7 @@ RequestTimeoutSeconds=5.0
[/Script/UnrealHyperTwist.HyperTwistHttpSpeechClient]
ProviderLabel=local-http-sidecar
ServiceBaseUrl=http://127.0.0.1:8766
ServiceBaseUrl="http://127.0.0.1:8766"
OpenSessionPath=/speech/session/open
TranscribeUtterancePath=/speech/utterance
CloseSessionPath=/speech/session/close
@ -27,7 +27,7 @@ RequestTimeoutSeconds=5.0
[/Script/UnrealHyperTwist.HyperTwistHttpVoiceClient]
ProviderLabel=local-http-sidecar
ServiceBaseUrl=http://127.0.0.1:8766
ServiceBaseUrl="http://127.0.0.1:8766"
ProfilesPath=/voice/profiles
SynthesizePath=/voice/narrate
HealthPath=/voice/health

View file

@ -0,0 +1,351 @@
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "Containers/Ticker.h"
#include "Engine/Engine.h"
#include "Engine/GameViewportClient.h"
#include "HAL/CriticalSection.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
#include "HAL/PlatformTime.h"
#include "Misc/CommandLine.h"
#include "Misc/DateTime.h"
#include "Misc/FileHelper.h"
#include "Misc/Parse.h"
#include "Misc/Paths.h"
#include "Misc/ScopeLock.h"
#include "UnrealClient.h"
DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistRuntimeDiagnostics, Log, All);
namespace
{
constexpr int32 ReadyFrameWarmupPassCount = 3;
constexpr double ReadyFrameInterCaptureDelaySeconds = 0.75;
constexpr double ReadyFrameReceiptTimeoutSeconds = 10.0;
const TCHAR* DefaultDiagnosticsLogFileName = TEXT("HyperTwistRuntime-latest.log");
FCriticalSection DiagnosticsLock;
FString DiagnosticsLogPath;
bool bDiagnosticsInitialized = false;
FString MakeSingleLine(FString Value)
{
Value.ReplaceInline(TEXT("\r"), TEXT(" "));
Value.ReplaceInline(TEXT("\n"), TEXT(" "));
return Value.TrimStartAndEnd();
}
bool InitializeSessionLocked()
{
if (bDiagnosticsInitialized)
{
return !DiagnosticsLogPath.IsEmpty();
}
bDiagnosticsInitialized = true;
FString RequestedPath;
FParse::Value(
FCommandLine::Get(),
TEXT("HyperTwistDiagnosticsLog="),
RequestedPath);
RequestedPath.TrimStartAndEndInline();
if (RequestedPath.Len() >= 2
&& RequestedPath.StartsWith(TEXT("\""))
&& RequestedPath.EndsWith(TEXT("\"")))
{
RequestedPath = RequestedPath.Mid(1, RequestedPath.Len() - 2);
}
DiagnosticsLogPath = RequestedPath.IsEmpty()
? FPaths::Combine(FPaths::ProjectLogDir(), DefaultDiagnosticsLogFileName)
: FPaths::ConvertRelativePathToFull(RequestedPath);
FPaths::NormalizeFilename(DiagnosticsLogPath);
const FString DiagnosticsDirectory = FPaths::GetPath(DiagnosticsLogPath);
if (DiagnosticsDirectory.IsEmpty()
|| !IFileManager::Get().MakeDirectory(*DiagnosticsDirectory, true))
{
DiagnosticsLogPath.Reset();
return false;
}
TArray<FString> HeaderLines;
HeaderLines.Reserve(5);
HeaderLines.Add(TEXT("format=hypertwist-runtime-diagnostics/v1"));
HeaderLines.Add(FString::Printf(
TEXT("session=%s"),
*FDateTime::UtcNow().ToString(TEXT("%Y%m%dT%H%M%SZ"))));
HeaderLines.Add(FString::Printf(
TEXT("process_id=%u"),
FPlatformProcess::GetCurrentProcessId()));
HeaderLines.Add(FString::Printf(
TEXT("command_line=%s"),
*MakeSingleLine(FCommandLine::Get())));
HeaderLines.Add(FString::Printf(
TEXT("diagnostics_path=%s"),
*DiagnosticsLogPath));
if (!FFileHelper::SaveStringArrayToFile(
HeaderLines,
*DiagnosticsLogPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
DiagnosticsLogPath.Reset();
return false;
}
return true;
}
}
namespace HyperTwistRuntimeDiagnostics
{
bool InitializeSession()
{
const FScopeLock Lock(&DiagnosticsLock);
return InitializeSessionLocked();
}
FString GetDiagnosticsLogPath()
{
const FScopeLock Lock(&DiagnosticsLock);
InitializeSessionLocked();
return DiagnosticsLogPath;
}
bool AppendEvent(const FString& Stage, const FString& Message)
{
const FScopeLock Lock(&DiagnosticsLock);
if (!InitializeSessionLocked())
{
return false;
}
const FString Line = FString::Printf(
TEXT("[%s] [%s] %s%s"),
*FDateTime::UtcNow().ToString(TEXT("%Y-%m-%dT%H:%M:%SZ")),
*MakeSingleLine(Stage.IsEmpty() ? TEXT("Runtime") : Stage),
*MakeSingleLine(Message),
LINE_TERMINATOR);
return FFileHelper::SaveStringToFile(
Line,
*DiagnosticsLogPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM,
&IFileManager::Get(),
FILEWRITE_Append);
}
bool IsReadyFrameCaptureRequested()
{
return FParse::Param(FCommandLine::Get(), TEXT("HyperTwistCaptureWhenReady"));
}
bool RequestReadyFrameCapture(const FString& SurfaceId)
{
if (!IsReadyFrameCaptureRequested())
{
return false;
}
FString SafeSurfaceId = SurfaceId.IsEmpty() ? TEXT("runtime-ready") : SurfaceId;
for (TCHAR& Character : SafeSurfaceId)
{
if (!FChar::IsAlnum(Character) && Character != TEXT('-') && Character != TEXT('_'))
{
Character = TEXT('-');
}
}
const FString CaptureDirectory = FPaths::Combine(
FPaths::ProjectSavedDir(),
TEXT("Screenshots"),
TEXT("HyperTwistDiagnostics"));
if (!IFileManager::Get().MakeDirectory(*CaptureDirectory, true))
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Error,
TEXT("Could not create semantic-ready capture directory '%s'."),
*CaptureDirectory);
return false;
}
const FString CapturePath = FPaths::Combine(
CaptureDirectory,
FString::Printf(TEXT("%s.png"), *SafeSurfaceId));
const FString WarmupCapturePath = FPaths::Combine(
CaptureDirectory,
FString::Printf(TEXT("%s-warmup.png"), *SafeSurfaceId));
if (IFileManager::Get().FileExists(*CapturePath)
&& !IFileManager::Get().Delete(*CapturePath, false, true))
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Error,
TEXT("Could not replace stale semantic-ready frame '%s'."),
*CapturePath);
return false;
}
if (IFileManager::Get().FileExists(*WarmupCapturePath)
&& !IFileManager::Get().Delete(*WarmupCapturePath, false, true))
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Error,
TEXT("Could not replace stale semantic-ready warm-up frame '%s'."),
*WarmupCapturePath);
return false;
}
// Repeated UI-inclusive captures prime and settle Slate's font atlas in
// packaged off-screen rendering. A single warm-up can still leave random
// glyphs absent, so the authoritative frame follows three acknowledged
// passes separated by real presentation time. File polling remains
// necessary because this viewport does not reliably deliver the
// screenshot-processed delegate.
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Display,
TEXT("Queued UI-inclusive semantic-ready frame for %s after presentation stabilization."),
*SurfaceId);
FTSTicker::GetCoreTicker().AddTicker(
FTickerDelegate::CreateLambda(
[
CapturePath,
WarmupCapturePath,
SurfaceId,
WarmupPassesCompleted = 0,
bWarmupRequestInFlight = false,
bFinalRequested = false,
NextCaptureRequestAt = 0.0,
ReceiptDeadline = 0.0
](const float DeltaTime) mutable
{
static_cast<void>(DeltaTime);
const double Now = FPlatformTime::Seconds();
if (WarmupPassesCompleted < ReadyFrameWarmupPassCount)
{
if (!bWarmupRequestInFlight)
{
if (Now < NextCaptureRequestAt)
{
return true;
}
if (IFileManager::Get().FileExists(*WarmupCapturePath)
&& !IFileManager::Get().Delete(*WarmupCapturePath, false, true))
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Error,
TEXT("Could not reset semantic-ready warm-up frame '%s'."),
*WarmupCapturePath);
return false;
}
FScreenshotRequest::RequestScreenshot(
WarmupCapturePath,
true,
false,
false);
bWarmupRequestInFlight = true;
ReceiptDeadline = Now + ReadyFrameReceiptTimeoutSeconds;
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Display,
TEXT("Requested UI-inclusive semantic-ready warm-up frame %d/%d for %s at '%s'."),
WarmupPassesCompleted + 1,
ReadyFrameWarmupPassCount,
*SurfaceId,
*WarmupCapturePath);
return true;
}
if (IFileManager::Get().FileSize(*WarmupCapturePath) > 0)
{
if (!IFileManager::Get().Delete(*WarmupCapturePath, false, true))
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Error,
TEXT("Could not retire semantic-ready warm-up frame '%s'."),
*WarmupCapturePath);
return false;
}
++WarmupPassesCompleted;
bWarmupRequestInFlight = false;
NextCaptureRequestAt = Now + ReadyFrameInterCaptureDelaySeconds;
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Display,
TEXT("Completed UI-inclusive semantic-ready warm-up frame %d/%d for %s."),
WarmupPassesCompleted,
ReadyFrameWarmupPassCount,
*SurfaceId);
return true;
}
if (Now >= ReceiptDeadline)
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Error,
TEXT("Could not save UI-inclusive semantic-ready warm-up frame %d/%d for %s at '%s'."),
WarmupPassesCompleted + 1,
ReadyFrameWarmupPassCount,
*SurfaceId,
*WarmupCapturePath);
return false;
}
return true;
}
if (!bFinalRequested)
{
if (Now < NextCaptureRequestAt)
{
return true;
}
FScreenshotRequest::RequestScreenshot(
CapturePath,
true,
false,
false);
bFinalRequested = true;
ReceiptDeadline = Now + ReadyFrameReceiptTimeoutSeconds;
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Display,
TEXT("Requested UI-inclusive semantic-ready frame for %s at '%s' after font-atlas warm-up."),
*SurfaceId,
*CapturePath);
return true;
}
if (IFileManager::Get().FileSize(*CapturePath) > 0)
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Display,
TEXT("Saved UI-inclusive semantic-ready frame for %s at '%s'."),
*SurfaceId,
*CapturePath);
return false;
}
if (Now >= ReceiptDeadline)
{
UE_LOG(
LogHyperTwistRuntimeDiagnostics,
Error,
TEXT("Could not save UI-inclusive semantic-ready frame for %s at '%s'."),
*SurfaceId,
*CapturePath);
return false;
}
return true;
}),
0.5f);
return true;
}
}

View file

@ -37,12 +37,23 @@ namespace HyperTwistHttpSpeechClientInternal
FString BuildUrl(const FString& BaseUrl, const FString& Path)
{
if (BaseUrl.IsEmpty())
FString TrimmedBase = BaseUrl.TrimStartAndEnd();
const int32 AuthorityStart = TrimmedBase.StartsWith(TEXT("http://"), ESearchCase::IgnoreCase)
? 7
: TrimmedBase.StartsWith(TEXT("https://"), ESearchCase::IgnoreCase) ? 8 : INDEX_NONE;
if (AuthorityStart == INDEX_NONE || TrimmedBase.Len() <= AuthorityStart)
{
return FString();
}
const FString TrimmedBase = BaseUrl.EndsWith(TEXT("/")) ? BaseUrl.LeftChop(1) : BaseUrl;
while (TrimmedBase.Len() > AuthorityStart && TrimmedBase.EndsWith(TEXT("/")))
{
TrimmedBase.LeftChopInline(1, EAllowShrinking::No);
}
if (TrimmedBase.Len() <= AuthorityStart)
{
return FString();
}
if (Path.IsEmpty())
{
return TrimmedBase;

View file

@ -36,12 +36,23 @@ namespace HyperTwistHttpVisionClientInternal
FString BuildUrl(const FString& BaseUrl, const FString& Path)
{
if (BaseUrl.IsEmpty())
FString TrimmedBase = BaseUrl.TrimStartAndEnd();
const int32 AuthorityStart = TrimmedBase.StartsWith(TEXT("http://"), ESearchCase::IgnoreCase)
? 7
: TrimmedBase.StartsWith(TEXT("https://"), ESearchCase::IgnoreCase) ? 8 : INDEX_NONE;
if (AuthorityStart == INDEX_NONE || TrimmedBase.Len() <= AuthorityStart)
{
return FString();
}
const FString TrimmedBase = BaseUrl.EndsWith(TEXT("/")) ? BaseUrl.LeftChop(1) : BaseUrl;
while (TrimmedBase.Len() > AuthorityStart && TrimmedBase.EndsWith(TEXT("/")))
{
TrimmedBase.LeftChopInline(1, EAllowShrinking::No);
}
if (TrimmedBase.Len() <= AuthorityStart)
{
return FString();
}
if (Path.IsEmpty())
{
return TrimmedBase;

View file

@ -28,12 +28,23 @@ namespace HyperTwistHttpVoiceClientInternal
FString BuildUrl(const FString& BaseUrl, const FString& Path)
{
if (BaseUrl.IsEmpty())
FString TrimmedBase = BaseUrl.TrimStartAndEnd();
const int32 AuthorityStart = TrimmedBase.StartsWith(TEXT("http://"), ESearchCase::IgnoreCase)
? 7
: TrimmedBase.StartsWith(TEXT("https://"), ESearchCase::IgnoreCase) ? 8 : INDEX_NONE;
if (AuthorityStart == INDEX_NONE || TrimmedBase.Len() <= AuthorityStart)
{
return FString();
}
const FString TrimmedBase = BaseUrl.EndsWith(TEXT("/")) ? BaseUrl.LeftChop(1) : BaseUrl;
while (TrimmedBase.Len() > AuthorityStart && TrimmedBase.EndsWith(TEXT("/")))
{
TrimmedBase.LeftChopInline(1, EAllowShrinking::No);
}
if (TrimmedBase.Len() <= AuthorityStart)
{
return FString();
}
if (Path.IsEmpty())
{
return TrimmedBase;

View file

@ -1,11 +1,14 @@
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
#include "Engine/World.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeCommandLibrary.h"
#include "HAL/PlatformTime.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h"
#include "ThirdParty/rob-twophase/cubie.h"
#include "ThirdParty/rob-twophase/face.h"
#include "ThirdParty/rob-twophase/move.h"
#include "UObject/ConstructorHelpers.h"
#include <mutex>
@ -27,7 +30,40 @@ namespace HyperTwistClassicCubeActorInternal
AHyperTwistClassicCubeActor::AHyperTwistClassicCubeActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
PrimaryActorTick.bTickEvenWhenPaused = true;
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
FaceMaterials.SetNum(static_cast<int32>(EHyperTwistClassicCubeFace::Right) + 1);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> UpMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Up.MI_HT_ClassicCube_Up")
);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> DownMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Down.MI_HT_ClassicCube_Down")
);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> FrontMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Front.MI_HT_ClassicCube_Front")
);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> BackMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Back.MI_HT_ClassicCube_Back")
);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> LeftMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Left.MI_HT_ClassicCube_Left")
);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> RightMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Right.MI_HT_ClassicCube_Right")
);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> InternalFaceMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Internal.MI_HT_ClassicCube_Internal")
);
FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Up)] = UpMaterial.Object;
FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Down)] = DownMaterial.Object;
FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Front)] = FrontMaterial.Object;
FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Back)] = BackMaterial.Object;
FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Left)] = LeftMaterial.Object;
FaceMaterials[static_cast<int32>(EHyperTwistClassicCubeFace::Right)] = RightMaterial.Object;
InternalMaterial = InternalFaceMaterial.Object;
}
void AHyperTwistClassicCubeActor::OnConstruction(const FTransform& Transform)
@ -42,29 +78,34 @@ void AHyperTwistClassicCubeActor::OnConstruction(const FTransform& Transform)
void AHyperTwistClassicCubeActor::BeginPlay()
{
Super::BeginPlay();
if (!bGenerateOnConstruction)
{
ResetCube();
}
SetActorTickEnabled(true);
EnsureRenderablePresentation();
}
void AHyperTwistClassicCubeActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!bIsAnimating || !ActiveRotation.Pivot)
if (!bIsAnimating)
{
return;
}
if (!IsValid(ActiveRotation.Pivot))
{
FinalizeRotation();
return;
}
const float Now = GetWorld()->GetTimeSeconds();
const float Elapsed = Now - ActiveRotation.StartTime;
float Alpha = FMath::Clamp(Elapsed / ActiveRotation.Duration, 0.0f, 1.0f);
const double Elapsed = FPlatformTime::Seconds() - ActiveRotation.StartTime;
float Alpha = FMath::Clamp(
static_cast<float>(Elapsed / FMath::Max(ActiveRotation.Duration, SMALL_NUMBER)),
0.0f,
1.0f);
// Smooth step for nicer animation feel
Alpha = Alpha * Alpha * (3.0f - 2.0f * Alpha);
const FQuat CurrentRot = FQuat::Slerp(ActiveRotation.PivotStartRot, ActiveRotation.PivotTargetRot, Alpha);
ActiveRotation.Pivot->SetWorldRotation(CurrentRot);
ActiveRotation.Pivot->SetRelativeRotation(CurrentRot);
if (Alpha >= 1.0f)
{
@ -258,14 +299,14 @@ void AHyperTwistClassicCubeActor::ClearPieces()
{
for (const FTrackedPiece& Piece : TrackedPieces)
{
if (Piece.Mesh && Piece.Mesh->IsValidLowLevel())
if (IsValid(Piece.Mesh))
{
Piece.Mesh->DestroyComponent();
}
}
TrackedPieces.Empty();
if (ActiveRotation.Pivot && ActiveRotation.Pivot->IsValidLowLevel())
if (IsValid(ActiveRotation.Pivot))
{
ActiveRotation.Pivot->DestroyComponent();
}
@ -326,7 +367,13 @@ void AHyperTwistClassicCubeActor::CreatePiece(const FVector& GridPos, const TArr
UProceduralMeshComponent* Mesh = nullptr;
if (GetWorld() != nullptr)
{
Mesh = NewObject<UProceduralMeshComponent>(this, NAME_None, RF_Transactional);
Mesh = NewObject<UProceduralMeshComponent>(
this,
NAME_None,
RF_Transactional | RF_Transient
);
AddInstanceComponent(Mesh);
Mesh->SetCanEverAffectNavigation(false);
Mesh->RegisterComponent();
Mesh->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
@ -394,72 +441,48 @@ void AHyperTwistClassicCubeActor::CreateCubeletFace(UProceduralMeshComponent* Me
case EHyperTwistClassicCubeFace::Up: // +Z
Normal = FVector(0, 0, 1);
TangentX = FVector(1, 0, 0);
FaceVerts = {
Center + FVector(-HalfSize, -HalfSize, +HalfSize),
Center + FVector(+HalfSize, -HalfSize, +HalfSize),
Center + FVector(+HalfSize, +HalfSize, +HalfSize),
Center + FVector(-HalfSize, +HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Down: // -Z
Normal = FVector(0, 0, -1);
TangentX = FVector(-1, 0, 0);
FaceVerts = {
Center + FVector(+HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, -HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Front: // +Y
Normal = FVector(0, 1, 0);
TangentX = FVector(1, 0, 0);
FaceVerts = {
Center + FVector(-HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, +HalfSize),
Center + FVector(-HalfSize, +HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Back: // -Y
Normal = FVector(0, -1, 0);
TangentX = FVector(-1, 0, 0);
FaceVerts = {
Center + FVector(+HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, +HalfSize),
Center + FVector(+HalfSize, -HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Right: // +X
Normal = FVector(1, 0, 0);
TangentX = FVector(0, 1, 0);
FaceVerts = {
Center + FVector(+HalfSize, -HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, +HalfSize),
Center + FVector(+HalfSize, -HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Left: // -X
Normal = FVector(-1, 0, 0);
TangentX = FVector(0, -1, 0);
FaceVerts = {
Center + FVector(-HalfSize, +HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, +HalfSize),
Center + FVector(-HalfSize, +HalfSize, +HalfSize)
};
break;
}
const FVector TangentY = FVector::CrossProduct(Normal, TangentX).GetSafeNormal();
const FVector FaceCenter = Center + Normal * HalfSize;
FaceVerts = {
FaceCenter - TangentX * HalfSize - TangentY * HalfSize,
FaceCenter + TangentX * HalfSize - TangentY * HalfSize,
FaceCenter + TangentX * HalfSize + TangentY * HalfSize,
FaceCenter - TangentX * HalfSize + TangentY * HalfSize
};
Vertices = FaceVerts;
Triangles = { 0, 1, 2, 0, 2, 3 };
// Unreal renders clockwise procedural-mesh winding from the outward side.
// The vertex basis above is mathematically counter-clockwise around Normal,
// so reverse each triangle rather than exposing the cubelet interior.
Triangles = { 0, 2, 1, 0, 3, 2 };
for (int32 i = 0; i < 4; ++i)
{
@ -475,7 +498,7 @@ void AHyperTwistClassicCubeActor::CreateCubeletFace(UProceduralMeshComponent* Me
if (UMaterialInstanceDynamic* DynamicMaterial =
Mesh->CreateDynamicMaterialInstance(SectionIndex, Material))
{
DynamicMaterial->SetScalarParameterValue(TEXT("GlowStrength"), 0.0f);
DynamicMaterial->SetScalarParameterValue(TEXT("GlowStrength"), BaseGlowStrength);
DynamicMaterial->SetScalarParameterValue(TEXT("FaceOpacity"), 1.0f);
}
else
@ -495,6 +518,294 @@ bool AHyperTwistClassicCubeActor::IsSettled() const
return !bIsAnimating && RotationQueue.IsEmpty();
}
int32 AHyperTwistClassicCubeActor::GetRenderablePieceCount() const
{
int32 RenderablePieceCount = 0;
for (const FTrackedPiece& Piece : TrackedPieces)
{
if (IsValid(Piece.Mesh)
&& Piece.Mesh->IsRegistered()
&& Piece.Mesh->GetNumSections() > 0)
{
++RenderablePieceCount;
}
}
return RenderablePieceCount;
}
bool AHyperTwistClassicCubeActor::HasValidRenderableState() const
{
return GetRenderableStateValidationError().IsEmpty();
}
FString AHyperTwistClassicCubeActor::GetRenderableStateValidationError() const
{
auto IsOutwardFaceAtGridPosition = [](const EHyperTwistClassicCubeFace Face, const FVector& Position)
{
switch (Face)
{
case EHyperTwistClassicCubeFace::Up:
return FMath::IsNearlyEqual(Position.Z, 1.0f);
case EHyperTwistClassicCubeFace::Down:
return FMath::IsNearlyEqual(Position.Z, -1.0f);
case EHyperTwistClassicCubeFace::Front:
return FMath::IsNearlyEqual(Position.Y, 1.0f);
case EHyperTwistClassicCubeFace::Back:
return FMath::IsNearlyEqual(Position.Y, -1.0f);
case EHyperTwistClassicCubeFace::Left:
return FMath::IsNearlyEqual(Position.X, -1.0f);
case EHyperTwistClassicCubeFace::Right:
return FMath::IsNearlyEqual(Position.X, 1.0f);
}
return false;
};
TSet<int32> OccupiedGridIndices;
TArray<int32> StickerCountByWorldFace;
StickerCountByWorldFace.Init(0, 6);
int32 VisiblePieceCount = 0;
int32 TotalStickerCount = 0;
const bool bRequireLiveMeshes = GetWorld() != nullptr;
const float CubeletSpacing = CubeletSize + Gap;
for (int32 PieceIndex = 0; PieceIndex < TrackedPieces.Num(); ++PieceIndex)
{
const FTrackedPiece& Piece = TrackedPieces[PieceIndex];
if (Piece.IdentityFaces.IsEmpty())
{
continue;
}
++VisiblePieceCount;
const FVector RoundedPosition(
FMath::RoundToFloat(Piece.GridPos.X),
FMath::RoundToFloat(Piece.GridPos.Y),
FMath::RoundToFloat(Piece.GridPos.Z));
if (!Piece.GridPos.Equals(RoundedPosition, KINDA_SMALL_NUMBER)
|| FMath::Abs(RoundedPosition.X) > 1.0f
|| FMath::Abs(RoundedPosition.Y) > 1.0f
|| FMath::Abs(RoundedPosition.Z) > 1.0f
|| RoundedPosition.IsNearlyZero())
{
return FString::Printf(
TEXT("piece %d occupies invalid grid position %s"),
PieceIndex,
*Piece.GridPos.ToCompactString());
}
const int32 GridIndex = GridToIndex(RoundedPosition);
if (OccupiedGridIndices.Contains(GridIndex))
{
return FString::Printf(
TEXT("piece %d duplicates occupied grid position %s"),
PieceIndex,
*RoundedPosition.ToCompactString());
}
OccupiedGridIndices.Add(GridIndex);
const int32 ExpectedStickerCount =
(FMath::IsNearlyEqual(FMath::Abs(RoundedPosition.X), 1.0f) ? 1 : 0)
+ (FMath::IsNearlyEqual(FMath::Abs(RoundedPosition.Y), 1.0f) ? 1 : 0)
+ (FMath::IsNearlyEqual(FMath::Abs(RoundedPosition.Z), 1.0f) ? 1 : 0);
if (Piece.IdentityFaces.Num() != ExpectedStickerCount)
{
return FString::Printf(
TEXT("piece %d has %d stickers at a %d-sticker grid position"),
PieceIndex,
Piece.IdentityFaces.Num(),
ExpectedStickerCount);
}
TSet<EHyperTwistClassicCubeFace> PieceWorldFaces;
for (const EHyperTwistClassicCubeFace IdentityFace : Piece.IdentityFaces)
{
EHyperTwistClassicCubeFace WorldFace = EHyperTwistClassicCubeFace::Up;
if (!TryResolveStickerWorldFace(Piece, IdentityFace, WorldFace))
{
return FString::Printf(
TEXT("piece %d has a sticker orientation that does not resolve to a cube axis"),
PieceIndex);
}
if (PieceWorldFaces.Contains(WorldFace))
{
return FString::Printf(
TEXT("piece %d resolves multiple stickers to world face %d"),
PieceIndex,
static_cast<int32>(WorldFace));
}
if (!IsOutwardFaceAtGridPosition(WorldFace, RoundedPosition))
{
return FString::Printf(
TEXT("piece %d resolves sticker %d to inward world face %d at %s"),
PieceIndex,
static_cast<int32>(IdentityFace),
static_cast<int32>(WorldFace),
*RoundedPosition.ToCompactString());
}
PieceWorldFaces.Add(WorldFace);
const int32 WorldFaceIndex = static_cast<int32>(WorldFace);
if (!StickerCountByWorldFace.IsValidIndex(WorldFaceIndex))
{
return FString::Printf(
TEXT("piece %d resolved an out-of-range world face %d"),
PieceIndex,
WorldFaceIndex);
}
++StickerCountByWorldFace[WorldFaceIndex];
++TotalStickerCount;
}
if (!bRequireLiveMeshes)
{
continue;
}
if (!IsValid(Piece.Mesh) || !Piece.Mesh->IsRegistered() || Piece.Mesh->GetNumSections() != 6)
{
return FString::Printf(TEXT("piece %d is missing its six-section live mesh"), PieceIndex);
}
const FVector SolvedCenter =
GetExpectedGridPositionFromIdentity(Piece.IdentityFaces) * CubeletSpacing;
for (int32 SectionIndex = 0; SectionIndex < 6; ++SectionIndex)
{
const FProcMeshSection* Section = Piece.Mesh->GetProcMeshSection(SectionIndex);
if (Section == nullptr
|| !Section->bSectionVisible
|| Section->ProcVertexBuffer.Num() != 4
|| Section->ProcIndexBuffer.Num() != 6)
{
return FString::Printf(
TEXT("piece %d section %d is not a visible four-vertex cubelet face"),
PieceIndex,
SectionIndex);
}
FVector LocalFaceCenter = FVector::ZeroVector;
for (const FProcMeshVertex& Vertex : Section->ProcVertexBuffer)
{
LocalFaceCenter += Vertex.Position;
}
LocalFaceCenter /= static_cast<float>(Section->ProcVertexBuffer.Num());
const EHyperTwistClassicCubeFace SectionFace =
static_cast<EHyperTwistClassicCubeFace>(SectionIndex);
const FVector LocalFaceNormal = GetFaceNormal(SectionFace);
const FVector ExpectedLocalFaceCenter =
SolvedCenter + LocalFaceNormal * (CubeletSize * 0.5f);
if (!LocalFaceCenter.Equals(ExpectedLocalFaceCenter, 0.01f))
{
return FString::Printf(
TEXT("piece %d section %d is centered at %s instead of %s"),
PieceIndex,
SectionIndex,
*LocalFaceCenter.ToCompactString(),
*ExpectedLocalFaceCenter.ToCompactString());
}
const uint32 IndexA = Section->ProcIndexBuffer[0];
const uint32 IndexB = Section->ProcIndexBuffer[1];
const uint32 IndexC = Section->ProcIndexBuffer[2];
if (!Section->ProcVertexBuffer.IsValidIndex(static_cast<int32>(IndexA))
|| !Section->ProcVertexBuffer.IsValidIndex(static_cast<int32>(IndexB))
|| !Section->ProcVertexBuffer.IsValidIndex(static_cast<int32>(IndexC)))
{
return FString::Printf(
TEXT("piece %d section %d contains an invalid triangle index"),
PieceIndex,
SectionIndex);
}
const FVector EdgeAB =
Section->ProcVertexBuffer[static_cast<int32>(IndexB)].Position
- Section->ProcVertexBuffer[static_cast<int32>(IndexA)].Position;
const FVector EdgeAC =
Section->ProcVertexBuffer[static_cast<int32>(IndexC)].Position
- Section->ProcVertexBuffer[static_cast<int32>(IndexA)].Position;
// Unreal treats clockwise procedural triangles as front-facing. Their
// mathematical cross product therefore points opposite the authored
// outward vertex normal.
const FVector FrontWindingNormal =
-FVector::CrossProduct(EdgeAB, EdgeAC).GetSafeNormal();
if (FVector::DotProduct(FrontWindingNormal, LocalFaceNormal)
< HyperTwistClassicCubeActorInternal::RotationDotTolerance)
{
return FString::Printf(
TEXT("piece %d section %d has inward-facing triangle winding"),
PieceIndex,
SectionIndex);
}
}
const FVector ExpectedWorldCenter = GetActorTransform().TransformPosition(
RoundedPosition * CubeletSpacing);
const FVector RenderedWorldCenter =
Piece.Mesh->GetComponentTransform().TransformPosition(SolvedCenter);
if (!RenderedWorldCenter.Equals(ExpectedWorldCenter, 0.05f))
{
return FString::Printf(
TEXT("piece %d renders at %s instead of expected center %s"),
PieceIndex,
*RenderedWorldCenter.ToCompactString(),
*ExpectedWorldCenter.ToCompactString());
}
for (const EHyperTwistClassicCubeFace IdentityFace : Piece.IdentityFaces)
{
EHyperTwistClassicCubeFace WorldFace = EHyperTwistClassicCubeFace::Up;
TryResolveStickerWorldFace(Piece, IdentityFace, WorldFace);
const FVector RenderedWorldNormal = Piece.Mesh->GetComponentQuat().RotateVector(
GetFaceNormal(IdentityFace)).GetSafeNormal();
const FVector ExpectedWorldNormal = GetActorQuat().RotateVector(
GetFaceNormal(WorldFace)).GetSafeNormal();
if (FVector::DotProduct(RenderedWorldNormal, ExpectedWorldNormal)
< HyperTwistClassicCubeActorInternal::RotationDotTolerance)
{
return FString::Printf(
TEXT("piece %d live mesh sticker normal diverges from tracked orientation"),
PieceIndex);
}
}
}
if (VisiblePieceCount != ExpectedRenderablePieceCount
|| OccupiedGridIndices.Num() != ExpectedRenderablePieceCount)
{
return FString::Printf(
TEXT("cube has %d visible pieces across %d unique positions instead of %d"),
VisiblePieceCount,
OccupiedGridIndices.Num(),
ExpectedRenderablePieceCount);
}
if (TotalStickerCount != 54)
{
return FString::Printf(TEXT("cube has %d outward stickers instead of 54"), TotalStickerCount);
}
for (int32 FaceIndex = 0; FaceIndex < StickerCountByWorldFace.Num(); ++FaceIndex)
{
if (StickerCountByWorldFace[FaceIndex] != 9)
{
return FString::Printf(
TEXT("world face %d has %d stickers instead of 9"),
FaceIndex,
StickerCountByWorldFace[FaceIndex]);
}
}
return FString();
}
bool AHyperTwistClassicCubeActor::EnsureRenderablePresentation()
{
if (GetRenderablePieceCount() == ExpectedRenderablePieceCount)
{
return false;
}
ResetCube();
return true;
}
bool AHyperTwistClassicCubeActor::IsSolved() const
{
if (bIsAnimating || !RotationQueue.IsEmpty())
@ -834,11 +1145,18 @@ void AHyperTwistClassicCubeActor::StartFaceRotation(EHyperTwistClassicCubeFace F
return;
}
// Create pivot at face center
USceneComponent* Pivot = NewObject<USceneComponent>(this, NAME_None, RF_Transactional);
// Keep the pivot in the cube actor's local frame. A world-space pivot makes
// a placed cube orbit the map origin during every animated turn.
USceneComponent* Pivot = NewObject<USceneComponent>(
this,
NAME_None,
RF_Transactional | RF_Transient
);
AddInstanceComponent(Pivot);
Pivot->AttachToComponent(RootComponent, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
Pivot->RegisterComponent();
Pivot->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
Pivot->SetWorldLocation(GetFaceCenter(Face) * (CubeletSize + Gap));
Pivot->SetRelativeLocation(GetFaceCenter(Face) * (CubeletSize + Gap));
Pivot->SetRelativeRotation(FQuat::Identity);
// Attach face pieces to pivot (keep world transform)
for (int32 Idx : Indices)
@ -854,7 +1172,7 @@ void AHyperTwistClassicCubeActor::StartFaceRotation(EHyperTwistClassicCubeFace F
ActiveRotation.PieceIndices = Indices;
ActiveRotation.PivotStartRot = FQuat::Identity;
ActiveRotation.PivotTargetRot = FaceRot;
ActiveRotation.StartTime = GetWorld()->GetTimeSeconds();
ActiveRotation.StartTime = FPlatformTime::Seconds();
ActiveRotation.Duration = TurnDuration;
ActiveRotation.Face = Face;
ActiveRotation.Direction = Direction;
@ -863,9 +1181,28 @@ void AHyperTwistClassicCubeActor::StartFaceRotation(EHyperTwistClassicCubeFace F
void AHyperTwistClassicCubeActor::FinalizeRotation()
{
if (!ActiveRotation.Pivot)
if (!IsValid(ActiveRotation.Pivot))
{
const EHyperTwistClassicCubeFace InterruptedFace = ActiveRotation.Face;
const EHyperTwistRotationDirection InterruptedDirection = ActiveRotation.Direction;
const bool bInterruptedGameplayMove = ActiveRotation.bGameplayMove;
for (const int32 PieceIndex : ActiveRotation.PieceIndices)
{
if (TrackedPieces.IsValidIndex(PieceIndex)
&& IsValid(TrackedPieces[PieceIndex].Mesh)
&& RootComponent != nullptr)
{
TrackedPieces[PieceIndex].Mesh->AttachToComponent(
RootComponent,
FAttachmentTransformRules::KeepWorldTransform);
}
}
ActiveRotation = FActiveRotation();
bIsAnimating = false;
ApplyQuarterTurnImmediate(
InterruptedFace,
InterruptedDirection,
bInterruptedGameplayMove);
ProcessRotationQueue();
return;
}
@ -936,48 +1273,27 @@ FVector AHyperTwistClassicCubeActor::IndexToGrid(int32 Index) const
FVector AHyperTwistClassicCubeActor::RotateGridPosition(const FVector& Pos, EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
{
const float X = Pos.X;
const float Y = Pos.Y;
const float Z = Pos.Z;
if (Direction == EHyperTwistRotationDirection::Clockwise)
{
switch (Face)
{
case EHyperTwistClassicCubeFace::Up: return FVector( Y, -X, Z);
case EHyperTwistClassicCubeFace::Down: return FVector(-Y, X, Z);
case EHyperTwistClassicCubeFace::Front: return FVector( Z, Y, -X);
case EHyperTwistClassicCubeFace::Back: return FVector(-Z, Y, X);
case EHyperTwistClassicCubeFace::Right: return FVector( X, -Z, Y);
case EHyperTwistClassicCubeFace::Left: return FVector( X, Z, -Y);
}
}
else
{
switch (Face)
{
case EHyperTwistClassicCubeFace::Up: return FVector(-Y, X, Z);
case EHyperTwistClassicCubeFace::Down: return FVector( Y, -X, Z);
case EHyperTwistClassicCubeFace::Front: return FVector(-Z, Y, X);
case EHyperTwistClassicCubeFace::Back: return FVector( Z, Y, -X);
case EHyperTwistClassicCubeFace::Right: return FVector( X, Z, -Y);
case EHyperTwistClassicCubeFace::Left: return FVector( X, -Z, Y);
}
}
return Pos;
return GetFaceRotationQuat(Face, Direction).RotateVector(Pos);
}
FQuat AHyperTwistClassicCubeActor::GetFaceRotationQuat(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction)
{
const bool bCW = (Direction == EHyperTwistRotationDirection::Clockwise);
const float QuarterTurnRadians = PI * 0.5f;
switch (Face)
{
case EHyperTwistClassicCubeFace::Up: return FQuat(FRotator(0.0f, bCW ? -90.0f : 90.0f, 0.0f));
case EHyperTwistClassicCubeFace::Down: return FQuat(FRotator(0.0f, bCW ? 90.0f : -90.0f, 0.0f));
case EHyperTwistClassicCubeFace::Front: return FQuat(FRotator(bCW ? 90.0f : -90.0f, 0.0f, 0.0f));
case EHyperTwistClassicCubeFace::Back: return FQuat(FRotator(bCW ? -90.0f : 90.0f, 0.0f, 0.0f));
case EHyperTwistClassicCubeFace::Right: return FQuat(FRotator(0.0f, 0.0f, bCW ? 90.0f : -90.0f));
case EHyperTwistClassicCubeFace::Left: return FQuat(FRotator(0.0f, 0.0f, bCW ? -90.0f : 90.0f));
case EHyperTwistClassicCubeFace::Up:
return FQuat(FVector::UpVector, bCW ? -QuarterTurnRadians : QuarterTurnRadians);
case EHyperTwistClassicCubeFace::Down:
return FQuat(FVector::UpVector, bCW ? QuarterTurnRadians : -QuarterTurnRadians);
case EHyperTwistClassicCubeFace::Front:
return FQuat(FVector::YAxisVector, bCW ? QuarterTurnRadians : -QuarterTurnRadians);
case EHyperTwistClassicCubeFace::Back:
return FQuat(FVector::YAxisVector, bCW ? -QuarterTurnRadians : QuarterTurnRadians);
case EHyperTwistClassicCubeFace::Right:
return FQuat(FVector::XAxisVector, bCW ? QuarterTurnRadians : -QuarterTurnRadians);
case EHyperTwistClassicCubeFace::Left:
return FQuat(FVector::XAxisVector, bCW ? -QuarterTurnRadians : QuarterTurnRadians);
}
return FQuat::Identity;
}
@ -1211,7 +1527,7 @@ void AHyperTwistClassicCubeActor::UpdateHintMaterialState()
DynamicMaterial->SetScalarParameterValue(TEXT("GlowStrength"), GlowStrength);
DynamicMaterial->SetScalarParameterValue(
TEXT("HintActive"),
GlowStrength > 0.0f ? 1.0f : 0.0f
GlowStrength > BaseGlowStrength + KINDA_SMALL_NUMBER ? 1.0f : 0.0f
);
}
}
@ -1224,14 +1540,14 @@ float AHyperTwistClassicCubeActor::ResolveHintGlowStrength(
{
if (!bHasHintedFace || !Piece.IdentityFaces.Contains(IdentityFace))
{
return 0.0f;
return BaseGlowStrength;
}
EHyperTwistClassicCubeFace WorldFace = EHyperTwistClassicCubeFace::Up;
if (!TryResolveStickerWorldFace(Piece, IdentityFace, WorldFace))
{
return 0.0f;
return BaseGlowStrength;
}
return WorldFace == HintedFace ? 7.5f : 0.0f;
return WorldFace == HintedFace ? HintGlowStrength : BaseGlowStrength;
}

View file

@ -1,8 +1,12 @@
#include "HyperTwistSimulation/HyperTwistClassicCubeGameMode.h"
#include "Async/Async.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "Blueprint/UserWidget.h"
#include "Components/AudioComponent.h"
#include "Components/DirectionalLightComponent.h"
#include "Engine/GameInstance.h"
#include "Engine/DirectionalLight.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "GameFramework/PlayerController.h"
@ -30,6 +34,8 @@
#include "UObject/UnrealType.h"
#include "VoiceModule.h"
DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistClassicCubeUi, Log, All);
namespace HyperTwistClassicCubeGameModeInternal
{
constexpr float AudioDuckMultiplier = 0.15f;
@ -309,6 +315,8 @@ namespace HyperTwistClassicCubeGameModeInternal
AHyperTwistClassicCubeGameMode::AHyperTwistClassicCubeGameMode()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
PrimaryActorTick.bTickEvenWhenPaused = true;
PlayerControllerClass = AHyperTwistClassicCubePlayerController::StaticClass();
DefaultPawnClass = AHyperTwistClassicCubeOrbitPawn::StaticClass();
CubeSpawnLocation = FVector(0.0f, 0.0f, 120.0f);
@ -365,7 +373,13 @@ bool AHyperTwistClassicCubeGameMode::IsClassicCubeAnyActionButtonHovered_Impleme
void AHyperTwistClassicCubeGameMode::BeginPlay()
{
Super::BeginPlay();
SetActorTickEnabled(true);
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("MapReady"),
FString::Printf(
TEXT("Runtime map ready: %s."),
GetWorld() != nullptr ? *GetWorld()->GetPathName() : TEXT("unknown")));
ApplyLaunchOverridesFromCommandLine();
if (APlayerController* PlayerController =
@ -376,7 +390,28 @@ void AHyperTwistClassicCubeGameMode::BeginPlay()
PlayerController->bEnableMouseOverEvents = true;
}
ResolveOrSpawnCubeActor();
ActiveCubeActor = ResolveOrSpawnCubeActor();
EnsureRuntimeLighting();
ActiveOrbitPawn = ResolveOrSpawnOrbitPawn();
if (ActiveCubeActor != nullptr)
{
const bool bRecoveredPresentation = ActiveCubeActor->EnsureRenderablePresentation();
UE_LOG(
LogHyperTwistClassicCubeUi,
Display,
TEXT("Classic cube presentation initialized with %d renderable pieces and orbit pawn %s%s."),
ActiveCubeActor->GetRenderablePieceCount(),
ActiveOrbitPawn != nullptr ? TEXT("ready") : TEXT("pending"),
bRecoveredPresentation ? TEXT(" after runtime geometry recovery") : TEXT("")
);
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("ClassicPresentation"),
FString::Printf(
TEXT("Classic cube presentation initialized with %d renderable pieces and orbit pawn %s%s."),
ActiveCubeActor->GetRenderablePieceCount(),
ActiveOrbitPawn != nullptr ? TEXT("ready") : TEXT("pending"),
bRecoveredPresentation ? TEXT(" after runtime geometry recovery") : TEXT("")));
}
if (bAutoCreateHud)
{
ResolveOrCreateHudWidget();
@ -414,6 +449,8 @@ void AHyperTwistClassicCubeGameMode::Tick(const float DeltaSeconds)
Super::Tick(DeltaSeconds);
ResolveOrSpawnCubeActor();
ResolveOrSpawnOrbitPawn();
EnsureRuntimeLighting();
if (bAutoCreateHud)
{
ResolveOrCreateHudWidget();
@ -424,11 +461,52 @@ void AHyperTwistClassicCubeGameMode::Tick(const float DeltaSeconds)
TickReplayAutoExit(DeltaSeconds);
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
bool bCaptureSettledRuntimeFrame = false;
if (ActiveCubeActor != nullptr)
{
if (bAwaitingScrambleSettlement && IsCubeSettled())
{
bAwaitingScrambleSettlement = false;
const FString RenderableStateError =
ActiveCubeActor->GetRenderableStateValidationError();
if (RenderableStateError.IsEmpty())
{
UE_LOG(
LogHyperTwistClassicCubeUi,
Display,
TEXT("Classic cube scramble settled with %d completed quarter turns, %d queued rotations, and %d renderable pieces; renderable state valid."),
ActiveCubeActor->GetTotalCompletedMoveCount(),
ActiveCubeActor->GetQueuedRotationCount(),
ActiveCubeActor->GetRenderablePieceCount());
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("ClassicScrambleSettlement"),
FString::Printf(
TEXT("Classic cube scramble settled with %d completed quarter turns, %d queued rotations, and %d renderable pieces; renderable state valid."),
ActiveCubeActor->GetTotalCompletedMoveCount(),
ActiveCubeActor->GetQueuedRotationCount(),
ActiveCubeActor->GetRenderablePieceCount()));
}
else
{
UE_LOG(
LogHyperTwistClassicCubeUi,
Error,
TEXT("Classic cube scramble settled with %d completed quarter turns, %d queued rotations, and %d renderable pieces; renderable state invalid: %s."),
ActiveCubeActor->GetTotalCompletedMoveCount(),
ActiveCubeActor->GetQueuedRotationCount(),
ActiveCubeActor->GetRenderablePieceCount(),
*RenderableStateError);
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("ClassicScrambleSettlementError"),
FString::Printf(
TEXT("Classic cube scramble settled with %d completed quarter turns, %d queued rotations, and %d renderable pieces; renderable state invalid: %s."),
ActiveCubeActor->GetTotalCompletedMoveCount(),
ActiveCubeActor->GetQueuedRotationCount(),
ActiveCubeActor->GetRenderablePieceCount(),
*RenderableStateError));
}
bCaptureSettledRuntimeFrame =
HyperTwistRuntimeDiagnostics::IsReadyFrameCaptureRequested();
if (TrainingSubsystem != nullptr)
{
TrainingSubsystem->StartActiveLiveTimer();
@ -452,6 +530,18 @@ void AHyperTwistClassicCubeGameMode::Tick(const float DeltaSeconds)
}
RefreshHud();
if (bCaptureSettledRuntimeFrame)
{
RequestRuntimeReadyDiagnosticsCapture();
}
}
void AHyperTwistClassicCubeGameMode::RequestRuntimeReadyDiagnosticsCapture()
{
HyperTwistRuntimeDiagnostics::RequestReadyFrameCapture(
ActiveSessionMode == EHyperTwistClassicCubeSessionMode::FollowAlong
? TEXT("classic-cube-follow-along-settled")
: TEXT("classic-cube-free-play-settled"));
}
void AHyperTwistClassicCubeGameMode::StartFreshAttempt()
@ -1233,6 +1323,123 @@ AHyperTwistClassicCubeActor* AHyperTwistClassicCubeGameMode::ResolveOrSpawnCubeA
return ActiveCubeActor;
}
AHyperTwistClassicCubeOrbitPawn* AHyperTwistClassicCubeGameMode::ResolveOrSpawnOrbitPawn()
{
if (GetWorld() == nullptr)
{
return nullptr;
}
APlayerController* PlayerController = GetWorld()->GetFirstPlayerController();
if (PlayerController == nullptr)
{
return nullptr;
}
if (AHyperTwistClassicCubeOrbitPawn* PossessedOrbitPawn =
Cast<AHyperTwistClassicCubeOrbitPawn>(PlayerController->GetPawn()))
{
ActiveOrbitPawn = PossessedOrbitPawn;
return ActiveOrbitPawn;
}
if (ActiveOrbitPawn == nullptr)
{
for (TActorIterator<AHyperTwistClassicCubeOrbitPawn> ActorIt(GetWorld()); ActorIt; ++ActorIt)
{
if (ActorIt->GetController() == nullptr || ActorIt->GetController() == PlayerController)
{
ActiveOrbitPawn = *ActorIt;
break;
}
}
}
if (ActiveOrbitPawn == nullptr && bEnsurePlayableOrbitPawn)
{
FActorSpawnParameters SpawnParameters;
SpawnParameters.Owner = PlayerController;
SpawnParameters.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
const FVector SpawnLocation = ActiveCubeActor != nullptr
? ActiveCubeActor->GetActorLocation()
: CubeSpawnLocation;
ActiveOrbitPawn = GetWorld()->SpawnActor<AHyperTwistClassicCubeOrbitPawn>(
AHyperTwistClassicCubeOrbitPawn::StaticClass(),
SpawnLocation,
FRotator::ZeroRotator,
SpawnParameters
);
if (ActiveOrbitPawn != nullptr)
{
UE_LOG(
LogHyperTwistClassicCubeUi,
Warning,
TEXT("Recovered a missing authored PlayerStart/default pawn with a runtime orbit pawn.")
);
}
}
if (ActiveOrbitPawn != nullptr && PlayerController->GetPawn() != ActiveOrbitPawn)
{
PlayerController->Possess(ActiveOrbitPawn);
PlayerController->SetViewTarget(ActiveOrbitPawn);
}
return ActiveOrbitPawn;
}
void AHyperTwistClassicCubeGameMode::EnsureRuntimeLighting()
{
if (bRuntimeLightingReady || GetWorld() == nullptr)
{
return;
}
if (!bEnsureRuntimeLighting)
{
bRuntimeLightingReady = true;
return;
}
for (TActorIterator<ADirectionalLight> ActorIt(GetWorld()); ActorIt; ++ActorIt)
{
if (ActorIt->GetLightComponent() != nullptr
&& ActorIt->GetLightComponent()->IsVisible())
{
bRuntimeLightingReady = true;
return;
}
}
FActorSpawnParameters SpawnParameters;
SpawnParameters.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ADirectionalLight* RuntimeLight = GetWorld()->SpawnActor<ADirectionalLight>(
ADirectionalLight::StaticClass(),
CubeSpawnLocation + FVector(-240.0f, -180.0f, 320.0f),
FRotator(-38.0f, -42.0f, 0.0f),
SpawnParameters
);
UDirectionalLightComponent* LightComponent = RuntimeLight != nullptr
? Cast<UDirectionalLightComponent>(RuntimeLight->GetLightComponent())
: nullptr;
if (LightComponent == nullptr)
{
return;
}
LightComponent->SetMobility(EComponentMobility::Movable);
LightComponent->SetIntensity(8.0f);
LightComponent->SetLightColor(FLinearColor(0.80f, 0.90f, 1.0f));
RuntimeLight->Tags.AddUnique(FName(TEXT("HyperTwistRuntimePresentationLight")));
bRuntimeLightingReady = true;
UE_LOG(
LogHyperTwistClassicCubeUi,
Warning,
TEXT("Recovered missing authored lighting with the runtime presentation key light.")
);
}
UHyperTwistClassicCubeHUDWidget* AHyperTwistClassicCubeGameMode::ResolveOrCreateHudWidget()
{
if (ActiveHudWidget != nullptr)
@ -1258,6 +1465,16 @@ UHyperTwistClassicCubeHUDWidget* AHyperTwistClassicCubeGameMode::ResolveOrCreate
ActiveHudWidget = CreateWidget<UHyperTwistClassicCubeHUDWidget>(PlayerController, ResolvedClass);
if (ActiveHudWidget != nullptr)
{
if (!ActiveHudWidget->PrepareClassicCubeHudSurface())
{
UE_LOG(
LogHyperTwistClassicCubeUi,
Error,
TEXT("Classic cube HUD creation produced no renderable root widget.")
);
ActiveHudWidget = nullptr;
return nullptr;
}
ActiveHudWidget->AddToViewport(HudZOrder);
}
@ -1432,15 +1649,19 @@ void AHyperTwistClassicCubeGameMode::RefreshSolverGuidance(const bool bForceClea
if (!IsCubeSettled())
{
++SolverGuidanceRequestSerial;
PendingSolverFaceletString.Reset();
bPendingSolverForceClearHint = false;
HintLineOverride = TEXT("hint: waiting for cube to settle");
return;
}
const bool bIsFollowAlong =
ActiveSessionMode == EHyperTwistClassicCubeSessionMode::FollowAlong;
const FString FaceletString = ActiveCubeActor->GetSolverFaceletString();
if (FaceletString.IsEmpty() || !UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString))
{
++SolverGuidanceRequestSerial;
PendingSolverFaceletString.Reset();
bPendingSolverForceClearHint = false;
ActiveSolutionNotation.Reset();
ActiveSolutionQuarterTurns.Reset();
FollowAlongGuideQuarterTurns.Reset();
@ -1448,64 +1669,171 @@ void AHyperTwistClassicCubeGameMode::RefreshSolverGuidance(const bool bForceClea
return;
}
ActiveSolutionNotation = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 1500, 32, 1);
ActiveSolutionQuarterTurns =
UHyperTwistClassicCubeCommandLibrary::ExpandMoveNotationSequence(ActiveSolutionNotation);
if (bIsFollowAlong
&& (bForceClearHint || FollowAlongGuideQuarterTurns.IsEmpty()))
{
FollowAlongGuideQuarterTurns = ActiveSolutionQuarterTurns;
}
const TArray<FHyperTwistClassicCubeMoveDescriptor>& GuidanceMoves =
bIsFollowAlong ? FollowAlongGuideQuarterTurns : ActiveSolutionQuarterTurns;
if (GuidanceMoves.IsEmpty())
if (ActiveCubeActor->IsSolved())
{
++SolverGuidanceRequestSerial;
PendingSolverFaceletString.Reset();
bPendingSolverForceClearHint = false;
ActiveSolutionNotation.Reset();
ActiveSolutionQuarterTurns.Reset();
FollowAlongGuideQuarterTurns.Reset();
ActiveCubeActor->ClearHintedFace();
HintLineOverride = ActiveCubeActor->IsSolved()
? TEXT("hint: cube solved")
: TEXT("hint: no solver guidance returned");
HintLineOverride = TEXT("hint: cube solved");
return;
}
if (bIsFollowAlong)
++SolverGuidanceRequestSerial;
PendingSolverFaceletString = FaceletString;
bPendingSolverForceClearHint = bForceClearHint;
ActiveSolutionNotation.Reset();
ActiveSolutionQuarterTurns.Reset();
HintLineOverride = TEXT("hint: preparing solver guidance in background");
StartPendingSolverGuidanceTask();
}
void AHyperTwistClassicCubeGameMode::StartPendingSolverGuidanceTask()
{
if (bSolverGuidanceTaskActive || PendingSolverFaceletString.IsEmpty())
{
if (!GuidanceMoves.IsValidIndex(FollowAlongStepIndex))
return;
}
const uint64 RequestSerial = SolverGuidanceRequestSerial;
const FString FaceletString = MoveTemp(PendingSolverFaceletString);
const bool bForceClearHint = bPendingSolverForceClearHint;
const int32 TimeLimitMs = FMath::Max(SolverTimeLimitMs, 1);
const int32 MaximumMoveCount = FMath::Max(SolverMaximumMoveCount, 1);
PendingSolverFaceletString.Reset();
bPendingSolverForceClearHint = false;
bSolverGuidanceTaskActive = true;
ActiveSolverFaceletString = FaceletString;
UE_LOG(
LogHyperTwistClassicCubeUi,
Display,
TEXT("Classic cube solver guidance request %llu queued on the background worker."),
RequestSerial);
const TWeakObjectPtr<AHyperTwistClassicCubeGameMode> WeakGameMode(this);
Async(
EAsyncExecution::ThreadPool,
[WeakGameMode, RequestSerial, FaceletString, bForceClearHint, TimeLimitMs, MaximumMoveCount]()
{
TArray<FString> SolutionNotation = UHyperTwistSolverLibrary::SolveClassicState(
FaceletString,
TimeLimitMs,
MaximumMoveCount,
1);
AsyncTask(
ENamedThreads::GameThread,
[WeakGameMode,
RequestSerial,
FaceletString,
bForceClearHint,
SolutionNotation = MoveTemp(SolutionNotation)]() mutable
{
ActiveCubeActor->ClearHintedFace();
HintLineOverride = TEXT("hint: guided run complete");
ModeLineOverride = FString::Printf(
TEXT("mode: follow-along complete, correct %d, wrong %d"),
FollowAlongCorrectMoveCount,
FollowAlongIncorrectMoveCount
);
return;
if (AHyperTwistClassicCubeGameMode* GameMode = WeakGameMode.Get())
{
GameMode->CompleteSolverGuidanceTask(
RequestSerial,
FaceletString,
bForceClearHint,
MoveTemp(SolutionNotation));
}
});
});
}
void AHyperTwistClassicCubeGameMode::CompleteSolverGuidanceTask(
const uint64 RequestSerial,
const FString& FaceletString,
const bool bForceClearHint,
TArray<FString>&& SolutionNotation)
{
bSolverGuidanceTaskActive = false;
ActiveSolverFaceletString.Reset();
ActiveCubeActor = ResolveOrSpawnCubeActor();
const bool bResultMatchesCurrentState =
RequestSerial == SolverGuidanceRequestSerial
&& ActiveCubeActor != nullptr
&& IsCubeSettled()
&& ActiveCubeActor->GetSolverFaceletString().Equals(FaceletString, ESearchCase::CaseSensitive);
if (bResultMatchesCurrentState)
{
ActiveSolutionNotation = MoveTemp(SolutionNotation);
ActiveSolutionQuarterTurns =
UHyperTwistClassicCubeCommandLibrary::ExpandMoveNotationSequence(ActiveSolutionNotation);
const bool bIsFollowAlong =
ActiveSessionMode == EHyperTwistClassicCubeSessionMode::FollowAlong;
if (bIsFollowAlong
&& (bForceClearHint || FollowAlongGuideQuarterTurns.IsEmpty()))
{
FollowAlongGuideQuarterTurns = ActiveSolutionQuarterTurns;
}
const FHyperTwistClassicCubeMoveDescriptor& GuidedMove =
GuidanceMoves[FollowAlongStepIndex];
ActiveCubeActor->SetHintedFace(GuidedMove.Face);
HintLineOverride = FString::Printf(
TEXT("hint: next move %s"),
*GuidedMove.Notation
);
ModeLineOverride = FString::Printf(
TEXT("mode: follow-along step %d/%d, correct %d, wrong %d"),
FMath::Clamp(FollowAlongStepIndex + 1, 1, FMath::Max(GuidanceMoves.Num(), 1)),
FMath::Max(GuidanceMoves.Num(), 1),
FollowAlongCorrectMoveCount,
FollowAlongIncorrectMoveCount
);
return;
const TArray<FHyperTwistClassicCubeMoveDescriptor>& GuidanceMoves =
bIsFollowAlong ? FollowAlongGuideQuarterTurns : ActiveSolutionQuarterTurns;
if (GuidanceMoves.IsEmpty())
{
ActiveCubeActor->ClearHintedFace();
HintLineOverride = ActiveCubeActor->IsSolved()
? TEXT("hint: cube solved")
: TEXT("hint: no solver guidance returned");
}
else if (bIsFollowAlong)
{
if (!GuidanceMoves.IsValidIndex(FollowAlongStepIndex))
{
ActiveCubeActor->ClearHintedFace();
HintLineOverride = TEXT("hint: guided run complete");
ModeLineOverride = FString::Printf(
TEXT("mode: follow-along complete, correct %d, wrong %d"),
FollowAlongCorrectMoveCount,
FollowAlongIncorrectMoveCount);
}
else
{
const FHyperTwistClassicCubeMoveDescriptor& GuidedMove =
GuidanceMoves[FollowAlongStepIndex];
ActiveCubeActor->SetHintedFace(GuidedMove.Face);
HintLineOverride = FString::Printf(
TEXT("hint: next move %s"),
*GuidedMove.Notation);
ModeLineOverride = FString::Printf(
TEXT("mode: follow-along step %d/%d, correct %d, wrong %d"),
FMath::Clamp(FollowAlongStepIndex + 1, 1, FMath::Max(GuidanceMoves.Num(), 1)),
FMath::Max(GuidanceMoves.Num(), 1),
FollowAlongCorrectMoveCount,
FollowAlongIncorrectMoveCount);
}
}
else
{
ActiveCubeActor->SetHintedFace(GuidanceMoves[0].Face);
HintLineOverride = FString::Printf(
TEXT("hint: next move %s"),
*GuidanceMoves[0].Notation);
}
UE_LOG(
LogHyperTwistClassicCubeUi,
Display,
TEXT("Classic cube solver guidance request %llu completed with %d moves."),
RequestSerial,
ActiveSolutionNotation.Num());
RefreshHud();
}
else
{
UE_LOG(
LogHyperTwistClassicCubeUi,
Verbose,
TEXT("Discarded stale classic cube solver guidance request %llu."),
RequestSerial);
}
ActiveCubeActor->SetHintedFace(GuidanceMoves[0].Face);
HintLineOverride = FString::Printf(
TEXT("hint: next move %s"),
*GuidanceMoves[0].Notation
);
StartPendingSolverGuidanceTask();
}
void AHyperTwistClassicCubeGameMode::RefreshVoiceProfiles()

View file

@ -1,9 +1,16 @@
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/ScrollBox.h"
#include "Components/SizeBox.h"
#include "Components/TextBlock.h"
#include "Components/UniformGridPanel.h"
#include "Components/UniformGridSlot.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "GameFramework/GameModeBase.h"
@ -11,6 +18,12 @@
namespace HyperTwistClassicCubeHUDWidgetInternal
{
const FLinearColor PanelColor(0.012f, 0.025f, 0.045f, 0.94f);
const FLinearColor AccentColor(0.15f, 0.90f, 0.95f, 1.0f);
const FLinearColor PrimaryTextColor(0.92f, 0.97f, 1.0f, 1.0f);
const FLinearColor MutedTextColor(0.58f, 0.70f, 0.78f, 1.0f);
const FLinearColor ButtonColor(0.04f, 0.16f, 0.23f, 1.0f);
UObject* ResolveOperatorSurfaceObject(const UUserWidget* Widget)
{
if (Widget == nullptr || Widget->GetWorld() == nullptr)
@ -29,12 +42,41 @@ namespace HyperTwistClassicCubeHUDWidgetInternal
}
}
void UHyperTwistClassicCubeHUDWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
EnsureWidgetTreeBuilt();
}
void UHyperTwistClassicCubeHUDWidget::NativeConstruct()
{
Super::NativeConstruct();
EnsureWidgetTreeBuilt();
}
TSharedRef<SWidget> UHyperTwistClassicCubeHUDWidget::RebuildWidget()
{
Initialize();
EnsureWidgetTreeBuilt();
return Super::RebuildWidget();
}
bool UHyperTwistClassicCubeHUDWidget::PrepareClassicCubeHudSurface()
{
EnsureWidgetTreeBuilt();
return IsClassicCubeHudSurfaceReady();
}
bool UHyperTwistClassicCubeHUDWidget::IsClassicCubeHudSurfaceReady() const
{
return WidgetTree != nullptr
&& WidgetTree->RootWidget != nullptr
&& StatusTextBlock != nullptr
&& TimerTextBlock != nullptr
&& NewScrambleButton != nullptr
&& HintButton != nullptr;
}
void UHyperTwistClassicCubeHUDWidget::SetHudLines(
const FString& InStatusLine,
const FString& InScrambleLine,
@ -147,64 +189,132 @@ void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
return;
}
UVerticalBox* RootLayout =
WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("ClassicCubeHudRoot"));
WidgetTree->RootWidget = RootLayout;
UHorizontalBox* ViewportLayout = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("ClassicCubeHudRoot")
);
ViewportLayout->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
WidgetTree->RootWidget = ViewportLayout;
StatusTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudStatus"));
ScrambleTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudScramble"));
TimerTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudTimer"));
InspectionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudInspection"));
ResultTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudResult"));
HintTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudHint"));
SolutionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudSolution"));
ReplayTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudReplay"));
LeaderboardTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudLeaderboard"));
ModeTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudMode"));
VoiceTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudVoice"));
ControlsTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudControls"));
USizeBox* PanelSize = WidgetTree->ConstructWidget<USizeBox>(
USizeBox::StaticClass(),
TEXT("ClassicCubeHudPanelSize")
);
PanelSize->SetWidthOverride(520.0f);
PanelSize->SetMaxDesiredHeight(1000.0f);
if (UHorizontalBoxSlot* PanelSlot = ViewportLayout->AddChildToHorizontalBox(PanelSize))
{
PanelSlot->SetPadding(FMargin(18.0f));
PanelSlot->SetVerticalAlignment(VAlign_Top);
}
UBorder* PanelBorder = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("ClassicCubeHudPanel")
);
PanelBorder->SetPadding(FMargin(18.0f));
PanelBorder->SetBrushColor(HyperTwistClassicCubeHUDWidgetInternal::PanelColor);
PanelSize->AddChild(PanelBorder);
UScrollBox* PanelScroll = WidgetTree->ConstructWidget<UScrollBox>(
UScrollBox::StaticClass(),
TEXT("ClassicCubeHudScroll")
);
PanelBorder->AddChild(PanelScroll);
UVerticalBox* RootLayout = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("ClassicCubeHudContent")
);
PanelScroll->AddChild(RootLayout);
UTextBlock* TitleText = AddLine(
RootLayout,
TEXT("ClassicCubeHudTitle"),
20,
HyperTwistClassicCubeHUDWidgetInternal::AccentColor
);
if (TitleText != nullptr)
{
TitleText->SetText(FText::FromString(TEXT("CLASSIC CUBE // TRAINING")));
}
StatusTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudStatus"), 15, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor);
TimerTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudTimer"), 27, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor);
InspectionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudInspection"), 14, HyperTwistClassicCubeHUDWidgetInternal::AccentColor);
ScrambleTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudScramble"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
ResultTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudResult"), 14, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor);
HintTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudHint"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
SolutionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudSolution"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
ModeTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudMode"), 13, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor);
ReplayTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudReplay"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
LeaderboardTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudLeaderboard"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
VoiceTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudVoice"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
ControlsTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudControls"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
UUniformGridPanel* ActionGrid = WidgetTree->ConstructWidget<UUniformGridPanel>(
UUniformGridPanel::StaticClass(),
TEXT("ClassicCubeHudActionGrid")
);
ActionGrid->SetMinDesiredSlotWidth(220.0f);
if (UVerticalBoxSlot* GridSlot = RootLayout->AddChildToVerticalBox(ActionGrid))
{
GridSlot->SetPadding(FMargin(0.0f, 12.0f, 0.0f, 0.0f));
}
NewScrambleButton = CreateActionButton(
RootLayout,
ActionGrid,
TEXT("ClassicCubeHudNewScrambleButton"),
TEXT("ClassicCubeHudNewScrambleButtonLabel"),
NewScrambleButtonLabel,
TEXT("New Scramble")
TEXT("New Scramble"),
0,
0
);
HintButton = CreateActionButton(
RootLayout,
ActionGrid,
TEXT("ClassicCubeHudHintButton"),
TEXT("ClassicCubeHudHintButtonLabel"),
HintButtonLabel,
TEXT("Hint")
TEXT("Hint"),
0,
1
);
SubmitSolveButton = CreateActionButton(
RootLayout,
ActionGrid,
TEXT("ClassicCubeHudSubmitSolveButton"),
TEXT("ClassicCubeHudSubmitSolveButtonLabel"),
SubmitSolveButtonLabel,
TEXT("Submit Solve")
TEXT("Submit Solve"),
1,
0
);
ModeToggleButton = CreateActionButton(
RootLayout,
ActionGrid,
TEXT("ClassicCubeHudModeToggleButton"),
TEXT("ClassicCubeHudModeToggleButtonLabel"),
ModeToggleButtonLabel,
TEXT("Switch To Follow Along")
TEXT("Follow Along"),
1,
1
);
VoiceHoldButton = CreateActionButton(
RootLayout,
ActionGrid,
TEXT("ClassicCubeHudVoiceHoldButton"),
TEXT("ClassicCubeHudVoiceHoldButtonLabel"),
VoiceHoldButtonLabel,
TEXT("Hold To Talk")
TEXT("Hold To Talk"),
2,
0
);
VoiceCycleButton = CreateActionButton(
RootLayout,
ActionGrid,
TEXT("ClassicCubeHudVoiceCycleButton"),
TEXT("ClassicCubeHudVoiceCycleButtonLabel"),
VoiceCycleButtonLabel,
TEXT("Voice")
TEXT("Voice"),
2,
1
);
if (NewScrambleButton != nullptr)
@ -249,7 +359,12 @@ void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
);
}
UTextBlock* UHyperTwistClassicCubeHUDWidget::AddLine(UVerticalBox* Parent, const TCHAR* WidgetName)
UTextBlock* UHyperTwistClassicCubeHUDWidget::AddLine(
UVerticalBox* Parent,
const TCHAR* WidgetName,
const int32 FontSize,
const FLinearColor& Color
)
{
if (WidgetTree == nullptr || Parent == nullptr)
{
@ -262,6 +377,11 @@ UTextBlock* UHyperTwistClassicCubeHUDWidget::AddLine(UVerticalBox* Parent, const
{
return nullptr;
}
TextBlock->SetAutoWrapText(true);
TextBlock->SetColorAndOpacity(FSlateColor(Color));
FSlateFontInfo Font = TextBlock->GetFont();
Font.Size = FontSize;
TextBlock->SetFont(Font);
if (UVerticalBoxSlot* VerticalBoxSlot = Parent->AddChildToVerticalBox(TextBlock))
{
@ -272,11 +392,13 @@ UTextBlock* UHyperTwistClassicCubeHUDWidget::AddLine(UVerticalBox* Parent, const
}
UButton* UHyperTwistClassicCubeHUDWidget::CreateActionButton(
UVerticalBox* Parent,
UUniformGridPanel* Parent,
const TCHAR* ButtonName,
const TCHAR* LabelName,
TObjectPtr<UTextBlock>& OutLabel,
const FString& InitialLabel
const FString& InitialLabel,
const int32 Row,
const int32 Column
)
{
if (WidgetTree == nullptr || Parent == nullptr)
@ -292,18 +414,22 @@ UButton* UHyperTwistClassicCubeHUDWidget::CreateActionButton(
{
return nullptr;
}
ActionButton->SetBackgroundColor(HyperTwistClassicCubeHUDWidgetInternal::ButtonColor);
OutLabel = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), LabelName);
if (OutLabel != nullptr)
{
OutLabel->SetText(FText::FromString(InitialLabel));
OutLabel->SetAutoWrapText(true);
OutLabel->SetJustification(ETextJustify::Center);
OutLabel->SetColorAndOpacity(FSlateColor(HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor));
FSlateFontInfo Font = OutLabel->GetFont();
Font.Size = 13;
OutLabel->SetFont(Font);
ActionButton->AddChild(OutLabel);
}
if (UVerticalBoxSlot* VerticalBoxSlot = Parent->AddChildToVerticalBox(ActionButton))
{
VerticalBoxSlot->SetPadding(FMargin(0.0f, 10.0f, 0.0f, 0.0f));
}
Parent->AddChildToUniformGrid(ActionButton, Row, Column);
if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(OutLabel != nullptr ? OutLabel->Slot : nullptr))
{

View file

@ -1,16 +1,9 @@
#include "HyperTwistSolverLibrary.h"
#include "HAL/PlatformFilemanager.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformMisc.h"
#include "HAL/PlatformTime.h"
#include "Misc/Paths.h"
#include "Misc/FileHelper.h"
#if PLATFORM_WINDOWS
#include <direct.h>
#define chdir _chdir
#else
#include <unistd.h>
#endif
#include "ThirdParty/rob-twophase/face.h"
#include "ThirdParty/rob-twophase/move.h"
@ -20,10 +13,13 @@
#include "ThirdParty/rob-twophase/prun.h"
#include "ThirdParty/rob-twophase/solve.h"
#include <atomic>
#include <mutex>
static bool bSolverInitialized = false;
static std::atomic_bool bSolverInitialized{false};
static std::once_flag CoreTablesInitOnceFlag;
static std::once_flag InitOnceFlag;
static std::mutex SolverInvocationMutex;
namespace
{
@ -55,41 +51,77 @@ namespace
return false;
}
void EnsureCoreTablesInitialized()
{
std::call_once(CoreTablesInitOnceFlag, []()
{
face::init();
move::init();
});
}
}
static void DoInitSolver()
{
// rob-twophase saves/loads pruning tables from the current working directory.
// Temporarily switch to ProjectSavedDir so tables are persisted across runs.
FString SavedDir = FPaths::ProjectSavedDir();
FString OriginalDir = FPaths::ProjectDir();
if (FPaths::DirectoryExists(SavedDir))
const FString SavedDirectory = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir());
if (!IFileManager::Get().MakeDirectory(*SavedDirectory, true))
{
chdir(TCHAR_TO_UTF8(*SavedDir));
UE_LOG(
LogTemp,
Error,
TEXT("HyperTwistSolver: Could not create solver cache directory '%s'."),
*SavedDirectory);
return;
}
face::init();
move::init();
const FString PruningTablePath = FPaths::Combine(
SavedDirectory,
UTF8_TO_TCHAR(prun::SAVE.c_str()));
const bool bExistingTable = IFileManager::Get().FileSize(*PruningTablePath) > 0;
const double InitializationStartedAt = FPlatformTime::Seconds();
UE_LOG(
LogTemp,
Display,
TEXT("HyperTwistSolver: %s pruning table at '%s'."),
bExistingTable ? TEXT("Loading") : TEXT("Generating first-run"),
*PruningTablePath);
EnsureCoreTablesInitialized();
coord::init();
sym::init();
const bool bPruningTablesReady = prun::init(true); // true = try to load tables, generate if missing
// Restore working directory
chdir(TCHAR_TO_UTF8(*OriginalDir));
bSolverInitialized = bPruningTablesReady;
const bool bPruningTablesReady = prun::init(
true,
TCHAR_TO_UTF8(*PruningTablePath));
bSolverInitialized.store(bPruningTablesReady, std::memory_order_release);
if (bPruningTablesReady)
{
UE_LOG(
LogTemp,
Display,
TEXT("HyperTwistSolver: Pruning table ready in %.2f seconds (%lld bytes)."),
FPlatformTime::Seconds() - InitializationStartedAt,
IFileManager::Get().FileSize(*PruningTablePath));
}
else
{
UE_LOG(
LogTemp,
Error,
TEXT("HyperTwistSolver: Pruning table failed after %.2f seconds."),
FPlatformTime::Seconds() - InitializationStartedAt);
}
}
bool UHyperTwistSolverLibrary::IsSolverInitialized()
{
return bSolverInitialized;
return bSolverInitialized.load(std::memory_order_acquire);
}
bool UHyperTwistSolverLibrary::InitializeSolver()
{
std::call_once(InitOnceFlag, DoInitSolver);
return bSolverInitialized;
return bSolverInitialized.load(std::memory_order_acquire);
}
TArray<FString> UHyperTwistSolverLibrary::SolveClassicState(
@ -99,6 +131,7 @@ TArray<FString> UHyperTwistSolverLibrary::SolveClassicState(
int32 NumSolutions
)
{
const std::lock_guard<std::mutex> SolverLock(SolverInvocationMutex);
TArray<FString> Result;
if (!InitializeSolver())
@ -171,6 +204,7 @@ bool UHyperTwistSolverLibrary::VerifyFaceletString(const FString& FaceletString)
return false;
}
EnsureCoreTablesInitialized();
std::string Facelets(TCHAR_TO_UTF8(*FaceletString));
cubie::cube Cube;
return face::to_cubie(Facelets, Cube) == 0;
@ -181,6 +215,7 @@ bool UHyperTwistSolverLibrary::VerifySolution(
const TArray<FString>& Moves
)
{
const std::lock_guard<std::mutex> SolverLock(SolverInvocationMutex);
if (!InitializeSolver() || !VerifyFaceletString(FaceletString))
{
return false;

View file

@ -1,8 +1,11 @@
#include "HyperTwistTraining/HyperTwistCoachDashboardActor.h"
#include "Blueprint/UserWidget.h"
#include "GameFramework/PlayerController.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h"
DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistCoachDashboardUi, Log, All);
AHyperTwistCoachDashboardActor::AHyperTwistCoachDashboardActor()
{
PrimaryActorTick.bCanEverTick = false;
@ -42,13 +45,36 @@ UHyperTwistCoachDashboardWidget* AHyperTwistCoachDashboardActor::CreateAndShowDa
return nullptr;
}
ActiveDashboardWidget = CreateWidget<UHyperTwistCoachDashboardWidget>(GetWorld(), ResolvedWidgetClass);
if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
{
ActiveDashboardWidget = CreateWidget<UHyperTwistCoachDashboardWidget>(
PlayerController,
ResolvedWidgetClass
);
}
else
{
ActiveDashboardWidget = CreateWidget<UHyperTwistCoachDashboardWidget>(
GetWorld(),
ResolvedWidgetClass
);
}
if (ActiveDashboardWidget == nullptr)
{
return nullptr;
}
ApplyDashboardDefaults(ActiveDashboardWidget);
if (!ActiveDashboardWidget->PrepareCoachDashboardSurface())
{
UE_LOG(
LogHyperTwistCoachDashboardUi,
Error,
TEXT("Coach dashboard creation produced no renderable root widget.")
);
ActiveDashboardWidget = nullptr;
return nullptr;
}
ActiveDashboardWidget->AddToViewport(DashboardZOrder);
if (bRefreshDashboardOnCreate)

View file

@ -6,11 +6,13 @@
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/EditableTextBox.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/ScrollBox.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
@ -4543,6 +4545,43 @@ namespace HyperTwistCoachDashboardWidgetInternal
Slot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, static_cast<float>(PaddingBottom)));
}
TextBlock->SetAutoWrapText(true);
TextBlock->SetLineHeightPercentage(1.05f);
const FString Name = WidgetName.ToString();
int32 FontSize = 12;
FLinearColor TextColor(0.69f, 0.75f, 0.81f, 1.0f);
bool bUseEmphasis = false;
if (Name.Contains(TEXT("Headline")))
{
FontSize = 20;
TextColor = FLinearColor(0.93f, 0.97f, 1.0f, 1.0f);
bUseEmphasis = true;
}
else if (Name.Contains(TEXT("Title")))
{
FontSize = 13;
TextColor = FLinearColor(0.04f, 0.93f, 0.89f, 1.0f);
bUseEmphasis = true;
}
else if (Name.EndsWith(TEXT("Header")))
{
FontSize = 13;
TextColor = FLinearColor(0.32f, 0.86f, 1.0f, 1.0f);
bUseEmphasis = true;
}
else if (Name.Contains(TEXT("Status")) || Name.Contains(TEXT("Focus")))
{
TextColor = FLinearColor(0.86f, 0.91f, 0.95f, 1.0f);
}
FSlateFontInfo Font = TextBlock->GetFont();
Font.Size = FontSize;
if (bUseEmphasis)
{
Font.TypefaceFontName = FName(TEXT("Bold"));
}
TextBlock->SetFont(Font);
TextBlock->SetColorAndOpacity(FSlateColor(TextColor));
return TextBlock;
}
@ -5808,6 +5847,12 @@ namespace HyperTwistCoachDashboardWidgetInternal
}
}
void UHyperTwistCoachDashboardWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
EnsureDefaultDashboardBuilt();
}
void UHyperTwistCoachDashboardWidget::NativeConstruct()
{
EnsureDefaultDashboardBuilt();
@ -5815,6 +5860,24 @@ void UHyperTwistCoachDashboardWidget::NativeConstruct()
RefreshCoachDashboardView();
}
TSharedRef<SWidget> UHyperTwistCoachDashboardWidget::RebuildWidget()
{
Initialize();
EnsureDefaultDashboardBuilt();
return Super::RebuildWidget();
}
bool UHyperTwistCoachDashboardWidget::PrepareCoachDashboardSurface()
{
EnsureDefaultDashboardBuilt();
return IsCoachDashboardSurfaceReady();
}
bool UHyperTwistCoachDashboardWidget::IsCoachDashboardSurfaceReady() const
{
return WidgetTree != nullptr && WidgetTree->RootWidget != nullptr;
}
void UHyperTwistCoachDashboardWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
@ -18225,13 +18288,38 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
return;
}
RootLayout = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("CoachDashboardRoot"));
if (RootLayout == nullptr)
UBorder* RootBorder = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("CoachDashboardRoot")
);
UScrollBox* RootScroll = WidgetTree->ConstructWidget<UScrollBox>(
UScrollBox::StaticClass(),
TEXT("CoachDashboardScroll")
);
RootLayout = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("CoachDashboardContent")
);
if (RootBorder == nullptr || RootScroll == nullptr || RootLayout == nullptr)
{
return;
}
WidgetTree->RootWidget = RootLayout;
RootBorder->SetPadding(FMargin(24.0f));
RootBorder->SetBrushColor(FLinearColor(0.008f, 0.014f, 0.024f, 0.97f));
RootScroll->AddChild(RootLayout);
RootBorder->AddChild(RootScroll);
WidgetTree->RootWidget = RootBorder;
if (UTextBlock* DashboardTitleBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachDashboardTitle"),
8
))
{
DashboardTitleBlock->SetText(FText::FromString(TEXT("HYPERTWIST // COACH DASHBOARD")));
}
HeadlineTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,

View file

@ -1,10 +1,13 @@
#include "HyperTwistTraining/HyperTwistFirstRunLaunchLibrary.h"
#include "Misc/CommandLine.h"
namespace HyperTwistFirstRunLaunchLibraryInternal
{
const TCHAR* SurfaceId = TEXT("first-run/native-launch-and-settings-surface");
const TCHAR* DefaultRouteId = TEXT("coach-dashboard");
const TCHAR* StartupFallbackRouteId = TEXT("classic-cube-training");
const TCHAR* PackagedStartupRouteArgumentName = TEXT("HyperTwistStartupRoute");
const TCHAR* FirstRunGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistFirstRunLaunchGameMode");
const TCHAR* CoachDashboardGameModeClassPath =
@ -13,6 +16,8 @@ namespace HyperTwistFirstRunLaunchLibraryInternal
TEXT("/Script/UnrealHyperTwist.HyperTwistClassicCubeGameMode");
const TCHAR* ClassicCubeFollowAlongGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode");
const TCHAR* HigherDimensionalTrainingGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode");
const TCHAR* XrTrainingGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistXrTrainingGameMode");
const TCHAR* ClassicCubeMapPath =
@ -23,7 +28,7 @@ namespace HyperTwistFirstRunLaunchLibraryInternal
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining");
const TCHAR* MagicCube5DMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining");
const TCHAR* RuntimeLogFileName = TEXT("UnrealHyperTwist.log");
const TCHAR* RuntimeLogFileName = TEXT("HyperTwistRuntime-latest.log");
const TCHAR* StartupDiagnosticsLogFileName = TEXT("HyperTwistFirstRunLaunch-latest.log");
FString BuildLaunchUrl(const FString& MapAssetPath, const FString& GameModeClassPath)
@ -115,6 +120,11 @@ FString UHyperTwistFirstRunLaunchLibrary::GetPackagedStartupMapAssetPath()
return HyperTwistFirstRunLaunchLibraryInternal::ClassicCubeMapPath;
}
FString UHyperTwistFirstRunLaunchLibrary::GetPackagedStartupRouteArgumentName()
{
return HyperTwistFirstRunLaunchLibraryInternal::PackagedStartupRouteArgumentName;
}
FString UHyperTwistFirstRunLaunchLibrary::GetRuntimeLogFileName()
{
return HyperTwistFirstRunLaunchLibraryInternal::RuntimeLogFileName;
@ -133,7 +143,7 @@ TArray<FString> UHyperTwistFirstRunLaunchLibrary::BuildFirstRunGuidanceLines()
TEXT("Classic cube, follow-along, Magic120Cell, and MagicCube5D launch into dedicated first-party Unreal maps."),
TEXT("XR launches through the OpenXR training game-mode override and remains gated until a live headset session plus controller input are observed."),
TEXT("The web simulator is a lightweight preview and account surface; the downloadable Unreal build is the authoritative high-fidelity simulator."),
TEXT("If first launch stalls, inspect Saved/Logs/UnrealHyperTwist.log and Saved/Logs/HyperTwistFirstRunLaunch-latest.log."),
TEXT("If first launch stalls, inspect Saved/Logs/HyperTwistRuntime-latest.log and Saved/Logs/HyperTwistFirstRunLaunch-latest.log."),
TEXT("If the launch menu cannot be shown, HyperTwist automatically falls back into Classic Cube Free Play instead of remaining on a blank boot map.")
};
}
@ -197,7 +207,7 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
TEXT("Magic120Cell Training"),
TEXT("Launch the family-owned 120-cell runtime-state, projection, persistence, and dedicated-map training surface."),
Magic120CellMapPath,
CoachDashboardGameModeClassPath,
HigherDimensionalTrainingGameModeClassPath,
TEXT("keyboard-mouse-higher-dimensional-training"),
TEXT("ready-dedicated-family-map"),
false,
@ -213,7 +223,7 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
TEXT("MagicCube5D Training"),
TEXT("Launch the family-owned 5D runtime-state, projection, persistence, and dedicated-map training surface."),
MagicCube5DMapPath,
CoachDashboardGameModeClassPath,
HigherDimensionalTrainingGameModeClassPath,
TEXT("keyboard-mouse-higher-dimensional-training"),
TEXT("ready-dedicated-family-map"),
false,
@ -260,3 +270,112 @@ bool UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(
OutRoute = FHyperTwistFirstRunLaunchRoute();
return false;
}
EHyperTwistPackagedStartupRouteParseResult
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
const TCHAR* CommandLine,
FHyperTwistFirstRunLaunchRoute& OutRoute,
FString& OutFailureReason
)
{
using namespace HyperTwistFirstRunLaunchLibraryInternal;
OutRoute = FHyperTwistFirstRunLaunchRoute();
OutFailureReason.Reset();
if (CommandLine == nullptr || CommandLine[0] == TEXT('\0'))
{
return EHyperTwistPackagedStartupRouteParseResult::NotPresent;
}
TArray<FString> Tokens;
TArray<FString> Switches;
FCommandLine::Parse(CommandLine, Tokens, Switches);
const FString ArgumentName(PackagedStartupRouteArgumentName);
const FString ArgumentPrefix = ArgumentName + TEXT("=");
TArray<FString> RouteValues;
for (FString Switch : Switches)
{
while (Switch.StartsWith(TEXT("-")))
{
Switch.RightChopInline(1, EAllowShrinking::No);
}
if (Switch.Equals(ArgumentName, ESearchCase::IgnoreCase))
{
RouteValues.Add(FString());
}
else if (Switch.StartsWith(ArgumentPrefix, ESearchCase::IgnoreCase))
{
RouteValues.Add(Switch.Mid(ArgumentPrefix.Len()));
}
}
if (RouteValues.IsEmpty())
{
return EHyperTwistPackagedStartupRouteParseResult::NotPresent;
}
if (RouteValues.Num() != 1)
{
OutFailureReason = FString::Printf(
TEXT("Expected exactly one -%s argument, but received %d."),
PackagedStartupRouteArgumentName,
RouteValues.Num()
);
return EHyperTwistPackagedStartupRouteParseResult::Invalid;
}
FString RouteId = RouteValues[0].TrimStartAndEnd();
if (RouteId.Len() >= 2
&& RouteId.StartsWith(TEXT("\""))
&& RouteId.EndsWith(TEXT("\"")))
{
RouteId = RouteId.Mid(1, RouteId.Len() - 2).TrimStartAndEnd();
}
if (RouteId.IsEmpty())
{
OutFailureReason = FString::Printf(
TEXT("-%s requires a registered route identifier."),
PackagedStartupRouteArgumentName
);
return EHyperTwistPackagedStartupRouteParseResult::Invalid;
}
if (RouteId.Len() > 64)
{
OutFailureReason = FString::Printf(
TEXT("-%s exceeded the 64-character route identifier limit."),
PackagedStartupRouteArgumentName
);
return EHyperTwistPackagedStartupRouteParseResult::Invalid;
}
for (const TCHAR Character : RouteId)
{
const bool bAllowedCharacter =
(Character >= TEXT('a') && Character <= TEXT('z'))
|| (Character >= TEXT('0') && Character <= TEXT('9'))
|| Character == TEXT('-');
if (!bAllowedCharacter)
{
OutFailureReason = FString::Printf(
TEXT("-%s contains an invalid route identifier."),
PackagedStartupRouteArgumentName
);
return EHyperTwistPackagedStartupRouteParseResult::Invalid;
}
}
if (!TryFindFirstRunLaunchRoute(RouteId, OutRoute)
|| !OutRoute.IsStructurallyValid())
{
OutRoute = FHyperTwistFirstRunLaunchRoute();
OutFailureReason = FString::Printf(
TEXT("-%s requested unknown route '%s'."),
PackagedStartupRouteArgumentName,
*RouteId
);
return EHyperTwistPackagedStartupRouteParseResult::Invalid;
}
return EHyperTwistPackagedStartupRouteParseResult::Valid;
}

View file

@ -3,12 +3,15 @@
#include "Blueprint/UserWidget.h"
#include "Engine/Engine.h"
#include "EngineUtils.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardActor.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchLibrary.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchWidget.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/CommandLine.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "TimerManager.h"
DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistFirstRunLaunch, Log, All);
@ -30,6 +33,11 @@ void AHyperTwistFirstRunLaunchPlayerController::BeginPlay()
);
ApplyFirstRunInputMode();
if (TryOpenCommandLineStartupRoute())
{
return;
}
if (bShowFirstRunLaunchOnBeginPlay)
{
if (ShowFirstRunLaunchMenu() == nullptr)
@ -38,7 +46,6 @@ void AHyperTwistFirstRunLaunchPlayerController::BeginPlay()
return;
}
PersistStartupDiagnostics(TEXT("launch-menu-visible"));
return;
}
@ -49,17 +56,90 @@ void AHyperTwistFirstRunLaunchPlayerController::BeginPlay()
PersistStartupDiagnostics(TEXT("launch-menu-disabled"));
}
bool AHyperTwistFirstRunLaunchPlayerController::TryOpenCommandLineStartupRoute()
{
FHyperTwistFirstRunLaunchRoute Route;
FString FailureReason;
const EHyperTwistPackagedStartupRouteParseResult ParseResult =
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
FCommandLine::Get(),
Route,
FailureReason
);
if (ParseResult == EHyperTwistPackagedStartupRouteParseResult::NotPresent)
{
return false;
}
if (ParseResult == EHyperTwistPackagedStartupRouteParseResult::Invalid)
{
const FString Message = FString::Printf(
TEXT("Rejected packaged startup-route override: %s Continuing with the visible first-run menu."),
FailureReason.IsEmpty() ? TEXT("invalid argument.") : *FailureReason
);
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), Message, true);
HyperTwistRuntimeDiagnostics::AppendEvent(TEXT("PackagedStartupRoute"), Message);
PersistStartupDiagnostics(TEXT("command-line-route-rejected"));
return false;
}
const FString AcceptedMessage = FString::Printf(
TEXT("Accepted packaged startup route '%s': map='%s', game_mode='%s'."),
*Route.RouteId,
Route.MapAssetPath.IsEmpty() ? TEXT("<dashboard>") : *Route.MapAssetPath,
Route.GameModeClassPath.IsEmpty() ? TEXT("<none>") : *Route.GameModeClassPath
);
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), AcceptedMessage);
HyperTwistRuntimeDiagnostics::AppendEvent(TEXT("PackagedStartupRoute"), AcceptedMessage);
if (OpenFirstRunRoute(Route.RouteId))
{
const FString OpenedMessage = FString::Printf(
TEXT("Opened packaged startup route '%s' through first-run product authority."),
*Route.RouteId
);
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), OpenedMessage);
HyperTwistRuntimeDiagnostics::AppendEvent(TEXT("PackagedStartupRoute"), OpenedMessage);
PersistStartupDiagnostics(TEXT("command-line-route-opened"));
return true;
}
const FString OpenFailureMessage = FString::Printf(
TEXT("Could not open packaged startup route '%s'; continuing with the visible first-run menu."),
*Route.RouteId
);
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), OpenFailureMessage, true);
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("PackagedStartupRoute"),
OpenFailureMessage
);
PersistStartupDiagnostics(TEXT("command-line-route-open-failed"));
return false;
}
UHyperTwistFirstRunLaunchWidget*
AHyperTwistFirstRunLaunchPlayerController::ShowFirstRunLaunchMenu()
{
if (ActiveFirstRunLaunchWidget != nullptr)
{
ActiveFirstRunLaunchWidget->RebuildFirstRunLaunchSurface();
if (!ActiveFirstRunLaunchWidget->IsFirstRunLaunchSurfaceReady())
{
AppendStartupDiagnosticsLine(
TEXT("ShowFirstRunLaunchMenu"),
TEXT("The existing first-run widget has no renderable root tree."),
true
);
PersistStartupDiagnostics(TEXT("launch-widget-root-missing"));
return nullptr;
}
if (!ActiveFirstRunLaunchWidget->IsInViewport())
{
ActiveFirstRunLaunchWidget->AddToViewport(FirstRunLaunchZOrder);
}
AppendStartupDiagnosticsLine(
TEXT("ShowFirstRunLaunchMenu"),
TEXT("Rebuilt the existing first-run launch widget.")
TEXT("Prepared the existing first-run launch widget for layout validation.")
);
PersistStartupDiagnostics(TEXT("launch-menu-rebuilt"));
ScheduleFirstRunSurfaceValidation();
return ActiveFirstRunLaunchWidget;
}
@ -102,6 +182,19 @@ AHyperTwistFirstRunLaunchPlayerController::ShowFirstRunLaunchMenu()
this,
&AHyperTwistFirstRunLaunchPlayerController::HandleFirstRunRouteRequested
);
ActiveFirstRunLaunchWidget->RebuildFirstRunLaunchSurface();
if (!ActiveFirstRunLaunchWidget->IsFirstRunLaunchSurfaceReady())
{
AppendStartupDiagnosticsLine(
TEXT("ShowFirstRunLaunchMenu"),
TEXT("The first-run widget could not construct its renderable root tree."),
true
);
ActiveFirstRunLaunchWidget = nullptr;
PersistStartupDiagnostics(TEXT("launch-widget-root-missing"));
return nullptr;
}
ActiveFirstRunLaunchWidget->AddToViewport(FirstRunLaunchZOrder);
AppendStartupDiagnosticsLine(
TEXT("ShowFirstRunLaunchMenu"),
@ -111,7 +204,7 @@ AHyperTwistFirstRunLaunchPlayerController::ShowFirstRunLaunchMenu()
)
);
ApplyFirstRunInputMode();
PersistStartupDiagnostics(TEXT("launch-widget-added"));
ScheduleFirstRunSurfaceValidation();
return ActiveFirstRunLaunchWidget;
}
@ -119,6 +212,8 @@ void AHyperTwistFirstRunLaunchPlayerController::RemoveFirstRunLaunchMenu()
{
if (ActiveFirstRunLaunchWidget != nullptr)
{
GetWorldTimerManager().ClearTimer(FirstRunSurfaceValidationTimerHandle);
FirstRunSurfaceValidationAttempt = 0;
ActiveFirstRunLaunchWidget->RemoveFromParent();
ActiveFirstRunLaunchWidget = nullptr;
AppendStartupDiagnosticsLine(
@ -486,6 +581,72 @@ bool AHyperTwistFirstRunLaunchPlayerController::TryRecoverFromFirstRunFailure(
return bRecovered;
}
void AHyperTwistFirstRunLaunchPlayerController::ScheduleFirstRunSurfaceValidation()
{
FirstRunSurfaceValidationAttempt = 0;
GetWorldTimerManager().ClearTimer(FirstRunSurfaceValidationTimerHandle);
GetWorldTimerManager().SetTimer(
FirstRunSurfaceValidationTimerHandle,
this,
&AHyperTwistFirstRunLaunchPlayerController::ValidateFirstRunSurfaceLayout,
FMath::Max(FirstRunSurfaceValidationIntervalSeconds, 0.01f),
false
);
PersistStartupDiagnostics(TEXT("launch-widget-added-pending-layout"));
}
void AHyperTwistFirstRunLaunchPlayerController::ValidateFirstRunSurfaceLayout()
{
if (ActiveFirstRunLaunchWidget == nullptr)
{
return;
}
++FirstRunSurfaceValidationAttempt;
const FVector2D LocalSize = ActiveFirstRunLaunchWidget->GetCachedGeometry().GetLocalSize();
const bool bHasRenderableLayout = ActiveFirstRunLaunchWidget->IsInViewport()
&& ActiveFirstRunLaunchWidget->IsVisible()
&& ActiveFirstRunLaunchWidget->IsFirstRunLaunchSurfaceReady()
&& LocalSize.X > 1.0f
&& LocalSize.Y > 1.0f;
if (bHasRenderableLayout)
{
AppendStartupDiagnosticsLine(
TEXT("ValidateFirstRunSurfaceLayout"),
FString::Printf(
TEXT("Confirmed a renderable first-run layout at %.0fx%.0f after %d attempt(s)."),
LocalSize.X,
LocalSize.Y,
FirstRunSurfaceValidationAttempt
)
);
PersistStartupDiagnostics(TEXT("launch-menu-renderable"));
HyperTwistRuntimeDiagnostics::RequestReadyFrameCapture(TEXT("first-run-launch-ready"));
return;
}
if (FirstRunSurfaceValidationAttempt < FMath::Max(MaxFirstRunSurfaceValidationAttempts, 1))
{
GetWorldTimerManager().SetTimer(
FirstRunSurfaceValidationTimerHandle,
this,
&AHyperTwistFirstRunLaunchPlayerController::ValidateFirstRunSurfaceLayout,
FMath::Max(FirstRunSurfaceValidationIntervalSeconds, 0.01f),
false
);
return;
}
TryRecoverFromFirstRunFailure(
FString::Printf(
TEXT("The first-run launch widget never produced a visible non-zero layout after %d attempts (last size %.0fx%.0f)."),
FirstRunSurfaceValidationAttempt,
LocalSize.X,
LocalSize.Y
)
);
}
FString AHyperTwistFirstRunLaunchPlayerController::DescribeCurrentWorldName() const
{
const UWorld* World = GetWorld();

View file

@ -10,15 +10,28 @@
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
void UHyperTwistFirstRunLaunchWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
RebuildFirstRunLaunchSurface();
}
void UHyperTwistFirstRunLaunchWidget::NativeConstruct()
{
Super::NativeConstruct();
RebuildFirstRunLaunchSurface();
}
TSharedRef<SWidget> UHyperTwistFirstRunLaunchWidget::RebuildWidget()
{
Initialize();
RebuildFirstRunLaunchSurface();
return Super::RebuildWidget();
}
void UHyperTwistFirstRunLaunchWidget::RebuildFirstRunLaunchSurface()
{
if (WidgetTree == nullptr)
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
{
return;
}
@ -106,6 +119,15 @@ void UHyperTwistFirstRunLaunchWidget::RebuildFirstRunLaunchSurface()
}
}
bool UHyperTwistFirstRunLaunchWidget::IsFirstRunLaunchSurfaceReady() const
{
return WidgetTree != nullptr
&& WidgetTree->RootWidget != nullptr
&& WidgetTree->FindWidget(FName(TEXT("HyperTwistFirstRunRoot"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("CoachDashboardRouteButton"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("ClassicCubeRouteButton"))) != nullptr;
}
void UHyperTwistFirstRunLaunchWidget::OpenCoachDashboard()
{
RequestRoute(TEXT("coach-dashboard"));

View file

@ -0,0 +1,598 @@
#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingGameMode.h"
#include "Camera/CameraComponent.h"
#include "CanvasItem.h"
#include "Components/DirectionalLightComponent.h"
#include "Components/SceneComponent.h"
#include "Engine/Canvas.h"
#include "Engine/DirectionalLight.h"
#include "Engine/Engine.h"
#include "Engine/Font.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "GameFramework/SpringArmComponent.h"
#include "HAL/FileManager.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardActor.h"
#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h"
#include "InputCoreTypes.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistHigherDimensionalGameMode, Log, All);
namespace HyperTwistHigherDimensionalTrainingGameModeInternal
{
const TCHAR* Magic120CellFamilyKey = TEXT("magic120cell");
const TCHAR* MagicCube5DFamilyKey = TEXT("magiccube5d");
FString BuildReadinessResult(const FString& FamilyKey)
{
return FamilyKey.Equals(Magic120CellFamilyKey, ESearchCase::IgnoreCase)
? TEXT("higher-dimensional-runtime-ready-magic120cell")
: TEXT("higher-dimensional-runtime-ready-magiccube5d");
}
}
AHyperTwistHigherDimensionalOrbitPawn::AHyperTwistHigherDimensionalOrbitPawn()
{
PrimaryActorTick.bCanEverTick = true;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(SceneRoot);
SpringArm->bDoCollisionTest = false;
SpringArm->bEnableCameraLag = true;
SpringArm->CameraLagSpeed = 12.0f;
SpringArm->bUsePawnControlRotation = false;
SpringArm->TargetArmLength = InitialArmLength;
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComponent->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
CameraComponent->bUsePawnControlRotation = false;
}
void AHyperTwistHigherDimensionalOrbitPawn::BeginPlay()
{
Super::BeginPlay();
ResetOrbit();
}
void AHyperTwistHigherDimensionalOrbitPawn::Tick(const float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
static_cast<void>(DeltaSeconds);
RefreshOrbitFocusPoint();
APlayerController* PlayerController = Cast<APlayerController>(GetController());
if (PlayerController == nullptr)
{
return;
}
if (SpringArm != nullptr)
{
const float MouseWheelDelta = PlayerController->GetInputAnalogKeyState(EKeys::MouseWheelAxis);
if (!FMath::IsNearlyZero(MouseWheelDelta))
{
SpringArm->TargetArmLength = FMath::Clamp(
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep),
MinimumArmLength,
MaximumArmLength);
}
}
if (!ShouldOrbitFromMouseInput())
{
return;
}
float MouseDeltaX = 0.0f;
float MouseDeltaY = 0.0f;
PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
if (FMath::IsNearlyZero(MouseDeltaX) && FMath::IsNearlyZero(MouseDeltaY))
{
return;
}
CurrentYawDegrees += MouseDeltaX * OrbitDegreesPerPixel;
CurrentPitchDegrees = FMath::Clamp(
CurrentPitchDegrees - (MouseDeltaY * OrbitDegreesPerPixel),
-78.0f,
55.0f);
ApplyOrbitTransform();
}
void AHyperTwistHigherDimensionalOrbitPawn::ResetOrbit()
{
CurrentYawDegrees = InitialYawDegrees;
CurrentPitchDegrees = InitialPitchDegrees;
if (SpringArm != nullptr)
{
SpringArm->TargetArmLength = FMath::Clamp(
InitialArmLength,
MinimumArmLength,
MaximumArmLength);
}
RefreshOrbitFocusPoint();
ApplyOrbitTransform();
}
void AHyperTwistHigherDimensionalOrbitPawn::RefreshOrbitFocusPoint()
{
if (GetWorld() == nullptr)
{
return;
}
TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
if (ActorIt)
{
const FVector NewFocusPoint = ActorIt->GetActorLocation()
+ FVector(0.0f, 0.0f, ActorIt->PreviewHeightOffset);
if (!NewFocusPoint.Equals(OrbitFocusPoint))
{
OrbitFocusPoint = NewFocusPoint;
ApplyOrbitTransform();
}
}
}
void AHyperTwistHigherDimensionalOrbitPawn::ApplyOrbitTransform()
{
SetActorLocation(OrbitFocusPoint);
if (SpringArm != nullptr)
{
SpringArm->SetRelativeRotation(FRotator(CurrentPitchDegrees, CurrentYawDegrees, 0.0f));
}
}
bool AHyperTwistHigherDimensionalOrbitPawn::ShouldOrbitFromMouseInput() const
{
const APlayerController* PlayerController = Cast<APlayerController>(GetController());
return PlayerController != nullptr
&& (PlayerController->IsInputKeyDown(EKeys::MiddleMouseButton)
|| (PlayerController->IsInputKeyDown(EKeys::RightMouseButton)
&& (PlayerController->IsInputKeyDown(EKeys::LeftShift)
|| PlayerController->IsInputKeyDown(EKeys::RightShift))));
}
void AHyperTwistHigherDimensionalTrainingHUD::DrawHUD()
{
Super::DrawHUD();
if (Canvas == nullptr)
{
return;
}
AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell();
if (TrainingShell == nullptr)
{
return;
}
const float PanelX = 18.0f;
const float PanelY = 18.0f;
const float PanelWidth = FMath::Min(540.0f, Canvas->SizeX - 36.0f);
const float PanelHeight = 270.0f;
FCanvasTileItem Background(
FVector2D(PanelX, PanelY),
GWhiteTexture,
FVector2D(PanelWidth, PanelHeight),
FLinearColor(0.018f, 0.035f, 0.060f, 0.93f));
Background.BlendMode = SE_BLEND_Translucent;
Canvas->DrawItem(Background);
const FLinearColor AccentColor = TrainingShell->FamilyKey.Equals(
HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey,
ESearchCase::IgnoreCase)
? FLinearColor(0.12f, 0.95f, 1.0f)
: FLinearColor(1.0f, 0.60f, 0.22f);
FCanvasTileItem Accent(
FVector2D(PanelX, PanelY),
GWhiteTexture,
FVector2D(6.0f, PanelHeight),
AccentColor);
Canvas->DrawItem(Accent);
float CursorY = PanelY + 16.0f;
DrawStatusLine(TrainingShell->Title, PanelX + 20.0f, CursorY, AccentColor, 1.15f);
DrawStatusLine(TrainingShell->GetRuntimeStatusSummary(), PanelX + 20.0f, CursorY, FLinearColor::White);
DrawStatusLine(
FString::Printf(TEXT("projection: %s"), *TrainingShell->ProjectionProfileId),
PanelX + 20.0f,
CursorY,
FLinearColor(0.72f, 0.80f, 0.88f),
0.86f);
DrawStatusLine(
FString::Printf(TEXT("persistence: %s"), *TrainingShell->LastPersistenceStatus),
PanelX + 20.0f,
CursorY,
FLinearColor(0.72f, 0.80f, 0.88f),
0.86f);
DrawStatusLine(
FString::Printf(TEXT("last action: %s"), *TrainingShell->LastRuntimeAction),
PanelX + 20.0f,
CursorY,
FLinearColor(0.72f, 0.80f, 0.88f),
0.86f);
CursorY += 5.0f;
DrawStatusLine(
TEXT("SPACE auto-rotate | Q / E rotate | PAGE UP / DOWN layers"),
PanelX + 20.0f,
CursorY,
FLinearColor::White,
0.88f);
DrawStatusLine(
TEXT("N new state | R reset | S save | L load | HOME camera"),
PanelX + 20.0f,
CursorY,
FLinearColor::White,
0.88f);
DrawStatusLine(
TEXT("Wheel zoom | MMB or SHIFT+RMB orbit | D diagnostics"),
PanelX + 20.0f,
CursorY,
FLinearColor::White,
0.88f);
}
AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingHUD::ResolveTrainingShell()
{
if (CachedTrainingShell.IsValid())
{
return CachedTrainingShell.Get();
}
if (GetWorld() == nullptr)
{
return nullptr;
}
TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
if (ActorIt)
{
CachedTrainingShell = *ActorIt;
return *ActorIt;
}
return nullptr;
}
void AHyperTwistHigherDimensionalTrainingHUD::DrawStatusLine(
const FString& Text,
const float X,
float& InOutY,
const FLinearColor& Color,
const float Scale)
{
UFont* Font = GEngine != nullptr ? GEngine->GetSmallFont() : nullptr;
if (Canvas == nullptr || Font == nullptr)
{
return;
}
FCanvasTextItem TextItem(FVector2D(X, InOutY), FText::FromString(Text), Font, Color);
TextItem.Scale = FVector2D(Scale);
TextItem.EnableShadow(FLinearColor(0.0f, 0.0f, 0.0f, 0.75f));
Canvas->DrawItem(TextItem);
InOutY += 23.0f * Scale;
}
AHyperTwistHigherDimensionalTrainingPlayerController::
AHyperTwistHigherDimensionalTrainingPlayerController()
{
bBootstrapCoachDashboardOnBeginPlay = false;
bShowCoachDashboardMouseCursor = true;
bUseGameAndUiInputMode = true;
bEnableClickEvents = true;
bEnableMouseOverEvents = true;
}
void AHyperTwistHigherDimensionalTrainingPlayerController::BeginPlay()
{
Super::BeginPlay();
bShowMouseCursor = true;
FInputModeGameAndUI InputMode;
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
}
void AHyperTwistHigherDimensionalTrainingPlayerController::PlayerTick(const float DeltaTime)
{
Super::PlayerTick(DeltaTime);
AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell();
if (TrainingShell == nullptr)
{
return;
}
if (WasInputKeyJustPressed(EKeys::SpaceBar))
{
TrainingShell->ToggleAutoRotateProjection();
}
if (WasInputKeyJustPressed(EKeys::Q))
{
TrainingShell->NudgeProjection(-12.0f);
}
if (WasInputKeyJustPressed(EKeys::E))
{
TrainingShell->NudgeProjection(12.0f);
}
if (WasInputKeyJustPressed(EKeys::PageUp))
{
TrainingShell->CycleProjectionLayer(1);
}
if (WasInputKeyJustPressed(EKeys::PageDown))
{
TrainingShell->CycleProjectionLayer(-1);
}
if (WasInputKeyJustPressed(EKeys::N))
{
TrainingShell->CreateScrambledRuntimeState();
}
if (WasInputKeyJustPressed(EKeys::R))
{
TrainingShell->ResetProjection();
}
if (WasInputKeyJustPressed(EKeys::S))
{
TrainingShell->SaveRuntimeState();
}
if (WasInputKeyJustPressed(EKeys::L))
{
TrainingShell->LoadRuntimeState();
}
if (WasInputKeyJustPressed(EKeys::Home))
{
if (AHyperTwistHigherDimensionalOrbitPawn* OrbitPawn =
Cast<AHyperTwistHigherDimensionalOrbitPawn>(GetPawn()))
{
OrbitPawn->ResetOrbit();
TrainingShell->LastRuntimeAction = TEXT("camera orbit reset");
}
}
if (WasInputKeyJustPressed(EKeys::D))
{
ToggleCoachDashboardSurface();
}
}
AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingPlayerController::ResolveTrainingShell()
{
if (CachedTrainingShell.IsValid())
{
return CachedTrainingShell.Get();
}
if (GetWorld() == nullptr)
{
return nullptr;
}
TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
if (ActorIt)
{
CachedTrainingShell = *ActorIt;
return *ActorIt;
}
return nullptr;
}
void AHyperTwistHigherDimensionalTrainingPlayerController::ToggleCoachDashboardSurface()
{
if (ActiveDashboardActor != nullptr && ActiveDashboardActor->HasActiveDashboard())
{
ActiveDashboardActor->RemoveDashboard();
return;
}
EnsureCoachDashboard();
}
AHyperTwistHigherDimensionalTrainingGameMode::AHyperTwistHigherDimensionalTrainingGameMode()
{
DefaultPawnClass = AHyperTwistHigherDimensionalOrbitPawn::StaticClass();
PlayerControllerClass = AHyperTwistHigherDimensionalTrainingPlayerController::StaticClass();
HUDClass = AHyperTwistHigherDimensionalTrainingHUD::StaticClass();
TrainingShellActorClass = AHyperTwistHigherDimensionalTrainingShellActor::StaticClass();
}
void AHyperTwistHigherDimensionalTrainingGameMode::BeginPlay()
{
Super::BeginPlay();
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("MapReady"),
FString::Printf(
TEXT("Runtime map ready: %s."),
GetWorld() != nullptr ? *GetWorld()->GetPathName() : TEXT("unknown")));
const FString FamilyKey = ResolveCurrentFamilyKey();
EnsureRuntimeLighting(FamilyKey);
ActiveTrainingShell = FindExistingTrainingShell();
if (ActiveTrainingShell == nullptr)
{
ActiveTrainingShell = SpawnTrainingShell(FamilyKey);
}
else
{
ActiveTrainingShell->ConfigureForFamily(FamilyKey);
}
if (ActiveTrainingShell == nullptr
|| ActiveTrainingShell->GetRenderableElementCount()
!= ActiveTrainingShell->GetCanonicalElementCount())
{
UE_LOG(
LogHyperTwistHigherDimensionalGameMode,
Error,
TEXT("Dedicated higher-dimensional runtime failed to initialize for family %s."),
*FamilyKey);
return;
}
PersistRuntimeReadinessDiagnostics(FamilyKey, ActiveTrainingShell);
UE_LOG(
LogHyperTwistHigherDimensionalGameMode,
Display,
TEXT("Dedicated higher-dimensional runtime ready for %s with %d canonical renderable elements."),
*FamilyKey,
ActiveTrainingShell->GetRenderableElementCount());
if (HyperTwistRuntimeDiagnostics::IsReadyFrameCaptureRequested())
{
RequestRuntimeReadyDiagnosticsCapture();
}
}
FString AHyperTwistHigherDimensionalTrainingGameMode::ResolveCurrentFamilyKey() const
{
const FString MapName = GetWorld() != nullptr ? GetWorld()->GetMapName() : FString();
return MapName.Contains(TEXT("Magic120Cell"), ESearchCase::IgnoreCase)
? HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey
: HyperTwistHigherDimensionalTrainingGameModeInternal::MagicCube5DFamilyKey;
}
AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingGameMode::FindExistingTrainingShell() const
{
if (GetWorld() == nullptr)
{
return nullptr;
}
TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(GetWorld());
if (ActorIt)
{
return *ActorIt;
}
return nullptr;
}
AHyperTwistHigherDimensionalTrainingShellActor*
AHyperTwistHigherDimensionalTrainingGameMode::SpawnTrainingShell(const FString& FamilyKey)
{
if (GetWorld() == nullptr)
{
return nullptr;
}
TSubclassOf<AHyperTwistHigherDimensionalTrainingShellActor> ResolvedClass =
TrainingShellActorClass;
if (*ResolvedClass == nullptr)
{
ResolvedClass = AHyperTwistHigherDimensionalTrainingShellActor::StaticClass();
}
FTransform SpawnTransform;
AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
GetWorld()->SpawnActorDeferred<AHyperTwistHigherDimensionalTrainingShellActor>(
ResolvedClass,
SpawnTransform,
nullptr,
nullptr,
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
if (TrainingShell == nullptr)
{
return nullptr;
}
TrainingShell->ConfigureForFamily(FamilyKey);
TrainingShell->FinishSpawning(SpawnTransform);
return TrainingShell;
}
void AHyperTwistHigherDimensionalTrainingGameMode::EnsureRuntimeLighting(const FString& FamilyKey)
{
if (GetWorld() == nullptr)
{
return;
}
for (TActorIterator<ADirectionalLight> ActorIt(GetWorld()); ActorIt; ++ActorIt)
{
if (ActorIt->GetLightComponent() != nullptr && ActorIt->GetLightComponent()->IsVisible())
{
ActivePresentationLight = *ActorIt;
return;
}
}
FActorSpawnParameters SpawnParameters;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ActivePresentationLight = GetWorld()->SpawnActor<ADirectionalLight>(
ADirectionalLight::StaticClass(),
FVector(-340.0f, 120.0f, 470.0f),
FamilyKey.Equals(HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey)
? FRotator(-34.0f, -22.0f, 0.0f)
: FRotator(-38.0f, 34.0f, 0.0f),
SpawnParameters);
UDirectionalLightComponent* LightComponent = ActivePresentationLight != nullptr
? Cast<UDirectionalLightComponent>(ActivePresentationLight->GetLightComponent())
: nullptr;
if (LightComponent == nullptr)
{
UE_LOG(
LogHyperTwistHigherDimensionalGameMode,
Error,
TEXT("Failed to create the self-healing higher-dimensional presentation light."));
return;
}
LightComponent->SetMobility(EComponentMobility::Movable);
LightComponent->SetIntensity(
FamilyKey.Equals(HyperTwistHigherDimensionalTrainingGameModeInternal::Magic120CellFamilyKey)
? 2.8f
: 3.2f);
LightComponent->SetLightColor(FLinearColor(0.86f, 0.93f, 1.0f));
ActivePresentationLight->Tags.AddUnique(FName(TEXT("HyperTwistHigherDimensionalRuntimeLight")));
UE_LOG(
LogHyperTwistHigherDimensionalGameMode,
Display,
TEXT("Created self-healing runtime presentation lighting for family %s."),
*FamilyKey);
}
void AHyperTwistHigherDimensionalTrainingGameMode::RequestRuntimeReadyDiagnosticsCapture()
{
const FString SurfaceId = FString::Printf(
TEXT("%s-runtime-ready"),
*ResolveCurrentFamilyKey());
HyperTwistRuntimeDiagnostics::RequestReadyFrameCapture(SurfaceId);
}
void AHyperTwistHigherDimensionalTrainingGameMode::PersistRuntimeReadinessDiagnostics(
const FString& FamilyKey,
const AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell) const
{
if (TrainingShell == nullptr)
{
return;
}
const FString DiagnosticsPath = FPaths::Combine(
FPaths::ProjectLogDir(),
TEXT("HyperTwistFirstRunLaunch-latest.log"));
const FString Result =
HyperTwistHigherDimensionalTrainingGameModeInternal::BuildReadinessResult(FamilyKey);
const FString Diagnostics = FString::Printf(
TEXT("[%s] [HigherDimensionalRuntime] family=%s canonical_elements=%d renderable_elements=%d projection=%s persistence=%s\nresult=%s\n"),
*FDateTime::UtcNow().ToIso8601(),
*FamilyKey,
TrainingShell->GetCanonicalElementCount(),
TrainingShell->GetRenderableElementCount(),
*TrainingShell->ProjectionProfileId,
*TrainingShell->PrimaryPersistenceBoundaryId,
*Result);
FFileHelper::SaveStringToFile(
Diagnostics,
*DiagnosticsPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM,
&IFileManager::Get(),
FILEWRITE_Append);
}

View file

@ -2,40 +2,116 @@
#include "Components/InstancedStaticMeshComponent.h"
#include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Components/TextRenderComponent.h"
#include "Engine/StaticMesh.h"
#include "HAL/FileManager.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/ConstructorHelpers.h"
DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistHigherDimensionalRuntime, Log, All);
namespace HyperTwistHigherDimensionalTrainingShellActorInternal
{
const TCHAR* Magic120CellFamilyKey = TEXT("magic120cell");
const TCHAR* MagicCube5DFamilyKey = TEXT("magiccube5d");
constexpr int32 PreviewColorLayerCount = 6;
constexpr int32 Magic120CellElementCount = 120;
constexpr int32 MagicCube5DOrderThreeElementCount = 242;
constexpr float ProjectionRefreshIntervalSeconds = 1.0f / 30.0f;
bool IsEvenPermutation(const int32 A, const int32 B, const int32 C, const int32 D)
{
const int32 Values[4] = {A, B, C, D};
int32 InversionCount = 0;
for (int32 Left = 0; Left < 4; ++Left)
{
for (int32 Right = Left + 1; Right < 4; ++Right)
{
if (Values[Left] > Values[Right])
{
++InversionCount;
}
}
}
return InversionCount % 2 == 0;
}
float ApplySign(const float Value, const bool bNegative)
{
return bNegative ? -Value : Value;
}
}
AHyperTwistHigherDimensionalTrainingShellActor::AHyperTwistHigherDimensionalTrainingShellActor()
{
PrimaryActorTick.bCanEverTick = false;
PrimaryActorTick.bCanEverTick = true;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
PresentationFloor = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PresentationFloor"));
PresentationFloor->SetupAttachment(SceneRoot);
PresentationFloor->SetMobility(EComponentMobility::Movable);
PresentationFloor->SetCollisionEnabled(ECollisionEnabled::NoCollision);
PresentationFloor->SetCastShadow(false);
PresentationFloor->SetCanEverAffectNavigation(false);
PresentationFloor->SetRelativeLocation(FVector(0.0f, 0.0f, -360.0f));
PresentationFloor->SetRelativeScale3D(FVector(14.0f, 14.0f, 0.05f));
PreviewInstances = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("PreviewInstances"));
PreviewInstances->SetupAttachment(SceneRoot);
PreviewInstances->SetMobility(EComponentMobility::Static);
PreviewInstances->SetCollisionEnabled(ECollisionEnabled::NoCollision);
PreviewInstances->SetCastShadow(false);
PreviewColorLayers.Add(PreviewInstances);
for (int32 LayerIndex = 1;
LayerIndex < HyperTwistHigherDimensionalTrainingShellActorInternal::PreviewColorLayerCount;
++LayerIndex)
{
const FName ComponentName(*FString::Printf(TEXT("PreviewInstances%d"), LayerIndex));
UInstancedStaticMeshComponent* PreviewLayer =
CreateDefaultSubobject<UInstancedStaticMeshComponent>(ComponentName);
PreviewLayer->SetupAttachment(SceneRoot);
PreviewColorLayers.Add(PreviewLayer);
}
for (UInstancedStaticMeshComponent* PreviewLayer : PreviewColorLayers)
{
if (PreviewLayer == nullptr)
{
continue;
}
PreviewLayer->SetMobility(EComponentMobility::Movable);
PreviewLayer->SetCollisionEnabled(ECollisionEnabled::NoCollision);
PreviewLayer->SetCastShadow(false);
PreviewLayer->SetCanEverAffectNavigation(false);
}
LabelComponent = CreateDefaultSubobject<UTextRenderComponent>(TEXT("LabelComponent"));
LabelComponent->SetupAttachment(SceneRoot);
LabelComponent->SetHorizontalAlignment(EHorizTextAligment::EHTA_Center);
LabelComponent->SetVerticalAlignment(EVerticalTextAligment::EVRTA_TextCenter);
LabelComponent->SetWorldSize(56.0f);
LabelComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 220.0f));
LabelComponent->SetWorldSize(42.0f);
LabelComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 470.0f));
LabelComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// The native HUD owns readable status. A fixed world-space label becomes
// mirrored as soon as the orbit camera reaches its back face.
LabelComponent->SetVisibility(false);
LabelComponent->SetHiddenInGame(true);
static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeMeshFinder(
TEXT("/Engine/BasicShapes/Cube.Cube"));
if (CubeMeshFinder.Succeeded())
{
CubePreviewMesh = CubeMeshFinder.Object;
PresentationFloor->SetStaticMesh(CubeMeshFinder.Object);
}
static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereMeshFinder(
@ -44,6 +120,79 @@ AHyperTwistHigherDimensionalTrainingShellActor::AHyperTwistHigherDimensionalTrai
{
SpherePreviewMesh = SphereMeshFinder.Object;
}
static ConstructorHelpers::FObjectFinder<UMaterialInterface> RedMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Right.MI_HT_ClassicCube_Right"));
static ConstructorHelpers::FObjectFinder<UMaterialInterface> BlueMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Back.MI_HT_ClassicCube_Back"));
static ConstructorHelpers::FObjectFinder<UMaterialInterface> GreenMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Front.MI_HT_ClassicCube_Front"));
static ConstructorHelpers::FObjectFinder<UMaterialInterface> WhiteMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Up.MI_HT_ClassicCube_Up"));
static ConstructorHelpers::FObjectFinder<UMaterialInterface> YellowMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Down.MI_HT_ClassicCube_Down"));
static ConstructorHelpers::FObjectFinder<UMaterialInterface> OrangeMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Left.MI_HT_ClassicCube_Left"));
static ConstructorHelpers::FObjectFinder<UMaterialInterface> FloorMaterial(
TEXT("/Game/HyperTwistTraining/Materials/MI_HT_ClassicCube_Internal.MI_HT_ClassicCube_Internal"));
PreviewMaterials =
{
RedMaterial.Object,
BlueMaterial.Object,
GreenMaterial.Object,
YellowMaterial.Object,
OrangeMaterial.Object,
WhiteMaterial.Object
};
if (FloorMaterial.Succeeded())
{
PresentationFloor->SetMaterial(0, FloorMaterial.Object);
}
}
void AHyperTwistHigherDimensionalTrainingShellActor::BeginPlay()
{
Super::BeginPlay();
LoadRuntimeState();
RefreshPreview();
UE_LOG(
LogHyperTwistHigherDimensionalRuntime,
Display,
TEXT("Higher-dimensional presentation initialized with %d renderable elements (canonical=%d) for family %s."),
GetRenderableElementCount(),
GetCanonicalElementCount(),
*GetNormalizedFamilyKey());
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("HigherDimensionalPresentation"),
FString::Printf(
TEXT("Higher-dimensional presentation initialized with %d renderable elements (canonical=%d) for family %s."),
GetRenderableElementCount(),
GetCanonicalElementCount(),
*GetNormalizedFamilyKey()));
UE_LOG(
LogHyperTwistHigherDimensionalRuntime,
Display,
TEXT("Higher-dimensional projection palette initialized with %d distinct layer materials."),
GetDistinctPreviewMaterialCount());
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("HigherDimensionalPalette"),
FString::Printf(
TEXT("Higher-dimensional projection palette initialized with %d distinct layer materials."),
GetDistinctPreviewMaterialCount()));
}
void AHyperTwistHigherDimensionalTrainingShellActor::EndPlay(
const EEndPlayReason::Type EndPlayReason)
{
if (EndPlayReason == EEndPlayReason::Quit || EndPlayReason == EEndPlayReason::LevelTransition)
{
SaveRuntimeState();
}
Super::EndPlay(EndPlayReason);
}
void AHyperTwistHigherDimensionalTrainingShellActor::OnConstruction(const FTransform& Transform)
@ -55,35 +204,306 @@ void AHyperTwistHigherDimensionalTrainingShellActor::OnConstruction(const FTrans
RefreshMetadataTags();
}
void AHyperTwistHigherDimensionalTrainingShellActor::RefreshPreview()
void AHyperTwistHigherDimensionalTrainingShellActor::Tick(const float DeltaSeconds)
{
if (PreviewInstances == nullptr)
Super::Tick(DeltaSeconds);
if (!bAutoRotateProjection || FMath::IsNearlyZero(ProjectionRotationSpeedDegrees))
{
return;
}
PreviewInstances->ClearInstances();
UStaticMesh* PreviewMesh = UsesMagic120CellPreview() ? SpherePreviewMesh : CubePreviewMesh;
if (PreviewMesh == nullptr)
ProjectionAngleDegrees = FMath::Fmod(
ProjectionAngleDegrees + (ProjectionRotationSpeedDegrees * DeltaSeconds),
360.0f);
ProjectionRefreshAccumulator += DeltaSeconds;
if (ProjectionRefreshAccumulator >=
HyperTwistHigherDimensionalTrainingShellActorInternal::ProjectionRefreshIntervalSeconds)
{
PreviewMesh = CubePreviewMesh != nullptr ? CubePreviewMesh : SpherePreviewMesh;
ProjectionRefreshAccumulator = 0.0f;
UpdatePreviewTransforms();
}
}
if (PreviewMesh == nullptr)
{
return;
}
void AHyperTwistHigherDimensionalTrainingShellActor::ConfigureForFamily(
const FString& InFamilyKey)
{
const bool bUseMagic120Cell = InFamilyKey.Equals(
HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey,
ESearchCase::IgnoreCase);
PreviewInstances->SetStaticMesh(PreviewMesh);
if (UsesMagic120CellPreview())
FamilyKey = bUseMagic120Cell
? HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey
: HyperTwistHigherDimensionalTrainingShellActorInternal::MagicCube5DFamilyKey;
if (bUseMagic120Cell)
{
BuildMagic120CellPreview();
TrainingShellId = TEXT("phase6c/magic120cell/dedicated-training-shell");
Title = TEXT("Magic120Cell // 120-cell projection lab");
Summary = TEXT("Interactive first-party 4D projection surface with exactly 120 cell centers, layer focus, runtime-state seeding, and local persistence.");
MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining");
ActivationProfileId = TEXT("magic120cell-cleanroom-runtime-activation");
HostSurfaceId = TEXT("phase6c/magic120cell/runtime-host-surface");
LaunchSurfaceId = TEXT("phase6c/magic120cell/dedicated-training-launch-surface");
ViewContextSurfaceId = TEXT("phase6c/magic120cell/dedicated-training-view-context-surface");
SessionSurfaceId = TEXT("phase6c/magic120cell/dedicated-training-session-surface");
InteractiveSceneSurfaceId = TEXT("phase6c/magic120cell/interactive-scene-surface");
SceneContextId = TEXT("phase6c/magic120cell/interactive-scene-context");
PuzzleId = TEXT("polychoron/magic120cell");
RuntimeModeId = TEXT("magic120cell-full-color-runtime-v1");
ProjectionProfileId = TEXT("magic120cell-4d-projection-distance-v1");
PrimaryPersistenceBoundaryId = TEXT("magic120cell-persistence-boundary");
}
else
{
BuildMagicCube5DPreview();
TrainingShellId = TEXT("phase6c/magiccube5d/dedicated-training-shell");
Title = TEXT("MagicCube5D // order-3 projection lab");
Summary = TEXT("Interactive first-party 5D projection surface with all 242 non-central order-3 cells, depth-layer focus, runtime-state seeding, and local persistence.");
MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining");
ActivationProfileId = TEXT("magiccube5d-cleanroom-runtime-activation");
HostSurfaceId = TEXT("phase6c/magiccube5d/runtime-host-surface");
LaunchSurfaceId = TEXT("phase6c/magiccube5d/dedicated-training-launch-surface");
ViewContextSurfaceId = TEXT("phase6c/magiccube5d/dedicated-training-view-context-surface");
SessionSurfaceId = TEXT("phase6c/magiccube5d/dedicated-training-session-surface");
InteractiveSceneSurfaceId = TEXT("phase6c/magiccube5d/interactive-scene-surface");
SceneContextId = TEXT("phase6c/magiccube5d/interactive-scene-context");
PuzzleId = TEXT("hypercube/magiccube5d/order3");
RuntimeModeId = TEXT("magiccube5d-order3-runtime-v1");
ProjectionProfileId = TEXT("magiccube5d-5d-projection-distance-v1");
PrimaryPersistenceBoundaryId = TEXT("magiccube5d-persistence-boundary");
}
AuthoringManifestRelativePath =
TEXT("docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json");
bDedicatedFamilyOwnership = true;
TrainingShellTags =
{
TEXT("phase6c"),
FString::Printf(TEXT("family:%s"), *FamilyKey),
TEXT("host:dedicated-family-map"),
FString::Printf(TEXT("projection:%s"), *ProjectionProfileId),
FString::Printf(TEXT("persistence:%s"), *PrimaryPersistenceBoundaryId),
TEXT("presentation:runtime-owned")
};
VisibleProjectionLayer = FMath::Clamp(VisibleProjectionLayer, -1, GetProjectionLayerCount() - 1);
RefreshPreview();
RefreshLabel();
RefreshMetadataTags();
}
void AHyperTwistHigherDimensionalTrainingShellActor::ToggleAutoRotateProjection()
{
bAutoRotateProjection = !bAutoRotateProjection;
LastRuntimeAction = bAutoRotateProjection ? TEXT("auto-rotation resumed") : TEXT("auto-rotation paused");
}
void AHyperTwistHigherDimensionalTrainingShellActor::NudgeProjection(const float DeltaDegrees)
{
ProjectionAngleDegrees = FMath::Fmod(ProjectionAngleDegrees + DeltaDegrees + 360.0f, 360.0f);
LastRuntimeAction = FString::Printf(TEXT("projection rotated %.0f degrees"), DeltaDegrees);
UpdatePreviewTransforms();
}
void AHyperTwistHigherDimensionalTrainingShellActor::CycleProjectionLayer(const int32 Direction)
{
const int32 LayerCount = GetProjectionLayerCount();
if (LayerCount <= 0 || Direction == 0)
{
return;
}
const int32 StateCount = LayerCount + 1;
const int32 CurrentState = VisibleProjectionLayer + 1;
const int32 NextState = ((CurrentState + Direction) % StateCount + StateCount) % StateCount;
VisibleProjectionLayer = NextState - 1;
LastRuntimeAction = FString::Printf(TEXT("layer focus: %s"), *GetProjectionLayerLabel());
UpdatePreviewTransforms();
}
void AHyperTwistHigherDimensionalTrainingShellActor::ResetProjection()
{
ProjectionAngleDegrees = 0.0f;
VisibleProjectionLayer = -1;
RuntimeStateSeed = 0;
bAutoRotateProjection = true;
LastRuntimeAction = TEXT("projection and state reset to solved defaults");
UpdatePreviewTransforms();
}
void AHyperTwistHigherDimensionalTrainingShellActor::CreateScrambledRuntimeState()
{
RuntimeStateSeed = RuntimeStateSeed >= MAX_int32 - 1 ? 1 : RuntimeStateSeed + 1;
ProjectionAngleDegrees = FMath::Fmod(
ProjectionAngleDegrees + 37.0f + static_cast<float>(RuntimeStateSeed % 29),
360.0f);
LastRuntimeAction = FString::Printf(TEXT("runtime state seed %d applied"), RuntimeStateSeed);
UpdatePreviewTransforms();
}
bool AHyperTwistHigherDimensionalTrainingShellActor::SaveRuntimeState()
{
const FString StatePath = GetRuntimeStatePath();
if (!IFileManager::Get().MakeDirectory(*FPaths::GetPath(StatePath), true))
{
LastPersistenceStatus = TEXT("save failed: state directory unavailable");
return false;
}
const TSharedRef<FJsonObject> StateObject = MakeShared<FJsonObject>();
StateObject->SetStringField(TEXT("schemaVersion"), TEXT("hypertwist/higher-dimensional-runtime-state/v1"));
StateObject->SetStringField(TEXT("familyKey"), GetNormalizedFamilyKey());
StateObject->SetStringField(TEXT("puzzleId"), PuzzleId);
StateObject->SetNumberField(TEXT("projectionAngleDegrees"), ProjectionAngleDegrees);
StateObject->SetNumberField(TEXT("visibleProjectionLayer"), VisibleProjectionLayer);
StateObject->SetNumberField(TEXT("runtimeStateSeed"), RuntimeStateSeed);
StateObject->SetBoolField(TEXT("autoRotateProjection"), bAutoRotateProjection);
StateObject->SetStringField(TEXT("savedAtUtc"), FDateTime::UtcNow().ToIso8601());
FString SerializedState;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&SerializedState);
if (!FJsonSerializer::Serialize(StateObject, Writer)
|| !FFileHelper::SaveStringToFile(SerializedState, *StatePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
LastPersistenceStatus = TEXT("save failed: state file could not be written");
return false;
}
LastPersistenceStatus = TEXT("saved local runtime state");
LastRuntimeAction = LastPersistenceStatus;
return true;
}
bool AHyperTwistHigherDimensionalTrainingShellActor::LoadRuntimeState()
{
const FString StatePath = GetRuntimeStatePath();
FString SerializedState;
if (!FFileHelper::LoadFileToString(SerializedState, *StatePath))
{
LastPersistenceStatus = TEXT("no saved state; using solved defaults");
return false;
}
TSharedPtr<FJsonObject> StateObject;
if (!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(SerializedState), StateObject)
|| !StateObject.IsValid())
{
LastPersistenceStatus = TEXT("load rejected: malformed state file");
return false;
}
FString StoredFamilyKey;
if (!StateObject->TryGetStringField(TEXT("familyKey"), StoredFamilyKey)
|| !StoredFamilyKey.Equals(GetNormalizedFamilyKey(), ESearchCase::IgnoreCase))
{
LastPersistenceStatus = TEXT("load rejected: family mismatch");
return false;
}
double StoredProjectionAngle = 0.0;
double StoredVisibleLayer = -1.0;
double StoredRuntimeStateSeed = 0.0;
bool bStoredAutoRotate = true;
if (!StateObject->TryGetNumberField(TEXT("projectionAngleDegrees"), StoredProjectionAngle)
|| !StateObject->TryGetNumberField(TEXT("visibleProjectionLayer"), StoredVisibleLayer)
|| !StateObject->TryGetNumberField(TEXT("runtimeStateSeed"), StoredRuntimeStateSeed)
|| !StateObject->TryGetBoolField(TEXT("autoRotateProjection"), bStoredAutoRotate))
{
LastPersistenceStatus = TEXT("load rejected: incomplete state fields");
return false;
}
ProjectionAngleDegrees = FMath::Fmod(static_cast<float>(StoredProjectionAngle) + 360.0f, 360.0f);
VisibleProjectionLayer = FMath::Clamp(
static_cast<int32>(StoredVisibleLayer),
-1,
GetProjectionLayerCount() - 1);
RuntimeStateSeed = FMath::Max(static_cast<int32>(StoredRuntimeStateSeed), 0);
bAutoRotateProjection = bStoredAutoRotate;
LastPersistenceStatus = TEXT("loaded local runtime state");
LastRuntimeAction = LastPersistenceStatus;
UpdatePreviewTransforms();
return true;
}
int32 AHyperTwistHigherDimensionalTrainingShellActor::GetRenderableElementCount() const
{
int32 ElementCount = 0;
for (const UInstancedStaticMeshComponent* PreviewLayer : PreviewColorLayers)
{
if (PreviewLayer != nullptr)
{
ElementCount += PreviewLayer->GetInstanceCount();
}
}
return ElementCount;
}
int32 AHyperTwistHigherDimensionalTrainingShellActor::GetCanonicalElementCount() const
{
return GetCanonicalElementCountForFamily(GetNormalizedFamilyKey());
}
int32 AHyperTwistHigherDimensionalTrainingShellActor::GetProjectionLayerCount() const
{
return UsesMagic120CellPreview() ? 6 : 3;
}
int32 AHyperTwistHigherDimensionalTrainingShellActor::GetDistinctPreviewMaterialCount() const
{
TSet<const UMaterialInterface*> DistinctMaterials;
for (const UMaterialInterface* Material : PreviewMaterials)
{
if (Material != nullptr)
{
DistinctMaterials.Add(Material);
}
}
return DistinctMaterials.Num();
}
FString AHyperTwistHigherDimensionalTrainingShellActor::GetProjectionLayerLabel() const
{
return VisibleProjectionLayer < 0
? TEXT("all layers")
: FString::Printf(TEXT("layer %d of %d"), VisibleProjectionLayer + 1, GetProjectionLayerCount());
}
FString AHyperTwistHigherDimensionalTrainingShellActor::GetRuntimeStatusSummary() const
{
return FString::Printf(
TEXT("%d / %d elements | %s | state seed %d | %s"),
GetRenderableElementCount(),
GetCanonicalElementCount(),
*GetProjectionLayerLabel(),
RuntimeStateSeed,
bAutoRotateProjection ? TEXT("auto-rotating") : TEXT("rotation paused"));
}
FString AHyperTwistHigherDimensionalTrainingShellActor::GetRuntimeStatePath() const
{
return FPaths::Combine(
FPaths::ProjectSavedDir(),
TEXT("HyperTwist"),
TEXT("HigherDimensional"),
FString::Printf(TEXT("%s-runtime-state.json"), *GetNormalizedFamilyKey()));
}
int32 AHyperTwistHigherDimensionalTrainingShellActor::GetCanonicalElementCountForFamily(
const FString& InFamilyKey)
{
return InFamilyKey.Equals(
HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey,
ESearchCase::IgnoreCase)
? HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellElementCount
: HyperTwistHigherDimensionalTrainingShellActorInternal::MagicCube5DOrderThreeElementCount;
}
void AHyperTwistHigherDimensionalTrainingShellActor::RefreshPreview()
{
RebuildSourcePoints();
ConfigurePreviewComponents();
UpdatePreviewTransforms();
}
void AHyperTwistHigherDimensionalTrainingShellActor::RefreshLabel()
@ -93,9 +513,10 @@ void AHyperTwistHigherDimensionalTrainingShellActor::RefreshLabel()
return;
}
LabelComponent->SetRelativeLocation(FVector(0.0f, 0.0f, PreviewHeightOffset + PreviewRadius + 120.0f));
LabelComponent->SetText(BuildLabelText());
LabelComponent->SetTextRenderColor(
UsesMagic120CellPreview() ? FColor(96, 208, 255) : FColor(255, 164, 96));
UsesMagic120CellPreview() ? FColor(54, 244, 255) : FColor(255, 177, 74));
}
void AHyperTwistHigherDimensionalTrainingShellActor::RefreshMetadataTags()
@ -137,56 +558,305 @@ void AHyperTwistHigherDimensionalTrainingShellActor::RefreshMetadataTags()
Tags = MoveTemp(NewTags);
}
void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagic120CellPreview()
void AHyperTwistHigherDimensionalTrainingShellActor::RebuildSourcePoints()
{
AddPreviewInstance(FVector(0.0f, 0.0f, PreviewHeightOffset), 1.1f);
SourcePoints4D.Reset();
SourceFifthCoordinates.Reset();
SourceLayerIndices.Reset();
SourceColorIndices.Reset();
for (int32 Index = 0; Index < 12; ++Index)
if (UsesMagic120CellPreview())
{
const float AngleRadians =
(static_cast<float>(Index) / 12.0f) * 2.0f * PI;
const float AlternatingHeight =
PreviewHeightOffset + ((Index % 2 == 0) ? 45.0f : -45.0f);
const FVector Position(
FMath::Cos(AngleRadians) * PreviewRadius,
FMath::Sin(AngleRadians) * PreviewRadius,
AlternatingHeight);
AddPreviewInstance(Position, 0.48f);
BuildMagic120CellSourcePoints();
}
else
{
BuildMagicCube5DSourcePoints();
}
AddPreviewInstance(FVector(0.0f, 0.0f, PreviewHeightOffset + 190.0f), 0.42f);
AddPreviewInstance(FVector(0.0f, 0.0f, PreviewHeightOffset - 190.0f), 0.42f);
}
void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagicCube5DPreview()
void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagic120CellSourcePoints()
{
AddPreviewInstance(FVector(0.0f, 0.0f, PreviewHeightOffset), 1.0f);
using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;
const TArray<FVector> AxisOffsets =
for (int32 Axis = 0; Axis < 4; ++Axis)
{
FVector(PreviewRadius, 0.0f, PreviewHeightOffset),
FVector(-PreviewRadius, 0.0f, PreviewHeightOffset),
FVector(0.0f, PreviewRadius, PreviewHeightOffset),
FVector(0.0f, -PreviewRadius, PreviewHeightOffset),
FVector(0.0f, 0.0f, PreviewHeightOffset + PreviewRadius),
FVector(0.0f, 0.0f, PreviewHeightOffset - PreviewRadius),
FVector(PreviewRadius * 0.78f, PreviewRadius * 0.78f, PreviewHeightOffset),
FVector(-PreviewRadius * 0.78f, -PreviewRadius * 0.78f, PreviewHeightOffset),
FVector(PreviewRadius * 0.78f, -PreviewRadius * 0.78f, PreviewHeightOffset),
FVector(-PreviewRadius * 0.78f, PreviewRadius * 0.78f, PreviewHeightOffset)
for (int32 Sign = -1; Sign <= 1; Sign += 2)
{
float Coordinates[4] = {0.0f, 0.0f, 0.0f, 0.0f};
Coordinates[Axis] = static_cast<float>(Sign);
const int32 Layer = FMath::Clamp(
FMath::FloorToInt(((Coordinates[3] + 1.001f) / 2.002f) * 6.0f),
0,
5);
AddSourcePoint(
FVector4(Coordinates[0], Coordinates[1], Coordinates[2], Coordinates[3]),
0.0f,
Layer,
SourcePoints4D.Num() % PreviewColorLayerCount);
}
}
for (int32 SignMask = 0; SignMask < 16; ++SignMask)
{
const FVector4 Point(
ApplySign(0.5f, (SignMask & 1) != 0),
ApplySign(0.5f, (SignMask & 2) != 0),
ApplySign(0.5f, (SignMask & 4) != 0),
ApplySign(0.5f, (SignMask & 8) != 0));
const int32 Layer = FMath::Clamp(
FMath::FloorToInt(((Point.W + 1.001f) / 2.002f) * 6.0f),
0,
5);
AddSourcePoint(Point, 0.0f, Layer, SourcePoints4D.Num() % PreviewColorLayerCount);
}
const float GoldenRatio = (1.0f + FMath::Sqrt(5.0f)) * 0.5f;
const float BaseValues[4] =
{
0.0f,
0.5f,
GoldenRatio * 0.5f,
1.0f / (2.0f * GoldenRatio)
};
for (const FVector& Offset : AxisOffsets)
for (int32 A = 0; A < 4; ++A)
{
AddPreviewInstance(Offset, 0.42f);
for (int32 B = 0; B < 4; ++B)
{
if (B == A)
{
continue;
}
for (int32 C = 0; C < 4; ++C)
{
if (C == A || C == B)
{
continue;
}
for (int32 D = 0; D < 4; ++D)
{
if (D == A || D == B || D == C || !IsEvenPermutation(A, B, C, D))
{
continue;
}
const int32 Permutation[4] = {A, B, C, D};
for (int32 SignMask = 0; SignMask < 8; ++SignMask)
{
float Coordinates[4] = {0.0f, 0.0f, 0.0f, 0.0f};
int32 SignIndex = 0;
for (int32 CoordinateIndex = 0; CoordinateIndex < 4; ++CoordinateIndex)
{
const float Value = BaseValues[Permutation[CoordinateIndex]];
if (FMath::IsNearlyZero(Value))
{
continue;
}
Coordinates[CoordinateIndex] = ApplySign(
Value,
(SignMask & (1 << SignIndex)) != 0);
++SignIndex;
}
const int32 Layer = FMath::Clamp(
FMath::FloorToInt(((Coordinates[3] + 1.001f) / 2.002f) * 6.0f),
0,
5);
AddSourcePoint(
FVector4(Coordinates[0], Coordinates[1], Coordinates[2], Coordinates[3]),
0.0f,
Layer,
SourcePoints4D.Num() % PreviewColorLayerCount);
}
}
}
}
}
ensureMsgf(
SourcePoints4D.Num() == Magic120CellElementCount,
TEXT("Magic120Cell projection source must contain exactly 120 cell centers; observed %d."),
SourcePoints4D.Num());
}
void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagicCube5DSourcePoints()
{
using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;
for (int32 X = -1; X <= 1; ++X)
{
for (int32 Y = -1; Y <= 1; ++Y)
{
for (int32 Z = -1; Z <= 1; ++Z)
{
for (int32 W = -1; W <= 1; ++W)
{
for (int32 V = -1; V <= 1; ++V)
{
if (X == 0 && Y == 0 && Z == 0 && W == 0 && V == 0)
{
continue;
}
const int32 ColorIndex = FMath::Abs(
(X * 31) + (Y * 17) + (Z * 13) + (W * 7) + (V * 3))
% PreviewColorLayerCount;
AddSourcePoint(
FVector4(
static_cast<float>(X),
static_cast<float>(Y),
static_cast<float>(Z),
static_cast<float>(W)),
static_cast<float>(V),
V + 1,
ColorIndex);
}
}
}
}
}
ensureMsgf(
SourcePoints4D.Num() == MagicCube5DOrderThreeElementCount,
TEXT("MagicCube5D order-3 projection source must contain exactly 242 non-central cells; observed %d."),
SourcePoints4D.Num());
}
void AHyperTwistHigherDimensionalTrainingShellActor::UpdatePreviewTransforms()
{
ClearPreviewInstances();
if (SourcePoints4D.Num() != SourceFifthCoordinates.Num()
|| SourcePoints4D.Num() != SourceLayerIndices.Num()
|| SourcePoints4D.Num() != SourceColorIndices.Num())
{
return;
}
const bool bMagic120Cell = UsesMagic120CellPreview();
for (int32 Index = 0; Index < SourcePoints4D.Num(); ++Index)
{
if (VisibleProjectionLayer >= 0 && SourceLayerIndices[Index] != VisibleProjectionLayer)
{
continue;
}
const FVector ProjectedPoint = bMagic120Cell
? ProjectMagic120CellPoint(SourcePoints4D[Index])
: ProjectMagicCube5DPoint(SourcePoints4D[Index], SourceFifthCoordinates[Index]);
const int32 ColorIndex = FMath::Abs(
SourceColorIndices[Index] + RuntimeStateSeed + (Index * FMath::Max(RuntimeStateSeed, 1)))
% HyperTwistHigherDimensionalTrainingShellActorInternal::PreviewColorLayerCount;
const float ElementScale = bMagic120Cell ? 0.145f : 0.092f;
AddPreviewInstance(ProjectedPoint, ElementScale, ColorIndex);
}
}
void AHyperTwistHigherDimensionalTrainingShellActor::ClearPreviewInstances()
{
for (UInstancedStaticMeshComponent* PreviewLayer : PreviewColorLayers)
{
if (PreviewLayer != nullptr)
{
PreviewLayer->ClearInstances();
}
}
}
void AHyperTwistHigherDimensionalTrainingShellActor::ConfigurePreviewComponents()
{
using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;
PreviewDynamicMaterials.SetNum(PreviewColorLayerCount);
UStaticMesh* PreviewMesh = UsesMagic120CellPreview() ? SpherePreviewMesh : CubePreviewMesh;
if (PreviewMesh == nullptr)
{
PreviewMesh = CubePreviewMesh != nullptr ? CubePreviewMesh : SpherePreviewMesh;
}
for (int32 LayerIndex = 0; LayerIndex < PreviewColorLayers.Num(); ++LayerIndex)
{
UInstancedStaticMeshComponent* PreviewLayer = PreviewColorLayers[LayerIndex];
if (PreviewLayer == nullptr)
{
continue;
}
PreviewLayer->SetStaticMesh(PreviewMesh);
if (PreviewMaterials.IsValidIndex(LayerIndex) && PreviewMaterials[LayerIndex] != nullptr)
{
UMaterialInstanceDynamic* LayerMaterial =
PreviewDynamicMaterials.IsValidIndex(LayerIndex)
? PreviewDynamicMaterials[LayerIndex]
: nullptr;
if (LayerMaterial == nullptr)
{
LayerMaterial = UMaterialInstanceDynamic::Create(
PreviewMaterials[LayerIndex],
this,
FName(*FString::Printf(TEXT("HigherDimensionalLayerMaterial%d"), LayerIndex)));
PreviewDynamicMaterials[LayerIndex] = LayerMaterial;
}
if (LayerMaterial != nullptr)
{
LayerMaterial->SetScalarParameterValue(TEXT("GlowStrength"), 0.8f);
LayerMaterial->SetScalarParameterValue(TEXT("FaceOpacity"), 1.0f);
PreviewLayer->SetMaterial(0, LayerMaterial);
}
else
{
PreviewLayer->SetMaterial(0, PreviewMaterials[LayerIndex]);
}
}
}
if (PresentationFloor != nullptr)
{
UMaterialInterface* FloorMaterial = PresentationFloor->GetMaterial(0);
if (FloorDynamicMaterial == nullptr && FloorMaterial != nullptr)
{
FloorDynamicMaterial = UMaterialInstanceDynamic::Create(
FloorMaterial,
this,
TEXT("HigherDimensionalFloorMaterial"));
}
if (FloorDynamicMaterial != nullptr)
{
const FLinearColor FloorColor = UsesMagic120CellPreview()
? FLinearColor(0.008f, 0.018f, 0.032f)
: FLinearColor(0.026f, 0.012f, 0.006f);
FloorDynamicMaterial->SetVectorParameterValue(TEXT("BaseColor"), FloorColor);
FloorDynamicMaterial->SetScalarParameterValue(TEXT("GlowStrength"), 0.015f);
PresentationFloor->SetMaterial(0, FloorDynamicMaterial);
}
}
}
void AHyperTwistHigherDimensionalTrainingShellActor::AddSourcePoint(
const FVector4& Point4D,
const float FifthCoordinate,
const int32 LayerIndex,
const int32 ColorIndex)
{
SourcePoints4D.Add(Point4D);
SourceFifthCoordinates.Add(FifthCoordinate);
SourceLayerIndices.Add(LayerIndex);
SourceColorIndices.Add(ColorIndex);
}
void AHyperTwistHigherDimensionalTrainingShellActor::AddPreviewInstance(
const FVector& RelativeLocation,
const float UniformScale)
const float UniformScale,
const int32 ColorIndex)
{
if (PreviewInstances == nullptr)
if (PreviewColorLayers.Num() == 0)
{
return;
}
const int32 LayerIndex = FMath::Abs(ColorIndex) % PreviewColorLayers.Num();
UInstancedStaticMeshComponent* PreviewLayer = PreviewColorLayers[LayerIndex];
if (PreviewLayer == nullptr || PreviewLayer->GetStaticMesh() == nullptr)
{
return;
}
@ -194,23 +864,79 @@ void AHyperTwistHigherDimensionalTrainingShellActor::AddPreviewInstance(
FTransform InstanceTransform;
InstanceTransform.SetLocation(RelativeLocation);
InstanceTransform.SetScale3D(FVector(UniformScale));
PreviewInstances->AddInstance(InstanceTransform);
PreviewLayer->AddInstance(InstanceTransform);
}
FVector AHyperTwistHigherDimensionalTrainingShellActor::ProjectMagic120CellPoint(
const FVector4& Point) const
{
const float Angle = FMath::DegreesToRadians(ProjectionAngleDegrees);
const float SecondaryAngle = Angle * 0.37f;
const float CosAngle = FMath::Cos(Angle);
const float SinAngle = FMath::Sin(Angle);
const float CosSecondary = FMath::Cos(SecondaryAngle);
const float SinSecondary = FMath::Sin(SecondaryAngle);
const float RotatedX = (Point.X * CosAngle) - (Point.W * SinAngle);
const float RotatedW = (Point.X * SinAngle) + (Point.W * CosAngle);
const float RotatedY = (Point.Y * CosSecondary) - (Point.Z * SinSecondary);
const float RotatedZ = (Point.Y * SinSecondary) + (Point.Z * CosSecondary);
constexpr float ProjectionDistance = 3.2f;
const float Perspective = ProjectionDistance /
FMath::Max(ProjectionDistance - RotatedW, 0.35f);
return FVector(RotatedX, RotatedY, RotatedZ) * PreviewRadius * Perspective
+ FVector(0.0f, 0.0f, PreviewHeightOffset);
}
FVector AHyperTwistHigherDimensionalTrainingShellActor::ProjectMagicCube5DPoint(
const FVector4& Point,
const float FifthCoordinate) const
{
const float Angle = FMath::DegreesToRadians(ProjectionAngleDegrees);
const float CosAngle = FMath::Cos(Angle);
const float SinAngle = FMath::Sin(Angle);
const float SecondaryAngle = Angle * 0.73f;
const float CosSecondary = FMath::Cos(SecondaryAngle);
const float SinSecondary = FMath::Sin(SecondaryAngle);
const float RotatedX = (Point.X * CosAngle) - (Point.W * SinAngle);
const float RotatedW = (Point.X * SinAngle) + (Point.W * CosAngle);
const float RotatedY = (Point.Y * CosSecondary) - (FifthCoordinate * SinSecondary);
const float RotatedV = (Point.Y * SinSecondary) + (FifthCoordinate * CosSecondary);
constexpr float FiveDimensionalProjectionDistance = 3.6f;
const float FiveDimensionalPerspective = FiveDimensionalProjectionDistance /
FMath::Max(FiveDimensionalProjectionDistance - RotatedV, 0.4f);
const FVector4 Projected4D(
RotatedX * FiveDimensionalPerspective,
RotatedY * FiveDimensionalPerspective,
Point.Z * FiveDimensionalPerspective,
RotatedW * FiveDimensionalPerspective);
constexpr float FourDimensionalProjectionDistance = 4.2f;
const float FourDimensionalPerspective = FourDimensionalProjectionDistance /
FMath::Max(FourDimensionalProjectionDistance - Projected4D.W, 0.45f);
const float SpatialScale = PreviewRadius * 0.42f;
return FVector(Projected4D.X, Projected4D.Y, Projected4D.Z)
* SpatialScale
* FourDimensionalPerspective
+ FVector(0.0f, 0.0f, PreviewHeightOffset);
}
FText AHyperTwistHigherDimensionalTrainingShellActor::BuildLabelText() const
{
const FString EffectiveTitle = !Title.IsEmpty()
? Title
: (UsesMagic120CellPreview() ? TEXT("Magic120Cell Training Shell") : TEXT("MagicCube5D Training Shell"));
const FString EffectiveSceneContextId = !SceneContextId.IsEmpty()
? SceneContextId
: TEXT("scene-context/pending");
: (UsesMagic120CellPreview()
? TEXT("Magic120Cell // 120-cell projection lab")
: TEXT("MagicCube5D // order-3 projection lab"));
return FText::FromString(FString::Printf(
TEXT("%s\n%s"),
TEXT("%s\n%d canonical elements // %s"),
*EffectiveTitle,
*EffectiveSceneContextId));
GetCanonicalElementCount(),
*GetProjectionLayerLabel()));
}
bool AHyperTwistHigherDimensionalTrainingShellActor::UsesMagic120CellPreview() const
@ -219,3 +945,10 @@ bool AHyperTwistHigherDimensionalTrainingShellActor::UsesMagic120CellPreview() c
HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey,
ESearchCase::IgnoreCase);
}
FString AHyperTwistHigherDimensionalTrainingShellActor::GetNormalizedFamilyKey() const
{
return UsesMagic120CellPreview()
? HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey
: HyperTwistHigherDimensionalTrainingShellActorInternal::MagicCube5DFamilyKey;
}

View file

@ -375,7 +375,7 @@ namespace prun {
return precheck[coord::N_SLICE2 * corners + coord::slice_to_slice2(slice)];
}
bool init(bool file) {
bool init(bool file, const std::string& save_path) {
init_base();
if (!file) {
@ -385,7 +385,7 @@ namespace prun {
return true;
}
FILE *f = open_file(SAVE, "rb");
FILE *f = open_file(save_path, "rb");
int err = 0;
if (f == NULL) {
@ -393,7 +393,9 @@ namespace prun {
init_phase2();
init_precheck();
f = open_file(SAVE, "wb");
f = open_file(save_path, "wb");
if (f == NULL)
return false;
if (fwrite(phase1, sizeof(prun1), N_FS1TWIST, f) != N_FS1TWIST)
err = 1;
if (fwrite(phase2, sizeof(uint8_t), N_CORNUD2, f) != N_CORNUD2)
@ -401,7 +403,7 @@ namespace prun {
if (fwrite(precheck, sizeof(uint8_t), N_CSLICE2, f) != N_CSLICE2)
err = 1;
if (err)
remove(SAVE.c_str()); // delete file if there was some error writing it
remove(save_path.c_str()); // delete file if there was some error writing it
} else {
phase1 = new prun1[N_FS1TWIST];
phase2 = new uint8_t[N_CORNUD2];

View file

@ -6,6 +6,7 @@
#define __PRUN__
#include <cstdint>
#include <string>
#include "coord.h"
#include "sym.h"
@ -24,12 +25,13 @@ namespace prun {
extern prun1 *phase1;
extern uint8_t *phase2;
extern uint8_t *precheck;
extern const std::string SAVE;
int get_phase1(int flip, int slice, int twist, int togo, move::mask& next);
int get_phase2(int corners, int udedges);
int get_precheck(int corners, int slice);
bool init(bool file = true);
bool init(bool file = true, const std::string& save_path = SAVE);
}

View file

@ -0,0 +1,31 @@
#pragma once
#include "CoreMinimal.h"
namespace HyperTwistRuntimeDiagnostics
{
/**
* Initialize the per-process diagnostics file. The optional
* -HyperTwistDiagnosticsLog=<absolute-path> command-line value overrides the
* normal Saved/Logs/HyperTwistRuntime-latest.log destination.
*/
UNREALHYPERTWIST_API bool InitializeSession();
/** Return the resolved direct diagnostics path for this process. */
UNREALHYPERTWIST_API FString GetDiagnosticsLogPath();
/**
* Append a timestamped semantic event without relying on UE_LOG. This
* remains available in stock Shipping builds where engine logging is
* compiled out.
*/
UNREALHYPERTWIST_API bool AppendEvent(
const FString& Stage,
const FString& Message);
/** True only when the packaged process explicitly requests a semantic-ready frame. */
UNREALHYPERTWIST_API bool IsReadyFrameCaptureRequested();
/** Capture the complete game window, including Slate, into Saved diagnostics evidence. */
UNREALHYPERTWIST_API bool RequestReadyFrameCapture(const FString& SurfaceId);
}

View file

@ -15,6 +15,9 @@ class UNREALHYPERTWIST_API AHyperTwistClassicCubeActor : public AActor
public:
AHyperTwistClassicCubeActor();
/** A 3x3x3 cube has 26 visible cubies; the enclosed core is intentionally not rendered. */
static constexpr int32 ExpectedRenderablePieceCount = 26;
/** Size of each cubelet (one small cube) in unreal units. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
float CubeletSize = 10.0f;
@ -39,6 +42,14 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
UMaterialInterface* InternalMaterial;
/** Low emissive baseline that keeps every sticker readable in sparse training maps. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube|Presentation", meta = (ClampMin = "0.0"))
float BaseGlowStrength = 0.65f;
/** Emissive strength applied to the solver-hinted world-facing side. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube|Presentation", meta = (ClampMin = "0.0"))
float HintGlowStrength = 7.5f;
/** Duration of a quarter-turn animation in seconds. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
float TurnDuration = 0.15f;
@ -138,6 +149,22 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube")
bool IsSettled() const;
/** Number of generated, registered cubelet mesh components with geometry. */
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube|Diagnostics")
int32 GetRenderablePieceCount() const;
/** True when cubie occupancy, sticker orientation, and live mesh transforms form one coherent 3x3x3 cube. */
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube|Diagnostics")
bool HasValidRenderableState() const;
/** Empty for a coherent renderable cube; otherwise identifies the first violated presentation invariant. */
UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube|Diagnostics")
FString GetRenderableStateValidationError() const;
/** Rebuild transient procedural geometry when a cooked map did not preserve a complete presentation. */
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Cube|Diagnostics")
bool EnsureRenderablePresentation();
virtual void OnConstruction(const FTransform& Transform) override;
protected:
@ -167,7 +194,7 @@ private:
TArray<int32> PieceIndices;
FQuat PivotStartRot;
FQuat PivotTargetRot;
float StartTime = 0.0f;
double StartTime = 0.0;
float Duration = 0.15f;
EHyperTwistClassicCubeFace Face = EHyperTwistClassicCubeFace::Up;
EHyperTwistRotationDirection Direction = EHyperTwistRotationDirection::Clockwise;

View file

@ -29,18 +29,6 @@ class UNREALHYPERTWIST_API AHyperTwistClassicCubeGameMode
public:
AHyperTwistClassicCubeGameMode();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
virtual void RequestClassicCubeFreshAttempt_Implementation() override;
virtual void RequestClassicCubeHint_Implementation() override;
virtual void RequestClassicCubeSubmitSolve_Implementation() override;
virtual void RequestClassicCubeToggleFollowAlongMode_Implementation() override;
virtual void RequestClassicCubeBeginVoiceCommandCapture_Implementation() override;
virtual void RequestClassicCubeEndVoiceCommandCapture_Implementation() override;
virtual void RequestClassicCubeCycleVoiceProfile_Implementation() override;
virtual bool CanClassicCubeAcceptGameplayMoveInput_Implementation() const override;
virtual bool IsClassicCubeAnyActionButtonHovered_Implementation() const override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|HUD")
bool bAutoCreateHud = true;
@ -53,6 +41,14 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
bool bAutoSpawnCubeActor = true;
/** Recover a missing PlayerStart/default pawn with a possessed orbit camera. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Presentation")
bool bEnsurePlayableOrbitPawn = true;
/** Supply a first-party key light when a sparse or damaged map has none. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Presentation")
bool bEnsureRuntimeLighting = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube")
FVector CubeSpawnLocation = FVector::ZeroVector;
@ -78,6 +74,14 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Solver")
bool bShowFullSolutionSequence = true;
/** Search budget used by the non-blocking classic-cube guidance worker. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Solver", meta = (ClampMin = "1"))
int32 SolverTimeLimitMs = 1500;
/** Maximum notation length accepted from the classic-cube guidance worker. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Solver", meta = (ClampMin = "1"))
int32 SolverMaximumMoveCount = 32;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Speech")
bool bEnableVoiceCommandCapture = true;
@ -96,6 +100,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube")
TObjectPtr<AHyperTwistClassicCubeActor> ActiveCubeActor = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube|Presentation")
TObjectPtr<AHyperTwistClassicCubeOrbitPawn> ActiveOrbitPawn = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|ClassicCube|HUD")
TObjectPtr<UHyperTwistClassicCubeHUDWidget> ActiveHudWidget = nullptr;
@ -137,10 +144,24 @@ public:
bool IsVoiceCommandCaptureActive() const;
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
virtual void RequestClassicCubeFreshAttempt_Implementation() override;
virtual void RequestClassicCubeHint_Implementation() override;
virtual void RequestClassicCubeSubmitSolve_Implementation() override;
virtual void RequestClassicCubeToggleFollowAlongMode_Implementation() override;
virtual void RequestClassicCubeBeginVoiceCommandCapture_Implementation() override;
virtual void RequestClassicCubeEndVoiceCommandCapture_Implementation() override;
virtual void RequestClassicCubeCycleVoiceProfile_Implementation() override;
virtual bool CanClassicCubeAcceptGameplayMoveInput_Implementation() const override;
virtual bool IsClassicCubeAnyActionButtonHovered_Implementation() const override;
void ApplyLaunchOverridesFromCommandLine();
void QueueReplayAutoExit(double CountdownSeconds);
UHyperTwistTrainingSubsystem* ResolveTrainingSubsystem() const;
AHyperTwistClassicCubeActor* ResolveOrSpawnCubeActor();
AHyperTwistClassicCubeOrbitPawn* ResolveOrSpawnOrbitPawn();
void EnsureRuntimeLighting();
UHyperTwistClassicCubeHUDWidget* ResolveOrCreateHudWidget();
void RefreshHud();
void TickReplayPlayback(float DeltaSeconds);
@ -154,6 +175,13 @@ protected:
void RefreshLocalLeaderboardLine();
void RecordSolvedAttemptToLocalLeaderboard();
void RefreshSolverGuidance(bool bForceClearHint = false);
void StartPendingSolverGuidanceTask();
void CompleteSolverGuidanceTask(
uint64 RequestSerial,
const FString& FaceletString,
bool bForceClearHint,
TArray<FString>&& SolutionNotation
);
void RefreshVoiceProfiles();
TArray<FString> ResolveScrambleMovesForCurrentSession() const;
void ProcessCompletedMoveHistory();
@ -210,6 +238,8 @@ private:
FString HintLineOverride;
FString ModeLineOverride;
FString VoiceLineOverride;
FString PendingSolverFaceletString;
FString ActiveSolverFaceletString;
bool bAwaitingScrambleSettlement = false;
bool bInspectionStarted = false;
bool bSolveStartedFromGameplayMove = false;
@ -224,6 +254,9 @@ private:
bool bVoiceCommandCaptureActive = false;
bool bScrambleReadyNarrated = false;
bool bSolveStartNarrated = false;
bool bRuntimeLightingReady = false;
bool bSolverGuidanceTaskActive = false;
bool bPendingSolverForceClearHint = false;
int32 ObservedCompletedMoveCount = 0;
int32 FollowAlongStepIndex = 0;
int32 FollowAlongCorrectMoveCount = 0;
@ -231,10 +264,13 @@ private:
int32 ReplayPlaybackNextMoveIndex = 0;
int32 VoiceCaptureSampleRateHz = 16000;
int32 VoiceCaptureChannelCount = 1;
uint64 SolverGuidanceRequestSerial = 0;
double ReplayPlaybackElapsedSeconds = 0.0;
double ReplayAutoQuitTailSeconds = 1.5;
double ReplayAutoQuitCountdownSeconds = -1.0;
double VoiceCaptureStartedAtSeconds = 0.0;
void RequestRuntimeReadyDiagnosticsCapture();
};
UCLASS(BlueprintType, Blueprintable)

View file

@ -6,6 +6,7 @@
class UButton;
class UTextBlock;
class UUniformGridPanel;
class UVerticalBox;
UCLASS(BlueprintType, Blueprintable)
@ -14,8 +15,15 @@ class UNREALHYPERTWIST_API UHyperTwistClassicCubeHUDWidget : public UUserWidget
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
virtual void NativeConstruct() override;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|HUD")
bool PrepareClassicCubeHudSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|HUD")
bool IsClassicCubeHudSurfaceReady() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|HUD")
void SetHudLines(
const FString& InStatusLine,
@ -44,15 +52,25 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|HUD")
void SetVoiceCycleButtonLabel(const FString& InLabel);
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
private:
void EnsureWidgetTreeBuilt();
UTextBlock* AddLine(UVerticalBox* Parent, const TCHAR* WidgetName);
UButton* CreateActionButton(
UTextBlock* AddLine(
UVerticalBox* Parent,
const TCHAR* WidgetName,
int32 FontSize,
const FLinearColor& Color
);
UButton* CreateActionButton(
UUniformGridPanel* Parent,
const TCHAR* ButtonName,
const TCHAR* LabelName,
TObjectPtr<UTextBlock>& OutLabel,
const FString& InitialLabel
const FString& InitialLabel,
int32 Row,
int32 Column
);
UFUNCTION()

View file

@ -27,16 +27,16 @@ public:
bool bFollowActiveCubeActor = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
float InitialArmLength = 360.0f;
float InitialArmLength = 118.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
float MinimumArmLength = 180.0f;
float MinimumArmLength = 72.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
float MaximumArmLength = 720.0f;
float MaximumArmLength = 480.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
float ZoomStep = 42.0f;
float ZoomStep = 24.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Camera")
float OrbitYawDegreesPerPixel = 0.25f;

View file

@ -25,7 +25,7 @@ public:
bool bRemoveDashboardOnEndPlay = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
int32 DashboardZOrder = 0;
int32 DashboardZOrder = 100;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
TSubclassOf<UHyperTwistCoachDashboardWidget> DashboardWidgetClass;

View file

@ -557,9 +557,16 @@ class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistT
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool PrepareCoachDashboardSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard")
bool IsCoachDashboardSurfaceReady() const;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
int32 DefaultDeferredHours = 24;
@ -1187,6 +1194,8 @@ public:
bool UndoLastLiveAttemptSplit();
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> RootLayout = nullptr;

View file

@ -5,6 +5,13 @@
#include "HyperTwistFirstRunLaunchLibrary.generated.h"
enum class EHyperTwistPackagedStartupRouteParseResult : uint8
{
NotPresent,
Valid,
Invalid
};
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistFirstRunLaunchRoute
{
@ -80,6 +87,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
static FString GetPackagedStartupMapAssetPath();
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
static FString GetPackagedStartupRouteArgumentName();
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
static FString GetRuntimeLogFileName();
@ -97,4 +107,10 @@ public:
const FString& RouteId,
FHyperTwistFirstRunLaunchRoute& OutRoute
);
static EHyperTwistPackagedStartupRouteParseResult ParsePackagedStartupRouteArgument(
const TCHAR* CommandLine,
FHyperTwistFirstRunLaunchRoute& OutRoute,
FString& OutFailureReason
);
};

View file

@ -24,7 +24,13 @@ public:
bool bShowFirstRunLaunchOnBeginPlay = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
int32 FirstRunLaunchZOrder = 0;
int32 FirstRunLaunchZOrder = 100;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun|Diagnostics", meta = (ClampMin = "1"))
int32 MaxFirstRunSurfaceValidationAttempts = 20;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun|Diagnostics", meta = (ClampMin = "0.01"))
float FirstRunSurfaceValidationIntervalSeconds = 0.1f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
TSubclassOf<UHyperTwistFirstRunLaunchWidget> FirstRunLaunchWidgetClass;
@ -67,6 +73,7 @@ public:
protected:
void ApplyFirstRunInputMode();
bool TryOpenCommandLineStartupRoute();
void OpenMapRoute(const FString& MapAssetPath, const FString& GameModeClassPath);
AHyperTwistCoachDashboardActor* FindExistingDashboardActor();
AHyperTwistCoachDashboardActor* SpawnDashboardActor();
@ -80,6 +87,8 @@ protected:
void PersistStartupDiagnostics(const FString& ResultStatus) const;
bool TryRecoverFromFirstRunFailure(const FString& FailureReason);
FString DescribeCurrentWorldName() const;
void ScheduleFirstRunSurfaceValidation();
void ValidateFirstRunSurfaceLayout();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|FirstRun|Diagnostics")
FString StartupDiagnosticsLogPath;
@ -97,4 +106,6 @@ protected:
bool bStartupRecoveryAttempted = false;
TArray<FString> StartupDiagnosticsLines;
FTimerHandle FirstRunSurfaceValidationTimerHandle;
int32 FirstRunSurfaceValidationAttempt = 0;
};

View file

@ -24,6 +24,7 @@ class UNREALHYPERTWIST_API UHyperTwistFirstRunLaunchWidget
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
virtual void NativeConstruct() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
@ -44,7 +45,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|FirstRun")
void RebuildFirstRunLaunchSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
bool IsFirstRunLaunchSurfaceReady() const;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
UFUNCTION()
void OpenCoachDashboard();

View file

@ -0,0 +1,135 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/HUD.h"
#include "GameFramework/Pawn.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardPlayerController.h"
#include "HyperTwistHigherDimensionalTrainingGameMode.generated.h"
class AHyperTwistHigherDimensionalTrainingShellActor;
class ADirectionalLight;
class UCameraComponent;
class USceneComponent;
class USpringArmComponent;
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistHigherDimensionalOrbitPawn : public APawn
{
GENERATED_BODY()
public:
AHyperTwistHigherDimensionalOrbitPawn();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|HigherDimensional|Camera")
TObjectPtr<USceneComponent> SceneRoot = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|HigherDimensional|Camera")
TObjectPtr<USpringArmComponent> SpringArm = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|HigherDimensional|Camera")
TObjectPtr<UCameraComponent> CameraComponent = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|Camera")
float InitialArmLength = 820.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|Camera")
float MinimumArmLength = 360.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|Camera")
float MaximumArmLength = 1550.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|Camera")
float InitialYawDegrees = -34.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|Camera")
float InitialPitchDegrees = -16.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|Camera")
float OrbitDegreesPerPixel = 0.24f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|Camera")
float ZoomStep = 85.0f;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|HigherDimensional|Camera")
void ResetOrbit();
private:
FVector OrbitFocusPoint = FVector(0.0f, 0.0f, 90.0f);
float CurrentYawDegrees = -34.0f;
float CurrentPitchDegrees = -16.0f;
void RefreshOrbitFocusPoint();
void ApplyOrbitTransform();
bool ShouldOrbitFromMouseInput() const;
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistHigherDimensionalTrainingHUD : public AHUD
{
GENERATED_BODY()
public:
virtual void DrawHUD() override;
private:
TWeakObjectPtr<AHyperTwistHigherDimensionalTrainingShellActor> CachedTrainingShell;
AHyperTwistHigherDimensionalTrainingShellActor* ResolveTrainingShell();
void DrawStatusLine(const FString& Text, float X, float& InOutY, const FLinearColor& Color, float Scale = 1.0f);
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistHigherDimensionalTrainingPlayerController
: public AHyperTwistCoachDashboardPlayerController
{
GENERATED_BODY()
public:
AHyperTwistHigherDimensionalTrainingPlayerController();
virtual void BeginPlay() override;
virtual void PlayerTick(float DeltaTime) override;
private:
TWeakObjectPtr<AHyperTwistHigherDimensionalTrainingShellActor> CachedTrainingShell;
AHyperTwistHigherDimensionalTrainingShellActor* ResolveTrainingShell();
void ToggleCoachDashboardSurface();
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistHigherDimensionalTrainingGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AHyperTwistHigherDimensionalTrainingGameMode();
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional")
TSubclassOf<AHyperTwistHigherDimensionalTrainingShellActor> TrainingShellActorClass;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|HigherDimensional")
TObjectPtr<AHyperTwistHigherDimensionalTrainingShellActor> ActiveTrainingShell = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|HigherDimensional|Presentation")
TObjectPtr<ADirectionalLight> ActivePresentationLight = nullptr;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|HigherDimensional")
FString ResolveCurrentFamilyKey() const;
private:
AHyperTwistHigherDimensionalTrainingShellActor* FindExistingTrainingShell() const;
AHyperTwistHigherDimensionalTrainingShellActor* SpawnTrainingShell(const FString& FamilyKey);
void EnsureRuntimeLighting(const FString& FamilyKey);
void RequestRuntimeReadyDiagnosticsCapture();
void PersistRuntimeReadinessDiagnostics(
const FString& FamilyKey,
const AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell
) const;
};

View file

@ -5,8 +5,11 @@
#include "HyperTwistHigherDimensionalTrainingShellActor.generated.h"
class UInstancedStaticMeshComponent;
class UMaterialInstanceDynamic;
class UMaterialInterface;
class USceneComponent;
class UStaticMesh;
class UStaticMeshComponent;
class UTextRenderComponent;
UCLASS(BlueprintType)
@ -17,7 +20,10 @@ class UNREALHYPERTWIST_API AHyperTwistHigherDimensionalTrainingShellActor : publ
public:
AHyperTwistHigherDimensionalTrainingShellActor();
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void OnConstruction(const FTransform& Transform) override;
virtual void Tick(float DeltaSeconds) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell")
TObjectPtr<USceneComponent> SceneRoot = nullptr;
@ -25,6 +31,12 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell")
TObjectPtr<UInstancedStaticMeshComponent> PreviewInstances = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell")
TArray<TObjectPtr<UInstancedStaticMeshComponent>> PreviewColorLayers;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell")
TObjectPtr<UStaticMeshComponent> PresentationFloor = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell")
TObjectPtr<UTextRenderComponent> LabelComponent = nullptr;
@ -91,16 +103,121 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell")
float PreviewHeightOffset = 90.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell|Projection")
bool bAutoRotateProjection = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell|Projection", meta = (ClampMin = "0.0"))
float ProjectionRotationSpeedDegrees = 12.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell|Projection", meta = (ClampMin = "0.0"))
float ProjectionAngleDegrees = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell|Projection")
int32 VisibleProjectionLayer = -1;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State")
int32 RuntimeStateSeed = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State")
FString LastRuntimeAction = TEXT("ready");
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|Persistence")
FString LastPersistenceStatus = TEXT("not-loaded");
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell")
void ConfigureForFamily(const FString& InFamilyKey);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Projection")
void ToggleAutoRotateProjection();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Projection")
void NudgeProjection(float DeltaDegrees);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Projection")
void CycleProjectionLayer(int32 Direction);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Projection")
void ResetProjection();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|State")
void CreateScrambledRuntimeState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Persistence")
bool SaveRuntimeState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Persistence")
bool LoadRuntimeState();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Shell")
int32 GetRenderableElementCount() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Shell")
int32 GetCanonicalElementCount() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Shell")
int32 GetProjectionLayerCount() const;
/** Number of distinct first-party materials assigned to the six projection layers. */
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Shell|Diagnostics")
int32 GetDistinctPreviewMaterialCount() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Shell")
FString GetProjectionLayerLabel() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Shell")
FString GetRuntimeStatusSummary() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Shell|Persistence")
FString GetRuntimeStatePath() const;
static int32 GetCanonicalElementCountForFamily(const FString& InFamilyKey);
private:
UPROPERTY(Transient)
TObjectPtr<UStaticMesh> CubePreviewMesh = nullptr;
UPROPERTY(Transient)
TObjectPtr<UStaticMesh> SpherePreviewMesh = nullptr;
UPROPERTY(Transient)
TArray<TObjectPtr<UMaterialInterface>> PreviewMaterials;
/** Retained emissive variants prevent GC and preserve layer color on cooked ISM geometry. */
UPROPERTY(Transient)
TArray<TObjectPtr<UMaterialInstanceDynamic>> PreviewDynamicMaterials;
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> FloorDynamicMaterial = nullptr;
TArray<FVector4> SourcePoints4D;
TArray<float> SourceFifthCoordinates;
TArray<int32> SourceLayerIndices;
TArray<int32> SourceColorIndices;
float ProjectionRefreshAccumulator = 0.0f;
void RefreshPreview();
void RefreshLabel();
void RefreshMetadataTags();
void BuildMagic120CellPreview();
void BuildMagicCube5DPreview();
void AddPreviewInstance(const FVector& RelativeLocation, float UniformScale);
void RebuildSourcePoints();
void BuildMagic120CellSourcePoints();
void BuildMagicCube5DSourcePoints();
void UpdatePreviewTransforms();
void ClearPreviewInstances();
void ConfigurePreviewComponents();
void AddSourcePoint(
const FVector4& Point4D,
float FifthCoordinate,
int32 LayerIndex,
int32 ColorIndex
);
void AddPreviewInstance(
const FVector& RelativeLocation,
float UniformScale,
int32 ColorIndex
);
FVector ProjectMagic120CellPoint(const FVector4& Point) const;
FVector ProjectMagicCube5DPoint(const FVector4& Point, float FifthCoordinate) const;
FText BuildLabelText() const;
bool UsesMagic120CellPreview() const;
FString GetNormalizedFamilyKey() const;
};

View file

@ -25,6 +25,70 @@ namespace HyperTwistClassicCubeActorTestInternal
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistClassicCubePresentationDefaultsTest,
"HyperTwist.Simulation.ClassicCube.PresentationDefaults",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistClassicCubePresentationDefaultsTest::RunTest(const FString& Parameters)
{
AHyperTwistClassicCubeActor* CubeActor =
NewObject<AHyperTwistClassicCubeActor>(GetTransientPackage());
TestNotNull(TEXT("The classic cube actor must be constructible for presentation checks."), CubeActor);
if (CubeActor == nullptr)
{
return false;
}
TestEqual(
TEXT("The runtime fallback cube must own all six first-party sticker materials."),
CubeActor->FaceMaterials.Num(),
6
);
for (int32 MaterialIndex = 0; MaterialIndex < CubeActor->FaceMaterials.Num(); ++MaterialIndex)
{
TestNotNull(
*FString::Printf(TEXT("Sticker material %d must be available without map-authored overrides."), MaterialIndex),
CubeActor->FaceMaterials[MaterialIndex]
);
}
TestNotNull(
TEXT("The runtime fallback cube must own its first-party internal material."),
CubeActor->InternalMaterial
);
TestTrue(
TEXT("The base glow must keep stickers readable in sparse maps."),
CubeActor->BaseGlowStrength > 0.0f
);
TestTrue(
TEXT("The hint glow must remain stronger than the presentation baseline."),
CubeActor->HintGlowStrength > CubeActor->BaseGlowStrength
);
TestEqual(
TEXT("The presentation invariant must cover all 26 visible cubies without inventing a rendered core."),
AHyperTwistClassicCubeActor::ExpectedRenderablePieceCount,
26
);
TestTrue(
TEXT("Classic turn animation must remain tick-capable in packaged runtime."),
CubeActor->PrimaryActorTick.bCanEverTick
);
TestTrue(
TEXT("Classic turn animation must start with actor ticking enabled."),
CubeActor->PrimaryActorTick.bStartWithTickEnabled
);
TestTrue(
TEXT("Classic turn animation must be able to finish through transient application pause states."),
CubeActor->PrimaryActorTick.bTickEvenWhenPaused
);
TestTrue(
TEXT("Classic quarter turns must retain a positive animation duration."),
CubeActor->TurnDuration > 0.0f
);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistClassicCubeImpactNormalMappingTest,
"HyperTwist.Simulation.ClassicCube.ImpactNormalMapping",

View file

@ -239,6 +239,66 @@ bool FHyperTwistClassicCubeRotationTest::RunTest(const FString& Parameters)
CubeActor->GetGameplayCompletedMoveCount(),
1
);
TestTrue(
TEXT("A single R turn must preserve a coherent 26-cubie, 54-sticker renderable state."),
CubeActor->HasValidRenderableState()
);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistClassicCubeRenderableStateInvariantTest,
"HyperTwist.Simulation.ClassicCube.Behavior.RenderableStateInvariant",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistClassicCubeRenderableStateInvariantTest::RunTest(const FString& Parameters)
{
AHyperTwistClassicCubeActor* CubeActor =
HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor();
TestNotNull(TEXT("The classic cube actor must exist for renderable-state checks."), CubeActor);
if (CubeActor == nullptr)
{
return false;
}
FRandomStream ScrambleStream(0xA17FA11A);
const TArray<FString> Moves =
HyperTwistClassicCubeBehaviorTestInternal::GenerateDeterministicScramble(
ScrambleStream,
120);
for (int32 MoveIndex = 0; MoveIndex < Moves.Num(); ++MoveIndex)
{
FHyperTwistClassicCubeMoveDescriptor MoveDescriptor;
if (!TestTrue(
*FString::Printf(TEXT("Move %d must parse for renderable-state validation."), MoveIndex),
UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(
Moves[MoveIndex],
MoveDescriptor)))
{
return false;
}
if (!TestTrue(
*FString::Printf(TEXT("Move %d must execute for renderable-state validation."), MoveIndex),
CubeActor->ExecuteMoveDescriptorImmediately(MoveDescriptor)))
{
return false;
}
const FString ValidationError = CubeActor->GetRenderableStateValidationError();
if (!TestTrue(
*FString::Printf(
TEXT("Move %d (%s) must preserve coherent cubie occupancy and outward sticker placement: %s"),
MoveIndex,
*Moves[MoveIndex],
*ValidationError),
ValidationError.IsEmpty()))
{
return false;
}
}
AddInfo(TEXT("Validated cubie occupancy and all 54 outward stickers after 120 deterministic moves."));
return true;
}

View file

@ -36,6 +36,14 @@ bool FHyperTwistClassicCubeGameModeDefaultsTest::RunTest(const FString& Paramete
);
TestTrue(TEXT("The classic cube game mode must still auto-create its HUD by default."), GameMode->bAutoCreateHud);
TestTrue(TEXT("The classic cube game mode must still auto-spawn the cube actor by default."), GameMode->bAutoSpawnCubeActor);
TestTrue(
TEXT("The classic cube game mode must recover a playable orbit camera when map anchors are missing."),
GameMode->bEnsurePlayableOrbitPawn
);
TestTrue(
TEXT("The classic cube game mode must recover presentation lighting in sparse maps."),
GameMode->bEnsureRuntimeLighting
);
TestTrue(
TEXT("The classic cube game mode must keep its default cube spawn above the generic validation-map floor."),
GameMode->CubeSpawnLocation.Z > 0.0f
@ -45,6 +53,26 @@ bool FHyperTwistClassicCubeGameModeDefaultsTest::RunTest(const FString& Paramete
GameMode->DefaultSessionMode,
EHyperTwistClassicCubeSessionMode::FreePlay
);
TestTrue(
TEXT("The Classic game mode must keep its runtime settlement loop tick-capable."),
GameMode->PrimaryActorTick.bCanEverTick
);
TestTrue(
TEXT("The Classic game mode must start with settlement processing enabled."),
GameMode->PrimaryActorTick.bStartWithTickEnabled
);
TestTrue(
TEXT("The Classic game mode must service wall-clock settlement diagnostics while the world is paused."),
GameMode->PrimaryActorTick.bTickEvenWhenPaused
);
TestTrue(
TEXT("The classic solver worker must have a positive time budget."),
GameMode->SolverTimeLimitMs > 0
);
TestTrue(
TEXT("The classic solver worker must accept at least one move."),
GameMode->SolverMaximumMoveCount > 0
);
return true;
}
@ -97,6 +125,10 @@ bool FHyperTwistClassicCubeOrbitPawnDefaultsTest::RunTest(const FString& Paramet
OrbitPawn->MinimumArmLength < OrbitPawn->InitialArmLength
&& OrbitPawn->InitialArmLength < OrbitPawn->MaximumArmLength
);
TestTrue(
TEXT("The default orbit framing must keep the 3x3 puzzle legible at desktop resolution."),
OrbitPawn->InitialArmLength <= 125.0f
);
TestTrue(
TEXT("The orbit pawn must follow the active cube actor by default."),
OrbitPawn->bFollowActiveCubeActor

View file

@ -3,6 +3,10 @@
#include "Misc/AutomationTest.h"
#include "Misc/ConfigCacheIni.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Button.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchGameMode.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchLibrary.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchPlayerController.h"
@ -78,16 +82,20 @@ bool FHyperTwistFirstRunLaunchRouteContractTest::RunTest(const FString& Paramete
);
TestTrue(
TEXT("The Magic120Cell route must target its dedicated family map."),
TEXT("The Magic120Cell route must target its dedicated family map and runtime game mode."),
UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(TEXT("magic-120-cell-training"), Route)
&& Route.MapAssetPath == TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining")
&& Route.GameModeClassPath == TEXT("/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode")
&& Route.LaunchUrl == TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining?game=/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode")
&& Route.RuntimeProofStatus == TEXT("ready-dedicated-family-map")
);
TestTrue(
TEXT("The MagicCube5D route must target its dedicated family map."),
TEXT("The MagicCube5D route must target its dedicated family map and runtime game mode."),
UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(TEXT("magic-cube-5d-training"), Route)
&& Route.MapAssetPath == TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining")
&& Route.GameModeClassPath == TEXT("/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode")
&& Route.LaunchUrl == TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining?game=/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode")
&& Route.RuntimeProofStatus == TEXT("ready-dedicated-family-map")
);
@ -103,6 +111,92 @@ bool FHyperTwistFirstRunLaunchRouteContractTest::RunTest(const FString& Paramete
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistPackagedStartupRouteArgumentContractTest,
"HyperTwist.FirstRun.PackagedStartupRouteArgumentContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistPackagedStartupRouteArgumentContractTest::RunTest(const FString& Parameters)
{
TestEqual(
TEXT("The packaged startup-route switch must have one stable public name."),
UHyperTwistFirstRunLaunchLibrary::GetPackagedStartupRouteArgumentName(),
FString(TEXT("HyperTwistStartupRoute"))
);
FHyperTwistFirstRunLaunchRoute Route;
FString FailureReason;
TestTrue(
TEXT("An unrelated command line must not request a startup route."),
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
TEXT("-windowed -notraceserver"),
Route,
FailureReason
) == EHyperTwistPackagedStartupRouteParseResult::NotPresent
);
TestTrue(TEXT("An absent route must not fabricate a route."), Route.RouteId.IsEmpty());
TestTrue(TEXT("An absent route must not fabricate an error."), FailureReason.IsEmpty());
TestTrue(
TEXT("A registered follow-along route must parse through first-run authority."),
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
TEXT("-windowed -HyperTwistStartupRoute=follow-along-training"),
Route,
FailureReason
) == EHyperTwistPackagedStartupRouteParseResult::Valid
);
TestEqual(
TEXT("The parsed route must retain the registered identifier."),
Route.RouteId,
FString(TEXT("follow-along-training"))
);
TestEqual(
TEXT("The parsed route must retain its dedicated map authority."),
Route.MapAssetPath,
FString(TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining"))
);
TestTrue(TEXT("A valid route must not retain an error."), FailureReason.IsEmpty());
TestTrue(
TEXT("An unknown route identifier must be rejected."),
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
TEXT("-HyperTwistStartupRoute=unknown-training"),
Route,
FailureReason
) == EHyperTwistPackagedStartupRouteParseResult::Invalid
);
TestTrue(TEXT("An unknown route must not escape as resolved state."), Route.RouteId.IsEmpty());
TestTrue(TEXT("An unknown route must explain its rejection."), !FailureReason.IsEmpty());
TestTrue(
TEXT("Duplicate startup-route switches must be rejected rather than resolved ambiguously."),
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
TEXT("-HyperTwistStartupRoute=follow-along-training -HyperTwistStartupRoute=magic-120-cell-training"),
Route,
FailureReason
) == EHyperTwistPackagedStartupRouteParseResult::Invalid
);
TestTrue(
TEXT("Map-option injection must be rejected before route lookup."),
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
TEXT("-HyperTwistStartupRoute=follow-along-training?game=Other"),
Route,
FailureReason
) == EHyperTwistPackagedStartupRouteParseResult::Invalid
);
TestTrue(
TEXT("A value-less startup-route switch must be rejected."),
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
TEXT("-HyperTwistStartupRoute"),
Route,
FailureReason
) == EHyperTwistPackagedStartupRouteParseResult::Invalid
);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistFirstRunLaunchGuidanceContractTest,
"HyperTwist.FirstRun.LaunchGuidanceContract",
@ -137,7 +231,7 @@ bool FHyperTwistFirstRunLaunchGuidanceContractTest::RunTest(const FString& Param
TEXT("The first-run guidance must tell operators where to find runtime and startup logs."),
HyperTwistFirstRunLaunchSurfaceTestInternal::ContainsGuidanceSubstring(
Lines,
TEXT("Saved/Logs/UnrealHyperTwist.log")
TEXT("Saved/Logs/HyperTwistRuntime-latest.log")
)
&& HyperTwistFirstRunLaunchSurfaceTestInternal::ContainsGuidanceSubstring(
Lines,
@ -183,7 +277,7 @@ bool FHyperTwistFirstRunLaunchGameModeDefaultsTest::RunTest(const FString& Param
TestEqual(
TEXT("The first-run contract must publish the default runtime log file name."),
UHyperTwistFirstRunLaunchLibrary::GetRuntimeLogFileName(),
FString(TEXT("UnrealHyperTwist.log"))
FString(TEXT("HyperTwistRuntime-latest.log"))
);
TestEqual(
TEXT("The first-run contract must publish the startup diagnostics log file name."),
@ -198,6 +292,16 @@ bool FHyperTwistFirstRunLaunchGameModeDefaultsTest::RunTest(const FString& Param
TEXT("The first-run player controller must auto-show the launch menu by default."),
PlayerController != nullptr && PlayerController->bShowFirstRunLaunchOnBeginPlay
);
TestTrue(
TEXT("The first-run surface must render above ordinary gameplay HUD layers."),
PlayerController != nullptr && PlayerController->FirstRunLaunchZOrder > 0
);
TestTrue(
TEXT("The first-run surface must use bounded delayed layout validation."),
PlayerController != nullptr
&& PlayerController->MaxFirstRunSurfaceValidationAttempts > 1
&& PlayerController->FirstRunSurfaceValidationIntervalSeconds > 0.0f
);
TestTrue(
TEXT("The first-run player controller must default to the native first-run widget."),
PlayerController != nullptr && PlayerController->FirstRunLaunchWidgetClass.Get() == nullptr
@ -261,4 +365,74 @@ bool FHyperTwistFirstRunLaunchGameModeDefaultsTest::RunTest(const FString& Param
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistNativeWidgetRootLifecycleTest,
"HyperTwist.FirstRun.NativeWidgetRootLifecycle",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistNativeWidgetRootLifecycleTest::RunTest(const FString& Parameters)
{
UHyperTwistFirstRunLaunchWidget* FirstRunWidget =
NewObject<UHyperTwistFirstRunLaunchWidget>();
TestNotNull(TEXT("The native first-run widget must be constructible."), FirstRunWidget);
if (FirstRunWidget != nullptr)
{
FirstRunWidget->TakeWidget();
TestTrue(
TEXT("TakeWidget must adopt a fully built first-run tree rather than caching an empty spacer."),
FirstRunWidget->IsFirstRunLaunchSurfaceReady()
);
const UButton* CoachDashboardButton = FirstRunWidget->WidgetTree != nullptr
? Cast<UButton>(FirstRunWidget->WidgetTree->FindWidget(FName(TEXT("CoachDashboardRouteButton"))))
: nullptr;
const UButton* ClassicCubeButton = FirstRunWidget->WidgetTree != nullptr
? Cast<UButton>(FirstRunWidget->WidgetTree->FindWidget(FName(TEXT("ClassicCubeRouteButton"))))
: nullptr;
TestNotNull(TEXT("The native launch tree must retain the coach-dashboard route button."), CoachDashboardButton);
TestNotNull(TEXT("The native launch tree must retain the Classic cube route button."), ClassicCubeButton);
TestTrue(
TEXT("The coach-dashboard route button must retain its click delegate after Slate rebuild."),
CoachDashboardButton != nullptr && CoachDashboardButton->OnClicked.IsBound()
);
TestTrue(
TEXT("The Classic cube route button must retain its click delegate after Slate rebuild."),
ClassicCubeButton != nullptr && ClassicCubeButton->OnClicked.IsBound()
);
}
UHyperTwistClassicCubeHUDWidget* ClassicHudWidget =
NewObject<UHyperTwistClassicCubeHUDWidget>();
TestNotNull(TEXT("The native classic cube HUD must be constructible."), ClassicHudWidget);
if (ClassicHudWidget != nullptr)
{
ClassicHudWidget->TakeWidget();
TestTrue(
TEXT("TakeWidget must adopt the native classic cube HUD tree."),
ClassicHudWidget->IsClassicCubeHudSurfaceReady()
);
}
UHyperTwistCoachDashboardWidget* DashboardWidget =
NewObject<UHyperTwistCoachDashboardWidget>();
TestNotNull(TEXT("The native coach dashboard must be constructible."), DashboardWidget);
if (DashboardWidget != nullptr)
{
DashboardWidget->TakeWidget();
TestTrue(
TEXT("TakeWidget must adopt the scrollable native coach dashboard tree."),
DashboardWidget->IsCoachDashboardSurfaceReady()
);
TestNotNull(
TEXT("The native coach dashboard must expose its scroll container."),
DashboardWidget->WidgetTree != nullptr
? DashboardWidget->WidgetTree->FindWidget(FName(TEXT("CoachDashboardScroll")))
: nullptr
);
}
return true;
}
#endif

View file

@ -14,7 +14,7 @@
#include "Serialization/JsonSerializer.h"
#include "HyperTwistSimulation/HyperTwistSimulationLibrary.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardGameMode.h"
#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingGameMode.h"
#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h"
#include "HyperTwistTraining/HyperTwistTrainingLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
@ -43,6 +43,10 @@ namespace HyperTwistHigherDimensionalPhase6CTestInternal
FString RuntimeModeId;
FString ProjectionProfileId;
FString PrimaryPersistenceBoundaryId;
FString AuthoredViaGameModeClassPath;
FString RuntimePresentationClassPath;
FString PresentationEnvironmentOwnership;
int32 CanonicalRenderableElementCount = 0;
};
struct FPhase6CDedicatedMapManifest
@ -171,8 +175,9 @@ namespace HyperTwistHigherDimensionalPhase6CTestInternal
FPhase6CDedicatedMapManifestEntry& OutEntry
)
{
return JsonObject.IsValid()
&& JsonObject->TryGetStringField(TEXT("mapKind"), OutEntry.MapKind)
double CanonicalRenderableElementCount = 0.0;
const bool bParsed = JsonObject.IsValid()
&& JsonObject->TryGetStringField(TEXT("mapKind"), OutEntry.MapKind)
&& JsonObject->TryGetStringField(TEXT("familyKey"), OutEntry.FamilyKey)
&& JsonObject->TryGetStringField(TEXT("mapAssetPath"), OutEntry.MapAssetPath)
&& JsonObject->TryGetStringField(TEXT("mapFileRelativePath"), OutEntry.MapFileRelativePath)
@ -186,9 +191,18 @@ namespace HyperTwistHigherDimensionalPhase6CTestInternal
&& JsonObject->TryGetStringField(TEXT("interactiveSceneSurfaceId"), OutEntry.InteractiveSceneSurfaceId)
&& JsonObject->TryGetStringField(TEXT("sceneContextId"), OutEntry.SceneContextId)
&& JsonObject->TryGetStringField(TEXT("puzzleId"), OutEntry.PuzzleId)
&& JsonObject->TryGetStringField(TEXT("runtimeModeId"), OutEntry.RuntimeModeId)
&& JsonObject->TryGetStringField(TEXT("projectionProfileId"), OutEntry.ProjectionProfileId)
&& JsonObject->TryGetStringField(TEXT("primaryPersistenceBoundaryId"), OutEntry.PrimaryPersistenceBoundaryId);
&& JsonObject->TryGetStringField(TEXT("runtimeModeId"), OutEntry.RuntimeModeId)
&& JsonObject->TryGetStringField(TEXT("projectionProfileId"), OutEntry.ProjectionProfileId)
&& JsonObject->TryGetStringField(TEXT("primaryPersistenceBoundaryId"), OutEntry.PrimaryPersistenceBoundaryId)
&& JsonObject->TryGetStringField(TEXT("authoredViaGameModeClassPath"), OutEntry.AuthoredViaGameModeClassPath)
&& JsonObject->TryGetStringField(TEXT("runtimePresentationClassPath"), OutEntry.RuntimePresentationClassPath)
&& JsonObject->TryGetStringField(TEXT("presentationEnvironmentOwnership"), OutEntry.PresentationEnvironmentOwnership)
&& JsonObject->TryGetNumberField(TEXT("canonicalRenderableElementCount"), CanonicalRenderableElementCount);
if (bParsed)
{
OutEntry.CanonicalRenderableElementCount = static_cast<int32>(CanonicalRenderableElementCount);
}
return bParsed;
}
bool TryLoadDedicatedMapManifest(FPhase6CDedicatedMapManifest& OutManifest)
@ -704,6 +718,69 @@ bool FHyperTwistHigherDimensionalPhase6CDedicatedMapOwnedShellTest::RunTest(
return false;
}
TestEqual(
TEXT("The runtime-owned Magic120Cell projection must expose exactly 120 canonical cell centers."),
AHyperTwistHigherDimensionalTrainingShellActor::GetCanonicalElementCountForFamily(
TEXT("magic120cell")),
120
);
TestEqual(
TEXT("The runtime-owned order-3 MagicCube5D projection must expose all 242 non-central cells."),
AHyperTwistHigherDimensionalTrainingShellActor::GetCanonicalElementCountForFamily(
TEXT("magiccube5d")),
242
);
const AHyperTwistHigherDimensionalTrainingGameMode* RuntimeGameModeDefaults =
GetDefault<AHyperTwistHigherDimensionalTrainingGameMode>();
TestNotNull(
TEXT("The dedicated higher-dimensional runtime game mode defaults must exist."),
RuntimeGameModeDefaults
);
if (RuntimeGameModeDefaults == nullptr)
{
return false;
}
TestEqual(
TEXT("The dedicated higher-dimensional runtime must use the orbit pawn."),
RuntimeGameModeDefaults->DefaultPawnClass.Get(),
AHyperTwistHigherDimensionalOrbitPawn::StaticClass()
);
TestEqual(
TEXT("The dedicated higher-dimensional runtime must use its non-modal player controller."),
RuntimeGameModeDefaults->PlayerControllerClass.Get(),
AHyperTwistHigherDimensionalTrainingPlayerController::StaticClass()
);
TestEqual(
TEXT("The dedicated higher-dimensional runtime must expose its native control HUD."),
RuntimeGameModeDefaults->HUDClass.Get(),
AHyperTwistHigherDimensionalTrainingHUD::StaticClass()
);
const AHyperTwistHigherDimensionalTrainingShellActor* RuntimeShellDefaults =
GetDefault<AHyperTwistHigherDimensionalTrainingShellActor>();
TestNotNull(
TEXT("The runtime-owned higher-dimensional shell defaults must exist."),
RuntimeShellDefaults
);
if (RuntimeShellDefaults == nullptr)
{
return false;
}
TestNotNull(
TEXT("The runtime-owned higher-dimensional shell must self-provision its presentation floor."),
RuntimeShellDefaults->PresentationFloor.Get()
);
TestEqual(
TEXT("The runtime-owned higher-dimensional shell must expose six independently colored preview layers."),
RuntimeShellDefaults->PreviewColorLayers.Num(),
6
);
TestEqual(
TEXT("The runtime-owned higher-dimensional shell must bind six distinct cook-safe first-party materials."),
RuntimeShellDefaults->GetDistinctPreviewMaterialCount(),
6
);
const auto VerifyDedicatedMapShell =
[this](const FPhase6CDedicatedMapManifestEntry& Entry) -> bool
{
@ -736,12 +813,13 @@ bool FHyperTwistHigherDimensionalPhase6CDedicatedMapOwnedShellTest::RunTest(
const int32 TrainingShellActorCount =
HyperTwistHigherDimensionalPhase6CTestInternal::CountActorsOfClass<
AHyperTwistHigherDimensionalTrainingShellActor>(MapWorld);
TestTrue(
*FString::Printf(
TEXT("The dedicated-family map must contain at most one optional training shell actor for %s."),
*Entry.FamilyKey),
TrainingShellActorCount <= 1
);
TestEqual(
*FString::Printf(
TEXT("The dedicated-family map must leave shell presentation exclusively to runtime ownership for %s."),
*Entry.FamilyKey),
TrainingShellActorCount,
0
);
TestEqual(
*FString::Printf(
TEXT("The dedicated-family map must contain exactly one PlayerStart anchor for %s."),
@ -806,15 +884,44 @@ bool FHyperTwistHigherDimensionalPhase6CDedicatedMapOwnedShellTest::RunTest(
return Actor != nullptr && Actor->ActorHasTag(FName(*Tag));
};
TestEqual(
*FString::Printf(
TEXT("The dedicated-family world settings must preserve the coach dashboard game mode for %s."),
TestEqual(
*FString::Printf(
TEXT("The dedicated-family world settings must preserve the interactive higher-dimensional game mode for %s."),
*Entry.FamilyKey),
WorldSettings->DefaultGameMode != nullptr
? WorldSettings->DefaultGameMode->GetPathName()
: FString(),
AHyperTwistCoachDashboardGameMode::StaticClass()->GetPathName()
);
AHyperTwistHigherDimensionalTrainingGameMode::StaticClass()->GetPathName()
);
TestEqual(
*FString::Printf(
TEXT("The manifest must bind the dedicated game mode for %s."),
*Entry.FamilyKey),
Entry.AuthoredViaGameModeClassPath,
AHyperTwistHigherDimensionalTrainingGameMode::StaticClass()->GetPathName()
);
TestEqual(
*FString::Printf(
TEXT("The manifest must bind the runtime shell presentation class for %s."),
*Entry.FamilyKey),
Entry.RuntimePresentationClassPath,
AHyperTwistHigherDimensionalTrainingShellActor::StaticClass()->GetPathName()
);
TestEqual(
*FString::Printf(
TEXT("The manifest must preserve runtime presentation-environment ownership for %s."),
*Entry.FamilyKey),
Entry.PresentationEnvironmentOwnership,
TEXT("runtime-game-mode-and-shell-components")
);
TestEqual(
*FString::Printf(
TEXT("The manifest must preserve the exact canonical runtime element count for %s."),
*Entry.FamilyKey),
Entry.CanonicalRenderableElementCount,
AHyperTwistHigherDimensionalTrainingShellActor::GetCanonicalElementCountForFamily(
Entry.FamilyKey)
);
if (TrainingShellActor != nullptr)
{
TestEqual(

View file

@ -0,0 +1,51 @@
#include "Misc/AutomationTest.h"
#include "HyperTwistRecognition/HyperTwistSpeechClient.h"
#include "HyperTwistRecognition/HyperTwistVisionClient.h"
#include "HyperTwistRecognition/HyperTwistVoiceClient.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistHttpClientConfigurationTest,
"HyperTwist.Recognition.HttpClients.DefaultConfiguration",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistHttpClientConfigurationTest::RunTest(const FString& Parameters)
{
static_cast<void>(Parameters);
const UHyperTwistHttpVisionClient* VisionClient = GetDefault<UHyperTwistHttpVisionClient>();
const UHyperTwistHttpSpeechClient* SpeechClient = GetDefault<UHyperTwistHttpSpeechClient>();
const UHyperTwistHttpVoiceClient* VoiceClient = GetDefault<UHyperTwistHttpVoiceClient>();
TestNotNull(TEXT("The default vision client must be available."), VisionClient);
TestNotNull(TEXT("The default speech client must be available."), SpeechClient);
TestNotNull(TEXT("The default voice client must be available."), VoiceClient);
if (VisionClient == nullptr || SpeechClient == nullptr || VoiceClient == nullptr)
{
return false;
}
const auto TestServiceUrl = [this](const TCHAR* Label, const FString& ServiceBaseUrl)
{
TestTrue(
FString::Printf(TEXT("%s must retain a complete HTTP service URL after config parsing."), Label),
ServiceBaseUrl.StartsWith(TEXT("http://"), ESearchCase::IgnoreCase)
|| ServiceBaseUrl.StartsWith(TEXT("https://"), ESearchCase::IgnoreCase)
);
TestFalse(
FString::Printf(TEXT("%s must not contain a collapsed single-slash scheme."), Label),
ServiceBaseUrl.StartsWith(TEXT("http:/"), ESearchCase::IgnoreCase)
&& !ServiceBaseUrl.StartsWith(TEXT("http://"), ESearchCase::IgnoreCase)
);
};
TestServiceUrl(TEXT("VisionClient.ServiceBaseUrl"), VisionClient->ServiceBaseUrl);
TestServiceUrl(TEXT("SpeechClient.ServiceBaseUrl"), SpeechClient->ServiceBaseUrl);
TestServiceUrl(TEXT("VoiceClient.ServiceBaseUrl"), VoiceClient->ServiceBaseUrl);
return true;
}
#endif

View file

@ -12,7 +12,7 @@ public class UnrealHyperTwist : ModuleRules
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "Json", "JsonUtilities", "UMG", "Voice", "ProceduralMeshComponent", "HeadMountedDisplay", "XRBase" });
PrivateDependencyModuleNames.AddRange(
new string[] { "Slate", "SlateCore", "HTTP", "WebBrowser", "WebBrowserWidget", "Projects" }
new string[] { "Slate", "SlateCore", "HTTP", "WebBrowser", "WebBrowserWidget", "Projects", "RenderCore" }
);
// External donor repo include paths

View file

@ -1,6 +1,28 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UnrealHyperTwist.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, UnrealHyperTwist, "UnrealHyperTwist");
class FUnrealHyperTwistModule final : public FDefaultGameModuleImpl
{
public:
virtual void StartupModule() override
{
FDefaultGameModuleImpl::StartupModule();
HyperTwistRuntimeDiagnostics::InitializeSession();
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("Process"),
TEXT("HyperTwist runtime module initialized."));
}
virtual void ShutdownModule() override
{
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("Process"),
TEXT("HyperTwist runtime module shutdown requested."));
FDefaultGameModuleImpl::ShutdownModule();
}
};
IMPLEMENT_PRIMARY_GAME_MODULE(FUnrealHyperTwistModule, UnrealHyperTwist, "UnrealHyperTwist");

View file

@ -0,0 +1,41 @@
# HyperTwist Alpha Test Evidence - 2026-07-23
This directory is the durable, source-controlled evidence index for the
Release 17 Shipping Alpha delivered to the Windows Downloads folder.
## Current delivery
- folder:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723`
- ZIP:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723.zip`
- outer executable SHA-256:
`886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6`
- ZIP SHA-256:
`1ad8e1c0fc967b227ca09fb68c515ed644eca3e4bda07fdf8c378b438000fc42`
- ZIP: `103` entries, `1082134734` bytes
## Evidence map
- `first_run_automation_index.json` - `5/5` focused first-run tests green
- `desktop_package_validation_report.json` - four semantic Shipping smokes
green for Classic, Follow-Along, Magic120Cell, and MagicCube5D
- `desktop_launch_surface_report.json` - corrected stock-Shipping launch gate
- `final_delivery_bootstrap_verification.json` - exact outer bootstrap resource
- `final_delivery_zip_export_report.json` - reopened archive plus seven
critical-entry stream hashes
- `final_delivery_first_run_visual_report.json` and
`final_delivery_first_run.png` - exact Downloads executable rendered the
first-run product surface
- `classic_route_visual_report.json` and `classic_route.png` - visible Classic
route click, exact map, 26 cubies, settled valid state, HUD, and camera
- `final_delivery_dashboard_nullrhi_diagnostics.log` - final executable
accepted and opened the registered coach-dashboard route under direct
Shipping diagnostics
- `superseded_july9_retirement_report.json` - old folder/ZIP retirement after
replacement verification
The dashboard diagnostics are route/runtime proof, not a substitute for a
fresh dashboard screenshot. A later visual retry occurred without a live
interactive desktop and was rejected. No failed or desktop-leaking image is
promoted as release evidence.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -0,0 +1,97 @@
{
"reportVersion": "ht-packaged-window-visual/v1",
"generatedAtUtc": "2026-07-23T08:05:02.2199519Z",
"launchStartedAtUtc": "2026-07-23T08:05:02.3060951Z",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"executableSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"launchArguments": [
"-windowed",
"-ResX=1280",
"-ResY=720",
"-HyperTwistCaptureWhenReady",
"-HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\visual\\classic-route-window-message\\HyperTwist-window-20260723T080501Z.hyperdiagnostics.log\""
],
"perMonitorV2DpiAware": true,
"processId": 24536,
"windowTitle": "UnrealHyperTwist ",
"windowClassName": "UnrealWindow",
"expectedWindowClassName": "UnrealWindow",
"clientBounds": {
"x": 368,
"y": 297,
"width": 1280,
"height": 720
},
"screenshotPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\visual\\classic-route-window-message\\HyperTwist-window-20260723T080501Z.png",
"progressPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\visual\\classic-route-window-message\\HyperTwist-window-20260723T080501Z.progress.log",
"screenshotSha256": "6e6b8f2c06f6ff8a68c2f4d9e0a576305056cd6f4d7353e9894a05e63d853ab5",
"captureMethod": "hyper-twist-semantic-ready-frame",
"expectedSemanticReadyCaptureName": "classic-cube-free-play-settled.png",
"semanticReadyCaptureSourcePath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Screenshots\\HyperTwistDiagnostics\\classic-cube-free-play-settled.png",
"semanticReadyCaptureSourceSha256": "4da3bc875f3e8551d427df1f894b806152a3064d0bf350f77207eec9ce84345e",
"metrics": {
"sampleStride": 6,
"sampleCount": 25680,
"nonBlackRatio": 0.937733644859813,
"brightRatio": 0.92628504672897194,
"averageLuminance": 136.14746721184946,
"luminanceDeviation": 60.493912122602573,
"minimumLuminance": 0.0722,
"maximumLuminance": 250.94099999999997,
"luminanceRange": 250.86879999999996,
"quantizedColorBucketCount": 218
},
"thresholds": {
"minimumNonBlackRatio": 0.02,
"minimumBrightRatio": 0.001,
"minimumLuminanceDeviation": 2.5,
"minimumLuminanceRange": 20
},
"interaction": {
"requested": true,
"normalizedX": 0.5,
"normalizedY": 0.566,
"screenX": 1008,
"screenY": 704,
"clientX": 640,
"clientY": 407,
"injectionMethod": "window-client-message",
"clicked": true,
"expectedStartupDiagnosticsResult": "map-route-opened"
},
"startupDiagnosticsPath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Logs\\HyperTwistFirstRunLaunch-latest.log",
"startupDiagnosticsLastWriteUtc": "2026-07-23T08:05:09.5575483Z",
"startupDiagnosticsResult": "result=map-route-opened",
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\visual\\classic-route-window-message\\HyperTwist-window-20260723T080501Z.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"runtimeDiagnosticsLines": [
"format=hypertwist-runtime-diagnostics/v1",
"session=20260723T080504Z",
"process_id=24536",
"command_line=-notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\visual\\classic-route-window-message\\HyperTwist-window-20260723T080501Z.hyperdiagnostics.log",
"diagnostics_path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/validation/visual/classic-route-window-message/HyperTwist-window-20260723T080501Z.hyperdiagnostics.log",
"[2026-07-23T08:05:04Z] [Process] HyperTwist runtime module initialized.",
"[2026-07-23T08:05:09Z] [MapReady] Runtime map ready: /Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining.L_HyperTwist_ClassicTraining.",
"[2026-07-23T08:05:09Z] [ClassicPresentation] Classic cube presentation initialized with 26 renderable pieces and orbit pawn ready.",
"[2026-07-23T08:05:21Z] [ClassicScrambleSettlement] Classic cube scramble settled with 29 completed quarter turns, 0 queued rotations, and 26 renderable pieces; renderable state valid."
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\visual\\classic-route-window-message\\HyperTwist-window-20260723T080501Z.hyperdiagnostics.log\" ",
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist\\Binaries\\Win64\\UnrealHyperTwist-Win64-Shipping.exe\" UnrealHyperTwist -notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\visual\\classic-route-window-message\\HyperTwist-window-20260723T080501Z.hyperdiagnostics.log\" ",
"C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/Win64/EpicWebHelper.exe --type=gpu-process --no-sandbox --use-adapter-luid=0,66319 --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales --log-severity=warning --resources-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --gpu-preferences=UAAAAAAAAADgABAMAAAAAAAAAAAAAAAAAABgAAEAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA --ipc-connection-timeout=60 --field-trial-handle=3112,i,12427174811344002632,6413397226995935749,262144 --variations-seed-version --enable-logging=handle --log-file=3196 --mojo-platform-channel-handle=3100 /prefetch:2",
"C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/Win64/EpicWebHelper.exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales --log-severity=warning --resources-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=2980,i,12427174811344002632,6413397226995935749,262144 --variations-seed-version --enable-logging=handle --log-file=2984 --mojo-platform-channel-handle=2968 /prefetch:11",
"C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/Win64/EpicWebHelper.exe --type=utility --utility-sub-type=storage.mojom.StorageService --lang=en-US --service-sandbox-type=service --no-sandbox --use-angle=d3d11 --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales --log-severity=warning --resources-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release17_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=3352,i,12427174811344002632,6413397226995935749,262144 --variations-seed-version --enable-logging=handle --log-file=3104 --mojo-platform-channel-handle=3160 /prefetch:13"
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"focusMethod": "topmost-attempt-1",
"result": "visual-passed",
"error": null
}

View file

@ -0,0 +1,71 @@
{
"reportVersion": "ht-desktop-package-launch-surface/v2",
"generatedAtUtc": "2026-07-23T07:42:23.9424772Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "",
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\desktop-runtime.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\desktop-runtime.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"runtimeDiagnosticsInitialized": true,
"detectedFatalLogLines": [
],
"smokeSeconds": 30,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"useNullRhi": true,
"noSound": true,
"requireStartupDiagnostics": true,
"expectedStartupDiagnosticsResult": "",
"startupDiagnosticsLogPath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Logs\\HyperTwistFirstRunLaunch-latest.log",
"startupDiagnosticsLogExists": true,
"startupDiagnosticsTail": [
"session=20260723T074227Z",
"world=L_HyperTwist_ClassicTraining",
"runtime_log=C:/Users/Anthracite Ace/AppData/Local/UnrealHyperTwist/Saved/Logs/HyperTwistRuntime-latest.log",
"latest_diagnostics_log=C:/Users/Anthracite Ace/AppData/Local/UnrealHyperTwist/Saved/Logs/HyperTwistFirstRunLaunch-latest.log",
"[2026-07-23T07:42:27Z] [BeginPlay] First-run controller booted on world \u0027L_HyperTwist_ClassicTraining\u0027.",
"[2026-07-23T07:42:27Z] [ApplyFirstRunInputMode] Applied game-and-UI input mode with unlocked cursor capture.",
"[2026-07-23T07:42:27Z] [ShowFirstRunLaunchMenu] Fell back to the native first-run widget class.",
"[2026-07-23T07:42:27Z] [ShowFirstRunLaunchMenu] Added the first-run launch widget to the viewport at z-order 100.",
"[2026-07-23T07:42:27Z] [ApplyFirstRunInputMode] Applied game-and-UI input mode with unlocked cursor capture.",
"[2026-07-23T07:42:29Z] [StartupRecovery] The first-run launch widget never produced a visible non-zero layout after 20 attempts (last size 0x0).",
"[2026-07-23T07:42:29Z] [StartupRecovery] Attempting fallback route \u0027classic-cube-training\u0027.",
"[2026-07-23T07:42:29Z] [OpenFirstRunRoute] Opening route \u0027classic-cube-training\u0027 (dedicated-map).",
"[2026-07-23T07:42:29Z] [OpenMapRoute] Opening map \u0027/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining\u0027 with options \u0027game=/Script/UnrealHyperTwist.HyperTwistClassicCubeGameMode\u0027.",
"result=startup-recovered",
"last_failure=The first-run launch widget never produced a visible non-zero layout after 20 attempts (last size 0x0)."
],
"startupDiagnosticsResult": "result=startup-recovered",
"result": "passed",
"processId": 4848,
"processIds": [
4848,
3556
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\desktop-runtime.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\desktop-runtime.hyperdiagnostics.log\" -NullRHI -nosound ",
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist\\Binaries\\Win64\\UnrealHyperTwist-Win64-Shipping.exe\" UnrealHyperTwist -notraceserver -traceautostart=0 -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\desktop-runtime.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\desktop-runtime.hyperdiagnostics.log\" -NullRHI -nosound "
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"processStopped": true,
"stoppedProcessIds": [
4848,
3556
],
"exitCode": null,
"error": null
}

View file

@ -0,0 +1,480 @@
{
"reportVersion": "ht-classic-cube-package-validation/v2",
"generatedAtUtc": "2026-07-23T07:32:10.4163716Z",
"projectRoot": "C:\\HyperTwist_worktrees\\phase10validate",
"archiveDirectory": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723",
"configuration": "Shipping",
"cookMaps": [
"/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining"
],
"smokeMaps": [
"/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining"
],
"additionalCookerOptions": [
"-DisablePlugins=MovieRenderPipeline",
"-SkipCookingEditorContent"
],
"headlessSmoke": true,
"cleanArchive": true,
"skipBuild": false,
"skipLaunch": false,
"result": "passed",
"packagedExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"bootstrapLaunchArguments": {
"result": "passed",
"traceControlProtection": "shipping-compile-time-disabled",
"expectedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"patchReportPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\bootstrap-launch-arguments-report.json",
"patchReport": {
"reportVersion": "ht-bootstrap-launch-arguments/v1",
"generatedAtUtc": "2026-07-23T07:37:11.2688738Z",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"resourceType": 10,
"resourceId": 202,
"verifyOnly": false,
"expectedArguments": [
"UnrealHyperTwist",
"-notraceserver",
"-traceautostart=0"
],
"expectedResourceValue": "UnrealHyperTwist -notraceserver -traceautostart=0",
"originalResourceValue": "UnrealHyperTwist",
"finalResourceValue": "UnrealHyperTwist -notraceserver -traceautostart=0",
"originalSha256": "b7c268fc083a565b7c3c1e0a84db773c9714e4f23f14cca3bffb14145e48f88f",
"finalSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"changed": true,
"result": "passed",
"error": null
},
"runtimeProofs": [
{
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"observedCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"expectedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"matchedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"missingArguments": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"result": "passed",
"error": null
},
{
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"observedCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=follow-along-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"expectedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"matchedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"missingArguments": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"result": "passed",
"error": null
},
{
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"observedCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=magic-120-cell-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"expectedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"matchedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"missingArguments": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"result": "passed",
"error": null
},
{
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"observedCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=magic-cube-5d-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"expectedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"matchedArguments": [
"-notraceserver",
"-traceautostart=0"
],
"missingArguments": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"result": "passed",
"error": null
}
]
},
"solverTable": {
"result": "passed",
"sourcePath": "C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\twophase-ht.tbl",
"expectedSizeBytes": 676207080,
"expectedSha256": "dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034",
"sourceEvidence": {
"path": "C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\twophase-ht.tbl",
"sizeBytes": 676207080,
"sha256": "dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034"
},
"packagedEvidence": {
"path": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist\\Saved\\twophase-ht.tbl",
"sizeBytes": 676207080,
"sha256": "dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034"
}
},
"smokeReports": [
{
"reportVersion": "ht-desktop-map-package-smoke/v7",
"generatedAtUtc": "2026-07-23T07:37:17.9023928Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723",
"launchMode": "bootstrap-default-map",
"startupRouteId": "",
"launcherExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining",
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"detectedFatalLogLines": [
],
"mapLoadLogLines": [
"[2026-07-23T07:37:23Z] [MapReady] Runtime map ready: /Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining.L_HyperTwist_ClassicTraining."
],
"requiresClassicPresentation": true,
"classicPresentationLogLines": [
"[2026-07-23T07:37:23Z] [ClassicPresentation] Classic cube presentation initialized with 26 renderable pieces and orbit pawn ready."
],
"classicScrambleSettlementLogLines": [
"[2026-07-23T07:37:34Z] [ClassicScrambleSettlement] Classic cube scramble settled with 27 completed quarter turns, 0 queued rotations, and 26 renderable pieces; renderable state valid."
],
"higherDimensionalFamily": "",
"expectedHigherDimensionalElementCount": 0,
"higherDimensionalPresentationLogLines": [
],
"higherDimensionalPaletteLogLines": [
],
"smokeSeconds": 30,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"useNullRhi": true,
"noSound": true,
"result": "passed",
"processId": 22668,
"processIds": [
22668
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_ClassicTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"startupRouteCommandLineEvidence": [
],
"startupRouteEvidenceLines": [
],
"processStopped": true,
"stoppedProcessIds": [
22668
],
"exitCode": null,
"error": null
},
{
"reportVersion": "ht-desktop-map-package-smoke/v7",
"generatedAtUtc": "2026-07-23T07:37:53.2904656Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723",
"launchMode": "first-party-startup-route",
"startupRouteId": "follow-along-training",
"launcherExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining",
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"detectedFatalLogLines": [
],
"mapLoadLogLines": [
"[2026-07-23T07:37:55Z] [MapReady] Runtime map ready: /Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining.L_HyperTwist_FollowAlongTraining."
],
"requiresClassicPresentation": true,
"classicPresentationLogLines": [
"[2026-07-23T07:37:55Z] [ClassicPresentation] Classic cube presentation initialized with 26 renderable pieces and orbit pawn ready."
],
"classicScrambleSettlementLogLines": [
"[2026-07-23T07:38:03Z] [ClassicScrambleSettlement] Classic cube scramble settled with 11 completed quarter turns, 0 queued rotations, and 26 renderable pieces; renderable state valid."
],
"higherDimensionalFamily": "",
"expectedHigherDimensionalElementCount": 0,
"higherDimensionalPresentationLogLines": [
],
"higherDimensionalPaletteLogLines": [
],
"smokeSeconds": 30,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"useNullRhi": true,
"noSound": true,
"result": "passed",
"processId": 21852,
"processIds": [
21852
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=follow-along-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"startupRouteCommandLineEvidence": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=follow-along-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_FollowAlongTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"startupRouteEvidenceLines": [
"[2026-07-23T07:37:55Z] [PackagedStartupRoute] Accepted packaged startup route \u0027follow-along-training\u0027: map=\u0027/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining\u0027, game_mode=\u0027/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode\u0027."
],
"processStopped": true,
"stoppedProcessIds": [
21852
],
"exitCode": null,
"error": null
},
{
"reportVersion": "ht-desktop-map-package-smoke/v7",
"generatedAtUtc": "2026-07-23T07:38:26.2167211Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723",
"launchMode": "first-party-startup-route",
"startupRouteId": "magic-120-cell-training",
"launcherExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"detectedFatalLogLines": [
],
"mapLoadLogLines": [
"[2026-07-23T07:38:29Z] [MapReady] Runtime map ready: /Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.L_HyperTwist_Magic120CellTraining."
],
"requiresClassicPresentation": false,
"classicPresentationLogLines": [
],
"classicScrambleSettlementLogLines": [
],
"higherDimensionalFamily": "magic120cell",
"expectedHigherDimensionalElementCount": 120,
"higherDimensionalPresentationLogLines": [
"[2026-07-23T07:38:29Z] [HigherDimensionalPresentation] Higher-dimensional presentation initialized with 120 renderable elements (canonical=120) for family magic120cell."
],
"higherDimensionalPaletteLogLines": [
"[2026-07-23T07:38:29Z] [HigherDimensionalPalette] Higher-dimensional projection palette initialized with 6 distinct layer materials."
],
"smokeSeconds": 30,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"useNullRhi": true,
"noSound": true,
"result": "passed",
"processId": 17440,
"processIds": [
17440
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=magic-120-cell-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"startupRouteCommandLineEvidence": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=magic-120-cell-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_Magic120CellTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"startupRouteEvidenceLines": [
"[2026-07-23T07:38:29Z] [PackagedStartupRoute] Accepted packaged startup route \u0027magic-120-cell-training\u0027: map=\u0027/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining\u0027, game_mode=\u0027/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode\u0027."
],
"processStopped": true,
"stoppedProcessIds": [
17440
],
"exitCode": null,
"error": null
},
{
"reportVersion": "ht-desktop-map-package-smoke/v7",
"generatedAtUtc": "2026-07-23T07:38:59.0982085Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723",
"launchMode": "first-party-startup-route",
"startupRouteId": "magic-cube-5d-training",
"launcherExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining",
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"detectedFatalLogLines": [
],
"mapLoadLogLines": [
"[2026-07-23T07:39:02Z] [MapReady] Runtime map ready: /Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.L_HyperTwist_MagicCube5DTraining."
],
"requiresClassicPresentation": false,
"classicPresentationLogLines": [
],
"classicScrambleSettlementLogLines": [
],
"higherDimensionalFamily": "magiccube5d",
"expectedHigherDimensionalElementCount": 242,
"higherDimensionalPresentationLogLines": [
"[2026-07-23T07:39:02Z] [HigherDimensionalPresentation] Higher-dimensional presentation initialized with 242 renderable elements (canonical=242) for family magiccube5d."
],
"higherDimensionalPaletteLogLines": [
"[2026-07-23T07:39:02Z] [HigherDimensionalPalette] Higher-dimensional projection palette initialized with 6 distinct layer materials."
],
"smokeSeconds": 30,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"useNullRhi": true,
"noSound": true,
"result": "passed",
"processId": 2936,
"processIds": [
2936
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=magic-cube-5d-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"startupRouteCommandLineEvidence": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\Windows\\UnrealHyperTwist.exe\" -HyperTwistStartupRoute=magic-cube-5d-training -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release17_20260723\\validation\\logs\\_Game_HyperTwistTraining_Maps_L_HyperTwist_MagicCube5DTraining.hyperdiagnostics.log\" -NullRHI -nosound "
],
"startupRouteEvidenceLines": [
"[2026-07-23T07:39:02Z] [PackagedStartupRoute] Accepted packaged startup route \u0027magic-cube-5d-training\u0027: map=\u0027/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining\u0027, game_mode=\u0027/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode\u0027."
],
"processStopped": true,
"stoppedProcessIds": [
2936
],
"exitCode": null,
"error": null
}
],
"error": null
}

View file

@ -0,0 +1,21 @@
{
"reportVersion": "ht-bootstrap-launch-arguments/v1",
"generatedAtUtc": "2026-07-23T08:07:08.1612366Z",
"executablePath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723\\UnrealHyperTwist.exe",
"resourceType": 10,
"resourceId": 202,
"verifyOnly": true,
"expectedArguments": [
"UnrealHyperTwist",
"-notraceserver",
"-traceautostart=0"
],
"expectedResourceValue": "UnrealHyperTwist -notraceserver -traceautostart=0",
"originalResourceValue": "UnrealHyperTwist -notraceserver -traceautostart=0",
"finalResourceValue": "UnrealHyperTwist -notraceserver -traceautostart=0",
"originalSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"finalSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"changed": false,
"result": "passed",
"error": null
}

View file

@ -0,0 +1,8 @@
format=hypertwist-runtime-diagnostics/v1
session=20260723T082546Z
process_id=9132
command_line=-notraceserver -traceautostart=0 -nullrhi -unattended -nosound -HyperTwistStartupRoute=coach-dashboard -HyperTwistDiagnosticsLog=C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\Saved\ValidationEvidence\alpha-release17-20260723\final-delivery-dashboard-nullrhi\dashboard.hyperdiagnostics.log
diagnostics_path=C:/HyperTwist_worktrees/phase10validate/UnrealHyperTwist/Saved/ValidationEvidence/alpha-release17-20260723/final-delivery-dashboard-nullrhi/dashboard.hyperdiagnostics.log
[2026-07-23T08:25:46Z] [Process] HyperTwist runtime module initialized.
[2026-07-23T08:25:46Z] [PackagedStartupRoute] Accepted packaged startup route 'coach-dashboard': map='<dashboard>', game_mode='/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode'.
[2026-07-23T08:25:46Z] [PackagedStartupRoute] Opened packaged startup route 'coach-dashboard' through first-run product authority.

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View file

@ -0,0 +1,94 @@
{
"reportVersion": "ht-packaged-window-visual/v1",
"generatedAtUtc": "2026-07-23T08:13:54.9632236Z",
"launchStartedAtUtc": "2026-07-23T08:13:55.0738236Z",
"executablePath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723\\UnrealHyperTwist.exe",
"executableSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"launchArguments": [
"-windowed",
"-ResX=1280",
"-ResY=720",
"-HyperTwistCaptureWhenReady",
"-HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\ValidationEvidence\\alpha-release17-20260723\\final-delivery-first-run-visual\\HyperTwist-window-20260723T081354Z.hyperdiagnostics.log\""
],
"perMonitorV2DpiAware": true,
"processId": 21520,
"windowTitle": "UnrealHyperTwist ",
"windowClassName": "UnrealWindow",
"expectedWindowClassName": "UnrealWindow",
"clientBounds": {
"x": 368,
"y": 297,
"width": 1280,
"height": 720
},
"screenshotPath": "C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\ValidationEvidence\\alpha-release17-20260723\\final-delivery-first-run-visual\\HyperTwist-window-20260723T081354Z.png",
"progressPath": "C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\ValidationEvidence\\alpha-release17-20260723\\final-delivery-first-run-visual\\HyperTwist-window-20260723T081354Z.progress.log",
"screenshotSha256": "6f5e3443d7b9188d6d3642c0a41f703e5a3beed7cee348982f7915621e786919",
"captureMethod": "hyper-twist-semantic-ready-frame",
"expectedSemanticReadyCaptureName": "first-run-launch-ready.png",
"semanticReadyCaptureSourcePath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Screenshots\\HyperTwistDiagnostics\\first-run-launch-ready.png",
"semanticReadyCaptureSourceSha256": "96ff8611601efea7d62cfa5f964ae8ddf2f389966d03cffebe505b203b30b8c8",
"metrics": {
"sampleStride": 6,
"sampleCount": 25680,
"nonBlackRatio": 0.99941588785046731,
"brightRatio": 0.795638629283489,
"averageLuminance": 68.259309470418017,
"luminanceDeviation": 47.014937250449606,
"minimumLuminance": 0,
"maximumLuminance": 254.99999999999997,
"luminanceRange": 254.99999999999997,
"quantizedColorBucketCount": 79
},
"thresholds": {
"minimumNonBlackRatio": 0.02,
"minimumBrightRatio": 0.001,
"minimumLuminanceDeviation": 2.5,
"minimumLuminanceRange": 20
},
"interaction": {
"requested": false,
"normalizedX": -1,
"normalizedY": -1,
"screenX": null,
"screenY": null,
"clientX": null,
"clientY": null,
"injectionMethod": null,
"clicked": false,
"expectedStartupDiagnosticsResult": "launch-menu-renderable"
},
"startupDiagnosticsPath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Logs\\HyperTwistFirstRunLaunch-latest.log",
"startupDiagnosticsLastWriteUtc": "2026-07-23T08:13:58.9072912Z",
"startupDiagnosticsResult": "result=launch-menu-renderable",
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\ValidationEvidence\\alpha-release17-20260723\\final-delivery-first-run-visual\\HyperTwist-window-20260723T081354Z.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"runtimeDiagnosticsLines": [
"format=hypertwist-runtime-diagnostics/v1",
"session=20260723T081358Z",
"process_id=21520",
"command_line=-notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\ValidationEvidence\\alpha-release17-20260723\\final-delivery-first-run-visual\\HyperTwist-window-20260723T081354Z.hyperdiagnostics.log",
"diagnostics_path=C:/HyperTwist_worktrees/phase10validate/UnrealHyperTwist/Saved/ValidationEvidence/alpha-release17-20260723/final-delivery-first-run-visual/HyperTwist-window-20260723T081354Z.hyperdiagnostics.log",
"[2026-07-23T08:13:58Z] [Process] HyperTwist runtime module initialized."
],
"processCommandLines": [
"\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723\\UnrealHyperTwist.exe\" -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\ValidationEvidence\\alpha-release17-20260723\\final-delivery-first-run-visual\\HyperTwist-window-20260723T081354Z.hyperdiagnostics.log\" ",
"\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723\\UnrealHyperTwist\\Binaries\\Win64\\UnrealHyperTwist-Win64-Shipping.exe\" UnrealHyperTwist -notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate\\UnrealHyperTwist\\Saved\\ValidationEvidence\\alpha-release17-20260723\\final-delivery-first-run-visual\\HyperTwist-window-20260723T081354Z.hyperdiagnostics.log\" ",
"\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/Win64/EpicWebHelper.exe\" --type=gpu-process --no-sandbox --use-adapter-luid=0,66319 --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales\" --log-severity=warning --resources-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources\" --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --gpu-preferences=UAAAAAAAAADgABAMAAAAAAAAAAAAAAAAAABgAAEAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA --ipc-connection-timeout=60 --field-trial-handle=3112,i,10081791697659688799,9021224499203579802,262144 --variations-seed-version --enable-logging=handle --log-file=3228 --mojo-platform-channel-handle=3108 /prefetch:2",
"\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/Win64/EpicWebHelper.exe\" --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales\" --log-severity=warning --resources-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources\" --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=3120,i,10081791697659688799,9021224499203579802,262144 --variations-seed-version --enable-logging=handle --log-file=3240 --mojo-platform-channel-handle=3236 /prefetch:11",
"\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/Win64/EpicWebHelper.exe\" --type=utility --utility-sub-type=storage.mojom.StorageService --lang=en-US --service-sandbox-type=service --no-sandbox --use-angle=d3d11 --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales\" --log-severity=warning --resources-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources\" --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=3312,i,10081791697659688799,9021224499203579802,262144 --variations-seed-version --enable-logging=handle --log-file=3340 --mojo-platform-channel-handle=3332 /prefetch:13"
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"focusMethod": "topmost-attempt-1",
"result": "visual-passed",
"error": null
}

View file

@ -0,0 +1,101 @@
{
"reportVersion": "ht-packaged-build-zip-export/v2",
"generatedAtUtc": "2026-07-23T08:07:25.2519424Z",
"packageRoot": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723",
"packagedExecutablePath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723\\UnrealHyperTwist.exe",
"destinationZipPath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723.zip",
"result": "passed",
"entryCount": 103,
"packagedExecutableEntry": "UnrealHyperTwist.exe",
"requiredEntries": [
{
"kind": "launcher-executable",
"relativePath": "UnrealHyperTwist.exe",
"sizeBytes": 165376,
"sha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6"
},
{
"kind": "game-executable",
"relativePath": "UnrealHyperTwist/Binaries/Win64/UnrealHyperTwist-Win64-Shipping.exe",
"sizeBytes": 172797440,
"sha256": "77309cde7d35200c0e8e364697e90eec9c5abbe7048ed87c97c519ca96267ca1"
},
{
"kind": "runtime-dependency-tbbmalloc",
"relativePath": "UnrealHyperTwist/Binaries/Win64/tbbmalloc.dll",
"sizeBytes": 117688,
"sha256": "f81a11f2e6e93036bab4e51a1daab9940fb8a9a5ec0f508c4cb76d398e27c100"
},
{
"kind": "cooked-pak",
"relativePath": "UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.pak",
"sizeBytes": 11069853,
"sha256": "a7ab3f4d8d30bfd1615893e6151d41320260daeaa3e0b5b9768a514e61575179"
},
{
"kind": "cooked-utoc",
"relativePath": "UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.utoc",
"sizeBytes": 214933,
"sha256": "00b6b0e5cb1ab919ae9d86941c39641aeca8751d247e2e0002969b1983d24c71"
},
{
"kind": "cooked-ucas",
"relativePath": "UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.ucas",
"sizeBytes": 281146192,
"sha256": "01dbc31748cc0e429c4c0d05752207b1f032d192c5a1792722bc36590014a005"
},
{
"kind": "solver-table",
"relativePath": "UnrealHyperTwist/Saved/twophase-ht.tbl",
"sizeBytes": 676207080,
"sha256": "dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034"
}
],
"archivedRequiredEntries": [
{
"kind": "launcher-executable",
"relativePath": "UnrealHyperTwist.exe",
"sizeBytes": 165376,
"sha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6"
},
{
"kind": "game-executable",
"relativePath": "UnrealHyperTwist/Binaries/Win64/UnrealHyperTwist-Win64-Shipping.exe",
"sizeBytes": 172797440,
"sha256": "77309cde7d35200c0e8e364697e90eec9c5abbe7048ed87c97c519ca96267ca1"
},
{
"kind": "runtime-dependency-tbbmalloc",
"relativePath": "UnrealHyperTwist/Binaries/Win64/tbbmalloc.dll",
"sizeBytes": 117688,
"sha256": "f81a11f2e6e93036bab4e51a1daab9940fb8a9a5ec0f508c4cb76d398e27c100"
},
{
"kind": "cooked-pak",
"relativePath": "UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.pak",
"sizeBytes": 11069853,
"sha256": "a7ab3f4d8d30bfd1615893e6151d41320260daeaa3e0b5b9768a514e61575179"
},
{
"kind": "cooked-utoc",
"relativePath": "UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.utoc",
"sizeBytes": 214933,
"sha256": "00b6b0e5cb1ab919ae9d86941c39641aeca8751d247e2e0002969b1983d24c71"
},
{
"kind": "cooked-ucas",
"relativePath": "UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.ucas",
"sizeBytes": 281146192,
"sha256": "01dbc31748cc0e429c4c0d05752207b1f032d192c5a1792722bc36590014a005"
},
{
"kind": "solver-table",
"relativePath": "UnrealHyperTwist/Saved/twophase-ht.tbl",
"sizeBytes": 676207080,
"sha256": "dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034"
}
],
"zipSizeBytes": 1082134734,
"zipSha256": "1ad8e1c0fc967b227ca09fb68c515ed644eca3e4bda07fdf8c378b438000fc42",
"error": null
}

View file

@ -0,0 +1,104 @@
{
"devices": [
{
"deviceName": "DESKTOP-KS3VGHU",
"instance": "D568E70942FCF242DDC9FA97E16B2D0C",
"instanceName": "DESKTOP-KS3VGHU-8840",
"platform": "WindowsEditor",
"oSVersion": "Windows 11 (23H2) [10.0.22631.4037] ",
"model": "Default",
"gPU": "GenericGPUBrand",
"cPUModel": "12th Gen Intel(R) Core(TM) i7-12700H",
"rAMInGB": 16,
"renderMode": "SM6",
"rHI": "",
"appInstanceLog": ""
}
],
"reportCreatedOn": "2026.07.23-07.18.34",
"succeeded": 5,
"succeededWithWarnings": 0,
"failed": 0,
"notRun": 0,
"inProcess": 0,
"totalDuration": 0.46825212240219116,
"comparisonExported": false,
"comparisonExportDirectory": "",
"tests": [
{
"testDisplayName": "GameModeDefaults",
"fullTestPath": "HyperTwist.FirstRun.GameModeDefaults",
"tags": [],
"state": "Success",
"deviceInstance": [
"D568E70942FCF242DDC9FA97E16B2D0C"
],
"duration": 0.015960302203893661,
"dateTime": "2026.07.23-07.18.33",
"entries": [],
"warnings": 0,
"errors": 0,
"artifacts": []
},
{
"testDisplayName": "LaunchGuidanceContract",
"fullTestPath": "HyperTwist.FirstRun.LaunchGuidanceContract",
"tags": [],
"state": "Success",
"deviceInstance": [
"D568E70942FCF242DDC9FA97E16B2D0C"
],
"duration": 0.016730200499296188,
"dateTime": "2026.07.23-07.18.33",
"entries": [],
"warnings": 0,
"errors": 0,
"artifacts": []
},
{
"testDisplayName": "LaunchRouteContract",
"fullTestPath": "HyperTwist.FirstRun.LaunchRouteContract",
"tags": [],
"state": "Success",
"deviceInstance": [
"D568E70942FCF242DDC9FA97E16B2D0C"
],
"duration": 0.016192298382520676,
"dateTime": "2026.07.23-07.18.33",
"entries": [],
"warnings": 0,
"errors": 0,
"artifacts": []
},
{
"testDisplayName": "NativeWidgetRootLifecycle",
"fullTestPath": "HyperTwist.FirstRun.NativeWidgetRootLifecycle",
"tags": [],
"state": "Success",
"deviceInstance": [
"D568E70942FCF242DDC9FA97E16B2D0C"
],
"duration": 0.40424719452857971,
"dateTime": "2026.07.23-07.18.33",
"entries": [],
"warnings": 0,
"errors": 0,
"artifacts": []
},
{
"testDisplayName": "PackagedStartupRouteArgumentContract",
"fullTestPath": "HyperTwist.FirstRun.PackagedStartupRouteArgumentContract",
"tags": [],
"state": "Success",
"deviceInstance": [
"D568E70942FCF242DDC9FA97E16B2D0C"
],
"duration": 0.015122100710868835,
"dateTime": "2026.07.23-07.18.33",
"entries": [],
"warnings": 0,
"errors": 0,
"artifacts": []
}
]
}

View file

@ -0,0 +1,19 @@
{
"reportVersion": "ht-superseded-delivery-retirement/v1",
"generatedAtUtc": "2026-07-23T08:14:43.2559068Z",
"oldFolder": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-desktop-first-run-authority-repair-20260709",
"oldFolderExisted": true,
"oldFolderFileCount": 288,
"oldFolderBytes": 1587731789,
"oldFolderDeleted": true,
"oldZip": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-desktop-first-run-authority-repair-20260709.zip",
"oldZipExisted": true,
"oldZipBytes": 719569118,
"oldZipDeleted": true,
"replacementFolder": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723",
"replacementFolderPresent": true,
"replacementZip": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723.zip",
"replacementZipPresent": true,
"replacementZipBytes": 1082134734,
"result": "passed"
}

View file

@ -1,17 +1,17 @@
{
"manifestId": "phase6c/dedicated-family-training-map-authoring",
"manifestVersion": "2026.06.18",
"manifestVersion": "2026.07.19",
"authorTag": "HyperTwistHigherDimensionalTrainingShell",
"authoringScriptRelativePath": "scripts/hypertwist_author_higher_dimensional_training_maps.py",
"authoredThroughGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode",
"classicReferenceMapHashMd5": "7772cc42fd9fd129cb6d99c0b918a24e",
"authoredThroughGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode",
"classicReferenceMapHashMd5": "4ae31fc4007d4a931e0f4e9bfe319a63",
"entries": [
{
"mapKind": "magic120cell",
"familyKey": "magic120cell",
"mapAssetPath": "/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
"mapFileRelativePath": "UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap",
"mapHashMd5": "9d9b9301f14cdad8c263d2c57b5f2c7f",
"mapHashMd5": "9f8e311e23e72fe451ec6e926b4ec511",
"trainingShellId": "phase6c/magic120cell/dedicated-training-shell",
"activationProfileId": "magic120cell-cleanroom-runtime-activation",
"hostSurfaceId": "phase6c/magic120cell/runtime-host-surface",
@ -26,7 +26,10 @@
"primaryPersistenceBoundaryId": "magic120cell-persistence-boundary",
"authorTag": "HyperTwistHigherDimensionalTrainingShell",
"authoringManifestRelativePath": "docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json",
"authoredViaGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode",
"authoredViaGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode",
"runtimePresentationClassPath": "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingShellActor",
"presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components",
"canonicalRenderableElementCount": 120,
"trainingShellTags": [
"phase6c",
"family:magic120cell",
@ -40,7 +43,7 @@
"familyKey": "magiccube5d",
"mapAssetPath": "/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining",
"mapFileRelativePath": "UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap",
"mapHashMd5": "15d2f7acfdce43402ad2fd291fc5c781",
"mapHashMd5": "411165b200e44ba442020739a53268c0",
"trainingShellId": "phase6c/magiccube5d/dedicated-training-shell",
"activationProfileId": "magiccube5d-cleanroom-runtime-activation",
"hostSurfaceId": "phase6c/magiccube5d/runtime-host-surface",
@ -55,7 +58,10 @@
"primaryPersistenceBoundaryId": "magiccube5d-persistence-boundary",
"authorTag": "HyperTwistHigherDimensionalTrainingShell",
"authoringManifestRelativePath": "docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json",
"authoredViaGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode",
"authoredViaGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode",
"runtimePresentationClassPath": "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingShellActor",
"presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components",
"canonicalRenderableElementCount": 242,
"trainingShellTags": [
"phase6c",
"family:magiccube5d",

View file

@ -0,0 +1,85 @@
HYPERTWIST ALPHA TEST - WINDOWS
Release date: 2026-07-23
START HERE
1. Extract the entire ZIP before launching. Do not run HyperTwist from inside
the ZIP.
2. Keep the extracted folder intact. HyperTwist needs its UnrealHyperTwist
content folder, engine files, and runtime DLLs beside the launcher.
3. Run the top-level UnrealHyperTwist.exe.
4. The first-run launch menu should appear. A headset is not required:
keyboard and mouse are the primary Alpha test path.
FIRST-RUN ROUTES
- Coach Dashboard and Settings: operator status, controls, local settings,
training surfaces, browser-runtime status, and diagnostics.
- Classic Cube Free Play: interactive 3x3 cube, scramble/timer, hints, replay,
local best times, follow-along mode, and optional voice-command surfaces.
- Classic Cube Follow-Along: guided classic-cube practice.
- Magic120Cell Training: dedicated 120-element state, projection, visibility,
focus, and local persistence training surface.
- MagicCube5D Training: dedicated 242-element order-three 5D state,
projection, visibility, focus, and local persistence training surface.
- OpenXR Validation: bounded headset/controller readiness surface. Its
configuration is present, but physical headset and controller behavior is
still an explicit live-device Alpha validation gate.
CLASSIC CUBE CONTROLS
- Left click a cube piece: clockwise face turn.
- Right click a cube piece: counter-clockwise face turn.
- Middle-mouse drag: orbit camera.
- Mouse wheel: zoom.
- R: new scramble.
- H: hint.
- Enter: submit solve.
- F: toggle free-play/follow-along mode.
- Hold V: voice capture when a local speech provider is available.
- C: cycle voice profile.
MAGIC120CELL AND MAGICCUBE5D CONTROLS
- Space: toggle projection auto-rotation.
- Q / E: rotate projection.
- Page Up / Page Down: change visible projection layer.
- N: create a new scrambled runtime state.
- R: reset projection.
- S: save local state.
- L: load local state.
- Home: reset camera.
- Mouse wheel: zoom.
- Middle-mouse drag or Shift+right-mouse drag: orbit.
- D: toggle diagnostics/dashboard surface.
GRAPHICS
HyperTwist defaults to Direct3D 11 for broad first-run compatibility. D3D12 is
an opt-in test path: create a shortcut to UnrealHyperTwist.exe and append
"-d3d12" to its Target only when intentionally testing that renderer.
DIAGNOSTICS
HyperTwist Shipping builds retain first-party diagnostics without enabling
Unreal Development trace-control listeners. If startup fails or a surface is
blank, collect these files:
- %LOCALAPPDATA%\UnrealHyperTwist\Saved\Logs\HyperTwistRuntime-latest.log
- %LOCALAPPDATA%\UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log
- %LOCALAPPDATA%\UnrealHyperTwist\Saved\Crashes (if present)
Include the route selected, exact reproduction steps, a screenshot, Windows
version, GPU model/driver, and whether any optional local voice, speech, or
vision sidecar was running. Core keyboard/mouse simulation must start without
those optional sidecars.
ALPHA BOUNDARY
This package is an Alpha test build, not a final production installer. The
Classic Cube route owns interactive twist behavior. The higher-dimensional
routes currently own dedicated family state, projection, focus/visibility,
local persistence, maps, and launch surfaces; they do not yet claim complete
parity with every mature higher-dimensional twist mechanic. Live physical
OpenXR headset/controller observation also remains pending until hardware is
available.

View file

@ -0,0 +1,394 @@
# HyperTwist Alpha Test Takeover, Black-Screen Repair, and Validation
Date: `2026-07-19`
Last updated: `2026-07-23`
Status: fixed Shipping Alpha delivered and archive-verified; first-run and
Classic interactive visuals green; coach-dashboard route green under
first-party headless diagnostics, with a fresh dashboard screenshot retained
as operator-connected visual QA rather than misreported from a disconnected
desktop
## Purpose
This document is the durable handoff and evidence authority for the first
operator-reported HyperTwist desktop Alpha failures:
- an earlier extracted executable opened to a black or functionally empty scene
- an earlier ZIP could not be opened reliably
- another loose executable failed because `tbbmalloc.dll` was absent
- prior package smoke reports proved process survival but did not prove that a
rendered launch surface, gameplay map, camera, HUD, or cube was present
The repair goal is stricter than "the process starts." A distributable is ready
only when the exact packaged binary:
1. creates a real Unreal window in the interactive Windows desktop session
2. renders the first-run launch surface
3. opens the coach dashboard from that surface
4. opens the Classic Cube route from that surface
5. renders all 26 visible cubies, the authored environment, the orbit camera,
and the gameplay HUD
6. writes current runtime and startup diagnostics
7. survives fresh packaged launches for all four shipped training maps
8. is delivered as a complete folder and a ZIP whose entries and hash are
independently verified
## Authority and source review
The takeover review used the live HyperTwist repository as implementation
authority and cross-referenced the following context families:
- `docs/`, including the comprehensive reconstruction roadmap, deep manual
pack, feature registry, development guide, packaging doctrine, hygiene
doctrine, reverse-SSH runbook, and previous Windows package evidence
- `/home/dev/src/visual_studio_solutions/multi_project/GPT 5.4 HyperTwist parse`
as historical product, donor, clean-room, and handoff lineage
- `/home/dev/src/Workspaces/HyperTwist` as repository-evaluation and clean-room
workspace lineage
- `/home/dev/src/Workspaces/HyperTwist-worktrees` as worktree-placement context
- the maintained Windows validation root
`C:\HyperTwist_worktrees\phase10validate`
Historical parse documents remain useful lineage, but their May 2026 maturity
descriptions do not outrank the current source, current roadmap, or current
packaged-runtime evidence.
## Failure analysis
### 1. Native widget trees were built too late
The first-run menu, Classic HUD, and coach dashboard are native `UUserWidget`
implementations. Their old lifecycle could let `Super::RebuildWidget()` cache
an empty fallback Slate widget before the code-created `WidgetTree` had a root.
Later construction changed UObject state, but not necessarily the already
cached rendered Slate tree. This explains a package that was alive and logging
without showing its intended UI.
Repair:
- build native widget roots before the superclass Slate rebuild completes
- keep initialization and construction idempotent
- expose explicit readiness predicates for all three surfaces
- reject and log a widget that has no renderable root
- place launch/dashboard surfaces above ordinary gameplay HUD layers
- validate first-run cached geometry after real layout ticks rather than
treating `AddToViewport()` as visual proof
### 2. Cooked runtime geometry ownership was incomplete
The Classic cube's 26 procedural cubie components could exist during editor
construction while not remaining authoritative in the cooked runtime. The old
`BeginPlay()` path skipped regeneration when construction generation was
enabled. A prior packaged log therefore truthfully reported `0 renderable
pieces` even though the map contained a placed cube actor.
Repair:
- define the physical invariant as 26 visible cubies; the enclosed core is not
rendered
- count registered procedural meshes with actual sections at runtime
- call `EnsureRenderablePresentation()` from both actor and game-mode startup
- regenerate when the count is incomplete
- own dynamic meshes and turn pivots as actor instance components
- mark runtime-generated presentation components transient so cooked map
serialization is not mistaken for runtime ownership
- log the exact final cubie count and whether recovery was required
### 3. The authored maps were presentation-incomplete
The earlier Classic and Follow-Along maps were too sparse to be trusted as
shipping launch surfaces. A later visual run also exposed a real-time SkyLight
without the required SkyAtmosphere context.
Repair:
- transactionally author both maps with a floor, directional light, SkyLight,
SkyAtmosphere, reflection capture, PlayerStart, and cube actor
- preserve the correct game-mode override per map
- validate the seven required actor labels before writing a durable completion
marker
- restore the pre-authoring map automatically if authoring does not complete
- use D3D11 offscreen authoring by default because the zero-extent headless
Plane path was unsafe on this UE 5.7 host
- use a thin Cube floor to retain volumetric bounds
- keep game-mode fallbacks for a missing orbit pawn or key light
Current source map SHA-256 values after transactional authoring:
- Classic: `b47d58cc2db66589cb0110548a002cf8446ce610c8448c50c9fbab329f3742ff`
- Follow-Along: `69a986cf36d53a6a22845dc89ecbe95119a1c2f383f344ecf020d6ee58ba0e47`
### 4. Unquoted INI URLs were parsed incorrectly
The configured local HTTP sidecar values used `http://...` without quotes.
Unreal INI parsing could collapse these values to `http:/...`, producing
malformed requests and noisy startup failures.
Repair:
- quote all three local HTTP service base URLs in `DefaultGame.ini`
- trim and validate complete `http://` or `https://` schemes in each HTTP
client before joining paths
- reject an absent authority rather than emitting a malformed request
- add a loaded-default configuration regression test for vision, speech, and
voice clients
Connection-refused warnings remain acceptable when an optional local sidecar
is not running. A malformed URL is not acceptable.
### 5. Package validation could report false success
Several harness behaviors allowed stale or incomplete evidence:
- old runtime/startup logs could be rediscovered
- a bootstrap process could exit while a child process survived untracked
- cleanup could leave packaged children running
- a raw path prefix could classify a sibling archive as an owned process
- a 10-second map smoke ended during plugin startup before any world loaded
- process survival and image brightness did not prove a specific UI route
Repair:
- remove the target runtime log before each launch and require the new file
- require fresh startup diagnostics by timestamp
- discover package descendants by executable path and protect pre-existing PIDs
- match owned paths at a directory boundary, not a raw string prefix
- stop the complete owned process family after validation
- classify Unreal low-level fatal, assertion, critical-error, swap-chain, and
unhandled-exception signatures as blocking
- require the expected `UnrealWindow` class for visual proof
- capture only the verified client rectangle with per-monitor DPI awareness
- inject interaction into the verified client and require a route-specific
startup-diagnostics result
- wait 30 seconds for packaged map smoke, require an actual target-map load
line, and require the exact 26-cubie marker for Classic and Follow-Along
- serialize plain strings into reports so PowerShell object formatting cannot
create a false result
- terminate timed-out interactive-task descendants before deleting the task
### 6. Generic map arguments did not own packaged startup
The project-level first-run game-mode authority can override a generic map
argument. Earlier package helpers therefore appeared to request a training map
while the product still resolved its normal first-launch owner. This was a
validation-route defect, not proof that the family map had opened.
Repair:
- add the bounded first-party
`-HyperTwistStartupRoute=<registered-route>` argument
- resolve the argument only through
`UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()`
- reject empty, duplicate, unknown, overlong, case-drifted, or character-
injected route identifiers
- invoke the same `OpenFirstRunRoute()` authority used by visible UI buttons
- make package launchers use the public outer bootstrap and registered route
ids instead of treating raw map arguments as product authority
- cover absent, valid, and invalid parser states in focused automation
### 7. Stock Shipping did not guarantee an Unreal log
The final package uses an ordinary Unreal Shipping target. It does not enable
the incompatible `bUseLoggingInShipping` target posture, and an engine
`-log`/`-abslog` file is therefore optional rather than release authority.
Repair:
- add `HyperTwistRuntimeDiagnostics`, a first-party, Shipping-compatible,
append-only diagnostics surface independent of `UE_LOG`
- accept an explicit `-HyperTwistDiagnosticsLog=<path>` for immutable
validation runs
- otherwise write `HyperTwistRuntime-latest.log` beneath the user Saved/Logs
directory
- require runtime initialization, semantic map/presentation markers, fatal
scans across every available log, package-owned process command lines, and
no unexpected listener/trace-control endpoint
- retain `HyperTwistFirstRunLaunch-latest.log` as the separate bounded
first-run/menu/route result authority
### 8. Desktop pixels alone were not semantic proof
`CopyFromScreen()` can capture the surrounding desktop when a window is hidden,
occluded, moved by DPI scaling, or unavailable in a disconnected session. A
bright screenshot is not sufficient evidence that HyperTwist rendered the
expected product surface.
Repair:
- have first-run, Classic, and higher-dimensional runtime owners request a
named ready-frame capture only after their semantic readiness invariant is
true
- require a fresh named PNG created after the launch timestamp for release
visual gates
- record both the copied evidence SHA-256 and the original semantic-frame
SHA-256
- keep client-window screenshots without a semantic capture as operator
corroboration only
- do not convert a disconnected/locked Windows desktop failure into a green
visual result
## Validation state
### Source and editor gates green
- UE 5.7 editor and game targets: `Result: Succeeded`
- final Shipping editor rebuild after the route and direct-diagnostics repair:
`Result: Succeeded`, 10 actions, UnrealBuildTool total `90.11 s`
- focused Classic automation: `16/16` green
- final focused first-run automation: `5/5` green, including
`PackagedStartupRouteArgumentContract`
- focused HTTP configuration automation: `1/1` green
- Sentrux source-only architecture check: all `7` rules pass
- GitNexus bounded source-only index: current and up to date
- Python map-authoring compile: green
- all current PowerShell package/launch/visual helpers parse successfully
### Historical superseded package archive
Build/archive root:
`C:\HyperTwist_worktrees\phase10validate_packaged_alpha_final_20260719`
Automated package facts:
- configuration: `Development`
- cooked map count: `4`
- packaged executable SHA-256:
`ccbc74bb6bcc8b7340d74ca15109c8159a83250f1413ea01f7f61a0c091f2c45`
- package file count before final delivery metadata: `127`
- package bytes before final delivery metadata: `1577648281`
- all four initial process-survival map launches completed without fatal log
signatures
- desktop bootstrap launch completed with a current runtime log and bounded
NullRHI startup recovery
The initial four-map report was not final semantic authority because its old
10-second interval ended before world load. It is retained only as failure
lineage and is superseded by the Release 17 Shipping evidence below.
### Current Release 17 Shipping package
Build/archive root:
`C:\HyperTwist_worktrees\phase10validate_packaged_alpha_release17_20260723`
Durable pulled evidence:
`docs/generated/alpha_test_20260723`
Final package facts:
- configuration: `Shipping`
- outer executable SHA-256:
`886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6`
- bootstrap resource:
`UnrealHyperTwist -notraceserver -traceautostart=0`
- exact solver table: `676207080` bytes, SHA-256
`dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034`
- Classic semantic smoke: exact map, `26` renderable pieces, settled valid,
no queued turns
- Follow-Along semantic smoke: accepted registered startup route, exact map,
`26` renderable pieces, settled valid, no queued turns
- Magic120Cell semantic smoke: accepted registered startup route, exact map,
`120` canonical renderable elements, `6` distinct layer materials
- MagicCube5D semantic smoke: accepted registered startup route, exact map,
`242` canonical renderable elements, `6` distinct layer materials
- all four smokes recorded runtime initialization and semantic `MapReady`
evidence with no fatal signature, trace listener, or package-owned listening
endpoint
The first Release 17 aggregate status retained a historical false failure
because the old desktop validator still required an absent Shipping Unreal log.
The corrected v2 launch report is the current authority: direct runtime
diagnostics initialized, first-run diagnostics were fresh, package-owned
processes were live, and no fatal or trace-control evidence was present.
Interactive proof:
- the exact final Downloads executable rendered an `UnrealWindow` and produced
a fresh semantic first-run frame with result `launch-menu-renderable`,
screenshot SHA-256
`6f5e3443d7b9188d6d3642c0a41f703e5a3beed7cee348982f7915621e786919`
- a real `WindowMessage` click on the visible Classic button produced
`map-route-opened`, the exact Classic map, `26` renderable pieces, `29`
completed quarter turns, `0` queued turns, and a fresh semantic frame with
screenshot SHA-256
`6e6b8f2c06f6ff8a68c2f4d9e0a576305056cd6f4d7353e9894a05e63d853ab5`
- a later dashboard visual retry correctly failed when no live interactive
desktop was available; no screenshot from that attempt is release evidence
- the same final executable then passed a first-party NullRHI dashboard-route
probe: runtime initialized, `coach-dashboard` was accepted, and the route
opened through first-run product authority
### Required final evidence
The following remain the acceptance ledger:
- [x] corrected v2 map-smoke reports prove all four target maps actually loaded
- [x] Classic and Follow-Along diagnostics confirm 26 renderable cubies
- [x] final first-run automation proves route delegates and strict packaged
startup-route parsing
- [x] interactive first-run screenshot and `launch-menu-renderable`
- [ ] fresh interactive coach-dashboard screenshot after an operator connects
or unlocks the Windows desktop; the route itself is green under direct
Shipping diagnostics and this visual-only item must not block use of the
delivered Alpha
- [x] interactive Classic route screenshot and `map-route-opened`
- [x] interactive Classic diagnostics have 26 cubies and no old presentation
warnings
- [x] complete folder copied to the Windows Downloads directory
- [x] ZIP created with `System.IO.Compression.ZipFile`
- [x] ZIP reopened, entries enumerated, and seven critical payloads stream-
hashed from the archive
- [x] final folder/ZIP SHA-256, sizes, and counts recorded
## Operator diagnostics
The delivered Alpha uses stock Shipping configuration and first-party direct
diagnostics. After launch, inspect:
- `%LOCALAPPDATA%\UnrealHyperTwist\Saved\Logs\HyperTwistRuntime-latest.log` -
Shipping-compatible runtime initialization, map, presentation, and route
events
- `%LOCALAPPDATA%\UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log`
- bounded first-run menu, fallback, and route result
An `UnrealHyperTwist.log` may be absent in stock Shipping and is not required
when direct diagnostics are present.
The package-validation helpers also write immutable-per-run evidence beneath
the archive's `validation` directory.
Do not diagnose a black screen by checking only that `UnrealHyperTwist.exe` is
running. Check the window class, current screenshots, startup result, target
map-load line, game-mode presentation marker, and fatal-signature scan.
## Distribution rule
Never distribute a loose `UnrealHyperTwist.exe`. The executable depends on the
complete staged `Windows` directory, including engine binaries, project
content containers, and runtime DLLs such as `tbbmalloc.dll`.
The approved Alpha distribution shape is:
- an extracted folder for direct operator testing
- a separately verified ZIP of that complete folder
- matching release metadata with SHA-256, byte count, file/entry count, and
validation references
Approved current delivery:
- folder:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723`
- folder facts: `103` files, `1887943160` bytes
- ZIP:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723.zip`
- ZIP facts: `103` entries, `1082134734` bytes, SHA-256
`1ad8e1c0fc967b227ca09fb68c515ed644eca3e4bda07fdf8c378b438000fc42`
The superseded July 9 folder and matching ZIP were deleted only after the
replacement folder, archive, critical-entry hashes, and final Downloads launch
were verified. The retirement report is retained at
`docs/generated/alpha_test_20260723/superseded_july9_retirement_report.json`.

View file

@ -509,6 +509,40 @@ Routing correction:
- [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`.
- [x] Shipping Alpha repair evidence on `2026-07-23`: the maintained
`localhost:22023` reverse-SSH lane rebuilt the current first-party source in
`C:\HyperTwist_worktrees\phase10validate`, passed all `5`
`HyperTwist.FirstRun` tests, and produced Release 17 at
`C:\HyperTwist_worktrees\phase10validate_packaged_alpha_release17_20260723`.
The package now resolves nondefault startup through strict registered
`-HyperTwistStartupRoute` ids instead of ineffective raw map arguments,
writes first-party direct diagnostics in stock Shipping, and carries
compile-time-disabled trace control plus verified outer-bootstrap
`-notraceserver -traceautostart=0` arguments.
- [x] Release 17 package semantics on `2026-07-23`: all four fresh smokes
passed with exact `MapReady` evidence; Classic and Follow-Along each exposed
`26` renderable cubies and settled valid state, Magic120Cell exposed exactly
`120` canonical elements with `6` layer materials, and MagicCube5D exposed
exactly `242` canonical elements with `6` layer materials. No package-owned
listener, trace-control endpoint, or fatal signature was observed.
- [x] Release 17 visual/distribution proof on `2026-07-23`: the exact final
Downloads executable rendered a fresh semantic first-run frame with
`launch-menu-renderable`; a real client-window message on the visible
Classic button produced `map-route-opened`, the exact Classic map, 26
renderable cubies, orbit camera, HUD, and settled valid state. The delivered
folder at
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723` contains
`103` files and the verified sibling ZIP contains `103` entries,
`1082134734` bytes, SHA-256
`1ad8e1c0fc967b227ca09fb68c515ed644eca3e4bda07fdf8c378b438000fc42`.
The ZIP was reopened and all seven critical payloads were independently
stream-hashed. Durable proof lives under
`docs/generated/alpha_test_20260723`.
- [ ] Operator-connected dashboard visual follow-up: the final Shipping
executable already accepts and opens the registered `coach-dashboard` route
under direct NullRHI diagnostics, but a fresh dashboard screenshot must wait
for an unlocked/live Windows desktop. Disconnected-session exit or
desktop-copy output must remain a rejected visual rather than a false green.
**Estimated actions:** 2030
**Estimated time:** 12 days
@ -553,6 +587,14 @@ Routing correction:
## Immediate Next Step
**Current Alpha next move (`2026-07-23`):** use the delivered Release 17 folder
for keyboard/mouse operator acceptance. On the next live Windows desktop
session, capture the remaining coach-dashboard visual, then prioritize
installer/uninstaller, code-signing, clean-machine prerequisite, entitlement,
and update-channel rollout gates before calling the desktop generally
production-deployable. Do not reopen simulator topology or renderer widening
to perform this release engineering.
**Phase 1 through Phase 5 are now closed on the intended simulator lane:** repo wiring, the
classic-cube renderer/runtime loop, dedicated training maps and materials, solver-guided training,
and speech command or narration surfaces are implemented and validated through Windows build,

View file

@ -3772,3 +3772,92 @@ Later same-day recommendation-history micro-follow-up (`2026-06-30`):
ownership reduction seam, especially inside
`DeriveCoachRecommendationHistorySummary(...)`, rather than reopening a
different product family prematurely
## Shipping Alpha startup, diagnostics, and visual authority (`2026-07-23`)
The Release 17 Alpha closes the user-reported black/empty first-launch and
corrupt/incomplete distribution lane with stronger product and validation
authority.
### Packaged startup route
Nondefault package launches must use:
```text
-HyperTwistStartupRoute=<registered-route-id>
```
Supported ids resolve through `UHyperTwistFirstRunLaunchLibrary` and execute
through the same `OpenFirstRunRoute()` path as visible first-run buttons. Do not
use a raw map argument as proof that Follow-Along, Magic120Cell, MagicCube5D, or
another product route opened; the project first-run owner can supersede it.
The parser is intentionally strict. Empty, duplicate, unknown, over-64-
character, uppercase, whitespace-bearing, or punctuation-injected values fail
closed and leave the normal first-run menu available.
### Stock-Shipping diagnostics
Do not enable an incompatible `bUseLoggingInShipping` target merely to satisfy
a validation script. The Shipping package owns:
- `%LOCALAPPDATA%\UnrealHyperTwist\Saved\Logs\HyperTwistRuntime-latest.log`
- `%LOCALAPPDATA%\UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log`
Validation can redirect the runtime file with
`-HyperTwistDiagnosticsLog=<absolute-path>`. An Unreal `-log`/`-abslog` file is
optional in stock Shipping; direct runtime initialization, semantic
map/presentation events, first-run result, process ownership, fatal scanning,
and listener/trace checks are the gate.
### Semantic visual gate
Release visuals for first-run, Classic, and higher-dimensional maps should
pass `-HyperTwistCaptureWhenReady` and require the exact expected named PNG.
The product owner requests that capture only after its semantic readiness
invariant is true. The visual report records the copied evidence hash and
original semantic-ready hash.
A `CopyFromScreen()` image without a fresh semantic capture is corroboration,
not release authority. This distinction is mandatory when the Windows desktop
is locked, disconnected, occluded, or running under a noninteractive SSH
session.
### Current commands and evidence
Primary helpers:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\Invoke-HyperTwistDesktopPackage.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\Launch-HyperTwistDesktopPackage.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\Test-HyperTwistPackagedWindowVisual.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\Export-HyperTwistPackagedBuildZip.ps1
```
Current durable evidence index:
`docs/generated/alpha_test_20260723/README.md`
Current operator delivery:
- `C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723`
- `C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723.zip`
The ZIP has `103` entries, size `1082134734` bytes, and SHA-256
`1ad8e1c0fc967b227ca09fb68c515ed644eca3e4bda07fdf8c378b438000fc42`.
### Remote-probe hygiene
A locally timed-out or interrupted SSH wrapper can leave its encoded
Windows PowerShell child running. Before another expensive build or visual
launch:
1. inspect process id, parent id, session id, working set, and full command line
2. identify ownership from the exact encoded command or package root
3. stop only the confirmed HyperTwist probe and its confirmed package children
4. never kill unrelated editor, compiler, VectorShell, or user processes
Avoid broad `Get-CimInstance Win32_Process` serialization inside a long-running
probe after a package launch; collecting and JSON-encoding every command line
can itself consume substantial memory. Prefer a narrow process-name/path query
and write the evidence report before cleanup.

View file

@ -398,6 +398,44 @@ When a new packet materially adds or changes a normalized feature:
## Summary
### Latest continuity note (`2026-07-23`)
- the current desktop Alpha authority is the Release 17 stock-Shipping package
delivered at
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723`, not the
July 9 repair artifact
- first-party packaged startup now owns a strict registered-route argument,
`-HyperTwistStartupRoute=<route-id>`:
- the route id must resolve through the same source-owned catalog used by the
visible first-run buttons
- empty, duplicate, unknown, overlong, case-drifted, and injected values are
rejected
- Follow-Along, Magic120Cell, and MagicCube5D package validation therefore
proves product-route ownership rather than relying on a raw map argument
- stock Shipping now writes direct first-party runtime diagnostics independently
of `UE_LOG`, while retaining separate bounded first-run route diagnostics
- the semantic package matrix is green:
- Classic and Follow-Along: exact maps, `26` renderable cubies, settled valid
state
- Magic120Cell: exact dedicated map, `120` canonical elements, `6` layer
materials
- MagicCube5D: exact dedicated map, `242` canonical elements, `6` layer
materials
- the exact final Downloads executable rendered a fresh semantic first-run
frame, and an injected visible-button interaction opened Classic with the
orbit camera, HUD, cube, and valid settled runtime visible
- archive verification reopened the `103`-entry ZIP and stream-hashed the
outer launcher, inner Shipping executable, `tbbmalloc.dll`, cooked
`pak`/`utoc`/`ucas`, and exact solver table; ZIP SHA-256 is
`1ad8e1c0fc967b227ca09fb68c515ed644eca3e4bda07fdf8c378b438000fc42`
- the final executable also accepts and opens `coach-dashboard` under direct
Shipping diagnostics; a fresh dashboard screenshot remains a narrow
operator-connected visual check because a disconnected desktop must not be
treated as image authority
- the July 9 folder and ZIP were retired only after replacement launch and
archive proof; durable evidence is indexed in
`docs/generated/alpha_test_20260723`
### Latest continuity note (`2026-07-02`)
- the first-party native desktop boot path now has a code-owned first-run
@ -412,7 +450,8 @@ When a new packet materially adds or changes a normalized feature:
Dashboard/settings, classic cube free play, follow-along training,
`Magic120Cell`, `MagicCube5D`, OpenXR validation, the packaged startup map,
the fallback route, and the runtime/startup log file names
- packaged first-run repair on `2026-07-09`: the maintained Windows package
- historical packaged first-run repair on `2026-07-09`, superseded by the
verified `2026-07-23` Shipping Alpha: the maintained Windows package
lane proved that `GameDefaultMap` alone was not sufficient because
`L_HyperTwist_ClassicTraining` still carried its own authored game-mode
override; `DefaultEngine.ini` now also sets `LocalMapOptions` to
@ -425,7 +464,8 @@ When a new packet materially adds or changes a normalized feature:
- `scripts/Export-HyperTwistPackagedBuildZip.ps1`
- validates that the produced archive is non-empty and still contains a
packaged `UnrealHyperTwist.exe` entry before reporting success
- refreshed Windows proof on `2026-07-09`: the repaired desktop artifact at
- historical Windows proof on `2026-07-09`, retained as provenance but no
longer distributed: the repaired desktop artifact at
`C:\HyperTwist_worktrees\phase10validate\packaged\desktop-first-run-authority-repair-20260709`
logged `BeginPlay`, `ApplyFirstRunInputMode`, and
`ShowFirstRunLaunchMenu` from `LogHyperTwistFirstRunLaunch`, and the fresh
@ -433,7 +473,9 @@ When a new packet materially adds or changes a normalized feature:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-desktop-first-run-authority-repair-20260709.zip`
passed archive verification with `142` entries, executable entry
`Windows\UnrealHyperTwist.exe`, size `719569118` bytes, and SHA-256
`4ec2d5f8e00e28d727590ca584ce40843eed5b85fa7e03c578b7a404c3f45d67`
`4ec2d5f8e00e28d727590ca584ce40843eed5b85fa7e03c578b7a404c3f45d67`;
both that Downloads folder and ZIP were deleted on `2026-07-23` after the
Release 17 replacement was fully verified
- this makes first launch materially less raw while keeping the same honest
boundary: the web simulator remains a lighter account/preview surface, the
Unreal download remains simulator authority, and physical headset/controller

View file

@ -486,6 +486,24 @@ Current consolidated milestone snapshot:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-desktop-first-run-authority-repair-20260709.zip`
with `142` entries and SHA-256
`4ec2d5f8e00e28d727590ca584ce40843eed5b85fa7e03c578b7a404c3f45d67`
- the `2026-07-23` Release 17 Shipping Alpha supersedes that July 9 artifact:
strict registered `-HyperTwistStartupRoute` ownership replaced ineffective
generic map launch arguments, stock-Shipping direct diagnostics replaced the
old assumption that an Unreal log must exist, first-run and Classic release
visuals now require fresh named semantic frames, and all four package smokes
now prove exact map and presentation state for Classic (`26` pieces),
Follow-Along (`26` pieces), Magic120Cell (`120` elements / `6` materials),
and MagicCube5D (`242` elements / `6` materials)
- the current Windows operator delivery is
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723` plus its
verified `103`-entry ZIP, SHA-256
`1ad8e1c0fc967b227ca09fb68c515ed644eca3e4bda07fdf8c378b438000fc42`;
the archive was reopened and seven critical payloads were stream-hashed,
then the superseded July 9 folder and ZIP were retired
- the narrow remaining Alpha visual check is a fresh coach-dashboard screenshot
during an unlocked Windows desktop session; dashboard route execution itself
is already green under direct Shipping diagnostics, and this visual check
does not reopen simulator topology
- classic-cube `Phase 9A` replay recording is now closed through first-party
runtime capture, `.json` replay persistence, local playback reconstruction,
schema-light replay normalization across save/load/viewer import, and live
@ -540,8 +558,11 @@ Current consolidated milestone snapshot:
`221` retained `Phase J` files carrying `443` deprecated validation
identifiers and `443` disabled flags with `0` remaining live
`HyperTwist.Validation.*` or `HyperTwist.FirstParty.Validation.*` names
- no mandatory roadmap gate remains inside the current `Phase 1` through
`Phase 10` packet set; the remaining optional widening branches are the
- no implementation roadmap gate remains inside the current `Phase 1` through
`Phase 10` packet set; the remaining Alpha/release-engineering checks are the
operator-connected dashboard visual plus installer/uninstaller, code-signing,
clean-machine prerequisites, entitlement, and update-channel rollout proof,
while the remaining optional product-widening branches are the
alternative full-browser client path and any later dedicated native
`MagicTile` interaction/runtime replacement work rather than new
structural-only `Phase J` growth

View file

@ -1,7 +1,9 @@
param(
[string]$PackageRoot = 'C:\HyperTwist\packaged\desktop',
[string]$DestinationZipPath = '',
[string]$ReportPath = ''
[string]$ReportPath = '',
[Int64]$ExpectedSolverTableSizeBytes = 676207080,
[string]$ExpectedSolverTableSha256 = 'dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034'
)
$ErrorActionPreference = 'Stop'
@ -90,6 +92,24 @@ function Resolve-PackagedExecutablePath {
return $CandidateExecutablePaths | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
}
function Resolve-PackagedGameExecutablePath {
param(
[Parameter(Mandatory = $true)]
[string]$PackagedProjectRoot
)
$GameExecutableDirectory = Join-Path $PackagedProjectRoot 'Binaries\Win64'
$CandidateExecutablePaths = @(
(Join-Path $GameExecutableDirectory 'UnrealHyperTwist.exe'),
(Join-Path $GameExecutableDirectory 'UnrealHyperTwist-Win64-Shipping.exe'),
(Join-Path $GameExecutableDirectory 'UnrealHyperTwist-Win64-Development.exe')
)
return $CandidateExecutablePaths |
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
Select-Object -First 1
}
function Invoke-ZipCreationWithRetry {
param(
[Parameter(Mandatory = $true)]
@ -148,6 +168,87 @@ function Test-ZipEntryMatchesExecutablePath {
return $NormalizedEntryPath.EndsWith('UnrealHyperTwist.exe', [System.StringComparison]::OrdinalIgnoreCase)
}
function Get-NormalizedRelativePath {
param(
[Parameter(Mandatory = $true)]
[string]$RootPath,
[Parameter(Mandatory = $true)]
[string]$FilePath
)
$RootWithSeparator = $RootPath.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
$FullFilePath = [System.IO.Path]::GetFullPath($FilePath)
if (-not $FullFilePath.StartsWith($RootWithSeparator, [System.StringComparison]::OrdinalIgnoreCase))
{
throw "Required package file '$FullFilePath' is not beneath package root '$RootPath'."
}
return $FullFilePath.Substring($RootWithSeparator.Length).Replace('\', '/')
}
function Get-RequiredPackageFileEvidence {
param(
[Parameter(Mandatory = $true)]
[string]$RootPath,
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Kind,
[Int64]$ExpectedSizeBytes = -1,
[string]$ExpectedSha256 = ''
)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf))
{
throw "Required packaged $Kind file was not found at '$Path'."
}
$Item = Get-Item -LiteralPath $Path
if ($Item.Length -le 0)
{
throw "Required packaged $Kind file '$Path' was empty."
}
if ($ExpectedSizeBytes -ge 0 -and $Item.Length -ne $ExpectedSizeBytes)
{
throw "Required packaged $Kind file '$Path' was $($Item.Length) bytes; expected exactly $ExpectedSizeBytes bytes."
}
$ActualSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
if (-not [string]::IsNullOrWhiteSpace($ExpectedSha256) `
-and $ActualSha256 -ne $ExpectedSha256.Trim().ToLowerInvariant())
{
throw "Required packaged $Kind file '$Path' did not match the expected SHA-256."
}
return [pscustomobject][ordered]@{
kind = $Kind
relativePath = Get-NormalizedRelativePath -RootPath $RootPath -FilePath $Item.FullName
sizeBytes = [Int64]$Item.Length
sha256 = $ActualSha256
}
}
function Get-ZipEntrySha256 {
param(
[Parameter(Mandatory = $true)]
[System.IO.Compression.ZipArchiveEntry]$Entry
)
$Sha256 = [System.Security.Cryptography.SHA256]::Create()
$EntryStream = $Entry.Open()
try
{
$Digest = $Sha256.ComputeHash($EntryStream)
return ([System.BitConverter]::ToString($Digest)).Replace('-', '').ToLowerInvariant()
}
finally
{
$EntryStream.Dispose()
$Sha256.Dispose()
}
}
if (-not (Test-Path -LiteralPath $PackageRoot))
{
throw "Package root '$PackageRoot' was not found."
@ -160,6 +261,47 @@ if ($null -eq $PackagedExecutablePath)
throw "No packaged UnrealHyperTwist executable was found beneath '$ResolvedPackageRoot'."
}
$PackagedExecutableDirectory = Split-Path -Parent $PackagedExecutablePath
$PackagedProjectRoot = Join-Path $PackagedExecutableDirectory 'UnrealHyperTwist'
$PackagedGameExecutablePath = Resolve-PackagedGameExecutablePath `
-PackagedProjectRoot $PackagedProjectRoot
if ($null -eq $PackagedGameExecutablePath)
{
throw "No packaged inner UnrealHyperTwist game executable was found beneath '$PackagedProjectRoot'."
}
$RequiredPackageFiles = @(
Get-RequiredPackageFileEvidence `
-RootPath $ResolvedPackageRoot `
-Path $PackagedExecutablePath `
-Kind 'launcher-executable'
Get-RequiredPackageFileEvidence `
-RootPath $ResolvedPackageRoot `
-Path $PackagedGameExecutablePath `
-Kind 'game-executable'
Get-RequiredPackageFileEvidence `
-RootPath $ResolvedPackageRoot `
-Path (Join-Path $PackagedProjectRoot 'Binaries\Win64\tbbmalloc.dll') `
-Kind 'runtime-dependency-tbbmalloc'
Get-RequiredPackageFileEvidence `
-RootPath $ResolvedPackageRoot `
-Path (Join-Path $PackagedProjectRoot 'Content\Paks\UnrealHyperTwist-Windows.pak') `
-Kind 'cooked-pak'
Get-RequiredPackageFileEvidence `
-RootPath $ResolvedPackageRoot `
-Path (Join-Path $PackagedProjectRoot 'Content\Paks\UnrealHyperTwist-Windows.utoc') `
-Kind 'cooked-utoc'
Get-RequiredPackageFileEvidence `
-RootPath $ResolvedPackageRoot `
-Path (Join-Path $PackagedProjectRoot 'Content\Paks\UnrealHyperTwist-Windows.ucas') `
-Kind 'cooked-ucas'
Get-RequiredPackageFileEvidence `
-RootPath $ResolvedPackageRoot `
-Path (Join-Path $PackagedProjectRoot 'Saved\twophase-ht.tbl') `
-Kind 'solver-table' `
-ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
-ExpectedSha256 $ExpectedSolverTableSha256
)
if ([string]::IsNullOrWhiteSpace($DestinationZipPath))
{
$PackageLeafName = Split-Path -Leaf $ResolvedPackageRoot
@ -188,7 +330,7 @@ if (Test-Path -LiteralPath $ResolvedDestinationZipPath)
}
$ZipReport = [ordered]@{
reportVersion = 'ht-packaged-build-zip-export/v1'
reportVersion = 'ht-packaged-build-zip-export/v2'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
packageRoot = $ResolvedPackageRoot
packagedExecutablePath = $PackagedExecutablePath
@ -196,6 +338,8 @@ $ZipReport = [ordered]@{
result = 'failed'
entryCount = 0
packagedExecutableEntry = $null
requiredEntries = @($RequiredPackageFiles)
archivedRequiredEntries = @()
zipSizeBytes = 0
zipSha256 = $null
error = $null
@ -226,6 +370,47 @@ try
{
throw "The exported zip '$ResolvedDestinationZipPath' contained zero entries."
}
foreach ($RequiredFile in $RequiredPackageFiles)
{
$MatchingRequiredEntries = @($Entries | Where-Object {
$_.FullName.Replace('\', '/').Equals(
$RequiredFile.relativePath,
[System.StringComparison]::OrdinalIgnoreCase)
})
if ($MatchingRequiredEntries.Count -ne 1)
{
throw (
"The exported zip '$ResolvedDestinationZipPath' contained " +
"$($MatchingRequiredEntries.Count) copies of required $($RequiredFile.kind) " +
"entry '$($RequiredFile.relativePath)'; expected exactly one."
)
}
$RequiredEntry = $MatchingRequiredEntries[0]
if ([Int64]$RequiredEntry.Length -ne [Int64]$RequiredFile.sizeBytes)
{
throw "Required zip entry '$($RequiredFile.relativePath)' had an unexpected uncompressed size."
}
$ArchivedSha256 = Get-ZipEntrySha256 -Entry $RequiredEntry
if ($ArchivedSha256 -ne $RequiredFile.sha256)
{
throw (
"Required zip entry '$($RequiredFile.relativePath)' did not preserve " +
"the source SHA-256 '$($RequiredFile.sha256)'."
)
}
$ZipReport.archivedRequiredEntries += @(
[pscustomobject][ordered]@{
kind = $RequiredFile.kind
relativePath = $RequiredFile.relativePath
sizeBytes = [Int64]$RequiredEntry.Length
sha256 = $ArchivedSha256
}
)
}
}
finally
{

View file

@ -1,7 +1,9 @@
param(
[string]$ProjectRoot = 'C:\HyperTwist',
[string]$UnrealEditorCmdPath = 'C:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe',
[string]$PythonScriptPath
[string]$PythonScriptPath,
[ValidateSet('d3d11', 'nullrhi')]
[string]$RenderBackend = 'd3d11'
)
$ErrorActionPreference = 'Stop'
@ -12,6 +14,7 @@ if ([string]::IsNullOrWhiteSpace($PythonScriptPath))
}
$UProjectPath = Join-Path $ProjectRoot 'UnrealHyperTwist\UnrealHyperTwist.uproject'
$AuthoringStateRoot = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\HyperTwistMapAuthoring'
$ClassicMapPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Maps\L_HyperTwist_ClassicTraining.umap'
$FollowAlongMapPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Maps\L_HyperTwist_FollowAlongTraining.umap'
$ExpectedMaterialPaths = @(
@ -40,6 +43,8 @@ if (-not (Test-Path $PythonScriptPath))
throw "Classic-cube map authoring script was not found at '$PythonScriptPath'."
}
[System.IO.Directory]::CreateDirectory($AuthoringStateRoot) | Out-Null
$NormalizedPythonScriptPath = $PythonScriptPath -replace '\\', '/'
$AuthoredTargets = @(
@{
@ -54,49 +59,102 @@ $AuthoredTargets = @(
foreach ($Target in $AuthoredTargets)
{
$env:HYPERTWIST_CLASSIC_CUBE_MAP_KIND = $Target.Label
$AuthoringStartedAtUtc = [DateTime]::UtcNow
Write-Host "Authoring HyperTwist classic cube training map target '$($Target.Label)' through UnrealEditor-Cmd..."
& $UnrealEditorCmdPath `
$UProjectPath `
"-ExecutePythonScript=$NormalizedPythonScriptPath" `
-unattended `
-nop4 `
-nullrhi `
-nosound `
-nosplash `
-stdout `
-FullStdOutLogOutput `
-log
$MarkerPath = Join-Path $AuthoringStateRoot "$($Target.Label)-complete.json"
$BackupPath = Join-Path $AuthoringStateRoot "$($Target.Label)-pre-authoring.umap"
$HadOriginalMap = Test-Path $Target.ExpectedMapPath
$AuthoringSucceeded = $false
if ($LASTEXITCODE -ne 0)
Remove-Item $MarkerPath, $BackupPath -Force -ErrorAction SilentlyContinue
if ($HadOriginalMap)
{
$FreshMapExists = $false
if (Test-Path $Target.ExpectedMapPath)
{
$FreshMapExists = (Get-Item $Target.ExpectedMapPath).LastWriteTimeUtc -ge $AuthoringStartedAtUtc.AddSeconds(-2)
}
Copy-Item $Target.ExpectedMapPath $BackupPath -Force
}
if ($FreshMapExists)
try
{
$env:HYPERTWIST_CLASSIC_CUBE_MAP_KIND = $Target.Label
$env:HYPERTWIST_CLASSIC_CUBE_AUTHORING_MARKER = $MarkerPath
$AuthoringStartedAtUtc = [DateTime]::UtcNow
$BackendArguments = if ($RenderBackend -eq 'nullrhi')
{
Write-Warning "UnrealEditor-Cmd exited with code $LASTEXITCODE after freshly writing '$($Target.ExpectedMapPath)'. Continuing because this host currently crashes during post-save shutdown on the headless authoring lane."
@('-nullrhi')
}
else
{
Remove-Item Env:\HYPERTWIST_CLASSIC_CUBE_MAP_KIND -ErrorAction SilentlyContinue
throw "Classic-cube map authoring failed with exit code $LASTEXITCODE."
@('-d3d11', '-RenderOffscreen')
}
}
Write-Host "Authoring HyperTwist classic cube training map target '$($Target.Label)' through UnrealEditor-Cmd ($RenderBackend)..."
& $UnrealEditorCmdPath `
$UProjectPath `
"-ExecutePythonScript=$NormalizedPythonScriptPath" `
-unattended `
-nop4 `
@BackendArguments `
-nosound `
-nosplash `
-stdout `
-FullStdOutLogOutput `
-log
if (-not (Test-Path $Target.ExpectedMapPath))
$EditorExitCode = $LASTEXITCODE
$FreshMapExists = (Test-Path $Target.ExpectedMapPath) -and (
(Get-Item $Target.ExpectedMapPath).LastWriteTimeUtc -ge $AuthoringStartedAtUtc.AddSeconds(-2)
)
$CompletionMarkerIsValid = $false
if (Test-Path $MarkerPath)
{
try
{
$CompletionMarker = Get-Content $MarkerPath -Raw | ConvertFrom-Json
$CompletionMarkerIsValid = `
$CompletionMarker.schemaVersion -eq 1 -and `
$CompletionMarker.stage -eq 'complete' -and `
$CompletionMarker.target -eq $Target.Label -and `
$CompletionMarker.mapAssetPath -eq (
"/Game/HyperTwistTraining/Maps/" + [System.IO.Path]::GetFileNameWithoutExtension($Target.ExpectedMapPath)
) -and `
@($CompletionMarker.actorLabels).Count -ge 7
}
catch
{
Write-Warning "Ignoring malformed authoring completion marker '$MarkerPath': $($_.Exception.Message)"
}
}
if (-not $FreshMapExists -or -not $CompletionMarkerIsValid)
{
throw "Classic-cube map authoring did not produce a fresh, fully validated '$($Target.Label)' map (editor exit code $EditorExitCode)."
}
if ($EditorExitCode -ne 0)
{
Write-Warning "UnrealEditor-Cmd exited with code $EditorExitCode after the complete map marker was durably written. The validated authored map is retained."
}
$AuthoringSucceeded = $true
}
finally
{
Remove-Item Env:\HYPERTWIST_CLASSIC_CUBE_MAP_KIND -ErrorAction SilentlyContinue
throw "Expected authored map was not found at '$($Target.ExpectedMapPath)'."
Remove-Item Env:\HYPERTWIST_CLASSIC_CUBE_AUTHORING_MARKER -ErrorAction SilentlyContinue
if (-not $AuthoringSucceeded)
{
if ($HadOriginalMap -and (Test-Path $BackupPath))
{
Copy-Item $BackupPath $Target.ExpectedMapPath -Force
Write-Warning "Restored the pre-authoring map after an incomplete '$($Target.Label)' authoring run."
}
elseif (Test-Path $Target.ExpectedMapPath)
{
Remove-Item $Target.ExpectedMapPath -Force
}
}
Remove-Item $BackupPath -Force -ErrorAction SilentlyContinue
}
}
Remove-Item Env:\HYPERTWIST_CLASSIC_CUBE_MAP_KIND -ErrorAction SilentlyContinue
foreach ($ExpectedMapPath in @($ClassicMapPath, $FollowAlongMapPath))
{
if (-not (Test-Path $ExpectedMapPath))

View file

@ -12,6 +12,13 @@ param(
),
[bool]$HeadlessSmoke = $true,
[string]$ValidationReportPath = '',
[string]$SolverTablePath = '',
[Int64]$ExpectedSolverTableSizeBytes = 676207080,
[string]$ExpectedSolverTableSha256 = 'dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034',
[string[]]$BootstrapLaunchArguments = @(
'-notraceserver',
'-traceautostart=0'
),
[switch]$CleanArchive,
[switch]$SkipBuild,
[switch]$SkipLaunch
@ -22,6 +29,7 @@ $ErrorActionPreference = 'Stop'
$RunUatPath = 'C:\Program Files\Epic Games\UE_5.7\Engine\Build\BatchFiles\RunUAT.bat'
$UProjectPath = Join-Path $ProjectRoot 'UnrealHyperTwist\UnrealHyperTwist.uproject'
$LaunchScriptPath = Join-Path $ProjectRoot 'scripts\Launch-HyperTwistClassicCubePackage.ps1'
$BootstrapLaunchArgumentsScriptPath = Join-Path $ProjectRoot 'scripts\Set-HyperTwistBootstrapLaunchArguments.ps1'
$GameTargetReceiptPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist.target'
$UnrealBuildToolSavedPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\UnrealBuildTool'
$CookMaps = @($CookMap) + $AdditionalCookMaps | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
@ -48,6 +56,12 @@ $ExpectedClassicCubeMaterialPaths = @(
)
$ValidationRootPath = Join-Path $ArchiveDirectory 'validation'
$SmokeReportDirectory = Join-Path $ValidationRootPath 'smoke'
$BootstrapLaunchArgumentsReportPath = Join-Path $ValidationRootPath 'bootstrap-launch-arguments-report.json'
if ([string]::IsNullOrWhiteSpace($SolverTablePath))
{
$SolverTablePath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\twophase-ht.tbl'
}
function Write-Utf8JsonFile {
param(
@ -90,6 +104,169 @@ function Read-JsonFile {
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}
function Get-BootstrapRuntimeArgumentEvidence {
param(
[string]$RuntimeLogPath,
[Parameter(Mandatory = $true)]
[string]$RuntimeDiagnosticsPath,
[string[]]$ObservedCommandLines = @(),
[object[]]$TraceControlListeningEndpoints = @(),
[Parameter(Mandatory = $true)]
[string[]]$ExpectedArguments
)
$RuntimeLogExists = -not [string]::IsNullOrWhiteSpace($RuntimeLogPath) `
-and (Test-Path -LiteralPath $RuntimeLogPath -PathType Leaf)
$RuntimeDiagnosticsExists = -not [string]::IsNullOrWhiteSpace($RuntimeDiagnosticsPath) `
-and (Test-Path -LiteralPath $RuntimeDiagnosticsPath -PathType Leaf)
if (-not $RuntimeDiagnosticsExists)
{
return [pscustomobject][ordered]@{
runtimeLogPath = $RuntimeLogPath
runtimeLogExists = $false
runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
runtimeDiagnosticsExists = $false
expectedArguments = @($ExpectedArguments)
matchedArguments = @()
missingArguments = @($ExpectedArguments)
traceControlListenerLines = @()
traceControlListeningEndpoints = @($TraceControlListeningEndpoints)
result = 'failed'
error = 'The direct HyperTwist diagnostics file was not found.'
}
}
$RuntimeLogLines = [string[]]@(
if ($RuntimeLogExists)
{
Get-Content -LiteralPath $RuntimeLogPath |
ForEach-Object { $_.ToString() }
}
)
$RuntimeDiagnosticsLines = [string[]]@(
if ($RuntimeDiagnosticsExists)
{
Get-Content -LiteralPath $RuntimeDiagnosticsPath |
ForEach-Object { $_.ToString() }
}
)
$RuntimeEvidenceLines = [string[]]@(
@($RuntimeLogLines) +
@($RuntimeDiagnosticsLines) +
@($ObservedCommandLines)
)
$RuntimeEvidenceText = [string]::Join([Environment]::NewLine, $RuntimeEvidenceLines)
$MatchedArguments = @(
$ExpectedArguments |
Where-Object {
$RuntimeEvidenceText.IndexOf(
$_,
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
)
$MissingArguments = @(
$ExpectedArguments |
Where-Object { $MatchedArguments -notcontains $_ }
)
$TraceControlListenerLines = [string[]]@(
@($RuntimeLogLines + $RuntimeDiagnosticsLines) |
Where-Object {
$_.IndexOf(
'Control listening on port',
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
)
$Passed = $MissingArguments.Count -eq 0 `
-and $TraceControlListenerLines.Count -eq 0 `
-and $TraceControlListeningEndpoints.Count -eq 0
return [pscustomobject][ordered]@{
runtimeLogPath = $RuntimeLogPath
runtimeLogExists = $RuntimeLogExists
runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
runtimeDiagnosticsExists = $RuntimeDiagnosticsExists
observedCommandLines = @($ObservedCommandLines)
expectedArguments = @($ExpectedArguments)
matchedArguments = @($MatchedArguments)
missingArguments = @($MissingArguments)
traceControlListenerLines = @($TraceControlListenerLines)
traceControlListeningEndpoints = @($TraceControlListeningEndpoints)
result = if ($Passed) { 'passed' } else { 'failed' }
error = if ($Passed)
{
$null
}
elseif ($MissingArguments.Count -gt 0)
{
"Runtime evidence omitted bootstrap arguments: $($MissingArguments -join ', ')"
}
else
{
'Runtime opened an Unreal trace-control listener.'
}
}
}
function Get-ValidatedSolverTableEvidence {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[Int64]$ExpectedSizeBytes,
[Parameter(Mandatory = $true)]
[string]$ExpectedSha256
)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf))
{
throw "Required two-phase solver table was not found at '$Path'."
}
$Item = Get-Item -LiteralPath $Path
if ($Item.Length -ne $ExpectedSizeBytes)
{
throw "Two-phase solver table '$Path' was $($Item.Length) bytes; expected exactly $ExpectedSizeBytes bytes."
}
$ActualSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
$NormalizedExpectedSha256 = $ExpectedSha256.Trim().ToLowerInvariant()
if ($ActualSha256 -ne $NormalizedExpectedSha256)
{
throw "Two-phase solver table '$Path' had SHA-256 '$ActualSha256'; expected '$NormalizedExpectedSha256'."
}
return [pscustomobject][ordered]@{
path = $Item.FullName
sizeBytes = [Int64]$Item.Length
sha256 = $ActualSha256
}
}
function Copy-ValidatedSolverTableToPackage {
param(
[Parameter(Mandatory = $true)]
[string]$SourcePath,
[Parameter(Mandatory = $true)]
[string]$PackagedExecutablePath,
[Parameter(Mandatory = $true)]
[Int64]$ExpectedSizeBytes,
[Parameter(Mandatory = $true)]
[string]$ExpectedSha256
)
$ExecutableDirectory = Split-Path -Parent $PackagedExecutablePath
$DestinationPath = Join-Path $ExecutableDirectory 'UnrealHyperTwist\Saved\twophase-ht.tbl'
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $DestinationPath) | Out-Null
Copy-Item -LiteralPath $SourcePath -Destination $DestinationPath -Force
return Get-ValidatedSolverTableEvidence `
-Path $DestinationPath `
-ExpectedSizeBytes $ExpectedSizeBytes `
-ExpectedSha256 $ExpectedSha256
}
function Resolve-PackagedExecutablePath {
param(
[string]$PackageRoot
@ -241,6 +418,11 @@ if (-not (Test-Path $UProjectPath))
throw "UnrealHyperTwist project file was not found at '$UProjectPath'."
}
if (-not (Test-Path -LiteralPath $BootstrapLaunchArgumentsScriptPath -PathType Leaf))
{
throw "Bootstrap launch-argument helper was not found at '$BootstrapLaunchArgumentsScriptPath'."
}
foreach ($TargetCookMap in $CookMaps)
{
Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetCookMap
@ -284,6 +466,7 @@ if ([string]::IsNullOrWhiteSpace($ValidationReportPath))
$RunUatArguments = @(
'BuildCookRun',
'-WaitForUATMutex',
"-project=$UProjectPath",
'-noP4',
'-platform=Win64',
@ -311,7 +494,7 @@ if ($ResolvedAdditionalCookerOptions.Count -gt 0)
}
$ValidationReport = [ordered]@{
reportVersion = 'ht-classic-cube-package-validation/v1'
reportVersion = 'ht-classic-cube-package-validation/v2'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
projectRoot = $ProjectRoot
archiveDirectory = $ArchiveDirectory
@ -325,12 +508,40 @@ $ValidationReport = [ordered]@{
skipLaunch = [bool]$SkipLaunch
result = 'failed'
packagedExecutablePath = $null
bootstrapLaunchArguments = [ordered]@{
result = 'pending'
traceControlProtection = if ($Configuration -eq 'Shipping')
{
'shipping-compile-time-disabled'
}
else
{
'runtime-listener-rejection-only'
}
expectedArguments = @($BootstrapLaunchArguments)
patchReportPath = $BootstrapLaunchArgumentsReportPath
patchReport = $null
runtimeProofs = @()
}
solverTable = [ordered]@{
result = 'pending'
sourcePath = $SolverTablePath
expectedSizeBytes = $ExpectedSolverTableSizeBytes
expectedSha256 = $ExpectedSolverTableSha256.ToLowerInvariant()
sourceEvidence = $null
packagedEvidence = $null
}
smokeReports = @()
error = $null
}
try
{
$ValidationReport.solverTable.sourceEvidence = Get-ValidatedSolverTableEvidence `
-Path $SolverTablePath `
-ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
-ExpectedSha256 $ExpectedSolverTableSha256
Write-Host "Packaging HyperTwist classic-cube validation lane to '$ArchiveDirectory'..."
& $RunUatPath @RunUatArguments
@ -346,6 +557,23 @@ try
}
$ValidationReport.packagedExecutablePath = $PackagedExecutablePath
& $BootstrapLaunchArgumentsScriptPath `
-ExecutablePath $PackagedExecutablePath `
-AdditionalArguments $BootstrapLaunchArguments `
-ReportPath $BootstrapLaunchArgumentsReportPath | Out-Null
$BootstrapPatchReport = Read-JsonFile -Path $BootstrapLaunchArgumentsReportPath
if ($null -eq $BootstrapPatchReport -or $BootstrapPatchReport.result -ne 'passed')
{
throw "Packaged bootstrap launch-argument patch did not record a passed result."
}
$ValidationReport.bootstrapLaunchArguments.patchReport = $BootstrapPatchReport
$ValidationReport.solverTable.packagedEvidence = Copy-ValidatedSolverTableToPackage `
-SourcePath $SolverTablePath `
-PackagedExecutablePath $PackagedExecutablePath `
-ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
-ExpectedSha256 $ExpectedSolverTableSha256
$ValidationReport.solverTable.result = 'passed'
if (-not $SkipLaunch)
{
@ -392,8 +620,28 @@ try
throw "Packaged classic-cube smoke report '$SmokeReportPath' targeted '$($SmokeReport.mapUrl)' instead of '$SmokeMap'."
}
$RuntimeArgumentEvidence = Get-BootstrapRuntimeArgumentEvidence `
-RuntimeLogPath $SmokeReport.runtimeLogPath `
-RuntimeDiagnosticsPath $SmokeReport.runtimeDiagnosticsPath `
-ObservedCommandLines @($SmokeReport.processCommandLines) `
-TraceControlListeningEndpoints @($SmokeReport.traceControlListeningEndpoints) `
-ExpectedArguments $BootstrapLaunchArguments
$ValidationReport.bootstrapLaunchArguments.runtimeProofs += @(
$RuntimeArgumentEvidence
)
if ($RuntimeArgumentEvidence.result -ne 'passed')
{
throw $RuntimeArgumentEvidence.error
}
$ValidationReport.smokeReports += @($SmokeReport)
}
$ValidationReport.bootstrapLaunchArguments.result = 'passed'
}
else
{
$ValidationReport.bootstrapLaunchArguments.result = 'patch-passed-runtime-not-run'
}
$ValidationReport.result = 'passed'

View file

@ -22,6 +22,9 @@ param(
[bool]$HeadlessSmoke = $true,
[string]$ValidationReportPath = '',
[string]$LaunchSurfaceReportPath = '',
[string]$SolverTablePath = '',
[Int64]$ExpectedSolverTableSizeBytes = 676207080,
[string]$ExpectedSolverTableSha256 = 'dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034',
[string]$ZipOutputPath = '',
[string]$ZipExportReportPath = '',
[switch]$CleanArchive,
@ -75,7 +78,11 @@ $InvokeParameters = @{
AdditionalCookMaps = $AdditionalCookMaps
SmokeMaps = $SmokeMaps
AdditionalCookerOptions = $AdditionalCookerOptions
HeadlessSmoke = $HeadlessSmoke
ValidationReportPath = $ValidationReportPath
SolverTablePath = $SolverTablePath
ExpectedSolverTableSizeBytes = $ExpectedSolverTableSizeBytes
ExpectedSolverTableSha256 = $ExpectedSolverTableSha256
}
if ($CleanArchive)
@ -97,12 +104,19 @@ if ($SkipLaunch)
if (-not $SkipLaunch)
{
& $DesktopLaunchScriptPath `
-PackageRoot $ArchiveDirectory `
-ReportPath $LaunchSurfaceReportPath `
-UseNullRHI:$HeadlessSmoke `
-NoSound:$HeadlessSmoke `
-RequireStartupDiagnostics
$DesktopLaunchParameters = @{
PackageRoot = $ArchiveDirectory
ReportPath = $LaunchSurfaceReportPath
UseNullRHI = $HeadlessSmoke
NoSound = $HeadlessSmoke
RequireStartupDiagnostics = $true
}
if (-not $HeadlessSmoke)
{
$DesktopLaunchParameters.ExpectedStartupDiagnosticsResult = 'launch-menu-renderable'
}
& $DesktopLaunchScriptPath @DesktopLaunchParameters
}
if ($CreateZip)
@ -110,5 +124,7 @@ if ($CreateZip)
& $ZipExportScriptPath `
-PackageRoot $ArchiveDirectory `
-DestinationZipPath $ZipOutputPath `
-ReportPath $ZipExportReportPath
-ReportPath $ZipExportReportPath `
-ExpectedSolverTableSizeBytes $ExpectedSolverTableSizeBytes `
-ExpectedSolverTableSha256 $ExpectedSolverTableSha256
}

View file

@ -16,6 +16,7 @@ $Magic120CellMapPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwi
$MagicCube5DMapPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Maps\L_HyperTwist_MagicCube5DTraining.umap'
$ClassicMapPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Maps\L_HyperTwist_ClassicTraining.umap'
$ManifestPath = Join-Path $ProjectRoot 'docs\generated\higher_dimensional_training_maps\phase6c_dedicated_family_map_manifest.json'
$ReceiptDirectory = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\HyperTwistMapAuthoring'
if (-not (Test-Path $UnrealEditorCmdPath))
{
@ -37,10 +38,12 @@ $AuthoredTargets = @(
@{
Label = 'magic120cell'
ExpectedMapPath = $Magic120CellMapPath
ReceiptPath = Join-Path $ReceiptDirectory 'phase6c_magic120cell_complete.json'
},
@{
Label = 'magiccube5d'
ExpectedMapPath = $MagicCube5DMapPath
ReceiptPath = Join-Path $ReceiptDirectory 'phase6c_magiccube5d_complete.json'
}
)
@ -48,6 +51,7 @@ foreach ($Target in $AuthoredTargets)
{
$env:HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND = $Target.Label
$AuthoringStartedAtUtc = [DateTime]::UtcNow
Remove-Item -LiteralPath $Target.ReceiptPath -Force -ErrorAction SilentlyContinue
Write-Host "Authoring HyperTwist higher-dimensional training map target '$($Target.Label)' through UnrealEditor-Cmd..."
& $UnrealEditorCmdPath `
$UProjectPath `
@ -61,23 +65,44 @@ foreach ($Target in $AuthoredTargets)
-FullStdOutLogOutput `
-log
if ($LASTEXITCODE -ne 0)
$AuthoringExitCode = $LASTEXITCODE
$Receipt = $null
if (Test-Path -LiteralPath $Target.ReceiptPath -PathType Leaf)
{
$FreshMapExists = $false
if (Test-Path $Target.ExpectedMapPath)
{
$FreshMapExists = (Get-Item $Target.ExpectedMapPath).LastWriteTimeUtc -ge $AuthoringStartedAtUtc.AddSeconds(-2)
}
$Receipt = Get-Content -LiteralPath $Target.ReceiptPath -Raw | ConvertFrom-Json
}
if ($FreshMapExists)
$ExpectedMapHash = if (Test-Path -LiteralPath $Target.ExpectedMapPath -PathType Leaf)
{
(Get-FileHash -LiteralPath $Target.ExpectedMapPath -Algorithm MD5).Hash.ToLowerInvariant()
}
else
{
''
}
$ReceiptIsFresh = $Receipt -and `
(Get-Item -LiteralPath $Target.ReceiptPath).LastWriteTimeUtc -ge $AuthoringStartedAtUtc.AddSeconds(-2)
$ReceiptIsValid = $ReceiptIsFresh -and `
$Receipt.authoringComplete -eq $true -and `
$Receipt.mapKind -eq $Target.Label -and `
$Receipt.gameModeClassPath -eq '/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode' -and `
$Receipt.presentationEnvironmentOwnership -eq 'runtime-game-mode-and-shell-components' -and `
$Receipt.mapHashMd5 -eq $ExpectedMapHash -and `
@($Receipt.validatedActorLabels).Count -eq 1
if (-not $ReceiptIsValid)
{
Remove-Item Env:\HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND -ErrorAction SilentlyContinue
if ($AuthoringExitCode -ne 0)
{
Write-Warning "UnrealEditor-Cmd exited with code $LASTEXITCODE after freshly writing '$($Target.ExpectedMapPath)'. Continuing because this host may still crash during post-save shutdown on the headless authoring lane."
}
else
{
Remove-Item Env:\HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND -ErrorAction SilentlyContinue
throw "Higher-dimensional map authoring failed with exit code $LASTEXITCODE."
throw "Higher-dimensional map authoring failed with exit code $AuthoringExitCode before a valid completion receipt was written for '$($Target.Label)'."
}
throw "Higher-dimensional map authoring exited without a valid hash-bound completion receipt for '$($Target.Label)'."
}
if ($AuthoringExitCode -ne 0)
{
Write-Warning "UnrealEditor-Cmd exited with code $AuthoringExitCode only after '$($Target.Label)' completed, validated, and wrote its hash-bound receipt. Accepting the known post-completion headless shutdown crash."
}
if (-not (Test-Path $Target.ExpectedMapPath))
@ -126,7 +151,10 @@ $ManifestEntries = @(
primaryPersistenceBoundaryId = 'magic120cell-persistence-boundary'
authorTag = 'HyperTwistHigherDimensionalTrainingShell'
authoringManifestRelativePath = 'docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json'
authoredViaGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode'
authoredViaGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode'
runtimePresentationClassPath = '/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingShellActor'
presentationEnvironmentOwnership = 'runtime-game-mode-and-shell-components'
canonicalRenderableElementCount = 120
trainingShellTags = @(
'phase6c',
'family:magic120cell',
@ -155,7 +183,10 @@ $ManifestEntries = @(
primaryPersistenceBoundaryId = 'magiccube5d-persistence-boundary'
authorTag = 'HyperTwistHigherDimensionalTrainingShell'
authoringManifestRelativePath = 'docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json'
authoredViaGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode'
authoredViaGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode'
runtimePresentationClassPath = '/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingShellActor'
presentationEnvironmentOwnership = 'runtime-game-mode-and-shell-components'
canonicalRenderableElementCount = 242
trainingShellTags = @(
'phase6c',
'family:magiccube5d',
@ -168,10 +199,10 @@ $ManifestEntries = @(
$Manifest = [ordered]@{
manifestId = 'phase6c/dedicated-family-training-map-authoring'
manifestVersion = '2026.06.18'
manifestVersion = '2026.07.19'
authorTag = 'HyperTwistHigherDimensionalTrainingShell'
authoringScriptRelativePath = 'scripts/hypertwist_author_higher_dimensional_training_maps.py'
authoredThroughGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode'
authoredThroughGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode'
classicReferenceMapHashMd5 = $ClassicReferenceHashMd5
entries = $ManifestEntries
}

View file

@ -50,6 +50,43 @@ function Convert-TaskToken {
return ($Value -replace '[^A-Za-z0-9_-]', '_')
}
function Get-TaskOwnedProcessIds {
param(
[Parameter(Mandatory = $true)]
[string]$RunnerPath
)
$ProcessSnapshots = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
$OwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
foreach ($ProcessSnapshot in $ProcessSnapshots)
{
if (-not [string]::IsNullOrWhiteSpace([string]$ProcessSnapshot.CommandLine) `
-and ([string]$ProcessSnapshot.CommandLine).IndexOf(
$RunnerPath,
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0)
{
[void]$OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId)
}
}
$AddedProcess = $true
while ($AddedProcess)
{
$AddedProcess = $false
foreach ($ProcessSnapshot in $ProcessSnapshots)
{
if ($OwnedProcessIds.Contains([int]$ProcessSnapshot.ParentProcessId) `
-and $OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId))
{
$AddedProcess = $true
}
}
}
return @($OwnedProcessIds)
}
if ([string]::IsNullOrWhiteSpace($InlineCommand))
{
throw 'Provide -InlineCommand.'
@ -126,9 +163,12 @@ $Result = [ordered]@{
createExitCode = $null
runExitCode = $null
taskOutput = $null
terminatedOwnedProcessIds = @()
result = 'failed'
error = $null
}
$TaskCreated = $false
$FailureResultPath = $null
try
{
@ -137,7 +177,6 @@ try
'/TN', $TaskName,
'/SC', 'ONCE',
'/ST', '23:59',
'/SD', (Get-Date).ToString('MM/dd/yyyy'),
'/TR', ('C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File {0}' -f $TaskRunnerPath),
'/RU', 'INTERACTIVE'
)
@ -147,6 +186,7 @@ try
{
throw "Creating the interactive desktop task failed with exit code $($Result.createExitCode)."
}
$TaskCreated = $true
& schtasks.exe /Run /TN ('\' + $TaskName) | Out-Null
$Result.runExitCode = $LASTEXITCODE
@ -201,7 +241,7 @@ catch
{
$Result.taskOutput = Read-JsonFile -Path $TaskResultPath
}
$ResolvedFailureResultPath = if ([string]::IsNullOrWhiteSpace($ResultPath))
$FailureResultPath = if ([string]::IsNullOrWhiteSpace($ResultPath))
{
$TaskResultPath
}
@ -209,18 +249,49 @@ catch
{
$ResultPath
}
Write-Utf8JsonFile -Path $ResolvedFailureResultPath -Value $Result
Write-Utf8JsonFile -Path $FailureResultPath -Value $Result
throw
}
finally
{
if (-not $KeepArtifacts)
{
& schtasks.exe /Delete /TN $TaskName /F | Out-Null
$TaskOwnedProcessIds = if ($TaskCreated)
{
@(Get-TaskOwnedProcessIds -RunnerPath $TaskRunnerPath)
}
else
{
@()
}
if ($TaskCreated)
{
# A timed-out interactive child must not survive as a hidden,
# memory-consuming desktop task.
& schtasks.exe /End /TN ('\' + $TaskName) *> $null
if ($LASTEXITCODE -eq 0)
{
Start-Sleep -Milliseconds 250
}
& schtasks.exe /Delete /TN $TaskName /F | Out-Null
}
foreach ($TaskOwnedProcessId in @($TaskOwnedProcessIds | Sort-Object -Descending))
{
if ($TaskOwnedProcessId -ne $PID `
-and $null -ne (Get-Process -Id $TaskOwnedProcessId -ErrorAction SilentlyContinue))
{
Stop-Process -Id $TaskOwnedProcessId -Force -ErrorAction SilentlyContinue
$Result.terminatedOwnedProcessIds += [int]$TaskOwnedProcessId
}
}
if (Test-Path -LiteralPath $TaskRunnerPath)
{
Remove-Item -LiteralPath $TaskRunnerPath -Force
}
if (-not [string]::IsNullOrWhiteSpace($FailureResultPath))
{
Write-Utf8JsonFile -Path $FailureResultPath -Value $Result
}
}
}

View file

@ -1,7 +1,7 @@
param(
[string]$PackageRoot = 'C:\HyperTwist\packaged\classic-cube',
[string]$MapUrl = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining',
[int]$SmokeSeconds = 10,
[int]$SmokeSeconds = 30,
[int]$ResX = 1600,
[int]$ResY = 900,
[string]$ReportPath = '',
@ -44,6 +44,9 @@ function Get-FatalRuntimeLogMatches {
$FatalPatterns = @(
'Fatal error:',
'LowLevelFatalError',
'Assertion failed:',
'Critical error:',
'appError called:',
'DXGI_ERROR_NOT_CURRENTLY_AVAILABLE',
'CreateSwapChainResult failed',
@ -65,18 +68,138 @@ function Get-FatalRuntimeLogMatches {
})
}
function Get-OwnedPackageProcessIds {
param(
[Parameter(Mandatory = $true)]
[string]$ResolvedPackageRoot,
[int[]]$ExcludedProcessIds = @()
)
return @(
Get-OwnedPackageProcessRecords `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExcludedProcessIds |
Select-Object -ExpandProperty ProcessId
)
}
function Get-OwnedPackageProcessRecords {
param(
[Parameter(Mandatory = $true)]
[string]$ResolvedPackageRoot,
[int[]]$ExcludedProcessIds = @()
)
$RootPrefix = $ResolvedPackageRoot.TrimEnd(
[System.IO.Path]::DirectorySeparatorChar,
[System.IO.Path]::AltDirectorySeparatorChar
) + [System.IO.Path]::DirectorySeparatorChar
return @(
Get-CimInstance Win32_Process -Filter "Name = 'UnrealHyperTwist.exe'" -ErrorAction SilentlyContinue |
Where-Object {
-not [string]::IsNullOrWhiteSpace([string]$_.ExecutablePath) `
-and ([string]$_.ExecutablePath).StartsWith(
$RootPrefix,
[System.StringComparison]::OrdinalIgnoreCase
) `
-and $ExcludedProcessIds -notcontains [int]$_.ProcessId
}
)
}
function Get-OwnedListeningTcpEndpoints {
param(
[int[]]$ProcessIds
)
if ($ProcessIds.Count -eq 0)
{
return @()
}
return @(
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $ProcessIds -contains [int]$_.OwningProcess } |
Sort-Object OwningProcess, LocalPort |
ForEach-Object {
[pscustomobject][ordered]@{
processId = [int]$_.OwningProcess
localAddress = [string]$_.LocalAddress
localPort = [int]$_.LocalPort
}
}
)
}
function Stop-OwnedPackageProcesses {
param(
[Parameter(Mandatory = $true)]
[string]$ResolvedPackageRoot,
[int[]]$ExcludedProcessIds = @()
)
$StoppedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
for ($Attempt = 0; $Attempt -lt 5; $Attempt += 1)
{
$OwnedProcessIds = @(
Get-OwnedPackageProcessIds `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExcludedProcessIds
)
if ($OwnedProcessIds.Count -eq 0)
{
break
}
foreach ($OwnedProcessId in $OwnedProcessIds)
{
Stop-Process -Id $OwnedProcessId -Force -ErrorAction SilentlyContinue
[void]$StoppedProcessIds.Add([int]$OwnedProcessId)
}
Start-Sleep -Milliseconds 200
}
return @($StoppedProcessIds)
}
$CandidateExecutablePaths = @(
(Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
(Join-Path $PackageRoot 'UnrealHyperTwist.exe')
)
$ExecutablePath = $CandidateExecutablePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($null -eq $ExecutablePath)
$LauncherExecutablePath = $CandidateExecutablePaths |
Where-Object { Test-Path $_ } |
Select-Object -First 1
if ($null -eq $LauncherExecutablePath)
{
throw "No packaged UnrealHyperTwist executable was found beneath '$PackageRoot'."
}
$PackagedExecutableDirectory = Split-Path -Parent $LauncherExecutablePath
$ClassicDefaultMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining'
$StartupRouteByMap = @{
$ClassicDefaultMap = ''
'/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining' = 'follow-along-training'
'/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining' = 'magic-120-cell-training'
'/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining' = 'magic-cube-5d-training'
}
if (-not $StartupRouteByMap.ContainsKey($MapUrl))
{
throw "No first-party packaged startup route is registered for smoke map '$MapUrl'."
}
$StartupRouteId = [string]$StartupRouteByMap[$MapUrl]
$LaunchMode = if ($MapUrl -eq $ClassicDefaultMap)
{
'bootstrap-default-map'
}
else
{
'first-party-startup-route'
}
$ExecutablePath = $LauncherExecutablePath
$LogDirectory = Join-Path $PackageRoot 'validation\logs'
New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null
$LogToken = if ([string]::IsNullOrWhiteSpace($MapUrl))
@ -88,15 +211,26 @@ else
($MapUrl -replace '[\\/:*?"<>| ]', '_')
}
$RuntimeLogPath = Join-Path $LogDirectory ("{0}.log" -f $LogToken)
$RuntimeDiagnosticsPath = Join-Path $LogDirectory ("{0}.hyperdiagnostics.log" -f $LogToken)
Remove-Item -LiteralPath $RuntimeLogPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $RuntimeDiagnosticsPath -Force -ErrorAction SilentlyContinue
$ArgumentList = @(
$MapUrl,
$ArgumentList = @()
if (-not [string]::IsNullOrWhiteSpace($StartupRouteId))
{
# Nondefault smokes use the same registered product route as the launch
# menu. Generic startup URLs are overridden by the packaged first-run
# game-mode authority and therefore are not valid route evidence.
$ArgumentList += "-HyperTwistStartupRoute=$StartupRouteId"
}
$ArgumentList += @(
"-ResX=$ResX",
"-ResY=$ResY",
'-windowed',
'-log',
'-FORCELOGFLUSH',
"-abslog=$RuntimeLogPath"
"-abslog=$RuntimeLogPath",
"-HyperTwistDiagnosticsLog=`"$RuntimeDiagnosticsPath`""
)
if ($UseNullRHI)
@ -111,17 +245,58 @@ if ($NoSound)
Write-Host "Launching packaged classic-cube validation lane from '$ExecutablePath'..."
$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
$ExistingPackageProcessIds = @(
Get-OwnedPackageProcessIds -ResolvedPackageRoot $ResolvedPackageRoot
)
$GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
$Process = $null
$RequiresClassicPresentation = $MapUrl -match '/L_HyperTwist_(Classic|FollowAlong)Training$'
$HigherDimensionalFamily = if ($MapUrl -match '/L_HyperTwist_Magic120CellTraining$')
{
'magic120cell'
}
elseif ($MapUrl -match '/L_HyperTwist_MagicCube5DTraining$')
{
'magiccube5d'
}
else
{
''
}
$ExpectedHigherDimensionalElementCount = if ($HigherDimensionalFamily -eq 'magic120cell')
{
120
}
elseif ($HigherDimensionalFamily -eq 'magiccube5d')
{
242
}
else
{
0
}
$Report = [ordered]@{
reportVersion = 'ht-classic-cube-package-smoke/v1'
reportVersion = 'ht-desktop-map-package-smoke/v7'
generatedAtUtc = $GeneratedAtUtc
packageRoot = $ResolvedPackageRoot
launchMode = $LaunchMode
startupRouteId = $StartupRouteId
launcherExecutablePath = $LauncherExecutablePath
executablePath = $ExecutablePath
mapUrl = $MapUrl
runtimeLogPath = $RuntimeLogPath
runtimeLogExists = $false
runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
runtimeDiagnosticsExists = $false
detectedFatalLogLines = @()
mapLoadLogLines = @()
requiresClassicPresentation = $RequiresClassicPresentation
classicPresentationLogLines = @()
classicScrambleSettlementLogLines = @()
higherDimensionalFamily = $HigherDimensionalFamily
expectedHigherDimensionalElementCount = $ExpectedHigherDimensionalElementCount
higherDimensionalPresentationLogLines = @()
higherDimensionalPaletteLogLines = @()
smokeSeconds = $SmokeSeconds
resolution = [ordered]@{
width = $ResX
@ -132,22 +307,60 @@ $Report = [ordered]@{
noSound = [bool]$NoSound
result = 'failed'
processId = $null
processIds = @()
processCommandLines = @()
ownedListeningTcpEndpoints = @()
traceControlListenerLines = @()
traceControlListeningEndpoints = @()
startupRouteCommandLineEvidence = @()
startupRouteEvidenceLines = @()
processStopped = $false
stoppedProcessIds = @()
exitCode = $null
error = $null
}
try
{
$Process = Start-Process -FilePath $ExecutablePath -ArgumentList $ArgumentList -PassThru
$Process = Start-Process `
-FilePath $ExecutablePath `
-ArgumentList $ArgumentList `
-WorkingDirectory (Split-Path -Parent $ExecutablePath) `
-PassThru
Start-Sleep -Seconds $SmokeSeconds
$Process.Refresh()
$Report.processId = $Process.Id
$OwnedProcessRecords = @(
Get-OwnedPackageProcessRecords `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExistingPackageProcessIds
)
$Report.processIds = @($OwnedProcessRecords | Select-Object -ExpandProperty ProcessId)
$Report.processCommandLines = @(
$OwnedProcessRecords |
ForEach-Object { [string]$_.CommandLine } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
$Report.ownedListeningTcpEndpoints = @(
Get-OwnedListeningTcpEndpoints -ProcessIds $Report.processIds
)
$Report.traceControlListeningEndpoints = @(
$Report.ownedListeningTcpEndpoints |
Where-Object { [int]$_.localPort -eq 1985 }
)
Start-Sleep -Milliseconds 500
$Report.runtimeLogExists = Test-Path -LiteralPath $RuntimeLogPath
$Report.detectedFatalLogLines = @(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeLogPath)
$Report.runtimeDiagnosticsExists = Test-Path -LiteralPath $RuntimeDiagnosticsPath
$Report.detectedFatalLogLines = @(
@(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeLogPath) +
@(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeDiagnosticsPath)
)
if (-not $Report.runtimeDiagnosticsExists)
{
throw 'Packaged launch did not write its direct HyperTwist diagnostics log.'
}
if ($Report.detectedFatalLogLines.Count -gt 0)
{
throw (
@ -156,10 +369,157 @@ try
)
}
if ($Process.HasExited)
$CurrentRuntimeLogLines = @(
if ($Report.runtimeLogExists)
{
Get-Content -LiteralPath $RuntimeLogPath
}
)
$CurrentRuntimeDiagnosticsLines = @(
if ($Report.runtimeDiagnosticsExists)
{
Get-Content -LiteralPath $RuntimeDiagnosticsPath
}
)
$CurrentEvidenceLines = @($CurrentRuntimeLogLines + $CurrentRuntimeDiagnosticsLines)
$Report.traceControlListenerLines = @(
$CurrentEvidenceLines |
Where-Object {
$_.ToString().IndexOf(
'Control listening on port',
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
} |
ForEach-Object { $_.ToString() }
)
if ($Report.traceControlListenerLines.Count -gt 0 `
-or $Report.traceControlListeningEndpoints.Count -gt 0)
{
$Report.exitCode = $Process.ExitCode
throw "Packaged classic-cube executable exited early with code $($Process.ExitCode)."
throw 'Packaged runtime opened an Unreal trace-control listener.'
}
if (-not [string]::IsNullOrWhiteSpace($StartupRouteId))
{
$ExpectedStartupRouteArgument = "-HyperTwistStartupRoute=$StartupRouteId"
$Report.startupRouteCommandLineEvidence = @(
$Report.processCommandLines |
Where-Object {
$_.IndexOf(
$ExpectedStartupRouteArgument,
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
)
if ($Report.startupRouteCommandLineEvidence.Count -eq 0)
{
throw (
"Packaged bootstrap did not propagate first-party startup route argument " +
"'$ExpectedStartupRouteArgument'."
)
}
$ExpectedStartupRouteMarker = "Accepted packaged startup route '$StartupRouteId'"
$Report.startupRouteEvidenceLines = @(
$CurrentEvidenceLines |
Where-Object { $_ -like "*$ExpectedStartupRouteMarker*" } |
ForEach-Object { $_.ToString() }
)
if ($Report.startupRouteEvidenceLines.Count -eq 0)
{
throw (
"Packaged runtime did not accept first-party startup route '$StartupRouteId' " +
'through the launch controller.'
)
}
}
$ExpectedMapLeaf = ($MapUrl -split '/')[-1]
$Report.mapLoadLogLines = @(
$CurrentEvidenceLines |
Where-Object {
$_ -like "*LogLoad: LoadMap: $MapUrl*" `
-or $_ -like "*Bringing World $MapUrl*up for play*" `
-or $_ -like "*Runtime map ready:*$ExpectedMapLeaf*"
} |
ForEach-Object { $_.ToString() }
)
if ($Report.mapLoadLogLines.Count -eq 0)
{
throw "Packaged launch stayed alive but did not load target map '$MapUrl' before the smoke interval ended."
}
$Report.classicPresentationLogLines = @(
$CurrentEvidenceLines |
Where-Object {
$_ -like '*Classic cube presentation initialized with 26 renderable pieces*'
} |
ForEach-Object { $_.ToString() }
)
if ($RequiresClassicPresentation -and $Report.classicPresentationLogLines.Count -eq 0)
{
throw (
"Packaged map '$MapUrl' loaded, but the runtime did not confirm its complete 26-cubie " +
'Classic cube presentation.'
)
}
$Report.classicScrambleSettlementLogLines = @(
$CurrentEvidenceLines |
Where-Object {
$_ -match 'Classic cube scramble settled with \d+ completed quarter turns, 0 queued rotations, and 26 renderable pieces; renderable state valid\.'
} |
ForEach-Object { $_.ToString() }
)
if ($RequiresClassicPresentation -and $Report.classicScrambleSettlementLogLines.Count -eq 0)
{
throw (
"Packaged map '$MapUrl' loaded its Classic presentation, but the initial scramble " +
'did not settle into a coherent 26-cubie state before the smoke interval ended.'
)
}
if (-not [string]::IsNullOrWhiteSpace($HigherDimensionalFamily))
{
$ExpectedPresentationMarker = (
'Higher-dimensional presentation initialized with {0} renderable elements ' +
'(canonical={0}) for family {1}.'
) -f $ExpectedHigherDimensionalElementCount, $HigherDimensionalFamily
$Report.higherDimensionalPresentationLogLines = @(
$CurrentEvidenceLines |
Where-Object { $_ -like "*$ExpectedPresentationMarker*" } |
ForEach-Object { $_.ToString() }
)
if ($Report.higherDimensionalPresentationLogLines.Count -eq 0)
{
throw (
"Packaged map '$MapUrl' loaded, but the runtime did not confirm its " +
"$ExpectedHigherDimensionalElementCount-element $HigherDimensionalFamily presentation."
)
}
$Report.higherDimensionalPaletteLogLines = @(
$CurrentEvidenceLines |
Where-Object {
$_ -like '*Higher-dimensional projection palette initialized with 6 distinct layer materials.*'
} |
ForEach-Object { $_.ToString() }
)
if ($Report.higherDimensionalPaletteLogLines.Count -eq 0)
{
throw (
"Packaged map '$MapUrl' loaded its projection, but the runtime did not confirm " +
'six distinct first-party layer materials.'
)
}
}
if ($Report.processIds.Count -eq 0)
{
if ($Process.HasExited)
{
$Report.exitCode = $Process.ExitCode
}
throw "Packaged classic-cube executable exited before the smoke interval completed."
}
$Report.result = 'passed'
@ -167,18 +527,18 @@ try
catch
{
$Report.error = $_.Exception.Message
if ($null -ne $Process)
if ($null -ne $Process -and $Process.HasExited)
{
$Process.Refresh()
if ($Process.HasExited)
{
$Report.exitCode = $Process.ExitCode
}
elseif (-not $KeepRunning)
{
Stop-Process -Id $Process.Id -Force
$Report.processStopped = $true
}
$Report.exitCode = $Process.ExitCode
}
if (-not $KeepRunning)
{
$Report.stoppedProcessIds = @(
Stop-OwnedPackageProcesses `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExistingPackageProcessIds
)
$Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
}
if (-not [string]::IsNullOrWhiteSpace($ReportPath))
@ -192,9 +552,13 @@ catch
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.'
$Report.stoppedProcessIds = @(
Stop-OwnedPackageProcesses `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExistingPackageProcessIds
)
$Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
Write-Host 'Stopped the complete packaged classic-cube smoke process tree after successful launch validation.'
}
if (-not [string]::IsNullOrWhiteSpace($ReportPath))

View file

@ -1,14 +1,15 @@
param(
[string]$PackageRoot = 'C:\HyperTwist\packaged\desktop',
[string]$MapUrl = '',
[int]$SmokeSeconds = 12,
[int]$SmokeSeconds = 30,
[int]$ResX = 1600,
[int]$ResY = 900,
[string]$ReportPath = '',
[switch]$KeepRunning,
[switch]$UseNullRHI,
[switch]$NoSound,
[switch]$RequireStartupDiagnostics
[switch]$RequireStartupDiagnostics,
[string]$ExpectedStartupDiagnosticsResult = ''
)
$ErrorActionPreference = 'Stop'
@ -36,6 +37,8 @@ function Resolve-StartupDiagnosticsLogPath {
param(
[Parameter(Mandatory = $true)]
[string]$ResolvedPackageRoot,
[Parameter(Mandatory = $true)]
[DateTime]$NotBeforeUtc,
[int]$TimeoutSeconds = 8,
[int]$PollMilliseconds = 250
)
@ -59,7 +62,8 @@ function Resolve-StartupDiagnosticsLogPath {
{
foreach ($CandidatePath in $CandidatePaths)
{
if (Test-Path -LiteralPath $CandidatePath)
if ((Test-Path -LiteralPath $CandidatePath) `
-and (Get-Item -LiteralPath $CandidatePath).LastWriteTimeUtc -ge $NotBeforeUtc.AddSeconds(-2))
{
return $CandidatePath
}
@ -73,6 +77,7 @@ function Resolve-StartupDiagnosticsLogPath {
}
$NewestDiagnosticsLog = Get-ChildItem -LiteralPath $CandidateDirectory -Filter 'HyperTwistFirstRunLaunch*.log' -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTimeUtc -ge $NotBeforeUtc.AddSeconds(-2) } |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
if ($null -ne $NewestDiagnosticsLog)
@ -93,6 +98,102 @@ function Resolve-StartupDiagnosticsLogPath {
return $null
}
function Get-OwnedPackageProcessIds {
param(
[Parameter(Mandatory = $true)]
[string]$ResolvedPackageRoot,
[int[]]$ExcludedProcessIds = @()
)
return @(
Get-OwnedPackageProcessRecords `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExcludedProcessIds |
Select-Object -ExpandProperty ProcessId
)
}
function Get-OwnedPackageProcessRecords {
param(
[Parameter(Mandatory = $true)]
[string]$ResolvedPackageRoot,
[int[]]$ExcludedProcessIds = @()
)
$RootPrefix = $ResolvedPackageRoot.TrimEnd(
[System.IO.Path]::DirectorySeparatorChar,
[System.IO.Path]::AltDirectorySeparatorChar
) + [System.IO.Path]::DirectorySeparatorChar
return @(
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object {
([string]$_.Name) -like 'UnrealHyperTwist*.exe' `
-and -not [string]::IsNullOrWhiteSpace([string]$_.ExecutablePath) `
-and ([string]$_.ExecutablePath).StartsWith(
$RootPrefix,
[System.StringComparison]::OrdinalIgnoreCase
) `
-and $ExcludedProcessIds -notcontains [int]$_.ProcessId
}
)
}
function Get-OwnedListeningTcpEndpoints {
param(
[int[]]$ProcessIds
)
if ($ProcessIds.Count -eq 0)
{
return @()
}
return @(
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $ProcessIds -contains [int]$_.OwningProcess } |
Sort-Object OwningProcess, LocalPort |
ForEach-Object {
[pscustomobject][ordered]@{
processId = [int]$_.OwningProcess
localAddress = [string]$_.LocalAddress
localPort = [int]$_.LocalPort
}
}
)
}
function Stop-OwnedPackageProcesses {
param(
[Parameter(Mandatory = $true)]
[string]$ResolvedPackageRoot,
[int[]]$ExcludedProcessIds = @()
)
$StoppedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
for ($Attempt = 0; $Attempt -lt 5; $Attempt += 1)
{
$OwnedProcessIds = @(
Get-OwnedPackageProcessIds `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExcludedProcessIds
)
if ($OwnedProcessIds.Count -eq 0)
{
break
}
foreach ($OwnedProcessId in $OwnedProcessIds)
{
Stop-Process -Id $OwnedProcessId -Force -ErrorAction SilentlyContinue
[void]$StoppedProcessIds.Add([int]$OwnedProcessId)
}
Start-Sleep -Milliseconds 200
}
return @($StoppedProcessIds)
}
function Get-FatalRuntimeLogMatches {
param(
[Parameter(Mandatory = $true)]
@ -106,6 +207,9 @@ function Get-FatalRuntimeLogMatches {
$FatalPatterns = @(
'Fatal error:',
'LowLevelFatalError',
'Assertion failed:',
'Critical error:',
'appError called:',
'DXGI_ERROR_NOT_CURRENTLY_AVAILABLE',
'CreateSwapChainResult failed',
@ -142,6 +246,9 @@ if ($null -eq $ExecutablePath)
$LogDirectory = Join-Path $PackageRoot 'validation\logs'
New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null
$RuntimeLogPath = Join-Path $LogDirectory 'desktop-runtime.log'
$RuntimeDiagnosticsPath = Join-Path $LogDirectory 'desktop-runtime.hyperdiagnostics.log'
Remove-Item -LiteralPath $RuntimeLogPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $RuntimeDiagnosticsPath -Force -ErrorAction SilentlyContinue
$ArgumentList = @(
"-ResX=$ResX",
@ -149,7 +256,8 @@ $ArgumentList = @(
'-windowed',
'-log',
'-FORCELOGFLUSH',
"-abslog=$RuntimeLogPath"
"-abslog=$RuntimeLogPath",
"-HyperTwistDiagnosticsLog=`"$RuntimeDiagnosticsPath`""
)
if ($UseNullRHI)
@ -169,16 +277,22 @@ if (-not [string]::IsNullOrWhiteSpace($MapUrl))
Write-Host "Launching packaged HyperTwist desktop experience from '$ExecutablePath'..."
$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
$ExistingPackageProcessIds = @(
Get-OwnedPackageProcessIds -ResolvedPackageRoot $ResolvedPackageRoot
)
$GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
$Process = $null
$Report = [ordered]@{
reportVersion = 'ht-desktop-package-launch-surface/v1'
reportVersion = 'ht-desktop-package-launch-surface/v2'
generatedAtUtc = $GeneratedAtUtc
packageRoot = $ResolvedPackageRoot
executablePath = $ExecutablePath
mapUrl = $MapUrl
runtimeLogPath = $RuntimeLogPath
runtimeLogExists = $false
runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
runtimeDiagnosticsExists = $false
runtimeDiagnosticsInitialized = $false
detectedFatalLogLines = @()
smokeSeconds = $SmokeSeconds
resolution = [ordered]@{
@ -189,52 +303,152 @@ $Report = [ordered]@{
useNullRhi = [bool]$UseNullRHI
noSound = [bool]$NoSound
requireStartupDiagnostics = [bool]$RequireStartupDiagnostics
expectedStartupDiagnosticsResult = $ExpectedStartupDiagnosticsResult
startupDiagnosticsLogPath = $null
startupDiagnosticsLogExists = $false
startupDiagnosticsTail = @()
startupDiagnosticsResult = $null
result = 'failed'
processId = $null
processIds = @()
processCommandLines = @()
ownedListeningTcpEndpoints = @()
traceControlListenerLines = @()
traceControlListeningEndpoints = @()
processStopped = $false
stoppedProcessIds = @()
exitCode = $null
error = $null
}
try
{
$LaunchStartedAtUtc = [DateTime]::UtcNow
$Process = Start-Process -FilePath $ExecutablePath -ArgumentList $ArgumentList -PassThru
Start-Sleep -Seconds $SmokeSeconds
$Process.Refresh()
$Report.processId = $Process.Id
$OwnedProcessRecords = @(
Get-OwnedPackageProcessRecords `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExistingPackageProcessIds
)
$Report.processIds = @($OwnedProcessRecords | Select-Object -ExpandProperty ProcessId)
$Report.processCommandLines = @(
$OwnedProcessRecords |
ForEach-Object { [string]$_.CommandLine } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
$Report.ownedListeningTcpEndpoints = @(
Get-OwnedListeningTcpEndpoints -ProcessIds $Report.processIds
)
$Report.traceControlListeningEndpoints = @(
$Report.ownedListeningTcpEndpoints |
Where-Object { [int]$_.localPort -eq 1985 }
)
Start-Sleep -Milliseconds 500
$Report.runtimeLogExists = Test-Path -LiteralPath $RuntimeLogPath
$Report.detectedFatalLogLines = @(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeLogPath)
$StartupDiagnosticsLogPath = Resolve-StartupDiagnosticsLogPath -ResolvedPackageRoot $ResolvedPackageRoot
$Report.runtimeDiagnosticsExists = Test-Path -LiteralPath $RuntimeDiagnosticsPath
$Report.detectedFatalLogLines = @(
@(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeLogPath) +
@(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeDiagnosticsPath)
)
$CurrentRuntimeLogLines = @(
if ($Report.runtimeLogExists)
{
Get-Content -LiteralPath $RuntimeLogPath
}
)
$CurrentRuntimeDiagnosticsLines = @(
if ($Report.runtimeDiagnosticsExists)
{
Get-Content -LiteralPath $RuntimeDiagnosticsPath
}
)
$CurrentRuntimeEvidenceLines = @(
$CurrentRuntimeLogLines + $CurrentRuntimeDiagnosticsLines
)
$Report.runtimeDiagnosticsInitialized = @(
$CurrentRuntimeDiagnosticsLines |
Where-Object {
$_.ToString().IndexOf(
'[Process] HyperTwist runtime module initialized.',
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
).Count -gt 0
$Report.traceControlListenerLines = @(
$CurrentRuntimeEvidenceLines |
Where-Object {
$_.ToString().IndexOf(
'Control listening on port',
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
} |
ForEach-Object { $_.ToString() }
)
$StartupDiagnosticsLogPath = Resolve-StartupDiagnosticsLogPath `
-ResolvedPackageRoot $ResolvedPackageRoot `
-NotBeforeUtc $LaunchStartedAtUtc
$Report.startupDiagnosticsLogPath = $StartupDiagnosticsLogPath
$Report.startupDiagnosticsLogExists = $null -ne $StartupDiagnosticsLogPath
if ($Report.startupDiagnosticsLogExists)
{
$Report.startupDiagnosticsTail = @(Get-Content -LiteralPath $StartupDiagnosticsLogPath -Tail 40)
$Report.startupDiagnosticsTail = @(
Get-Content -LiteralPath $StartupDiagnosticsLogPath -Tail 40 |
ForEach-Object { $_.ToString() }
)
$Report.startupDiagnosticsResult = [string](
$Report.startupDiagnosticsTail |
Where-Object { $_ -like 'result=*' } |
Select-Object -Last 1
)
}
if (-not $Report.runtimeDiagnosticsExists)
{
throw 'Packaged desktop launch did not write its direct HyperTwist diagnostics log.'
}
if (-not $Report.runtimeDiagnosticsInitialized)
{
throw 'Packaged desktop launch did not record direct HyperTwist runtime initialization.'
}
if ($Report.detectedFatalLogLines.Count -gt 0)
{
throw (
"Packaged desktop runtime log recorded fatal startup evidence: " +
"Packaged desktop runtime diagnostics recorded fatal startup evidence: " +
($Report.detectedFatalLogLines -join ' | ')
)
}
if ($Report.traceControlListenerLines.Count -gt 0 `
-or $Report.traceControlListeningEndpoints.Count -gt 0)
{
throw 'Packaged desktop runtime opened an Unreal trace-control listener.'
}
if ($RequireStartupDiagnostics -and -not $Report.startupDiagnosticsLogExists)
{
throw "Packaged desktop launch did not write HyperTwistFirstRunLaunch-latest.log."
}
if ($Process.HasExited)
if (-not [string]::IsNullOrWhiteSpace($ExpectedStartupDiagnosticsResult) `
-and $Report.startupDiagnosticsResult -ne "result=$ExpectedStartupDiagnosticsResult")
{
$Report.exitCode = $Process.ExitCode
throw "Packaged desktop executable exited early with code $($Process.ExitCode)."
throw (
"Expected startup diagnostics result '$ExpectedStartupDiagnosticsResult' but observed " +
"'$($Report.startupDiagnosticsResult)'."
)
}
if ($Report.processIds.Count -eq 0)
{
if ($Process.HasExited)
{
$Report.exitCode = $Process.ExitCode
}
throw 'Packaged desktop executable exited before the smoke interval completed.'
}
$Report.result = 'passed'
@ -242,18 +456,18 @@ try
catch
{
$Report.error = $_.Exception.Message
if ($null -ne $Process)
if ($null -ne $Process -and $Process.HasExited)
{
$Process.Refresh()
if ($Process.HasExited)
{
$Report.exitCode = $Process.ExitCode
}
elseif (-not $KeepRunning)
{
Stop-Process -Id $Process.Id -Force
$Report.processStopped = $true
}
$Report.exitCode = $Process.ExitCode
}
if (-not $KeepRunning)
{
$Report.stoppedProcessIds = @(
Stop-OwnedPackageProcesses `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExistingPackageProcessIds
)
$Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
}
if (-not [string]::IsNullOrWhiteSpace($ReportPath))
@ -267,9 +481,13 @@ catch
Write-Host "Packaged desktop smoke launch succeeded (PID $($Process.Id))."
if (-not $KeepRunning)
{
Stop-Process -Id $Process.Id -Force
$Report.processStopped = $true
Write-Host 'Stopped packaged desktop smoke process after successful launch validation.'
$Report.stoppedProcessIds = @(
Stop-OwnedPackageProcesses `
-ResolvedPackageRoot $ResolvedPackageRoot `
-ExcludedProcessIds $ExistingPackageProcessIds
)
$Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
Write-Host 'Stopped the complete packaged desktop smoke process tree after successful launch validation.'
}
if (-not [string]::IsNullOrWhiteSpace($ReportPath))

View file

@ -0,0 +1,408 @@
param(
[Parameter(Mandatory = $true)]
[string]$ExecutablePath,
[string]$ProjectToken = 'UnrealHyperTwist',
[string[]]$AdditionalArguments = @(
'-notraceserver',
'-traceautostart=0'
),
[string]$ReportPath = '',
[switch]$VerifyOnly
)
$ErrorActionPreference = 'Stop'
$RawDataResourceType = 10
$BootstrapArgumentsResourceId = 202
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
}
[System.IO.File]::WriteAllText(
$Path,
($Value | ConvertTo-Json -Depth 10),
(New-Object System.Text.UTF8Encoding($false))
)
}
if (-not ('HyperTwistBootstrapResources.NativeMethods' -as [type]))
{
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
namespace HyperTwistBootstrapResources
{
public static class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LoadLibraryEx(
string fileName,
IntPtr fileHandle,
uint flags
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FreeLibrary(IntPtr moduleHandle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr FindResource(
IntPtr moduleHandle,
IntPtr resourceName,
IntPtr resourceType
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint SizeofResource(
IntPtr moduleHandle,
IntPtr resourceInfo
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadResource(
IntPtr moduleHandle,
IntPtr resourceInfo
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LockResource(IntPtr resourceData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr BeginUpdateResource(
string fileName,
bool deleteExistingResources
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool UpdateResource(
IntPtr updateHandle,
IntPtr resourceType,
IntPtr resourceName,
ushort language,
byte[] data,
uint dataLength
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool EndUpdateResource(
IntPtr updateHandle,
bool discard
);
}
}
'@
}
function New-Win32Exception {
param(
[Parameter(Mandatory = $true)]
[string]$Operation
)
$ErrorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
return New-Object `
-TypeName ComponentModel.Win32Exception `
-ArgumentList $ErrorCode, "$Operation failed with Win32 error $ErrorCode."
}
function Get-BootstrapArgumentsResource {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$LoadLibraryAsDataFile = 0x00000002
$LoadLibraryAsImageResource = 0x00000020
$ModuleHandle = [HyperTwistBootstrapResources.NativeMethods]::LoadLibraryEx(
$Path,
[IntPtr]::Zero,
($LoadLibraryAsDataFile -bor $LoadLibraryAsImageResource)
)
if ($ModuleHandle -eq [IntPtr]::Zero)
{
throw (New-Win32Exception -Operation "LoadLibraryEx('$Path')")
}
try
{
$ResourceInfo = [HyperTwistBootstrapResources.NativeMethods]::FindResource(
$ModuleHandle,
[IntPtr]$BootstrapArgumentsResourceId,
[IntPtr]$RawDataResourceType
)
if ($ResourceInfo -eq [IntPtr]::Zero)
{
throw (
New-Win32Exception -Operation (
"FindResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$ResourceSize = [HyperTwistBootstrapResources.NativeMethods]::SizeofResource(
$ModuleHandle,
$ResourceInfo
)
if ($ResourceSize -eq 0)
{
throw (
New-Win32Exception -Operation (
"SizeofResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$LoadedResource = [HyperTwistBootstrapResources.NativeMethods]::LoadResource(
$ModuleHandle,
$ResourceInfo
)
if ($LoadedResource -eq [IntPtr]::Zero)
{
throw (
New-Win32Exception -Operation (
"LoadResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$ResourcePointer = [HyperTwistBootstrapResources.NativeMethods]::LockResource(
$LoadedResource
)
if ($ResourcePointer -eq [IntPtr]::Zero)
{
throw (
New-Win32Exception -Operation (
"LockResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$ResourceBytes = New-Object byte[] $ResourceSize
[Runtime.InteropServices.Marshal]::Copy(
$ResourcePointer,
$ResourceBytes,
0,
[int]$ResourceSize
)
return [Text.Encoding]::Unicode.GetString($ResourceBytes).TrimEnd([char]0)
}
finally
{
[void][HyperTwistBootstrapResources.NativeMethods]::FreeLibrary($ModuleHandle)
}
}
function Set-BootstrapArgumentsResource {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Value
)
$ResourceBytes = [Text.Encoding]::Unicode.GetBytes($Value + [char]0)
$UpdateHandle = [HyperTwistBootstrapResources.NativeMethods]::BeginUpdateResource(
$Path,
$false
)
if ($UpdateHandle -eq [IntPtr]::Zero)
{
throw (New-Win32Exception -Operation "BeginUpdateResource('$Path')")
}
$UpdateClosed = $false
try
{
if (-not [HyperTwistBootstrapResources.NativeMethods]::UpdateResource(
$UpdateHandle,
[IntPtr]$RawDataResourceType,
[IntPtr]$BootstrapArgumentsResourceId,
[uint16]0,
$ResourceBytes,
[uint32]$ResourceBytes.Length
))
{
throw (
New-Win32Exception -Operation (
"UpdateResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$UpdateCompleted = [HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource(
$UpdateHandle,
$false
)
$UpdateClosed = $true
if (-not $UpdateCompleted)
{
throw (New-Win32Exception -Operation "EndUpdateResource('$Path')")
}
}
finally
{
if (-not $UpdateClosed)
{
[void][HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource(
$UpdateHandle,
$true
)
}
}
}
if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf))
{
throw "HyperTwist bootstrap executable '$ExecutablePath' was not found."
}
if ([string]::IsNullOrWhiteSpace($ProjectToken))
{
throw 'ProjectToken must not be empty.'
}
$NormalizedAdditionalArguments = @(
$AdditionalArguments |
ForEach-Object {
if ($null -eq $_)
{
return
}
$_.Trim()
} |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
foreach ($Argument in $NormalizedAdditionalArguments)
{
if ($Argument.IndexOf([char]0) -ge 0 `
-or $Argument.IndexOf("`r") -ge 0 `
-or $Argument.IndexOf("`n") -ge 0)
{
throw "Bootstrap launch argument '$Argument' contains an unsupported control character."
}
}
$ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path
$ExpectedArguments = @($ProjectToken) + $NormalizedAdditionalArguments
$ExpectedResourceValue = $ExpectedArguments -join ' '
if ([string]::IsNullOrWhiteSpace($ReportPath))
{
$ReportPath = "$ResolvedExecutablePath.bootstrap-launch-arguments.json"
}
$OriginalHash = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant()
$OriginalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath
$Result = [ordered]@{
reportVersion = 'ht-bootstrap-launch-arguments/v1'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
executablePath = $ResolvedExecutablePath
resourceType = $RawDataResourceType
resourceId = $BootstrapArgumentsResourceId
verifyOnly = [bool]$VerifyOnly
expectedArguments = @($ExpectedArguments)
expectedResourceValue = $ExpectedResourceValue
originalResourceValue = $OriginalResourceValue
finalResourceValue = $null
originalSha256 = $OriginalHash
finalSha256 = $null
changed = $false
result = 'failed'
error = $null
}
$TemporaryPath = $null
$BackupPath = $null
try
{
if ($VerifyOnly)
{
if ($OriginalResourceValue -ne $ExpectedResourceValue)
{
throw (
"Bootstrap argument resource was '$OriginalResourceValue'; " +
"expected '$ExpectedResourceValue'."
)
}
}
elseif ($OriginalResourceValue -ne $ExpectedResourceValue)
{
$TemporaryPath = '{0}.bootstrap-{1}-{2}.tmp' -f (
$ResolvedExecutablePath,
$PID,
[Guid]::NewGuid().ToString('N')
)
$BackupPath = '{0}.bootstrap-{1}-{2}.bak' -f (
$ResolvedExecutablePath,
$PID,
[Guid]::NewGuid().ToString('N')
)
Copy-Item -LiteralPath $ResolvedExecutablePath -Destination $TemporaryPath -Force
Set-BootstrapArgumentsResource -Path $TemporaryPath -Value $ExpectedResourceValue
$TemporaryResourceValue = Get-BootstrapArgumentsResource -Path $TemporaryPath
if ($TemporaryResourceValue -ne $ExpectedResourceValue)
{
throw (
"Patched bootstrap argument resource was '$TemporaryResourceValue'; " +
"expected '$ExpectedResourceValue'."
)
}
[System.IO.File]::Replace(
$TemporaryPath,
$ResolvedExecutablePath,
$BackupPath,
$true
)
$TemporaryPath = $null
$Result.changed = $true
}
$Result.finalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath
if ($Result.finalResourceValue -ne $ExpectedResourceValue)
{
throw (
"Final bootstrap argument resource was '$($Result.finalResourceValue)'; " +
"expected '$ExpectedResourceValue'."
)
}
$Result.finalSha256 = (
Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256
).Hash.ToLowerInvariant()
$Result.result = 'passed'
}
catch
{
$Result.error = $_.Exception.Message
throw
}
finally
{
if (-not [string]::IsNullOrWhiteSpace($TemporaryPath) `
-and (Test-Path -LiteralPath $TemporaryPath))
{
Remove-Item -LiteralPath $TemporaryPath -Force -ErrorAction SilentlyContinue
}
if (-not [string]::IsNullOrWhiteSpace($BackupPath) `
-and (Test-Path -LiteralPath $BackupPath))
{
Remove-Item -LiteralPath $BackupPath -Force -ErrorAction SilentlyContinue
}
Write-Utf8JsonFile -Path $ReportPath -Value $Result
}
$Result | ConvertTo-Json -Depth 10

View file

@ -0,0 +1,343 @@
param(
[Parameter(Mandatory = $true)]
[string]$ExecutablePath,
[Parameter(Mandatory = $true)]
[string]$OutputDirectory,
[string]$TargetMap = '',
[string[]]$ExpectedLogSubstring = @(),
[ValidateSet('Startup', 'RuntimeReady')]
[string]$CaptureTrigger = 'RuntimeReady',
[int]$TimeoutSeconds = 45,
[int]$SampleStride = 6,
[double]$MinimumNonBlackRatio = 0.02,
[double]$MinimumLuminanceDeviation = 2.5,
[double]$MinimumLuminanceRange = 20.0
)
$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
}
[System.IO.File]::WriteAllText(
$Path,
($Value | ConvertTo-Json -Depth 12),
(New-Object System.Text.UTF8Encoding($false))
)
}
function Update-HyperTwistOwnedProcessIds {
param(
[Parameter(Mandatory = $true)]
[System.Collections.Generic.HashSet[int]]$OwnedProcessIds,
[Parameter(Mandatory = $true)]
[string]$ProcessName,
[int[]]$IgnoredProcessIds = @()
)
foreach ($Process in @(Get-Process -Name $ProcessName -ErrorAction SilentlyContinue))
{
if ($IgnoredProcessIds -notcontains $Process.Id)
{
[void]$OwnedProcessIds.Add([int]$Process.Id)
}
}
}
function Measure-HyperTwistBitmap {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[int]$Stride = 6
)
Add-Type -AssemblyName System.Drawing
$Bitmap = [System.Drawing.Bitmap]::FromFile($Path)
try
{
$SafeStride = [Math]::Max($Stride, 1)
$SampleCount = 0
$NonBlackCount = 0
$LuminanceSum = 0.0
$LuminanceSquaredSum = 0.0
$MinimumLuminance = 255.0
$MaximumLuminance = 0.0
for ($Y = 0; $Y -lt $Bitmap.Height; $Y += $SafeStride)
{
for ($X = 0; $X -lt $Bitmap.Width; $X += $SafeStride)
{
$Pixel = $Bitmap.GetPixel($X, $Y)
$Luminance = (0.2126 * $Pixel.R) + (0.7152 * $Pixel.G) + (0.0722 * $Pixel.B)
$SampleCount += 1
$LuminanceSum += $Luminance
$LuminanceSquaredSum += ($Luminance * $Luminance)
$MinimumLuminance = [Math]::Min($MinimumLuminance, $Luminance)
$MaximumLuminance = [Math]::Max($MaximumLuminance, $Luminance)
if ($Luminance -gt 10.0)
{
$NonBlackCount += 1
}
}
}
if ($SampleCount -le 0)
{
throw 'The off-screen frame contained no image samples.'
}
$AverageLuminance = $LuminanceSum / $SampleCount
$Variance = [Math]::Max(
($LuminanceSquaredSum / $SampleCount) - ($AverageLuminance * $AverageLuminance),
0.0
)
return [ordered]@{
width = $Bitmap.Width
height = $Bitmap.Height
sampleStride = $SafeStride
sampleCount = $SampleCount
nonBlackRatio = $NonBlackCount / $SampleCount
averageLuminance = $AverageLuminance
luminanceDeviation = [Math]::Sqrt($Variance)
minimumLuminance = $MinimumLuminance
maximumLuminance = $MaximumLuminance
luminanceRange = $MaximumLuminance - $MinimumLuminance
}
}
finally
{
$Bitmap.Dispose()
}
}
if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf))
{
throw "HyperTwist executable '$ExecutablePath' does not exist."
}
New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
$ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path
$ResolvedOutputDirectory = (Resolve-Path -LiteralPath $OutputDirectory).Path
$ExecutableDirectory = Split-Path -Parent $ResolvedExecutablePath
$ExecutableProcessName = [System.IO.Path]::GetFileNameWithoutExtension($ResolvedExecutablePath)
$SavedDirectory = Join-Path $ExecutableDirectory 'UnrealHyperTwist\Saved'
$ScreenshotDirectory = if ($CaptureTrigger -eq 'RuntimeReady')
{
Join-Path $SavedDirectory 'Screenshots\HyperTwistDiagnostics'
}
else
{
Join-Path $SavedDirectory 'Screenshots\Windows'
}
$RuntimeLogPath = Join-Path $SavedDirectory 'Logs\UnrealHyperTwist.log'
$CapturedFramePath = Join-Path $ResolvedOutputDirectory 'HyperTwist-offscreen-frame.png'
$CapturedLogPath = Join-Path $ResolvedOutputDirectory 'UnrealHyperTwist.log'
$ReportPath = Join-Path $ResolvedOutputDirectory 'HyperTwist-offscreen-visual-report.json'
$ExistingProcessIds = @(
Get-Process -Name $ExecutableProcessName -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Id
)
$ExistingScreenshotSignatures = @{}
foreach ($ExistingScreenshot in @(
Get-ChildItem -LiteralPath $ScreenshotDirectory -File -ErrorAction SilentlyContinue
))
{
$ExistingScreenshotSignatures[$ExistingScreenshot.FullName.ToLowerInvariant()] =
'{0}:{1}' -f $ExistingScreenshot.LastWriteTimeUtc.Ticks, $ExistingScreenshot.Length
}
$OwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
$LaunchArguments = @()
if (-not [string]::IsNullOrWhiteSpace($TargetMap))
{
$LaunchArguments += $TargetMap
}
$LaunchArguments += @(
'-RenderOffScreen',
'-windowed',
'-ResX=1280',
'-ResY=720'
)
if ($CaptureTrigger -eq 'RuntimeReady')
{
$LaunchArguments += '-HyperTwistCaptureWhenReady'
}
else
{
$LaunchArguments += '-ExecCmds="SHOT SHOWUI"'
}
$Result = [ordered]@{
reportVersion = 'ht-packaged-offscreen-visual/v1'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
executablePath = $ResolvedExecutablePath
executableSha256 = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant()
targetMap = $TargetMap
captureTrigger = $CaptureTrigger
launchArguments = @($LaunchArguments)
expectedLogSubstrings = @($ExpectedLogSubstring)
matchedLogSubstrings = @()
screenshotPath = $CapturedFramePath
screenshotSha256 = $null
runtimeLogPath = $CapturedLogPath
metrics = $null
thresholds = [ordered]@{
minimumNonBlackRatio = $MinimumNonBlackRatio
minimumLuminanceDeviation = $MinimumLuminanceDeviation
minimumLuminanceRange = $MinimumLuminanceRange
}
processIds = @()
terminatedProcessIds = @()
result = 'failed'
error = $null
}
$LaunchedProcess = $null
try
{
$LaunchStartedAtUtc = [DateTime]::UtcNow
$LaunchedProcess = Start-Process `
-FilePath $ResolvedExecutablePath `
-ArgumentList $LaunchArguments `
-WorkingDirectory $ExecutableDirectory `
-PassThru
[void]$OwnedProcessIds.Add($LaunchedProcess.Id)
$Deadline = $LaunchStartedAtUtc.AddSeconds([Math]::Max($TimeoutSeconds, 1))
$FreshScreenshot = $null
$RuntimeLog = ''
while ([DateTime]::UtcNow -lt $Deadline)
{
Update-HyperTwistOwnedProcessIds `
-OwnedProcessIds $OwnedProcessIds `
-ProcessName $ExecutableProcessName `
-IgnoredProcessIds $ExistingProcessIds
$FreshScreenshot = Get-ChildItem -LiteralPath $ScreenshotDirectory -File -ErrorAction SilentlyContinue |
Where-Object {
$ScreenshotKey = $_.FullName.ToLowerInvariant()
$CurrentSignature = '{0}:{1}' -f $_.LastWriteTimeUtc.Ticks, $_.Length
-not $ExistingScreenshotSignatures.ContainsKey($ScreenshotKey) `
-or $ExistingScreenshotSignatures[$ScreenshotKey] -ne $CurrentSignature
} |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
if (Test-Path -LiteralPath $RuntimeLogPath)
{
$RuntimeLogItem = Get-Item -LiteralPath $RuntimeLogPath
if ($RuntimeLogItem.LastWriteTimeUtc -ge $LaunchStartedAtUtc.AddSeconds(-1))
{
$LoadedRuntimeLog = Get-Content -LiteralPath $RuntimeLogPath -Raw
$RuntimeLog = if ($null -eq $LoadedRuntimeLog)
{
''
}
else
{
[string]$LoadedRuntimeLog
}
}
}
$MatchedLogSubstrings = @(
$ExpectedLogSubstring |
Where-Object {
$RuntimeLog.IndexOf($_, [System.StringComparison]::Ordinal) -ge 0
}
)
$Result.matchedLogSubstrings = @($MatchedLogSubstrings)
if ($null -ne $FreshScreenshot `
-and $MatchedLogSubstrings.Count -eq $ExpectedLogSubstring.Count)
{
break
}
$LiveOwnedProcesses = @(
$OwnedProcessIds |
ForEach-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue } |
Where-Object { $null -ne $_ }
)
if ($LiveOwnedProcesses.Count -eq 0)
{
throw 'HyperTwist exited before off-screen visual validation completed.'
}
Start-Sleep -Milliseconds 500
}
if ($null -eq $FreshScreenshot)
{
throw "HyperTwist did not write a fresh UI-inclusive screenshot within $TimeoutSeconds seconds."
}
if ($Result.matchedLogSubstrings.Count -ne $ExpectedLogSubstring.Count)
{
$MissingLogSubstrings = @(
$ExpectedLogSubstring |
Where-Object { $Result.matchedLogSubstrings -notcontains $_ }
)
throw "The current runtime log omitted required markers: $($MissingLogSubstrings -join '; ')"
}
if ($RuntimeLog -match '(?im)Fatal error:|Unhandled Exception|Assertion failed|CreateSwapChainResult failed')
{
throw 'The current runtime log contains a fatal startup signature.'
}
Copy-Item -LiteralPath $FreshScreenshot.FullName -Destination $CapturedFramePath -Force
Copy-Item -LiteralPath $RuntimeLogPath -Destination $CapturedLogPath -Force
$Result.screenshotSha256 = (
Get-FileHash -LiteralPath $CapturedFramePath -Algorithm SHA256
).Hash.ToLowerInvariant()
$Result.metrics = Measure-HyperTwistBitmap -Path $CapturedFramePath -Stride $SampleStride
if ($Result.metrics.nonBlackRatio -lt $MinimumNonBlackRatio `
-or $Result.metrics.luminanceDeviation -lt $MinimumLuminanceDeviation `
-or $Result.metrics.luminanceRange -lt $MinimumLuminanceRange)
{
throw 'The UI-inclusive off-screen frame did not satisfy the configured visual thresholds.'
}
$Result.processIds = @($OwnedProcessIds | Sort-Object)
$Result.result = 'passed'
}
catch
{
$Result.error = $_.Exception.Message
}
finally
{
Update-HyperTwistOwnedProcessIds `
-OwnedProcessIds $OwnedProcessIds `
-ProcessName $ExecutableProcessName `
-IgnoredProcessIds $ExistingProcessIds
foreach ($ProcessId in @($OwnedProcessIds | Sort-Object -Descending))
{
$Process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
if ($null -ne $Process)
{
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
$Result.terminatedProcessIds += $ProcessId
}
}
Write-Utf8JsonFile -Path $ReportPath -Value $Result
}
if ($Result.result -ne 'passed')
{
throw $Result.error
}
Write-Output "HyperTwist off-screen visual validation passed: $ReportPath"

View file

@ -0,0 +1,939 @@
param(
[Parameter(Mandatory = $true)]
[string]$ExecutablePath,
[Parameter(Mandatory = $true)]
[string]$OutputDirectory,
[string[]]$LaunchArguments = @(
'-windowed',
'-ResX=1280',
'-ResY=720'
),
[int]$StartupTimeoutSeconds = 45,
[int]$LayoutSettleSeconds = 5,
[int]$SampleStride = 6,
[double]$MinimumNonBlackRatio = 0.02,
[double]$MinimumBrightRatio = 0.001,
[double]$MinimumLuminanceDeviation = 2.5,
[double]$MinimumLuminanceRange = 20.0,
[double]$ClickNormalizedX = -1.0,
[double]$ClickNormalizedY = -1.0,
[ValidateSet('PhysicalMouse', 'WindowMessage')]
[string]$ClickInjectionMode = 'PhysicalMouse',
[int]$PostClickSettleSeconds = 5,
[string]$ExpectedWindowClassName = 'UnrealWindow',
[string]$ExpectedStartupDiagnosticsResult = '',
[string]$ExpectedSemanticReadyCaptureName = '',
[int]$SemanticReadyCaptureTimeoutSeconds = 30,
[switch]$KeepProcess
)
$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
}
[System.IO.File]::WriteAllText(
$Path,
($Value | ConvertTo-Json -Depth 12),
(New-Object System.Text.UTF8Encoding($false))
)
}
function Write-HyperTwistVisualCheckpoint {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Stage
)
$Line = "{0}`t{1}{2}" -f `
[DateTime]::UtcNow.ToString('o'), `
$Stage, `
[Environment]::NewLine
[System.IO.File]::AppendAllText(
$Path,
$Line,
(New-Object System.Text.UTF8Encoding($false))
)
}
function Get-HyperTwistClientBounds {
param(
[Parameter(Mandatory = $true)]
[IntPtr]$WindowHandle
)
$ClientRect = New-Object HyperTwistVisualGate.NativeMethods+RECT
if (-not [HyperTwistVisualGate.NativeMethods]::GetClientRect($WindowHandle, [ref]$ClientRect))
{
throw 'GetClientRect failed for the HyperTwist window.'
}
$ClientOrigin = New-Object HyperTwistVisualGate.NativeMethods+POINT
$ClientOrigin.X = 0
$ClientOrigin.Y = 0
if (-not [HyperTwistVisualGate.NativeMethods]::ClientToScreen($WindowHandle, [ref]$ClientOrigin))
{
throw 'ClientToScreen failed for the HyperTwist window.'
}
return [ordered]@{
x = $ClientOrigin.X
y = $ClientOrigin.Y
width = $ClientRect.Right - $ClientRect.Left
height = $ClientRect.Bottom - $ClientRect.Top
}
}
function Set-HyperTwistWindowForeground {
param(
[Parameter(Mandatory = $true)]
[IntPtr]$WindowHandle,
[switch]$RequireForeground
)
for ($Attempt = 1; $Attempt -le 3; $Attempt += 1)
{
[void][HyperTwistVisualGate.NativeMethods]::ShowWindowAsync($WindowHandle, 9)
# Pulse topmost status to expose an obscured game window, then restore
# normal z-order before capture.
[void][HyperTwistVisualGate.NativeMethods]::SetWindowPos(
$WindowHandle,
[IntPtr](-1),
0,
0,
0,
0,
0x0043
)
[void][HyperTwistVisualGate.NativeMethods]::SetWindowPos(
$WindowHandle,
[IntPtr](-2),
0,
0,
0,
0,
0x0043
)
[void][HyperTwistVisualGate.NativeMethods]::BringWindowToTop($WindowHandle)
[void][HyperTwistVisualGate.NativeMethods]::SetForegroundWindow($WindowHandle)
Start-Sleep -Milliseconds 300
if ([HyperTwistVisualGate.NativeMethods]::GetForegroundWindow() -eq $WindowHandle)
{
return "topmost-attempt-$Attempt"
}
Start-Sleep -Milliseconds 300
}
if ($RequireForeground)
{
throw 'Could not focus the HyperTwist window before interactive visual capture.'
}
return 'topmost-foreground-unconfirmed'
}
function Measure-HyperTwistBitmap {
param(
[Parameter(Mandatory = $true)]
[System.Drawing.Bitmap]$Bitmap,
[int]$Stride = 6
)
$SafeStride = [Math]::Max($Stride, 1)
$SampleCount = 0
$NonBlackCount = 0
$BrightCount = 0
$LuminanceSum = 0.0
$LuminanceSquaredSum = 0.0
$MinimumLuminance = 255.0
$MaximumLuminance = 0.0
$ColorBuckets = New-Object 'System.Collections.Generic.HashSet[string]'
for ($Y = 0; $Y -lt $Bitmap.Height; $Y += $SafeStride)
{
for ($X = 0; $X -lt $Bitmap.Width; $X += $SafeStride)
{
$Pixel = $Bitmap.GetPixel($X, $Y)
$Luminance = (0.2126 * $Pixel.R) + (0.7152 * $Pixel.G) + (0.0722 * $Pixel.B)
$SampleCount += 1
$LuminanceSum += $Luminance
$LuminanceSquaredSum += ($Luminance * $Luminance)
$MinimumLuminance = [Math]::Min($MinimumLuminance, $Luminance)
$MaximumLuminance = [Math]::Max($MaximumLuminance, $Luminance)
if ($Luminance -gt 10.0)
{
$NonBlackCount += 1
}
if ($Luminance -gt 35.0)
{
$BrightCount += 1
}
$Bucket = '{0}-{1}-{2}' -f `
[Math]::Floor($Pixel.R / 16), `
[Math]::Floor($Pixel.G / 16), `
[Math]::Floor($Pixel.B / 16)
[void]$ColorBuckets.Add($Bucket)
}
}
if ($SampleCount -le 0)
{
throw 'The captured HyperTwist client image contained no samples.'
}
$AverageLuminance = $LuminanceSum / $SampleCount
$Variance = [Math]::Max(
($LuminanceSquaredSum / $SampleCount) - ($AverageLuminance * $AverageLuminance),
0.0
)
return [ordered]@{
sampleStride = $SafeStride
sampleCount = $SampleCount
nonBlackRatio = $NonBlackCount / $SampleCount
brightRatio = $BrightCount / $SampleCount
averageLuminance = $AverageLuminance
luminanceDeviation = [Math]::Sqrt($Variance)
minimumLuminance = $MinimumLuminance
maximumLuminance = $MaximumLuminance
luminanceRange = $MaximumLuminance - $MinimumLuminance
quantizedColorBucketCount = $ColorBuckets.Count
}
}
function Update-HyperTwistOwnedProcessIds {
param(
[Parameter(Mandatory = $true)]
[AllowEmptyCollection()]
[System.Collections.Generic.HashSet[int]]$OwnedProcessIds,
[Parameter(Mandatory = $true)]
[string]$ResolvedExecutableRoot,
[int[]]$IgnoredProcessIds = @()
)
$RootPrefix = $ResolvedExecutableRoot.TrimEnd(
[System.IO.Path]::DirectorySeparatorChar,
[System.IO.Path]::AltDirectorySeparatorChar
) + [System.IO.Path]::DirectorySeparatorChar
$ProcessSnapshots = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
foreach ($ProcessSnapshot in $ProcessSnapshots)
{
$ExecutablePath = [string]$ProcessSnapshot.ExecutablePath
if ($IgnoredProcessIds -notcontains [int]$ProcessSnapshot.ProcessId `
-and ([string]$ProcessSnapshot.Name) -like 'UnrealHyperTwist*.exe' `
-and -not [string]::IsNullOrWhiteSpace($ExecutablePath) `
-and $ExecutablePath.StartsWith(
$RootPrefix,
[System.StringComparison]::OrdinalIgnoreCase
))
{
[void]$OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId)
}
}
# Preserve ownership across the bootstrap-to-Shipping handoff even when
# Windows temporarily withholds a child's ExecutablePath during startup.
$AddedDescendant = $true
while ($AddedDescendant)
{
$AddedDescendant = $false
foreach ($ProcessSnapshot in $ProcessSnapshots)
{
if ($IgnoredProcessIds -notcontains [int]$ProcessSnapshot.ProcessId `
-and $OwnedProcessIds.Contains([int]$ProcessSnapshot.ParentProcessId) `
-and $OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId))
{
$AddedDescendant = $true
}
}
}
}
if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf))
{
throw "HyperTwist executable '$ExecutablePath' does not exist."
}
New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
$ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path
$ResolvedOutputDirectory = (Resolve-Path -LiteralPath $OutputDirectory).Path
$ExecutableDirectory = Split-Path -Parent $ResolvedExecutablePath
$CaptureToken = [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ')
$ScreenshotPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.png"
$ReportPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.json"
$ProgressPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.progress.log"
$RuntimeDiagnosticsPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.hyperdiagnostics.log"
$EffectiveLaunchArguments = @($LaunchArguments)
if (-not @($EffectiveLaunchArguments | Where-Object {
$_ -like '-HyperTwistDiagnosticsLog=*'
}).Count)
{
$EffectiveLaunchArguments += "-HyperTwistDiagnosticsLog=`"$RuntimeDiagnosticsPath`""
}
$SemanticReadyCaptureCandidates = @()
if (-not [string]::IsNullOrWhiteSpace($ExpectedSemanticReadyCaptureName))
{
if ([System.IO.Path]::GetFileName($ExpectedSemanticReadyCaptureName) `
-ne $ExpectedSemanticReadyCaptureName `
-or [System.IO.Path]::GetExtension($ExpectedSemanticReadyCaptureName) -ne '.png')
{
throw 'ExpectedSemanticReadyCaptureName must be one PNG file name without a path.'
}
$SemanticReadyCaptureCandidates = @(
(Join-Path `
$env:LOCALAPPDATA `
"UnrealHyperTwist\Saved\Screenshots\HyperTwistDiagnostics\$ExpectedSemanticReadyCaptureName"),
(Join-Path `
$ExecutableDirectory `
"UnrealHyperTwist\Saved\Screenshots\HyperTwistDiagnostics\$ExpectedSemanticReadyCaptureName"),
(Join-Path `
$ExecutableDirectory `
"Saved\Screenshots\HyperTwistDiagnostics\$ExpectedSemanticReadyCaptureName")
) | Select-Object -Unique
}
$ExistingOwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
Update-HyperTwistOwnedProcessIds `
-OwnedProcessIds $ExistingOwnedProcessIds `
-ResolvedExecutableRoot $ExecutableDirectory
$ExistingProcessIds = @($ExistingOwnedProcessIds)
Add-Type -AssemblyName System.Drawing
if (-not ('HyperTwistVisualGate.NativeMethods' -as [type]))
{
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace HyperTwistVisualGate
{
public static class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll")]
public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int x,
int y,
int cx,
int cy,
uint flags
);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")]
public static extern void mouse_event(
uint dwFlags,
uint dx,
uint dy,
uint dwData,
UIntPtr dwExtraInfo
);
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam
);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetClassName(
IntPtr hWnd,
StringBuilder lpClassName,
int nMaxCount
);
public static string ReadWindowClassName(IntPtr hWnd)
{
var className = new StringBuilder(256);
return GetClassName(hWnd, className, className.Capacity) > 0
? className.ToString()
: String.Empty;
}
}
}
'@
}
# CopyFromScreen uses physical pixels. Match the calling thread to modern UE
# per-monitor DPI coordinates so the measured rectangle cannot drift onto the
# surrounding desktop at display scaling values above 100 percent.
$PreviousDpiAwarenessContext = [HyperTwistVisualGate.NativeMethods]::SetThreadDpiAwarenessContext(
[IntPtr](-4)
)
if ($PreviousDpiAwarenessContext -eq [IntPtr]::Zero)
{
throw 'Could not enable per-monitor-v2 DPI awareness for the visual capture thread.'
}
$Result = [ordered]@{
reportVersion = 'ht-packaged-window-visual/v2'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
launchStartedAtUtc = $null
executablePath = $ResolvedExecutablePath
executableSha256 = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant()
launchArguments = @($EffectiveLaunchArguments)
perMonitorV2DpiAware = $true
processId = $null
windowTitle = $null
windowClassName = $null
expectedWindowClassName = $ExpectedWindowClassName
clientBounds = $null
screenshotPath = $ScreenshotPath
progressPath = $ProgressPath
screenshotSha256 = $null
captureMethod = $null
expectedSemanticReadyCaptureName = $ExpectedSemanticReadyCaptureName
semanticReadyCaptureSourcePath = $null
semanticReadyCaptureSourceSha256 = $null
metrics = $null
thresholds = [ordered]@{
minimumNonBlackRatio = $MinimumNonBlackRatio
minimumBrightRatio = $MinimumBrightRatio
minimumLuminanceDeviation = $MinimumLuminanceDeviation
minimumLuminanceRange = $MinimumLuminanceRange
}
interaction = [ordered]@{
requested = $ClickNormalizedX -ge 0.0 -or $ClickNormalizedY -ge 0.0
normalizedX = $ClickNormalizedX
normalizedY = $ClickNormalizedY
screenX = $null
screenY = $null
clientX = $null
clientY = $null
injectionMethod = $null
clicked = $false
expectedStartupDiagnosticsResult = $ExpectedStartupDiagnosticsResult
}
startupDiagnosticsPath = $null
startupDiagnosticsLastWriteUtc = $null
startupDiagnosticsResult = $null
runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
runtimeDiagnosticsExists = $false
runtimeDiagnosticsLines = @()
processCommandLines = @()
ownedListeningTcpEndpoints = @()
traceControlListenerLines = @()
traceControlListeningEndpoints = @()
focusMethod = $null
result = 'failed'
error = $null
}
$LaunchedProcess = $null
$WindowProcess = $null
$OwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
try
{
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'launch-starting'
$LaunchStartedAtUtc = [DateTime]::UtcNow
$Result.launchStartedAtUtc = $LaunchStartedAtUtc.ToString('o')
$LaunchedProcess = Start-Process `
-FilePath $ResolvedExecutablePath `
-ArgumentList $EffectiveLaunchArguments `
-WorkingDirectory $ExecutableDirectory `
-PassThru
[void]$OwnedProcessIds.Add($LaunchedProcess.Id)
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'launch-process-started'
$Deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($StartupTimeoutSeconds, 1))
while ([DateTime]::UtcNow -lt $Deadline)
{
Update-HyperTwistOwnedProcessIds `
-OwnedProcessIds $OwnedProcessIds `
-ResolvedExecutableRoot $ExecutableDirectory `
-IgnoredProcessIds $ExistingProcessIds
$CandidateProcesses = @($OwnedProcessIds) |
Where-Object { $ExistingProcessIds -notcontains $_ } |
ForEach-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue } |
Where-Object { $null -ne $_ }
foreach ($Candidate in $CandidateProcesses)
{
[void]$OwnedProcessIds.Add($Candidate.Id)
$Candidate.Refresh()
if ($Candidate.MainWindowHandle -ne [IntPtr]::Zero)
{
$CandidateWindowClass = [HyperTwistVisualGate.NativeMethods]::ReadWindowClassName(
$Candidate.MainWindowHandle
)
if ($CandidateWindowClass -eq 'ConsoleWindowClass')
{
continue
}
if (-not [string]::IsNullOrWhiteSpace($ExpectedWindowClassName) `
-and $CandidateWindowClass -ne $ExpectedWindowClassName)
{
continue
}
$WindowProcess = $Candidate
break
}
}
if ($null -ne $WindowProcess)
{
break
}
if ($LaunchedProcess.HasExited)
{
throw "HyperTwist exited with code $($LaunchedProcess.ExitCode) before creating a window."
}
Start-Sleep -Milliseconds 500
}
if ($null -eq $WindowProcess)
{
throw "HyperTwist did not expose a top-level window within $StartupTimeoutSeconds seconds."
}
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'window-detected'
Start-Sleep -Seconds ([Math]::Max($LayoutSettleSeconds, 0))
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-layout-settled'
$WindowProcess.Refresh()
if ($WindowProcess.HasExited -or $WindowProcess.MainWindowHandle -eq [IntPtr]::Zero)
{
throw 'The HyperTwist window closed before visual capture.'
}
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-window-refreshed'
$Bounds = Get-HyperTwistClientBounds -WindowHandle $WindowProcess.MainWindowHandle
if ($Bounds.width -lt 320 -or $Bounds.height -lt 200)
{
throw "HyperTwist exposed an invalid client area ($($Bounds.width)x$($Bounds.height))."
}
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-bounds-measured'
$InteractionRequested = $ClickNormalizedX -ge 0.0 -or $ClickNormalizedY -ge 0.0
$Result.focusMethod = Set-HyperTwistWindowForeground `
-WindowHandle $WindowProcess.MainWindowHandle `
-RequireForeground:$InteractionRequested
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-window-focused'
if ($InteractionRequested)
{
if ($ClickNormalizedX -lt 0.0 -or $ClickNormalizedX -gt 1.0 `
-or $ClickNormalizedY -lt 0.0 -or $ClickNormalizedY -gt 1.0)
{
throw 'Provide both normalized click coordinates within the inclusive range 0.0 to 1.0.'
}
$ClickX = [int][Math]::Round(
$Bounds.x + ([Math]::Max($Bounds.width - 1, 0) * $ClickNormalizedX)
)
$ClickY = [int][Math]::Round(
$Bounds.y + ([Math]::Max($Bounds.height - 1, 0) * $ClickNormalizedY)
)
$ClientClickX = [int][Math]::Round(
[Math]::Max($Bounds.width - 1, 0) * $ClickNormalizedX
)
$ClientClickY = [int][Math]::Round(
[Math]::Max($Bounds.height - 1, 0) * $ClickNormalizedY
)
if (-not [HyperTwistVisualGate.NativeMethods]::SetCursorPos($ClickX, $ClickY))
{
throw 'Could not position the cursor for the requested HyperTwist interaction.'
}
Start-Sleep -Milliseconds 300
if ($ClickInjectionMode -eq 'PhysicalMouse')
{
[HyperTwistVisualGate.NativeMethods]::mouse_event(
0x0002,
0,
0,
0,
[UIntPtr]::Zero
)
Start-Sleep -Milliseconds 75
[HyperTwistVisualGate.NativeMethods]::mouse_event(
0x0004,
0,
0,
0,
[UIntPtr]::Zero
)
$Result.interaction.injectionMethod = 'physical-mouse-event'
}
else
{
$PackedClientPoint = [IntPtr](
(($ClientClickY -band 0xffff) -shl 16) `
-bor ($ClientClickX -band 0xffff)
)
[void][HyperTwistVisualGate.NativeMethods]::SendMessage(
$WindowProcess.MainWindowHandle,
0x0200,
[IntPtr]::Zero,
$PackedClientPoint
)
[void][HyperTwistVisualGate.NativeMethods]::SendMessage(
$WindowProcess.MainWindowHandle,
0x0201,
[IntPtr](1),
$PackedClientPoint
)
Start-Sleep -Milliseconds 75
[void][HyperTwistVisualGate.NativeMethods]::SendMessage(
$WindowProcess.MainWindowHandle,
0x0202,
[IntPtr]::Zero,
$PackedClientPoint
)
$Result.interaction.injectionMethod = 'window-client-message'
}
$Result.interaction.screenX = $ClickX
$Result.interaction.screenY = $ClickY
$Result.interaction.clientX = $ClientClickX
$Result.interaction.clientY = $ClientClickY
$Result.interaction.clicked = $true
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'interaction-injected'
Start-Sleep -Seconds ([Math]::Max($PostClickSettleSeconds, 0))
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'post-interaction-settled'
$WindowProcess.Refresh()
if ($WindowProcess.HasExited -or $WindowProcess.MainWindowHandle -eq [IntPtr]::Zero)
{
throw 'The HyperTwist window closed after the requested interaction.'
}
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'post-interaction-window-refreshed'
$Bounds = Get-HyperTwistClientBounds -WindowHandle $WindowProcess.MainWindowHandle
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'post-interaction-bounds-measured'
}
# The initial focus is retained through physical interaction. Refocusing
# after OpenLevel is both redundant and unsafe because Windows can block an
# activation request while Unreal is replacing its viewport.
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'capture-window-ready'
$SemanticReadyCaptureSourcePath = $null
if ($SemanticReadyCaptureCandidates.Count -gt 0)
{
Write-HyperTwistVisualCheckpoint `
-Path $ProgressPath `
-Stage 'semantic-ready-capture-waiting'
$SemanticReadyCaptureDeadline = [DateTime]::UtcNow.AddSeconds(
[Math]::Max($SemanticReadyCaptureTimeoutSeconds, 1)
)
while ([DateTime]::UtcNow -lt $SemanticReadyCaptureDeadline)
{
$SemanticReadyCaptureSourcePath = $SemanticReadyCaptureCandidates |
Where-Object {
(Test-Path -LiteralPath $_ -PathType Leaf) `
-and (Get-Item -LiteralPath $_).Length -gt 0 `
-and (Get-Item -LiteralPath $_).LastWriteTimeUtc `
-ge $LaunchStartedAtUtc.AddSeconds(-2)
} |
Select-Object -First 1
if (-not [string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
{
break
}
Start-Sleep -Milliseconds 250
}
if ([string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
{
throw (
"HyperTwist did not write fresh semantic-ready frame " +
"'$ExpectedSemanticReadyCaptureName' within " +
"$SemanticReadyCaptureTimeoutSeconds seconds."
)
}
Write-HyperTwistVisualCheckpoint `
-Path $ProgressPath `
-Stage 'semantic-ready-capture-detected'
}
if (-not [string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
{
$SemanticReadyImage = [System.Drawing.Image]::FromFile(
$SemanticReadyCaptureSourcePath
)
try
{
$Bitmap = New-Object System.Drawing.Bitmap($SemanticReadyImage)
}
finally
{
$SemanticReadyImage.Dispose()
}
$Result.captureMethod = 'hyper-twist-semantic-ready-frame'
$Result.semanticReadyCaptureSourcePath = $SemanticReadyCaptureSourcePath
$Result.semanticReadyCaptureSourceSha256 = (
Get-FileHash `
-LiteralPath $SemanticReadyCaptureSourcePath `
-Algorithm SHA256
).Hash.ToLowerInvariant()
}
else
{
$Bitmap = New-Object System.Drawing.Bitmap(
$Bounds.width,
$Bounds.height,
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb
)
$Result.captureMethod = 'physical-client-screen-copy'
}
try
{
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'capture-bitmap-created'
if ([string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
{
$Graphics = [System.Drawing.Graphics]::FromImage($Bitmap)
try
{
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'screen-copy-starting'
$Graphics.CopyFromScreen(
$Bounds.x,
$Bounds.y,
0,
0,
(New-Object System.Drawing.Size($Bounds.width, $Bounds.height)),
[System.Drawing.CopyPixelOperation]::SourceCopy
)
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'screen-copy-complete'
}
finally
{
$Graphics.Dispose()
}
}
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'bitmap-measurement-starting'
$Metrics = Measure-HyperTwistBitmap -Bitmap $Bitmap -Stride $SampleStride
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'bitmap-measurement-complete'
$Bitmap.Save($ScreenshotPath, [System.Drawing.Imaging.ImageFormat]::Png)
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'screenshot-saved'
}
finally
{
$Bitmap.Dispose()
}
$Result.processId = $WindowProcess.Id
$Result.windowTitle = $WindowProcess.MainWindowTitle
$Result.windowClassName = [HyperTwistVisualGate.NativeMethods]::ReadWindowClassName(
$WindowProcess.MainWindowHandle
)
if (-not [string]::IsNullOrWhiteSpace($ExpectedWindowClassName) `
-and $Result.windowClassName -ne $ExpectedWindowClassName)
{
throw (
"Expected window class '$ExpectedWindowClassName' but observed " +
"'$($Result.windowClassName)'."
)
}
$Result.clientBounds = $Bounds
$Result.screenshotSha256 = (Get-FileHash -LiteralPath $ScreenshotPath -Algorithm SHA256).Hash.ToLowerInvariant()
$Result.metrics = $Metrics
$StartupDiagnosticsCandidates = @(
(Join-Path $ExecutableDirectory 'UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log'),
(Join-Path $ExecutableDirectory 'Windows\UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log'),
(Join-Path $env:LOCALAPPDATA 'UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log')
)
$StartupDiagnosticsPath = $StartupDiagnosticsCandidates |
Where-Object {
(Test-Path -LiteralPath $_ -PathType Leaf) `
-and (Get-Item -LiteralPath $_).LastWriteTimeUtc -ge $LaunchStartedAtUtc.AddSeconds(-2)
} |
Select-Object -First 1
if (-not [string]::IsNullOrWhiteSpace($StartupDiagnosticsPath))
{
$Result.startupDiagnosticsPath = $StartupDiagnosticsPath
$Result.startupDiagnosticsLastWriteUtc = (
Get-Item -LiteralPath $StartupDiagnosticsPath
).LastWriteTimeUtc.ToString('o')
# Detach provider metadata before JSON serialization; decorated FileSystem
# strings otherwise expand recursively under Windows PowerShell 5.1.
$Result.startupDiagnosticsResult = [string](
Get-Content -LiteralPath $StartupDiagnosticsPath |
ForEach-Object { $_.ToString() } |
Where-Object { $_ -like 'result=*' } |
Select-Object -Last 1
)
}
if (-not [string]::IsNullOrWhiteSpace($ExpectedStartupDiagnosticsResult) `
-and $Result.startupDiagnosticsResult -ne "result=$ExpectedStartupDiagnosticsResult")
{
throw (
"Expected startup diagnostics result '$ExpectedStartupDiagnosticsResult' but observed " +
"'$($Result.startupDiagnosticsResult)'."
)
}
$Result.runtimeDiagnosticsExists = Test-Path -LiteralPath $RuntimeDiagnosticsPath -PathType Leaf
if (-not $Result.runtimeDiagnosticsExists)
{
throw 'HyperTwist did not write its direct Shipping-compatible runtime diagnostics file.'
}
$Result.runtimeDiagnosticsLines = @(
Get-Content -LiteralPath $RuntimeDiagnosticsPath |
ForEach-Object { $_.ToString() }
)
if (-not @($Result.runtimeDiagnosticsLines | Where-Object {
$_ -like '*HyperTwist runtime module initialized.*'
}).Count)
{
throw 'HyperTwist direct diagnostics did not confirm runtime-module initialization.'
}
Update-HyperTwistOwnedProcessIds `
-OwnedProcessIds $OwnedProcessIds `
-ResolvedExecutableRoot $ExecutableDirectory `
-IgnoredProcessIds $ExistingProcessIds
$OwnedProcessIdSnapshot = [int[]]@($OwnedProcessIds)
$Result.processCommandLines = @(
foreach ($OwnedProcessId in $OwnedProcessIdSnapshot)
{
Get-CimInstance Win32_Process `
-Filter "ProcessId = $OwnedProcessId" `
-ErrorAction SilentlyContinue |
ForEach-Object { [string]$_.CommandLine }
}
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
$Result.ownedListeningTcpEndpoints = @(
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $OwnedProcessIdSnapshot -contains [int]$_.OwningProcess } |
Sort-Object OwningProcess, LocalPort |
ForEach-Object {
[pscustomobject][ordered]@{
processId = [int]$_.OwningProcess
localAddress = [string]$_.LocalAddress
localPort = [int]$_.LocalPort
}
}
)
$Result.traceControlListenerLines = @(
$Result.runtimeDiagnosticsLines |
Where-Object {
$_.IndexOf(
'Control listening on port',
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
)
$Result.traceControlListeningEndpoints = @(
$Result.ownedListeningTcpEndpoints |
Where-Object { [int]$_.localPort -eq 1985 }
)
if ($Result.traceControlListenerLines.Count -gt 0 `
-or $Result.traceControlListeningEndpoints.Count -gt 0)
{
throw 'HyperTwist opened a Development trace-control listener during the visual gate.'
}
$VisualPassed = $Metrics.nonBlackRatio -ge $MinimumNonBlackRatio `
-and $Metrics.brightRatio -ge $MinimumBrightRatio `
-and $Metrics.luminanceDeviation -ge $MinimumLuminanceDeviation `
-and $Metrics.luminanceRange -ge $MinimumLuminanceRange
if (-not $VisualPassed)
{
throw (
'HyperTwist client capture is blank or visually uniform: ' +
"nonBlack=$($Metrics.nonBlackRatio), bright=$($Metrics.brightRatio), " +
"deviation=$($Metrics.luminanceDeviation), range=$($Metrics.luminanceRange)."
)
}
$Result.result = 'visual-passed'
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'visual-gate-passed'
}
catch
{
$Result.error = $_.Exception.Message
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'visual-gate-failed'
}
finally
{
if (-not $KeepProcess)
{
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'owned-process-scan-starting'
Update-HyperTwistOwnedProcessIds `
-OwnedProcessIds $OwnedProcessIds `
-ResolvedExecutableRoot $ExecutableDirectory `
-IgnoredProcessIds $ExistingProcessIds
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'owned-process-scan-complete'
foreach ($OwnedProcessId in @($OwnedProcessIds))
{
Stop-Process -Id $OwnedProcessId -Force -ErrorAction SilentlyContinue
}
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'owned-process-cleanup-complete'
}
}
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'report-write-starting'
Write-Utf8JsonFile -Path $ReportPath -Value $Result
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'report-write-complete'
$Result | ConvertTo-Json -Depth 12
if ($Result.result -ne 'visual-passed')
{
throw $Result.error
}

View file

@ -1,4 +1,6 @@
import os
import json
import datetime
import traceback
import unreal
@ -9,6 +11,7 @@ MATERIAL_ROOT = "/Game/HyperTwistTraining/Materials"
CLASSIC_MAP_PATH = f"{MAP_ROOT}/L_HyperTwist_ClassicTraining"
FOLLOW_ALONG_MAP_PATH = f"{MAP_ROOT}/L_HyperTwist_FollowAlongTraining"
AUTHOR_TAG = "HyperTwistClassicCubeTrainingMap"
AUTHORING_MARKER_PATH = os.getenv("HYPERTWIST_CLASSIC_CUBE_AUTHORING_MARKER", "").strip()
SAFE_TRANSIENT_LEVEL_PATH = "/Engine/Maps/Entry"
MASTER_MATERIAL_PATH = f"{MATERIAL_ROOT}/M_HT_ClassicCubeFaceMaster"
INTERNAL_MATERIAL_PATH = f"{MATERIAL_ROOT}/MI_HT_ClassicCube_Internal"
@ -67,78 +70,88 @@ def create_asset(asset_path: str, asset_class, factory):
def ensure_face_master_material():
if unreal.EditorAssetLibrary.does_asset_exist(MASTER_MATERIAL_PATH):
return require_asset(MASTER_MATERIAL_PATH)
material = require_asset(MASTER_MATERIAL_PATH)
else:
material = create_asset(
MASTER_MATERIAL_PATH,
unreal.Material,
unreal.MaterialFactoryNew(),
)
base_color = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionVectorParameter,
-640,
-120,
)
base_color.set_editor_property("parameter_name", "BaseColor")
base_color.set_editor_property("default_value", unreal.LinearColor(1.0, 1.0, 1.0, 1.0))
glow_strength = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionScalarParameter,
-640,
80,
)
glow_strength.set_editor_property("parameter_name", "GlowStrength")
glow_strength.set_editor_property("default_value", 0.0)
face_opacity = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionScalarParameter,
-640,
240,
)
face_opacity.set_editor_property("parameter_name", "FaceOpacity")
face_opacity.set_editor_property("default_value", 1.0)
emissive = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionMultiply,
-280,
0,
)
unreal.MaterialEditingLibrary.connect_material_expressions(base_color, "", emissive, "A")
unreal.MaterialEditingLibrary.connect_material_expressions(glow_strength, "", emissive, "B")
unreal.MaterialEditingLibrary.connect_material_property(
base_color,
"",
unreal.MaterialProperty.MP_BASE_COLOR,
)
unreal.MaterialEditingLibrary.connect_material_property(
emissive,
"",
unreal.MaterialProperty.MP_EMISSIVE_COLOR,
)
unreal.MaterialEditingLibrary.layout_material_expressions(material)
material = create_asset(
MASTER_MATERIAL_PATH,
unreal.Material,
unreal.MaterialFactoryNew(),
)
material.set_editor_property("two_sided", False)
base_color = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionVectorParameter,
-640,
-120,
)
base_color.set_editor_property("parameter_name", "BaseColor")
base_color.set_editor_property("default_value", unreal.LinearColor(1.0, 1.0, 1.0, 1.0))
glow_strength = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionScalarParameter,
-640,
80,
)
glow_strength.set_editor_property("parameter_name", "GlowStrength")
glow_strength.set_editor_property("default_value", 0.0)
face_opacity = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionScalarParameter,
-640,
240,
)
face_opacity.set_editor_property("parameter_name", "FaceOpacity")
face_opacity.set_editor_property("default_value", 1.0)
emissive = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionMultiply,
-280,
0,
)
unreal.MaterialEditingLibrary.connect_material_expressions(base_color, "", emissive, "A")
unreal.MaterialEditingLibrary.connect_material_expressions(glow_strength, "", emissive, "B")
unreal.MaterialEditingLibrary.connect_material_property(
base_color,
"",
unreal.MaterialProperty.MP_BASE_COLOR,
)
unreal.MaterialEditingLibrary.connect_material_property(
emissive,
"",
unreal.MaterialProperty.MP_EMISSIVE_COLOR,
)
unreal.MaterialEditingLibrary.layout_material_expressions(material)
# Phase 6C uses the same palette on instanced projection geometry. Without
# this authored usage flag, cooked builds silently substitute a neutral
# fallback shader even though the six material bindings remain non-null.
material.set_editor_property("used_with_instanced_static_meshes", True)
unreal.MaterialEditingLibrary.recompile_material(material)
if not unreal.EditorAssetLibrary.save_loaded_asset(material, False):
raise RuntimeError(f"Failed to save face master material: {MASTER_MATERIAL_PATH}")
log(f"Created face master material {MASTER_MATERIAL_PATH}")
if not material.get_editor_property("used_with_instanced_static_meshes"):
raise RuntimeError(
"Face master material did not retain instanced-static-mesh usage after save."
)
log(f"Verified face master material and instanced-mesh usage: {MASTER_MATERIAL_PATH}")
return material
def ensure_material_instance(asset_path: str, parent_material, color: unreal.LinearColor):
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
return require_asset(asset_path)
instance = require_asset(asset_path)
else:
instance = create_asset(
asset_path,
unreal.MaterialInstanceConstant,
unreal.MaterialInstanceConstantFactoryNew(),
)
instance = create_asset(
asset_path,
unreal.MaterialInstanceConstant,
unreal.MaterialInstanceConstantFactoryNew(),
)
unreal.MaterialEditingLibrary.set_material_instance_parent(instance, parent_material)
unreal.MaterialEditingLibrary.set_material_instance_vector_parameter_value(
instance,
@ -157,7 +170,7 @@ def ensure_material_instance(asset_path: str, parent_material, color: unreal.Lin
)
if not unreal.EditorAssetLibrary.save_loaded_asset(instance, False):
raise RuntimeError(f"Failed to save material instance: {asset_path}")
log(f"Created material instance {asset_path}")
log(f"Verified material instance {asset_path}")
return instance
@ -194,16 +207,18 @@ def ensure_floor_plane() -> None:
floor_actor = spawn_actor(
unreal.StaticMeshActor,
"HT_ClassicCubeTraining_Floor",
unreal.Vector(0.0, 0.0, -20.0),
unreal.Vector(0.0, 0.0, -24.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
static_mesh_component = floor_actor.static_mesh_component
static_mesh_component.set_editor_property(
"static_mesh",
require_asset("/Engine/BasicShapes/Plane.Plane"),
require_asset("/Engine/BasicShapes/Cube.Cube"),
)
static_mesh_component.set_editor_property("mobility", unreal.ComponentMobility.STATIC)
floor_actor.set_actor_scale3d(unreal.Vector(14.0, 14.0, 1.0))
# A thin volumetric floor avoids the zero-extent bounds path that can crash
# UE 5.7 editor automation when a Plane is authored under a headless RHI.
floor_actor.set_actor_scale3d(unreal.Vector(14.0, 14.0, 0.08))
def ensure_directional_light() -> None:
@ -233,6 +248,15 @@ def ensure_skylight() -> None:
skylight_component.set_editor_property("intensity", 1.2)
def ensure_sky_atmosphere() -> None:
spawn_actor(
unreal.SkyAtmosphere,
"HT_ClassicCubeTraining_Atmosphere",
unreal.Vector(0.0, 0.0, 0.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
def ensure_reflection_capture() -> None:
reflection_capture = spawn_actor(
unreal.SphereReflectionCapture,
@ -276,6 +300,59 @@ def save_current_level_or_raise(level_subsystem, label: str) -> None:
log(f"Saved current level after {label}")
def validate_authored_actor_family():
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
if actor_subsystem is None:
raise RuntimeError("EditorActorSubsystem was not available for authored-map validation.")
expected_labels = {
"HT_ClassicCubeTraining_Floor",
"HT_ClassicCubeTraining_Sun",
"HT_ClassicCubeTraining_Sky",
"HT_ClassicCubeTraining_Atmosphere",
"HT_ClassicCubeTraining_Reflection",
"HT_ClassicCubeTraining_PlayerStart",
"HT_ClassicCubeTraining_Cube",
}
actor_labels = {
actor.get_actor_label()
for actor in actor_subsystem.get_all_level_actors()
if actor is not None
}
missing_labels = sorted(expected_labels - actor_labels)
if missing_labels:
raise RuntimeError(
"Authored map is missing required actors: " + ", ".join(missing_labels)
)
return sorted(expected_labels)
def write_completion_marker(target: str, map_asset_path: str, actor_labels) -> None:
if not AUTHORING_MARKER_PATH:
raise RuntimeError(
"HYPERTWIST_CLASSIC_CUBE_AUTHORING_MARKER must identify the transactional completion marker."
)
marker_path = os.path.abspath(AUTHORING_MARKER_PATH)
os.makedirs(os.path.dirname(marker_path), exist_ok=True)
marker_payload = {
"schemaVersion": 1,
"stage": "complete",
"target": target,
"mapAssetPath": map_asset_path,
"actorLabels": actor_labels,
"completedAtUtc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
}
temporary_path = marker_path + ".tmp"
with open(temporary_path, "w", encoding="utf-8", newline="\n") as marker_file:
json.dump(marker_payload, marker_file, indent=2, sort_keys=True)
marker_file.write("\n")
marker_file.flush()
os.fsync(marker_file.fileno())
os.replace(temporary_path, marker_path)
log(f"Wrote transactional completion marker {marker_path}")
def configure_world_settings(game_mode_class_path: str) -> None:
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
if editor_subsystem is None:
@ -311,7 +388,7 @@ def recreate_level(level_subsystem, map_asset_path: str) -> None:
log(f"Created new level {map_asset_path}")
def author_map(map_asset_path: str, game_mode_class_path: str) -> None:
def author_map(map_asset_path: str, game_mode_class_path: str):
level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
if level_subsystem is None:
raise RuntimeError("LevelEditorSubsystem was not available.")
@ -326,13 +403,17 @@ def author_map(map_asset_path: str, game_mode_class_path: str) -> None:
save_current_level_or_raise(level_subsystem, "directional light placement")
ensure_skylight()
save_current_level_or_raise(level_subsystem, "skylight placement")
ensure_sky_atmosphere()
save_current_level_or_raise(level_subsystem, "sky-atmosphere placement")
ensure_reflection_capture()
save_current_level_or_raise(level_subsystem, "reflection capture placement")
ensure_player_start()
save_current_level_or_raise(level_subsystem, "player start placement")
ensure_cube_actor()
save_current_level_or_raise(level_subsystem, "cube placement")
actor_labels = validate_authored_actor_family()
log(f"Finished authoring {map_asset_path}")
return actor_labels
def main() -> None:
@ -371,8 +452,14 @@ def main() -> None:
f"Unsupported HYPERTWIST_CLASSIC_CUBE_MAP_KIND value: {requested_map_kind}"
)
if len(targets) != 1:
raise RuntimeError(
"Transactional map authoring requires one target per UnrealEditor process."
)
for map_asset_path, game_mode_class_path in targets:
author_map(map_asset_path, game_mode_class_path)
actor_labels = author_map(map_asset_path, game_mode_class_path)
write_completion_marker(requested_map_kind, map_asset_path, actor_labels)
log("Classic cube authored maps are ready.")

View file

@ -12,7 +12,9 @@ SAFE_TRANSIENT_LEVEL_PATH = "/Engine/Maps/Entry"
AUTHOR_TAG = "HyperTwistHigherDimensionalTrainingShell"
LEGACY_CLASSIC_AUTHOR_TAG = "HyperTwistClassicCubeTrainingMap"
TRAINING_SHELL_CLASS_PATH = "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingShellActor"
COACH_DASHBOARD_GAME_MODE_PATH = "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode"
HIGHER_DIMENSIONAL_GAME_MODE_PATH = (
"/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode"
)
ENABLE_HEADLESS_TRAINING_SHELL_ACTOR = (
os.getenv("HYPERTWIST_ENABLE_TRAINING_SHELL_ACTOR", "").strip().lower()
in {"1", "true", "yes"}
@ -30,6 +32,12 @@ MANIFEST_RELATIVE_PATH = os.path.join(
"phase6c_dedicated_family_map_manifest.json",
)
MANIFEST_ABSOLUTE_PATH = os.path.join(PROJECT_ROOT, MANIFEST_RELATIVE_PATH)
AUTHORING_RECEIPT_DIRECTORY = os.path.join(
PROJECT_ROOT,
"UnrealHyperTwist",
"Saved",
"HyperTwistMapAuthoring",
)
MAP_CONFIGS = (
{
@ -61,10 +69,6 @@ MAP_CONFIGS = (
"projection_profile_id": "magic120cell-4d-projection-distance-v1",
"primary_persistence_boundary_id": "magic120cell-persistence-boundary",
"player_start_location": unreal.Vector(-560.0, 0.0, 190.0),
"sun_location": unreal.Vector(-340.0, 120.0, 470.0),
"sun_rotation": unreal.Rotator(-34.0, -22.0, 0.0),
"sun_intensity": 7.6,
"floor_scale": unreal.Vector(18.0, 18.0, 1.0),
"shell_location": unreal.Vector(0.0, 0.0, 0.0),
"shell_rotation": unreal.Rotator(0.0, 22.0, 0.0),
"training_shell_tags": [
@ -104,10 +108,6 @@ MAP_CONFIGS = (
"projection_profile_id": "magiccube5d-5d-projection-distance-v1",
"primary_persistence_boundary_id": "magiccube5d-persistence-boundary",
"player_start_location": unreal.Vector(-620.0, -120.0, 200.0),
"sun_location": unreal.Vector(-260.0, -200.0, 500.0),
"sun_rotation": unreal.Rotator(-38.0, 34.0, 0.0),
"sun_intensity": 8.8,
"floor_scale": unreal.Vector(20.0, 20.0, 1.0),
"shell_location": unreal.Vector(0.0, 0.0, 0.0),
"shell_rotation": unreal.Rotator(0.0, -18.0, 0.0),
"training_shell_tags": [
@ -125,13 +125,6 @@ def log(message: str) -> None:
unreal.log(f"[HyperTwistHigherDimensionalMapAuthoring] {message}")
def require_asset(asset_path: str):
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
if asset is None:
raise RuntimeError(f"Required asset was not found: {asset_path}")
return asset
def require_class(class_path: str):
loaded_class = unreal.load_class(None, class_path)
if loaded_class is None:
@ -277,62 +270,6 @@ def configure_world_settings(game_mode_class_path: str) -> None:
world_settings.set_editor_property("default_game_mode", require_class(game_mode_class_path))
def ensure_floor_plane(config) -> None:
floor_actor = spawn_actor(
unreal.StaticMeshActor,
f"HT_{config['family_key']}_Floor",
unreal.Vector(0.0, 0.0, -20.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
static_mesh_component = floor_actor.static_mesh_component
static_mesh_component.set_editor_property(
"static_mesh",
require_asset("/Engine/BasicShapes/Plane.Plane"),
)
static_mesh_component.set_editor_property("mobility", unreal.ComponentMobility.STATIC)
floor_actor.set_actor_scale3d(config["floor_scale"])
def ensure_directional_light(config) -> None:
directional_light = spawn_actor(
unreal.DirectionalLight,
f"HT_{config['family_key']}_Sun",
config["sun_location"],
config["sun_rotation"],
)
light_component = directional_light.get_component_by_class(unreal.DirectionalLightComponent)
if light_component is not None:
light_component.set_editor_property("mobility", unreal.ComponentMobility.MOVABLE)
light_component.set_editor_property("intensity", config["sun_intensity"])
def ensure_skylight(config) -> None:
skylight = spawn_actor(
unreal.SkyLight,
f"HT_{config['family_key']}_Sky",
unreal.Vector(0.0, 0.0, 260.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
skylight_component = skylight.get_component_by_class(unreal.SkyLightComponent)
if skylight_component is not None:
skylight_component.set_editor_property("mobility", unreal.ComponentMobility.MOVABLE)
skylight_component.set_editor_property("real_time_capture", True)
skylight_component.set_editor_property(
"intensity",
1.35 if config["family_key"] == "magic120cell" else 1.55,
)
def ensure_reflection_capture(config) -> None:
reflection_capture = spawn_actor(
unreal.SphereReflectionCapture,
f"HT_{config['family_key']}_Reflection",
unreal.Vector(0.0, 0.0, 150.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
reflection_capture.set_actor_scale3d(unreal.Vector(14.0, 14.0, 14.0))
def ensure_player_start(config) -> None:
player_start = spawn_actor(
unreal.PlayerStart,
@ -446,6 +383,70 @@ def compute_md5(file_path: str) -> str:
return digest.hexdigest()
def get_target_receipt_absolute_path(config) -> str:
return os.path.join(
AUTHORING_RECEIPT_DIRECTORY,
f"phase6c_{config['map_kind']}_complete.json",
)
def validate_authored_map(config):
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
if actor_subsystem is None or editor_subsystem is None:
raise RuntimeError("Editor subsystems were unavailable during authored-map validation.")
actor_labels = {
actor.get_actor_label()
for actor in actor_subsystem.get_all_level_actors()
if actor is not None
}
expected_actor_labels = {f"HT_{config['family_key']}_DedicatedSurfaceAnchor"}
missing_actor_labels = sorted(expected_actor_labels - actor_labels)
if missing_actor_labels:
raise RuntimeError(
"Authored map is missing required presentation actors: "
+ ", ".join(missing_actor_labels)
)
world = editor_subsystem.get_editor_world()
world_settings = world.get_world_settings() if world is not None else None
game_mode_class = (
world_settings.get_editor_property("default_game_mode")
if world_settings is not None
else None
)
if game_mode_class is None or game_mode_class.get_name() != "HyperTwistHigherDimensionalTrainingGameMode":
raise RuntimeError("Authored map does not own the dedicated higher-dimensional game mode.")
return sorted(expected_actor_labels)
def write_target_completion_receipt(config, actor_labels) -> None:
map_file_absolute_path = resolve_map_file_absolute_path(config)
if not os.path.exists(map_file_absolute_path):
raise RuntimeError(
f"Cannot write completion receipt because the authored map is missing: {map_file_absolute_path}"
)
os.makedirs(AUTHORING_RECEIPT_DIRECTORY, exist_ok=True)
receipt_path = get_target_receipt_absolute_path(config)
receipt = {
"schemaVersion": "hypertwist/phase6c-map-authoring-receipt/v1",
"mapKind": config["map_kind"],
"mapAssetPath": config["map_asset_path"],
"mapHashMd5": compute_md5(map_file_absolute_path),
"gameModeClassPath": HIGHER_DIMENSIONAL_GAME_MODE_PATH,
"presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components",
"validatedActorLabels": actor_labels,
"authoringComplete": True,
}
with open(receipt_path, "w", encoding="utf-8") as handle:
json.dump(receipt, handle, indent=2)
handle.write("\n")
log(f"Wrote hash-bound completion receipt to {receipt_path}")
def ensure_manifest_directory() -> None:
os.makedirs(os.path.dirname(MANIFEST_ABSOLUTE_PATH), exist_ok=True)
@ -478,7 +479,12 @@ def build_manifest_entries():
"primaryPersistenceBoundaryId": config["primary_persistence_boundary_id"],
"authorTag": AUTHOR_TAG,
"authoringManifestRelativePath": MANIFEST_RELATIVE_PATH.replace("\\", "/"),
"authoredViaGameModeClassPath": COACH_DASHBOARD_GAME_MODE_PATH,
"authoredViaGameModeClassPath": HIGHER_DIMENSIONAL_GAME_MODE_PATH,
"runtimePresentationClassPath": TRAINING_SHELL_CLASS_PATH,
"presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components",
"canonicalRenderableElementCount": (
120 if config["family_key"] == "magic120cell" else 242
),
"trainingShellTags": config["training_shell_tags"],
}
)
@ -497,10 +503,10 @@ def write_manifest() -> None:
)
manifest = {
"manifestId": "phase6c/dedicated-family-training-map-authoring",
"manifestVersion": "2026.06.18",
"manifestVersion": "2026.07.19",
"authorTag": AUTHOR_TAG,
"authoringScriptRelativePath": "scripts/hypertwist_author_higher_dimensional_training_maps.py",
"authoredThroughGameModeClassPath": COACH_DASHBOARD_GAME_MODE_PATH,
"authoredThroughGameModeClassPath": HIGHER_DIMENSIONAL_GAME_MODE_PATH,
"classicReferenceMapHashMd5": (
compute_md5(classic_map_absolute_path)
if os.path.exists(classic_map_absolute_path)
@ -522,26 +528,34 @@ def author_map(config) -> None:
log(f"Starting authored rebuild for {config['map_asset_path']}")
recreate_level(level_subsystem, config)
log("Created clean target level")
configure_world_settings(COACH_DASHBOARD_GAME_MODE_PATH)
log("Configured dedicated training-shell game mode")
save_current_level_or_raise(level_subsystem, "clean level recreation")
configure_world_settings(HIGHER_DIMENSIONAL_GAME_MODE_PATH)
log("Configured dedicated higher-dimensional runtime game mode")
save_current_level_or_raise(
level_subsystem,
f"{config['family_key']} dedicated training-shell game mode",
)
ensure_player_start(config)
save_current_level_or_raise(level_subsystem, "dedicated surface-anchor placement")
log(
"Placed dedicated surface anchor; floor, lighting, and projection presentation "
"are self-healing runtime-owned surfaces"
)
if ENABLE_HEADLESS_TRAINING_SHELL_ACTOR:
ensure_training_shell_actor(config)
save_current_level_or_raise(level_subsystem, "dedicated training-shell actor placement")
log("Placed dedicated training shell actor")
else:
log(
"Skipped dedicated training shell actor on the headless authoring lane "
"because custom actor placement still crashes UnrealEditor-Cmd here"
"Skipped dedicated training shell actor on the headless authoring lane; "
"the dedicated game mode owns deterministic runtime spawning"
)
ensure_player_start(config)
log("Placed dedicated surface anchor")
save_current_level_or_raise(
level_subsystem,
f"{config['family_key']} dedicated shell surfaces",
f"{config['family_key']} final dedicated shell surfaces",
)
validated_actor_labels = validate_authored_map(config)
write_target_completion_receipt(config, validated_actor_labels)
log(f"Finished authoring {config['map_asset_path']}")

View file

@ -75,23 +75,17 @@ ps_single_quote() {
}
write_base64_to_file() {
local base64_payload="$1"
local destination_path="$2"
local destination_path="$1"
python3 - <<'PY' "$base64_payload" "$destination_path"
python3 -c '
import base64
import pathlib
import sys
payload = sys.argv[1]
destination = pathlib.Path(sys.argv[2])
destination = pathlib.Path(sys.argv[1])
destination.parent.mkdir(parents=True, exist_ok=True)
if payload:
destination.write_bytes(base64.b64decode(payload.encode("ascii")))
else:
destination.write_bytes(b"")
PY
destination.write_bytes(base64.b64decode(sys.stdin.buffer.read()))
' "$destination_path"
}
for mapping in "${maps[@]}"; do
@ -149,7 +143,7 @@ EOF
| tr -d '\n'
)"
write_base64_to_file "$base64_payload" "$local_destination"
printf '%s' "$base64_payload" | write_base64_to_file "$local_destination"
local_hash="$(sha256sum "$local_destination" | awk '{print tolower($1)}')"
if [[ "$local_hash" != "$remote_hash" ]]; then

View file

@ -36,6 +36,11 @@ if ! command -v sha256sum >/dev/null 2>&1; then
exit 1
fi
if ! command -v scp >/dev/null 2>&1; then
echo "scp is required but was not found on PATH." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required but was not found on PATH." >&2
exit 1
@ -96,30 +101,30 @@ sys.stdout.write(base64.b64encode(script.encode("utf-16le")).decode("ascii"))
PY
}
stream_file_as_base64() {
local file_path="$1"
python3 - <<'PY' "$file_path"
import base64
import pathlib
import sys
file_path = pathlib.Path(sys.argv[1])
sys.stdout.write(base64.b64encode(file_path.read_bytes()).decode("ascii"))
PY
ps_single_quote() {
printf "'%s'" "${1//\'/\'\'}"
}
measure_base64_length() {
local file_path="$1"
ssh_base=(
sshpass -p "${HYPERTWIST_REMOTE_WINDOWS_PASSWORD}"
ssh
-o StrictHostKeyChecking=no
-o PreferredAuthentications=password
-o PubkeyAuthentication=no
-p "${remote_port}"
-l "${remote_user}"
"${remote_host}"
)
python3 - <<'PY' "$file_path"
import pathlib
import sys
size = pathlib.Path(sys.argv[1]).stat().st_size
sys.stdout.write(str(((size + 2) // 3) * 4))
PY
}
scp_base=(
sshpass -p "${HYPERTWIST_REMOTE_WINDOWS_PASSWORD}"
scp
-q
-o StrictHostKeyChecking=no
-o PreferredAuthentications=password
-o PubkeyAuthentication=no
-P "${remote_port}"
)
for relative_path in "${files[@]}"; do
if [[ "$relative_path" = /* ]]; then
@ -135,59 +140,57 @@ for relative_path in "${files[@]}"; do
windows_relative_path="${relative_path//\//\\}"
windows_dest_path="${remote_root}\\${windows_relative_path}"
local_hash="$(sha256sum "$relative_path" | awk '{print tolower($1)}')"
expected_base64_length="$(measure_base64_length "$relative_path")"
windows_incoming_path="${windows_dest_path}.hypertwist-incoming-${local_hash:0:12}"
if [[ "$dry_run" == "true" ]]; then
printf '%s -> %s (%s)\n' "$relative_path" "$windows_dest_path" "$local_hash"
continue
fi
ssh_base=(
sshpass -p "${HYPERTWIST_REMOTE_WINDOWS_PASSWORD}"
ssh
-o StrictHostKeyChecking=no
-o PreferredAuthentications=password
-o PubkeyAuthentication=no
-p "${remote_port}"
-l "${remote_user}"
"${remote_host}"
)
remote_write_script="$(
remote_prepare_script="$(
cat <<EOF
\$ProgressPreference = 'SilentlyContinue'
\$ErrorActionPreference = 'Stop'
\$Path='${windows_dest_path}'
\$ExpectedBase64Length = ${expected_base64_length}
\$builder = New-Object System.Text.StringBuilder
\$reader = [Console]::In
while (\$builder.Length -lt \$ExpectedBase64Length) {
\$remaining = \$ExpectedBase64Length - \$builder.Length
\$chunkSize = [Math]::Min(4096, \$remaining)
\$buffer = New-Object char[] \$chunkSize
\$read = \$reader.Read(\$buffer, 0, \$chunkSize)
if (\$read -le 0) {
break
}
[void]\$builder.Append(\$buffer, 0, \$read)
}
\$content = \$builder.ToString()
if (\$content.Length -ne \$ExpectedBase64Length) {
throw ('Expected ' + \$ExpectedBase64Length + ' base64 characters but received ' + \$content.Length + '.')
}
\$bytes = [Convert]::FromBase64String(\$content)
\$Path = $(ps_single_quote "$windows_dest_path")
\$IncomingPath = $(ps_single_quote "$windows_incoming_path")
\$dir = [System.IO.Path]::GetDirectoryName(\$Path)
if (\$dir) {
[System.IO.Directory]::CreateDirectory(\$dir) | Out-Null
}
[System.IO.File]::WriteAllBytes(\$Path, \$bytes)
if (Test-Path -LiteralPath \$IncomingPath) {
Remove-Item -LiteralPath \$IncomingPath -Force
}
EOF
)"
encoded_remote_prepare_script="$(encode_powershell_command "$remote_prepare_script")"
"${ssh_base[@]}" "powershell -NoProfile -EncodedCommand ${encoded_remote_prepare_script}" >/dev/null
incoming_scp_path="${windows_incoming_path//\\//}"
"${scp_base[@]}" \
"$relative_path" \
"${remote_user}@${remote_host}:${incoming_scp_path}"
remote_finalize_script="$(
cat <<EOF
\$ErrorActionPreference = 'Stop'
\$Path = $(ps_single_quote "$windows_dest_path")
\$IncomingPath = $(ps_single_quote "$windows_incoming_path")
\$ExpectedHash = '${local_hash}'
if (-not (Test-Path -LiteralPath \$IncomingPath -PathType Leaf)) {
throw "Incoming upload was not found at '\$IncomingPath'."
}
\$IncomingHash = (Get-FileHash -LiteralPath \$IncomingPath -Algorithm SHA256).Hash.ToLowerInvariant()
if (\$IncomingHash -ne \$ExpectedHash) {
Remove-Item -LiteralPath \$IncomingPath -Force
throw "Incoming hash mismatch: expected \$ExpectedHash but received \$IncomingHash."
}
Move-Item -LiteralPath \$IncomingPath -Destination \$Path -Force
Write-Output ('REMOTE_HASH=' + (Get-FileHash -LiteralPath \$Path -Algorithm SHA256).Hash.ToLowerInvariant())
EOF
)"
encoded_remote_write_script="$(encode_powershell_command "$remote_write_script")"
encoded_remote_finalize_script="$(encode_powershell_command "$remote_finalize_script")"
remote_hash="$(
stream_file_as_base64 "$relative_path" \
| "${ssh_base[@]}" "powershell -NoProfile -EncodedCommand ${encoded_remote_write_script}" \
"${ssh_base[@]}" "powershell -NoProfile -EncodedCommand ${encoded_remote_finalize_script}" \
| tr -d '\r' \
| sed -n 's/^REMOTE_HASH=//p'
)"