fix: deliver validated HyperTwist desktop alpha
This commit is contained in:
parent
a8471737c8
commit
49422acf8f
85 changed files with 9238 additions and 688 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
41
docs/generated/alpha_test_20260723/README.md
Normal file
41
docs/generated/alpha_test_20260723/README.md
Normal 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.
|
||||
BIN
docs/generated/alpha_test_20260723/classic_route.png
Normal file
BIN
docs/generated/alpha_test_20260723/classic_route.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
BIN
docs/generated/alpha_test_20260723/final_delivery_first_run.png
Normal file
BIN
docs/generated/alpha_test_20260723/final_delivery_first_run.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
85
docs/ops/HYPERTWIST_ALPHA_TEST_README_2026-07-23.txt
Normal file
85
docs/ops/HYPERTWIST_ALPHA_TEST_README_2026-07-23.txt
Normal 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.
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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:** 20–30
|
||||
**Estimated time:** 1–2 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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
408
scripts/Set-HyperTwistBootstrapLaunchArguments.ps1
Normal file
408
scripts/Set-HyperTwistBootstrapLaunchArguments.ps1
Normal 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
|
||||
343
scripts/Test-HyperTwistPackagedOffscreenVisual.ps1
Normal file
343
scripts/Test-HyperTwistPackagedOffscreenVisual.ps1
Normal 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"
|
||||
939
scripts/Test-HyperTwistPackagedWindowVisual.ps1
Normal file
939
scripts/Test-HyperTwistPackagedWindowVisual.ps1
Normal 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
|
||||
}
|
||||
|
|
@ -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.")
|
||||
|
||||
|
|
|
|||
|
|
@ -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']}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue