diff --git a/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap b/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap index 028fa9f..9564d08 100644 Binary files a/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap and b/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap differ diff --git a/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap b/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap index 028fa9f..d7e3b4f 100644 Binary files a/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap and b/UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap differ diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.cpp new file mode 100644 index 0000000..a311ab2 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.cpp @@ -0,0 +1,221 @@ +#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h" + +#include "Components/InstancedStaticMeshComponent.h" +#include "Components/SceneComponent.h" +#include "Components/TextRenderComponent.h" +#include "Engine/StaticMesh.h" +#include "UObject/ConstructorHelpers.h" + +namespace HyperTwistHigherDimensionalTrainingShellActorInternal +{ + const TCHAR* Magic120CellFamilyKey = TEXT("magic120cell"); +} + +AHyperTwistHigherDimensionalTrainingShellActor::AHyperTwistHigherDimensionalTrainingShellActor() +{ + PrimaryActorTick.bCanEverTick = false; + + SceneRoot = CreateDefaultSubobject(TEXT("SceneRoot")); + SetRootComponent(SceneRoot); + + PreviewInstances = CreateDefaultSubobject(TEXT("PreviewInstances")); + PreviewInstances->SetupAttachment(SceneRoot); + PreviewInstances->SetMobility(EComponentMobility::Static); + PreviewInstances->SetCollisionEnabled(ECollisionEnabled::NoCollision); + PreviewInstances->SetCastShadow(false); + + LabelComponent = CreateDefaultSubobject(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)); + + static ConstructorHelpers::FObjectFinder CubeMeshFinder( + TEXT("/Engine/BasicShapes/Cube.Cube")); + if (CubeMeshFinder.Succeeded()) + { + CubePreviewMesh = CubeMeshFinder.Object; + } + + static ConstructorHelpers::FObjectFinder SphereMeshFinder( + TEXT("/Engine/BasicShapes/Sphere.Sphere")); + if (SphereMeshFinder.Succeeded()) + { + SpherePreviewMesh = SphereMeshFinder.Object; + } +} + +void AHyperTwistHigherDimensionalTrainingShellActor::OnConstruction(const FTransform& Transform) +{ + Super::OnConstruction(Transform); + + RefreshPreview(); + RefreshLabel(); + RefreshMetadataTags(); +} + +void AHyperTwistHigherDimensionalTrainingShellActor::RefreshPreview() +{ + if (PreviewInstances == nullptr) + { + return; + } + + PreviewInstances->ClearInstances(); + + UStaticMesh* PreviewMesh = UsesMagic120CellPreview() ? SpherePreviewMesh : CubePreviewMesh; + if (PreviewMesh == nullptr) + { + PreviewMesh = CubePreviewMesh != nullptr ? CubePreviewMesh : SpherePreviewMesh; + } + + if (PreviewMesh == nullptr) + { + return; + } + + PreviewInstances->SetStaticMesh(PreviewMesh); + if (UsesMagic120CellPreview()) + { + BuildMagic120CellPreview(); + } + else + { + BuildMagicCube5DPreview(); + } +} + +void AHyperTwistHigherDimensionalTrainingShellActor::RefreshLabel() +{ + if (LabelComponent == nullptr) + { + return; + } + + LabelComponent->SetText(BuildLabelText()); + LabelComponent->SetTextRenderColor( + UsesMagic120CellPreview() ? FColor(96, 208, 255) : FColor(255, 164, 96)); +} + +void AHyperTwistHigherDimensionalTrainingShellActor::RefreshMetadataTags() +{ + TArray NewTags; + + const auto AddTag = [&NewTags](const FString& Tag) + { + if (!Tag.IsEmpty()) + { + NewTags.AddUnique(FName(*Tag)); + } + }; + + AddTag(TEXT("HyperTwistHigherDimensionalTrainingShell")); + AddTag(FString::Printf(TEXT("training-shell-id:%s"), *TrainingShellId)); + AddTag(FString::Printf(TEXT("activation-profile:%s"), *ActivationProfileId)); + AddTag(FString::Printf(TEXT("host-surface:%s"), *HostSurfaceId)); + AddTag(FString::Printf(TEXT("launch-surface:%s"), *LaunchSurfaceId)); + AddTag(FString::Printf(TEXT("view-context-surface:%s"), *ViewContextSurfaceId)); + AddTag(FString::Printf(TEXT("session-surface:%s"), *SessionSurfaceId)); + AddTag(FString::Printf(TEXT("interactive-scene-surface:%s"), *InteractiveSceneSurfaceId)); + AddTag(FString::Printf(TEXT("scene-context:%s"), *SceneContextId)); + AddTag(FString::Printf(TEXT("puzzle:%s"), *PuzzleId)); + AddTag(FString::Printf(TEXT("runtime-mode:%s"), *RuntimeModeId)); + AddTag(FString::Printf(TEXT("projection:%s"), *ProjectionProfileId)); + AddTag(FString::Printf(TEXT("persistence:%s"), *PrimaryPersistenceBoundaryId)); + AddTag(FString::Printf(TEXT("manifest:%s"), *AuthoringManifestRelativePath)); + AddTag(FString::Printf(TEXT("family:%s"), *FamilyKey)); + AddTag(bDedicatedFamilyOwnership + ? TEXT("ownership:dedicated-family") + : TEXT("ownership:shared-or-unspecified")); + + for (const FString& TrainingShellTag : TrainingShellTags) + { + AddTag(TrainingShellTag); + } + + Tags = MoveTemp(NewTags); +} + +void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagic120CellPreview() +{ + AddPreviewInstance(FVector(0.0f, 0.0f, PreviewHeightOffset), 1.1f); + + for (int32 Index = 0; Index < 12; ++Index) + { + const float AngleRadians = + (static_cast(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); + } + + 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() +{ + AddPreviewInstance(FVector(0.0f, 0.0f, PreviewHeightOffset), 1.0f); + + const TArray AxisOffsets = + { + 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 (const FVector& Offset : AxisOffsets) + { + AddPreviewInstance(Offset, 0.42f); + } +} + +void AHyperTwistHigherDimensionalTrainingShellActor::AddPreviewInstance( + const FVector& RelativeLocation, + const float UniformScale) +{ + if (PreviewInstances == nullptr) + { + return; + } + + FTransform InstanceTransform; + InstanceTransform.SetLocation(RelativeLocation); + InstanceTransform.SetScale3D(FVector(UniformScale)); + PreviewInstances->AddInstance(InstanceTransform); +} + +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"); + + return FText::FromString(FString::Printf( + TEXT("%s\n%s"), + *EffectiveTitle, + *EffectiveSceneContextId)); +} + +bool AHyperTwistHigherDimensionalTrainingShellActor::UsesMagic120CellPreview() const +{ + return FamilyKey.Equals( + HyperTwistHigherDimensionalTrainingShellActorInternal::Magic120CellFamilyKey, + ESearchCase::IgnoreCase); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h new file mode 100644 index 0000000..41d0e43 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h @@ -0,0 +1,106 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "HyperTwistHigherDimensionalTrainingShellActor.generated.h" + +class UInstancedStaticMeshComponent; +class USceneComponent; +class UStaticMesh; +class UTextRenderComponent; + +UCLASS(BlueprintType) +class UNREALHYPERTWIST_API AHyperTwistHigherDimensionalTrainingShellActor : public AActor +{ + GENERATED_BODY() + +public: + AHyperTwistHigherDimensionalTrainingShellActor(); + + virtual void OnConstruction(const FTransform& Transform) override; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell") + TObjectPtr SceneRoot = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell") + TObjectPtr PreviewInstances = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell") + TObjectPtr LabelComponent = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString FamilyKey = TEXT("magic120cell"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString TrainingShellId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString Title; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell", meta = (MultiLine = "true")) + FString Summary; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString MapAssetPath; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString ActivationProfileId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString HostSurfaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString LaunchSurfaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString ViewContextSurfaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString SessionSurfaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString InteractiveSceneSurfaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString SceneContextId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString PuzzleId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString RuntimeModeId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString ProjectionProfileId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString PrimaryPersistenceBoundaryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + FString AuthoringManifestRelativePath; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + bool bDedicatedFamilyOwnership = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + TArray TrainingShellTags; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + float PreviewRadius = 260.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Shell") + float PreviewHeightOffset = 90.0f; + +private: + TObjectPtr CubePreviewMesh = nullptr; + TObjectPtr SpherePreviewMesh = nullptr; + + void RefreshPreview(); + void RefreshLabel(); + void RefreshMetadataTags(); + void BuildMagic120CellPreview(); + void BuildMagicCube5DPreview(); + void AddPreviewInstance(const FVector& RelativeLocation, float UniformScale); + FText BuildLabelText() const; + bool UsesMagic120CellPreview() const; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHigherDimensionalPhase6CActivationContractTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHigherDimensionalPhase6CActivationContractTest.cpp index b4ac842..65d966a 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHigherDimensionalPhase6CActivationContractTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHigherDimensionalPhase6CActivationContractTest.cpp @@ -1,10 +1,21 @@ // Copyright HyperTwist, Inc. All Rights Reserved. #include "Engine/GameInstance.h" +#include "Engine/Level.h" +#include "Engine/World.h" +#include "GameFramework/PlayerStart.h" +#include "GameFramework/WorldSettings.h" +#include "Misc/FileHelper.h" #include "Misc/AutomationTest.h" #include "Misc/PackageName.h" +#include "Misc/Paths.h" +#include "Misc/SecureHash.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" #include "HyperTwistSimulation/HyperTwistSimulationLibrary.h" +#include "HyperTwistTraining/HyperTwistCoachDashboardGameMode.h" +#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h" #include "HyperTwistTraining/HyperTwistTrainingLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" @@ -13,6 +24,35 @@ namespace HyperTwistHigherDimensionalPhase6CTestInternal { + struct FPhase6CDedicatedMapManifestEntry + { + FString MapKind; + FString FamilyKey; + FString MapAssetPath; + FString MapFileRelativePath; + FString MapHashMd5; + FString TrainingShellId; + FString ActivationProfileId; + FString HostSurfaceId; + FString LaunchSurfaceId; + FString ViewContextSurfaceId; + FString SessionSurfaceId; + FString InteractiveSceneSurfaceId; + FString SceneContextId; + FString PuzzleId; + FString RuntimeModeId; + FString ProjectionProfileId; + FString PrimaryPersistenceBoundaryId; + }; + + struct FPhase6CDedicatedMapManifest + { + FString ManifestId; + FString ManifestVersion; + FString ClassicReferenceMapHashMd5; + TArray Entries; + }; + FHyperTwistTrainingDeck MakeDeck(const FString& DeckId, const FString& PuzzleId) { FHyperTwistTrainingDeck Deck; @@ -86,6 +126,193 @@ namespace HyperTwistHigherDimensionalPhase6CTestInternal TrainingSubsystem->RefreshCompanionSpeechServiceHealthForAutomation(); return TrainingSubsystem; } + + FString GetManifestPath() + { + return FPaths::ConvertRelativePathToFull(FPaths::Combine( + FPaths::ProjectDir(), + TEXT("../docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json"))); + } + + FString GetMapFilePathFromRelativePath(const FString& RelativePath) + { + return FPaths::ConvertRelativePathToFull(FPaths::Combine( + FPaths::ProjectDir(), + TEXT("../"), + RelativePath)); + } + + FString ComputeFileMd5(const FString& FilePath) + { + if (!FPaths::FileExists(FilePath)) + { + return FString(); + } + + TArray FileBytes; + if (!FFileHelper::LoadFileToArray(FileBytes, *FilePath)) + { + return FString(); + } + + FMD5 Md5; + if (FileBytes.Num() > 0) + { + Md5.Update(FileBytes.GetData(), FileBytes.Num()); + } + + uint8 Digest[16]; + Md5.Final(Digest); + return BytesToHex(Digest, UE_ARRAY_COUNT(Digest)).ToLower(); + } + + bool ParseManifestEntry( + const TSharedPtr& JsonObject, + FPhase6CDedicatedMapManifestEntry& OutEntry + ) + { + return 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) + && JsonObject->TryGetStringField(TEXT("mapHashMd5"), OutEntry.MapHashMd5) + && JsonObject->TryGetStringField(TEXT("trainingShellId"), OutEntry.TrainingShellId) + && JsonObject->TryGetStringField(TEXT("activationProfileId"), OutEntry.ActivationProfileId) + && JsonObject->TryGetStringField(TEXT("hostSurfaceId"), OutEntry.HostSurfaceId) + && JsonObject->TryGetStringField(TEXT("launchSurfaceId"), OutEntry.LaunchSurfaceId) + && JsonObject->TryGetStringField(TEXT("viewContextSurfaceId"), OutEntry.ViewContextSurfaceId) + && JsonObject->TryGetStringField(TEXT("sessionSurfaceId"), OutEntry.SessionSurfaceId) + && 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); + } + + bool TryLoadDedicatedMapManifest(FPhase6CDedicatedMapManifest& OutManifest) + { + const FString ManifestPath = GetManifestPath(); + FString Json; + if (!FFileHelper::LoadFileToString(Json, *ManifestPath)) + { + return false; + } + + TSharedPtr RootObject; + if (!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Json), RootObject) + || !RootObject.IsValid()) + { + return false; + } + + const TArray>* EntryValues = nullptr; + if (!RootObject->TryGetStringField(TEXT("manifestId"), OutManifest.ManifestId) + || !RootObject->TryGetStringField(TEXT("manifestVersion"), OutManifest.ManifestVersion) + || !RootObject->TryGetStringField( + TEXT("classicReferenceMapHashMd5"), + OutManifest.ClassicReferenceMapHashMd5) + || !RootObject->TryGetArrayField(TEXT("entries"), EntryValues) + || EntryValues == nullptr) + { + return false; + } + + OutManifest.Entries.Reset(); + for (const TSharedPtr& EntryValue : *EntryValues) + { + const TSharedPtr EntryObject = EntryValue.IsValid() + ? EntryValue->AsObject() + : nullptr; + FPhase6CDedicatedMapManifestEntry Entry; + if (!ParseManifestEntry(EntryObject, Entry)) + { + return false; + } + + OutManifest.Entries.Add(Entry); + } + + return true; + } + + const FPhase6CDedicatedMapManifestEntry* FindManifestEntryByFamilyKey( + const FPhase6CDedicatedMapManifest& Manifest, + const FString& FamilyKey + ) + { + for (const FPhase6CDedicatedMapManifestEntry& Entry : Manifest.Entries) + { + if (Entry.FamilyKey.Equals(FamilyKey, ESearchCase::IgnoreCase)) + { + return &Entry; + } + } + + return nullptr; + } + + FString BuildMapObjectPath(const FString& MapAssetPath) + { + if (MapAssetPath.IsEmpty()) + { + return FString(); + } + + const FString MapName = FPackageName::GetShortName(MapAssetPath); + return FString::Printf(TEXT("%s.%s"), *MapAssetPath, *MapName); + } + + UWorld* LoadDedicatedMapWorld(const FString& MapAssetPath) + { + const FString MapObjectPath = BuildMapObjectPath(MapAssetPath); + if (MapObjectPath.IsEmpty()) + { + return nullptr; + } + + return LoadObject(nullptr, *MapObjectPath); + } + + template + int32 CountActorsOfClass(UWorld* World) + { + if (World == nullptr || World->PersistentLevel == nullptr) + { + return 0; + } + + int32 Count = 0; + for (AActor* Actor : World->PersistentLevel->Actors) + { + if (Cast(Actor) != nullptr) + { + ++Count; + } + } + + return Count; + } + + template + ActorType* FindActorOfClass(UWorld* World) + { + if (World == nullptr || World->PersistentLevel == nullptr) + { + return nullptr; + } + + for (AActor* Actor : World->PersistentLevel->Actors) + { + if (ActorType* TypedActor = Cast(Actor)) + { + return TypedActor; + } + } + + return nullptr; + } } IMPLEMENT_SIMPLE_AUTOMATION_TEST( @@ -188,6 +415,572 @@ bool FHyperTwistHigherDimensionalPhase6CHostCatalogTest::RunTest(const FString& return true; } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistHigherDimensionalPhase6CDedicatedMapAuthoringManifestTest, + "HyperTwist.FirstParty.HigherDimensional.Phase6C.DedicatedMapAuthoringManifest", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistHigherDimensionalPhase6CDedicatedMapAuthoringManifestTest::RunTest( + const FString& Parameters) +{ + using namespace HyperTwistHigherDimensionalPhase6CTestInternal; + + FPhase6CDedicatedMapManifest Manifest; + TestTrue( + TEXT("The Phase 6C dedicated-family map authoring manifest must load."), + TryLoadDedicatedMapManifest(Manifest) + ); + if (Manifest.Entries.Num() == 0) + { + return false; + } + + TestEqual( + TEXT("The dedicated-family map authoring manifest id must stay stable."), + Manifest.ManifestId, + TEXT("phase6c/dedicated-family-training-map-authoring") + ); + TestEqual( + TEXT("The dedicated-family map authoring manifest must expose both authored family maps."), + Manifest.Entries.Num(), + 2 + ); + + const FString ClassicMapFilePath = GetMapFilePathFromRelativePath( + TEXT("UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining.umap")); + const FString ClassicMapHashMd5 = ComputeFileMd5(ClassicMapFilePath); + TestTrue( + TEXT("The classic reference training map file must exist for dedicated-map differentiation checks."), + !ClassicMapHashMd5.IsEmpty() + ); + TestEqual( + TEXT("The dedicated-family map manifest must track the current classic reference hash."), + Manifest.ClassicReferenceMapHashMd5, + ClassicMapHashMd5 + ); + + const FPhase6CDedicatedMapManifestEntry* Magic120CellEntry = + FindManifestEntryByFamilyKey(Manifest, TEXT("magic120cell")); + const FPhase6CDedicatedMapManifestEntry* MagicCube5DEntry = + FindManifestEntryByFamilyKey(Manifest, TEXT("magiccube5d")); + TestNotNull( + TEXT("The dedicated-family map manifest must contain the Magic120Cell authored entry."), + Magic120CellEntry + ); + TestNotNull( + TEXT("The dedicated-family map manifest must contain the MagicCube5D authored entry."), + MagicCube5DEntry + ); + if (Magic120CellEntry == nullptr || MagicCube5DEntry == nullptr) + { + return false; + } + + const FString Magic120CellMapFilePath = GetMapFilePathFromRelativePath( + Magic120CellEntry->MapFileRelativePath); + const FString MagicCube5DMapFilePath = GetMapFilePathFromRelativePath( + MagicCube5DEntry->MapFileRelativePath); + const FString Magic120CellMapHashMd5 = ComputeFileMd5(Magic120CellMapFilePath); + const FString MagicCube5DMapHashMd5 = ComputeFileMd5(MagicCube5DMapFilePath); + TestTrue( + TEXT("The Magic120Cell dedicated-family map file must exist."), + !Magic120CellMapHashMd5.IsEmpty() + ); + TestTrue( + TEXT("The MagicCube5D dedicated-family map file must exist."), + !MagicCube5DMapHashMd5.IsEmpty() + ); + TestEqual( + TEXT("The Magic120Cell dedicated-family map hash must match the manifest."), + Magic120CellEntry->MapHashMd5, + Magic120CellMapHashMd5 + ); + TestEqual( + TEXT("The MagicCube5D dedicated-family map hash must match the manifest."), + MagicCube5DEntry->MapHashMd5, + MagicCube5DMapHashMd5 + ); + TestNotEqual( + TEXT("The Magic120Cell dedicated-family map must no longer be byte-identical to the classic training map."), + Magic120CellMapHashMd5, + ClassicMapHashMd5 + ); + TestNotEqual( + TEXT("The MagicCube5D dedicated-family map must no longer be byte-identical to the classic training map."), + MagicCube5DMapHashMd5, + ClassicMapHashMd5 + ); + TestNotEqual( + TEXT("The two dedicated-family maps must no longer be byte-identical to each other."), + Magic120CellMapHashMd5, + MagicCube5DMapHashMd5 + ); + + FHyperTwistTrainingHigherDimensionalRuntimeHostSurface Magic120CellHostSurface; + TestTrue( + TEXT("The Magic120Cell host surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalRuntimeHostSurfaceByActivationProfileId( + Magic120CellEntry->ActivationProfileId, + Magic120CellHostSurface + ) + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the host-surface map asset path."), + Magic120CellEntry->MapAssetPath, + Magic120CellHostSurface.EffectiveHostMapPath + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the host-surface id."), + Magic120CellEntry->HostSurfaceId, + Magic120CellHostSurface.HostSurfaceId + ); + + FHyperTwistTrainingHigherDimensionalRuntimeLaunchSurface Magic120CellLaunchSurface; + TestTrue( + TEXT("The Magic120Cell launch surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalRuntimeLaunchSurfaceByActivationProfileId( + Magic120CellEntry->ActivationProfileId, + Magic120CellLaunchSurface + ) + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the launch-surface id."), + Magic120CellEntry->LaunchSurfaceId, + Magic120CellLaunchSurface.LaunchSurfaceId + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the launch-surface persistence boundary."), + Magic120CellEntry->PrimaryPersistenceBoundaryId, + Magic120CellLaunchSurface.PrimaryPersistenceBoundaryId + ); + + FHyperTwistTrainingHigherDimensionalRuntimeViewContextSurface Magic120CellViewContextSurface; + TestTrue( + TEXT("The Magic120Cell view-context surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalRuntimeViewContextSurfaceByActivationProfileId( + Magic120CellEntry->ActivationProfileId, + Magic120CellViewContextSurface + ) + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the view-context surface id."), + Magic120CellEntry->ViewContextSurfaceId, + Magic120CellViewContextSurface.ViewContextSurfaceId + ); + + FHyperTwistTrainingHigherDimensionalRuntimeSessionSurface Magic120CellSessionSurface; + TestTrue( + TEXT("The Magic120Cell session surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalRuntimeSessionSurfaceByActivationProfileId( + Magic120CellEntry->ActivationProfileId, + Magic120CellSessionSurface + ) + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the session surface id."), + Magic120CellEntry->SessionSurfaceId, + Magic120CellSessionSurface.SessionSurfaceId + ); + + FHyperTwistTrainingHigherDimensionalInteractiveSceneSurface Magic120CellSceneSurface; + TestTrue( + TEXT("The Magic120Cell interactive scene surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalInteractiveSceneSurfaceByActivationProfileId( + Magic120CellEntry->ActivationProfileId, + Magic120CellSceneSurface + ) + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the interactive-scene surface id."), + Magic120CellEntry->InteractiveSceneSurfaceId, + Magic120CellSceneSurface.SceneSurfaceId + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the scene-context id."), + Magic120CellEntry->SceneContextId, + Magic120CellSceneSurface.SceneContext.SceneContextId + ); + TestEqual( + TEXT("The Magic120Cell manifest must stay aligned with the interactive-scene projection profile."), + Magic120CellEntry->ProjectionProfileId, + Magic120CellSceneSurface.SceneContext.ProjectionSettings.ProjectionProfile + ); + + FHyperTwistTrainingHigherDimensionalRuntimeHostSurface MagicCube5DHostSurface; + TestTrue( + TEXT("The MagicCube5D host surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalRuntimeHostSurfaceByActivationProfileId( + MagicCube5DEntry->ActivationProfileId, + MagicCube5DHostSurface + ) + ); + TestEqual( + TEXT("The MagicCube5D manifest must stay aligned with the host-surface map asset path."), + MagicCube5DEntry->MapAssetPath, + MagicCube5DHostSurface.EffectiveHostMapPath + ); + + FHyperTwistTrainingHigherDimensionalRuntimeLaunchSurface MagicCube5DLaunchSurface; + TestTrue( + TEXT("The MagicCube5D launch surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalRuntimeLaunchSurfaceByActivationProfileId( + MagicCube5DEntry->ActivationProfileId, + MagicCube5DLaunchSurface + ) + ); + TestEqual( + TEXT("The MagicCube5D manifest must stay aligned with the launch-surface id."), + MagicCube5DEntry->LaunchSurfaceId, + MagicCube5DLaunchSurface.LaunchSurfaceId + ); + TestEqual( + TEXT("The MagicCube5D manifest must stay aligned with the persistence boundary."), + MagicCube5DEntry->PrimaryPersistenceBoundaryId, + MagicCube5DLaunchSurface.PrimaryPersistenceBoundaryId + ); + + FHyperTwistTrainingHigherDimensionalInteractiveSceneSurface MagicCube5DSceneSurface; + TestTrue( + TEXT("The MagicCube5D interactive scene surface must resolve for map-manifest reconciliation."), + UHyperTwistTrainingRuntimeLibrary::TryGetBundledHigherDimensionalInteractiveSceneSurfaceByActivationProfileId( + MagicCube5DEntry->ActivationProfileId, + MagicCube5DSceneSurface + ) + ); + TestEqual( + TEXT("The MagicCube5D manifest must stay aligned with the interactive-scene surface id."), + MagicCube5DEntry->InteractiveSceneSurfaceId, + MagicCube5DSceneSurface.SceneSurfaceId + ); + TestEqual( + TEXT("The MagicCube5D manifest must stay aligned with the scene-context id."), + MagicCube5DEntry->SceneContextId, + MagicCube5DSceneSurface.SceneContext.SceneContextId + ); + TestEqual( + TEXT("The MagicCube5D manifest must stay aligned with the interactive-scene projection profile."), + MagicCube5DEntry->ProjectionProfileId, + MagicCube5DSceneSurface.SceneContext.ProjectionSettings.ProjectionProfile + ); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistHigherDimensionalPhase6CDedicatedMapOwnedShellTest, + "HyperTwist.FirstParty.HigherDimensional.Phase6C.DedicatedMapOwnedShell", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistHigherDimensionalPhase6CDedicatedMapOwnedShellTest::RunTest( + const FString& Parameters) +{ + using namespace HyperTwistHigherDimensionalPhase6CTestInternal; + + FPhase6CDedicatedMapManifest Manifest; + TestTrue( + TEXT("The Phase 6C dedicated-family map manifest must load for owned-shell checks."), + TryLoadDedicatedMapManifest(Manifest) + ); + if (Manifest.Entries.Num() == 0) + { + return false; + } + + const FPhase6CDedicatedMapManifestEntry* Magic120CellEntry = + FindManifestEntryByFamilyKey(Manifest, TEXT("magic120cell")); + const FPhase6CDedicatedMapManifestEntry* MagicCube5DEntry = + FindManifestEntryByFamilyKey(Manifest, TEXT("magiccube5d")); + TestNotNull( + TEXT("The owned-shell checks must find the Magic120Cell manifest entry."), + Magic120CellEntry + ); + TestNotNull( + TEXT("The owned-shell checks must find the MagicCube5D manifest entry."), + MagicCube5DEntry + ); + if (Magic120CellEntry == nullptr || MagicCube5DEntry == nullptr) + { + return false; + } + + const auto VerifyDedicatedMapShell = + [this](const FPhase6CDedicatedMapManifestEntry& Entry) -> bool + { + UWorld* MapWorld = + HyperTwistHigherDimensionalPhase6CTestInternal::LoadDedicatedMapWorld( + Entry.MapAssetPath); + TestNotNull( + *FString::Printf( + TEXT("The dedicated-family map world asset must load for %s."), + *Entry.FamilyKey), + MapWorld + ); + if (MapWorld == nullptr) + { + return false; + } + + AWorldSettings* WorldSettings = MapWorld->GetWorldSettings(); + TestNotNull( + *FString::Printf( + TEXT("The dedicated-family world settings must load for %s."), + *Entry.FamilyKey), + WorldSettings + ); + if (WorldSettings == nullptr) + { + return false; + } + + 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 contain exactly one PlayerStart anchor for %s."), + *Entry.FamilyKey), + HyperTwistHigherDimensionalPhase6CTestInternal::CountActorsOfClass< + APlayerStart>(MapWorld), + 1 + ); + + AHyperTwistHigherDimensionalTrainingShellActor* TrainingShellActor = + HyperTwistHigherDimensionalPhase6CTestInternal::FindActorOfClass< + AHyperTwistHigherDimensionalTrainingShellActor>(MapWorld); + APlayerStart* PlayerStart = + HyperTwistHigherDimensionalPhase6CTestInternal::FindActorOfClass< + APlayerStart>(MapWorld); + TestNotNull( + *FString::Printf( + TEXT("The dedicated-family PlayerStart anchor must load for %s."), + *Entry.FamilyKey), + PlayerStart + ); + if (PlayerStart == nullptr) + { + return false; + } + + const FString ExpectedTitle = Entry.FamilyKey.Equals( + TEXT("magic120cell"), + ESearchCase::IgnoreCase) + ? TEXT("Magic120Cell dedicated training shell") + : TEXT("MagicCube5D dedicated training shell"); + const FString ExpectedManifestPath = + TEXT("docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json"); + const FString ExpectedHostTag = + FString::Printf(TEXT("host-surface:%s"), *Entry.HostSurfaceId); + const FString ExpectedLaunchTag = + FString::Printf(TEXT("launch-surface:%s"), *Entry.LaunchSurfaceId); + const FString ExpectedViewContextTag = + FString::Printf(TEXT("view-context-surface:%s"), *Entry.ViewContextSurfaceId); + const FString ExpectedSessionTag = + FString::Printf(TEXT("session-surface:%s"), *Entry.SessionSurfaceId); + const FString ExpectedInteractiveSceneTag = + FString::Printf( + TEXT("interactive-scene-surface:%s"), + *Entry.InteractiveSceneSurfaceId); + const FString ExpectedSceneContextTag = + FString::Printf(TEXT("scene-context:%s"), *Entry.SceneContextId); + const FString ExpectedProjectionTag = + FString::Printf(TEXT("projection:%s"), *Entry.ProjectionProfileId); + const FString ExpectedPersistenceTag = + FString::Printf(TEXT("persistence:%s"), *Entry.PrimaryPersistenceBoundaryId); + const FString ExpectedTrainingShellTag = + FString::Printf(TEXT("training-shell-id:%s"), *Entry.TrainingShellId); + const FString ExpectedActivationTag = + FString::Printf(TEXT("activation-profile:%s"), *Entry.ActivationProfileId); + const FString ExpectedPuzzleTag = + FString::Printf(TEXT("puzzle:%s"), *Entry.PuzzleId); + const FString ExpectedRuntimeModeTag = + FString::Printf(TEXT("runtime-mode:%s"), *Entry.RuntimeModeId); + const auto HasActorTag = [](const AActor* Actor, const FString& Tag) -> bool + { + 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."), + *Entry.FamilyKey), + WorldSettings->DefaultGameMode != nullptr + ? WorldSettings->DefaultGameMode->GetPathName() + : FString(), + AHyperTwistCoachDashboardGameMode::StaticClass()->GetPathName() + ); + if (TrainingShellActor != nullptr) + { + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the family key for %s."), + *Entry.FamilyKey), + TrainingShellActor->FamilyKey, + Entry.FamilyKey + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the title for %s."), + *Entry.FamilyKey), + TrainingShellActor->Title, + ExpectedTitle + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the map asset path for %s."), + *Entry.FamilyKey), + TrainingShellActor->MapAssetPath, + Entry.MapAssetPath + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the training-shell id for %s."), + *Entry.FamilyKey), + TrainingShellActor->TrainingShellId, + Entry.TrainingShellId + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the activation profile id for %s."), + *Entry.FamilyKey), + TrainingShellActor->ActivationProfileId, + Entry.ActivationProfileId + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the scene-context id for %s."), + *Entry.FamilyKey), + TrainingShellActor->SceneContextId, + Entry.SceneContextId + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the projection profile id for %s."), + *Entry.FamilyKey), + TrainingShellActor->ProjectionProfileId, + Entry.ProjectionProfileId + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the persistence boundary id for %s."), + *Entry.FamilyKey), + TrainingShellActor->PrimaryPersistenceBoundaryId, + Entry.PrimaryPersistenceBoundaryId + ); + TestEqual( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve the authoring manifest path for %s."), + *Entry.FamilyKey), + TrainingShellActor->AuthoringManifestRelativePath, + ExpectedManifestPath + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must preserve dedicated ownership for %s."), + *Entry.FamilyKey), + TrainingShellActor->bDedicatedFamilyOwnership + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the authored shell tag for %s."), + *Entry.FamilyKey), + HasActorTag( + TrainingShellActor, + TEXT("HyperTwistHigherDimensionalTrainingShell")) + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the authored training-shell id tag for %s."), + *Entry.FamilyKey), + HasActorTag(TrainingShellActor, ExpectedTrainingShellTag) + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the authored host-surface tag for %s."), + *Entry.FamilyKey), + HasActorTag(TrainingShellActor, ExpectedHostTag) + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the authored launch-surface tag for %s."), + *Entry.FamilyKey), + HasActorTag(TrainingShellActor, ExpectedLaunchTag) + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the authored scene-context tag for %s."), + *Entry.FamilyKey), + HasActorTag(TrainingShellActor, ExpectedSceneContextTag) + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the authored projection tag for %s."), + *Entry.FamilyKey), + HasActorTag(TrainingShellActor, ExpectedProjectionTag) + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the authored persistence tag for %s."), + *Entry.FamilyKey), + HasActorTag(TrainingShellActor, ExpectedPersistenceTag) + ); + TestTrue( + *FString::Printf( + TEXT("The optional dedicated-family training shell must carry the dedicated ownership tag for %s."), + *Entry.FamilyKey), + HasActorTag(TrainingShellActor, TEXT("ownership:dedicated-family")) + ); + } + TestTrue( + *FString::Printf( + TEXT("The dedicated-family PlayerStart anchor must carry the activation-profile tag for %s."), + *Entry.FamilyKey), + HasActorTag(PlayerStart, ExpectedActivationTag) + ); + TestTrue( + *FString::Printf( + TEXT("The dedicated-family PlayerStart anchor must carry the view-context tag for %s."), + *Entry.FamilyKey), + HasActorTag(PlayerStart, ExpectedViewContextTag) + ); + TestTrue( + *FString::Printf( + TEXT("The dedicated-family PlayerStart anchor must carry the session-surface tag for %s."), + *Entry.FamilyKey), + HasActorTag(PlayerStart, ExpectedSessionTag) + ); + TestTrue( + *FString::Printf( + TEXT("The dedicated-family PlayerStart anchor must carry the interactive-scene tag for %s."), + *Entry.FamilyKey), + HasActorTag(PlayerStart, ExpectedInteractiveSceneTag) + ); + TestTrue( + *FString::Printf( + TEXT("The dedicated-family PlayerStart anchor must carry the puzzle tag for %s."), + *Entry.FamilyKey), + HasActorTag(PlayerStart, ExpectedPuzzleTag) + ); + TestTrue( + *FString::Printf( + TEXT("The dedicated-family PlayerStart anchor must carry the runtime-mode tag for %s."), + *Entry.FamilyKey), + HasActorTag(PlayerStart, ExpectedRuntimeModeTag) + ); + + return true; + }; + + return VerifyDedicatedMapShell(*Magic120CellEntry) + && VerifyDedicatedMapShell(*MagicCube5DEntry); +} + IMPLEMENT_SIMPLE_AUTOMATION_TEST( FHyperTwistHigherDimensionalPhase6CLaunchCatalogTest, "HyperTwist.FirstParty.HigherDimensional.Phase6C.LaunchCatalog", diff --git a/docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json b/docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json new file mode 100644 index 0000000..1a4b612 --- /dev/null +++ b/docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json @@ -0,0 +1,68 @@ +{ + "manifestId": "phase6c/dedicated-family-training-map-authoring", + "manifestVersion": "2026.06.18", + "authorTag": "HyperTwistHigherDimensionalTrainingShell", + "authoringScriptRelativePath": "scripts/hypertwist_author_higher_dimensional_training_maps.py", + "authoredThroughGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode", + "classicReferenceMapHashMd5": "7772cc42fd9fd129cb6d99c0b918a24e", + "entries": [ + { + "mapKind": "magic120cell", + "familyKey": "magic120cell", + "mapAssetPath": "/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining", + "mapFileRelativePath": "UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap", + "mapHashMd5": "9d9b9301f14cdad8c263d2c57b5f2c7f", + "trainingShellId": "phase6c/magic120cell/dedicated-training-shell", + "activationProfileId": "magic120cell-cleanroom-runtime-activation", + "hostSurfaceId": "phase6c/magic120cell/runtime-host-surface", + "launchSurfaceId": "phase6c/magic120cell/dedicated-training-launch-surface", + "viewContextSurfaceId": "phase6c/magic120cell/dedicated-training-view-context-surface", + "sessionSurfaceId": "phase6c/magic120cell/dedicated-training-session-surface", + "interactiveSceneSurfaceId": "phase6c/magic120cell/interactive-scene-surface", + "sceneContextId": "phase6c/magic120cell/interactive-scene-context", + "puzzleId": "polychoron/magic120cell", + "runtimeModeId": "magic120cell-full-color-runtime-v1", + "projectionProfileId": "magic120cell-4d-projection-distance-v1", + "primaryPersistenceBoundaryId": "magic120cell-persistence-boundary", + "authorTag": "HyperTwistHigherDimensionalTrainingShell", + "authoringManifestRelativePath": "docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json", + "authoredViaGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode", + "trainingShellTags": [ + "phase6c", + "family:magic120cell", + "host:dedicated-family-map", + "projection:magic120cell-4d-projection-distance-v1", + "persistence:magic120cell-persistence-boundary" + ] + }, + { + "mapKind": "magiccube5d", + "familyKey": "magiccube5d", + "mapAssetPath": "/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining", + "mapFileRelativePath": "UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap", + "mapHashMd5": "15d2f7acfdce43402ad2fd291fc5c781", + "trainingShellId": "phase6c/magiccube5d/dedicated-training-shell", + "activationProfileId": "magiccube5d-cleanroom-runtime-activation", + "hostSurfaceId": "phase6c/magiccube5d/runtime-host-surface", + "launchSurfaceId": "phase6c/magiccube5d/dedicated-training-launch-surface", + "viewContextSurfaceId": "phase6c/magiccube5d/dedicated-training-view-context-surface", + "sessionSurfaceId": "phase6c/magiccube5d/dedicated-training-session-surface", + "interactiveSceneSurfaceId": "phase6c/magiccube5d/interactive-scene-surface", + "sceneContextId": "phase6c/magiccube5d/interactive-scene-context", + "puzzleId": "hypercube/magiccube5d/order3", + "runtimeModeId": "magiccube5d-order3-runtime-v1", + "projectionProfileId": "magiccube5d-5d-projection-distance-v1", + "primaryPersistenceBoundaryId": "magiccube5d-persistence-boundary", + "authorTag": "HyperTwistHigherDimensionalTrainingShell", + "authoringManifestRelativePath": "docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json", + "authoredViaGameModeClassPath": "/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode", + "trainingShellTags": [ + "phase6c", + "family:magiccube5d", + "host:dedicated-family-map", + "projection:magiccube5d-5d-projection-distance-v1", + "persistence:magiccube5d-persistence-boundary" + ] + } + ] +} diff --git a/docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json b/docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json new file mode 100644 index 0000000..f4320d6 --- /dev/null +++ b/docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json @@ -0,0 +1,224 @@ +{ + "devices": [ + { + "deviceName": "DESKTOP-KS3VGHU", + "instance": "8A15E1D540837281594FD7AB21A7E713", + "instanceName": "DESKTOP-KS3VGHU-33204", + "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.06.18-10.22.23", + "succeeded": 13, + "succeededWithWarnings": 0, + "failed": 0, + "notRun": 0, + "inProcess": 0, + "totalDuration": 43.566970825195312, + "comparisonExported": false, + "comparisonExportDirectory": "", + "tests": [ + { + "testDisplayName": "ActivationCatalog", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.ActivationCatalog", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.016251299530267715, + "dateTime": "2026.06.18-10.21.36", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "DedicatedMapAuthoringManifest", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.DedicatedMapAuthoringManifest", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.13871729373931885, + "dateTime": "2026.06.18-10.21.36", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "DedicatedMapOwnedShell", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.DedicatedMapOwnedShell", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.018342901021242142, + "dateTime": "2026.06.18-10.21.36", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "HostCatalog", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.HostCatalog", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.016522001475095749, + "dateTime": "2026.06.18-10.21.36", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "InteractiveSceneCatalog", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.InteractiveSceneCatalog", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.15781599283218384, + "dateTime": "2026.06.18-10.21.36", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "LaunchCatalog", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.LaunchCatalog", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.017018400132656097, + "dateTime": "2026.06.18-10.21.36", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "LaunchExecution", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.LaunchExecution", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 43.018119812011719, + "dateTime": "2026.06.18-10.21.36", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "Magic120CellActivationProfile", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.Magic120CellActivationProfile", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.014199301600456238, + "dateTime": "2026.06.18-10.22.23", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "MagicCube5DActivationProfile", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.MagicCube5DActivationProfile", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.015723902732133865, + "dateTime": "2026.06.18-10.22.23", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "RunStateActivationResolution", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.RunStateActivationResolution", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.01621440052986145, + "dateTime": "2026.06.18-10.22.23", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "SessionCatalog", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.SessionCatalog", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.098687797784805298, + "dateTime": "2026.06.18-10.22.23", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "TrainingRunDefinitionBridge", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.TrainingRunDefinitionBridge", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.015297800302505493, + "dateTime": "2026.06.18-10.22.23", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + }, + { + "testDisplayName": "ViewContextCatalog", + "fullTestPath": "HyperTwist.FirstParty.HigherDimensional.Phase6C.ViewContextCatalog", + "tags": [], + "state": "Success", + "deviceInstance": [ + "8A15E1D540837281594FD7AB21A7E713" + ], + "duration": 0.024057600647211075, + "dateTime": "2026.06.18-10.22.23", + "entries": [], + "warnings": 0, + "errors": 0, + "artifacts": [] + } + ] +} \ No newline at end of file diff --git a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md index 46677b4..2720039 100644 --- a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md +++ b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md @@ -367,6 +367,8 @@ Closure read: - `2026-06-18` Windows checkpoint: the same isolated worktree `C:\HyperTwist_worktrees\phase10validate` rebuilt the dedicated-family runtime-state slice with `Result: Succeeded` / UnrealBuildTool `Total execution time: 1648.33 seconds`; after the selector-batching hardening rebuild with `Result: Succeeded` / UnrealBuildTool `Total execution time: 93.44 seconds`, targeted `UnrealEditor-Cmd` automation exported `C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\Saved\AutomationReports\Phase6C-DedicatedFamily-Optimized\index.json` with all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests passing, `totalDuration` `196.08790588378906`, and `LaunchExecution` duration `195.65591430664062`; the commandlet still emitted `Ignoring very large delta of 195.58 seconds`, but that quiet interval is materially down from the immediately prior `385.00 seconds` proof on this lane while retaining `**** TEST COMPLETE. EXIT CODE: 0 ****` - `2026-06-18` continuation proof: current code now also defers repository-view projection across the internal generated-mode launch handoff itself, so the seed-run, launch-request persistence, and generated-run transition keep their repository state intact without forcing intermediate view rebuilds that the caller never reads - `2026-06-18` Windows checkpoint: after that projection-deferral hardening rebuild with `Result: Succeeded` / UnrealBuildTool `Total execution time: 400.77 seconds`, targeted `UnrealEditor-Cmd` automation exported `C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\Saved\AutomationReports\Phase6C-DedicatedFamily-Optimized2\index.json` with all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests passing, `totalDuration` `41.88494873046875`, and `LaunchExecution` duration `41.53509521484375`; the controller quiet interval fell again to `41.52 seconds` while retaining `**** TEST COMPLETE. EXIT CODE: 0 ****` +- `2026-06-18` recovery proof: current code now deterministically reauthors the dedicated-family `Magic120Cell` / `MagicCube5D` maps through `scripts/hypertwist_author_higher_dimensional_training_maps.py`, stamps `AHyperTwistCoachDashboardGameMode` plus manifest-backed `PlayerStart` ownership tags for activation, host, launch, view-context, session, interactive-scene, scene-context, puzzle, runtime-mode, projection, persistence, and dedicated-family ownership, and records the refreshed owned-map hashes in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json`; the optional `AHyperTwistHigherDimensionalTrainingShellActor` remains available for manual/editor authoring, but the headless `UnrealEditor-Cmd` lane now intentionally skips spawning it because that path still crashes the commandlet with `EXCEPTION_INT_DIVIDE_BY_ZERO` +- `2026-06-18` Windows checkpoint: after patching the missing dedicated-anchor puzzle tag, the same isolated worktree `C:\HyperTwist_worktrees\phase10validate` reran `scripts\Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1` to `Higher-dimensional authored maps verified.`, then targeted `UnrealEditor-Cmd` automation exported `C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\Saved\AutomationReports\Phase6C-DedicatedMapOwnership\index.json`; the pulled local proof at `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json` now shows all `13` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests passing, including `DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell`, with `**** TEST COMPLETE. EXIT CODE: 0 ****` - [x] Replace the current placeholder interactive-scene envelopes with full owned `120-cell` and `5D` simulation-state, projection, and persistence ownership in Unreal above the landed higher-dimensional scene-context catalog - [x] Replace the current shared-map execution bridge with full owned dedicated-family interactive launch, map, and persistence surfaces for `120-cell` and `5D` @@ -582,4 +584,16 @@ Recommended next widening order: the same `localhost:22022` lane still passed all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests while dropping the `LaunchExecution` quiet interval again from `195.58` to `41.52` seconds +- `2026-06-18`: the next `Phase 6C` recovery-proof pass then turned the new + dedicated-family maps into deterministically regenerated headless-owned + artifacts through `scripts/hypertwist_author_higher_dimensional_training_maps.py`, + stamped explicit `PlayerStart` puzzle and ownership tags plus the rest of + the higher-dimensional ownership contract into those maps, preserved the + optional `AHyperTwistHigherDimensionalTrainingShellActor` only for manual or + editor authoring because `UnrealEditor-Cmd` still crashes when spawning it, + and revalidated the corrected slice on `localhost:22022` with all `13` + `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests passing, including + `DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell`; pulled proof + now lives in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json` + and `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json` - later classic-cube polish only when a concrete presentation gap remains, not as the default next move diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index ad48d6f..bbfc068 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -271,7 +271,7 @@ repo. | Higher-dimensional initial shared-map launch and view-context execution bridge | Implemented now | landed `Phase 6C` continuation packet | First-party current code first bundled higher-dimensional launch surfaces and view-context surfaces for `Magic120Cell` and `MagicCube5D`, resolved those surfaces through the training runtime library plus the live training panel/session-actor caches, and let the training subsystem start the shared-map seed run, apply default selector posture, persist the generated-mode launch request, and start the owned generated run without widening into external-process ownership. After recovering an interrupted follow-up, the primary reverse-SSH `localhost:22022` Windows lane rebuilt that cleaned slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 124.81 seconds`, and passed all `8` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including `LaunchCatalog`, `ViewContextCatalog`, `LaunchExecution`, and `RunStateActivationResolution`; that initial bridge is now superseded by the later dedicated-family map ownership continuation. | | Higher-dimensional explicit dedicated-host seam | Implemented now | landed `Phase 6C` host-surface continuation packet | First-party current code then recorded the preferred dedicated-family `Magic120Cell` / `MagicCube5D` host-map targets in an explicit higher-dimensional host-surface catalog while the slice still kept the shared training map as the interim effective runtime fallback, mirrored that resolved host seam through the training runtime library plus the panel/session caches, and threaded the host-surface id into the launch/view-context surfaces and generated-mode lineage. The primary reverse-SSH `localhost:22022` Windows lane rebuilt this widened slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 2858.02 seconds`, and passed all `9` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including the new `HostCatalog`; that interim fallback is now superseded by the later dedicated-family map ownership continuation. | | Higher-dimensional executed runtime session surface | Implemented now | landed `Phase 6C` session-surface continuation packet | First-party current code now merges the higher-dimensional launch, host, and view-context seams into an explicit runtime-session catalog for `Magic120Cell` and `MagicCube5D`, mirrors that session surface through the training runtime library plus the panel/session caches, upgrades generated-mode deck lineage to record the resolved session surface, and applies the merged launch-plus-view selector posture at start time instead of only the narrower launch defaults. The primary reverse-SSH `localhost:22022` Windows lane rebuilt this widened slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 3541.98 seconds`, and passed all `10` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including the new `SessionCatalog`; the longer `LaunchExecution` quiet interval of `573.84 seconds` remained known-good. | -| Higher-dimensional dedicated-family interactive scene ownership | Implemented now | landed `Phase 6C` interactive-scene and dedicated-host continuation packet | First-party current code now promotes the higher-dimensional runtime-session seams into explicit `Magic120Cell` and `MagicCube5D` interactive-scene surfaces with family-owned runtime-state, projection, and persistence envelopes, replaces the shared-map bridge with dedicated-family training-map assets plus dedicated host/launch/view-context/session surfaces, exposes explicit scene-envelope decoders through the runtime library, batches default selector application so generated-mode launch execution no longer rebuilds repository views once per selector, and then defers repository-view projection across the internal generated-mode handoff itself so only the final launch state forces a view refresh. The primary reverse-SSH `localhost:22022` Windows lane rebuilt the dedicated-family ownership slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 1648.33 seconds`; the later projection-deferral hardening rebuild completed in `400.77 seconds`, and the exported automation report passed all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests with `totalDuration` `41.88494873046875`, `LaunchExecution` duration `41.53509521484375`, and the controller quiet interval reduced to `41.52 seconds`. | +| Higher-dimensional dedicated-family interactive scene ownership | Implemented now | landed `Phase 6C` interactive-scene and dedicated-host continuation packet | First-party current code now promotes the higher-dimensional runtime-session seams into explicit `Magic120Cell` and `MagicCube5D` interactive-scene surfaces with family-owned runtime-state, projection, and persistence envelopes, replaces the shared-map bridge with dedicated-family training-map assets plus dedicated host/launch/view-context/session surfaces, exposes explicit scene-envelope decoders through the runtime library, batches default selector application so generated-mode launch execution no longer rebuilds repository views once per selector, and then defers repository-view projection across the internal generated-mode handoff itself so only the final launch state forces a view refresh. The primary reverse-SSH `localhost:22022` Windows lane rebuilt the dedicated-family ownership slice on `2026-06-18` with `Result: Succeeded`, UnrealBuildTool `Total execution time: 1648.33 seconds`; the later projection-deferral hardening rebuild completed in `400.77 seconds`, and the exported automation report passed all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests with `totalDuration` `41.88494873046875`, `LaunchExecution` duration `41.53509521484375`, and the controller quiet interval reduced to `41.52 seconds`. The current recovery-proof continuation now also deterministically reauthors the dedicated-family maps through `scripts/hypertwist_author_higher_dimensional_training_maps.py`, records manifest-backed owned hashes in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json`, preserves the optional `AHyperTwistHigherDimensionalTrainingShellActor` for manual/editor authoring while intentionally skipping it on the headless commandlet lane because that spawn path still crashes `UnrealEditor-Cmd`, and passes all `13` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests through pulled proof in `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json`, including `DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell`. | | Broad non-Euclidean interaction shell and host ownership | Deep-source grounded retained | `MagicTile` retained remainder | Broad interaction-shell, WinForms/OpenTK host ownership, and generic runtime replacement remain deferred after the landed `Phase 6R-T` slice. | ### 7. Speech input and voice sidecars diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index 08a81c1..083440d 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -179,10 +179,25 @@ Current consolidated milestone snapshot: again passed all `11` `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests with `totalDuration` `41.88494873046875` and `LaunchExecution` duration `41.53509521484375` +- `2026-06-18`: the next `Phase 6C` recovery-proof pass then made those + dedicated-family maps deterministic headless-owned artifacts through + `scripts/hypertwist_author_higher_dimensional_training_maps.py`, stamped + explicit `PlayerStart` ownership tags for activation, host, launch, + view-context, session, interactive-scene, scene-context, puzzle, + runtime-mode, projection, persistence, and dedicated-family ownership, + preserved the optional `AHyperTwistHigherDimensionalTrainingShellActor` + only for manual/editor authoring because `UnrealEditor-Cmd` still crashes + when spawning it, and revalidated the corrected slice with pulled proof in + `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json` + plus `docs/generated/higher_dimensional_training_maps/phase6c_dedicated_map_ownership_automation_report.json`; + the same `localhost:22022` lane passed all `13` + `HyperTwist.FirstParty.HigherDimensional.Phase6C` tests, including + `DedicatedMapAuthoringManifest` and `DedicatedMapOwnedShell` - the functional `Phase 6C` ownership gap is now closed; the remaining work - in this lane is optional further micro-profiling and richer dedicated-family - authored map content, not missing activation, host, session, - interactive-scene, runtime-state, or persistence ownership seams + in this lane is optional further micro-profiling and richer non-headless + decorative/manual-authored family map dressing, not missing activation, + host, session, interactive-scene, runtime-state, or persistence ownership + seams - the canonical HyperTwist repo-row portfolio is now treated as `75` rows, not `71` - currently implemented rows are now `35`, not `20` diff --git a/scripts/Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1 b/scripts/Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1 new file mode 100644 index 0000000..d0a203d --- /dev/null +++ b/scripts/Invoke-HyperTwistHigherDimensionalMapAuthoring.ps1 @@ -0,0 +1,180 @@ +param( + [string]$ProjectRoot = 'C:\HyperTwist', + [string]$UnrealEditorCmdPath = 'C:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe', + [string]$PythonScriptPath +) + +$ErrorActionPreference = 'Stop' + +if ([string]::IsNullOrWhiteSpace($PythonScriptPath)) +{ + $PythonScriptPath = Join-Path $ProjectRoot 'scripts\hypertwist_author_higher_dimensional_training_maps.py' +} + +$UProjectPath = Join-Path $ProjectRoot 'UnrealHyperTwist\UnrealHyperTwist.uproject' +$Magic120CellMapPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Maps\L_HyperTwist_Magic120CellTraining.umap' +$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' + +if (-not (Test-Path $UnrealEditorCmdPath)) +{ + throw "UnrealEditor-Cmd.exe was not found at '$UnrealEditorCmdPath'." +} + +if (-not (Test-Path $UProjectPath)) +{ + throw "UnrealHyperTwist project file was not found at '$UProjectPath'." +} + +if (-not (Test-Path $PythonScriptPath)) +{ + throw "Higher-dimensional map authoring script was not found at '$PythonScriptPath'." +} + +$NormalizedPythonScriptPath = $PythonScriptPath -replace '\\', '/' +$AuthoredTargets = @( + @{ + Label = 'magic120cell' + ExpectedMapPath = $Magic120CellMapPath + }, + @{ + Label = 'magiccube5d' + ExpectedMapPath = $MagicCube5DMapPath + } +) + +foreach ($Target in $AuthoredTargets) +{ + $env:HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND = $Target.Label + $AuthoringStartedAtUtc = [DateTime]::UtcNow + Write-Host "Authoring HyperTwist higher-dimensional training map target '$($Target.Label)' through UnrealEditor-Cmd..." + & $UnrealEditorCmdPath ` + $UProjectPath ` + "-ExecutePythonScript=$NormalizedPythonScriptPath" ` + -unattended ` + -nop4 ` + -nullrhi ` + -nosound ` + -nosplash ` + -stdout ` + -FullStdOutLogOutput ` + -log + + if ($LASTEXITCODE -ne 0) + { + $FreshMapExists = $false + if (Test-Path $Target.ExpectedMapPath) + { + $FreshMapExists = (Get-Item $Target.ExpectedMapPath).LastWriteTimeUtc -ge $AuthoringStartedAtUtc.AddSeconds(-2) + } + + if ($FreshMapExists) + { + 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." + } + } + + if (-not (Test-Path $Target.ExpectedMapPath)) + { + Remove-Item Env:\HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND -ErrorAction SilentlyContinue + throw "Expected authored map was not found at '$($Target.ExpectedMapPath)'." + } +} + +Remove-Item Env:\HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND -ErrorAction SilentlyContinue + +foreach ($ExpectedMapPath in @($Magic120CellMapPath, $MagicCube5DMapPath)) +{ + if (-not (Test-Path $ExpectedMapPath)) + { + throw "Expected authored map was not found at '$ExpectedMapPath'." + } +} + +if (-not (Test-Path $ClassicMapPath)) +{ + throw "Classic reference map was not found at '$ClassicMapPath'." +} + +New-Item -ItemType Directory -Force -Path ([System.IO.Path]::GetDirectoryName($ManifestPath)) | Out-Null + +$ClassicReferenceHashMd5 = (Get-FileHash $ClassicMapPath -Algorithm MD5).Hash.ToLowerInvariant() +$ManifestEntries = @( + [ordered]@{ + mapKind = 'magic120cell' + familyKey = 'magic120cell' + mapAssetPath = '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining' + mapFileRelativePath = 'UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining.umap' + mapHashMd5 = (Get-FileHash $Magic120CellMapPath -Algorithm MD5).Hash.ToLowerInvariant() + trainingShellId = 'phase6c/magic120cell/dedicated-training-shell' + activationProfileId = 'magic120cell-cleanroom-runtime-activation' + hostSurfaceId = 'phase6c/magic120cell/runtime-host-surface' + launchSurfaceId = 'phase6c/magic120cell/dedicated-training-launch-surface' + viewContextSurfaceId = 'phase6c/magic120cell/dedicated-training-view-context-surface' + sessionSurfaceId = 'phase6c/magic120cell/dedicated-training-session-surface' + interactiveSceneSurfaceId = 'phase6c/magic120cell/interactive-scene-surface' + sceneContextId = 'phase6c/magic120cell/interactive-scene-context' + puzzleId = 'polychoron/magic120cell' + runtimeModeId = 'magic120cell-full-color-runtime-v1' + projectionProfileId = 'magic120cell-4d-projection-distance-v1' + primaryPersistenceBoundaryId = 'magic120cell-persistence-boundary' + authorTag = 'HyperTwistHigherDimensionalTrainingShell' + authoringManifestRelativePath = 'docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json' + authoredViaGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode' + trainingShellTags = @( + 'phase6c', + 'family:magic120cell', + 'host:dedicated-family-map', + 'projection:magic120cell-4d-projection-distance-v1', + 'persistence:magic120cell-persistence-boundary' + ) + }, + [ordered]@{ + mapKind = 'magiccube5d' + familyKey = 'magiccube5d' + mapAssetPath = '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining' + mapFileRelativePath = 'UnrealHyperTwist/Content/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining.umap' + mapHashMd5 = (Get-FileHash $MagicCube5DMapPath -Algorithm MD5).Hash.ToLowerInvariant() + trainingShellId = 'phase6c/magiccube5d/dedicated-training-shell' + activationProfileId = 'magiccube5d-cleanroom-runtime-activation' + hostSurfaceId = 'phase6c/magiccube5d/runtime-host-surface' + launchSurfaceId = 'phase6c/magiccube5d/dedicated-training-launch-surface' + viewContextSurfaceId = 'phase6c/magiccube5d/dedicated-training-view-context-surface' + sessionSurfaceId = 'phase6c/magiccube5d/dedicated-training-session-surface' + interactiveSceneSurfaceId = 'phase6c/magiccube5d/interactive-scene-surface' + sceneContextId = 'phase6c/magiccube5d/interactive-scene-context' + puzzleId = 'hypercube/magiccube5d/order3' + runtimeModeId = 'magiccube5d-order3-runtime-v1' + projectionProfileId = 'magiccube5d-5d-projection-distance-v1' + primaryPersistenceBoundaryId = 'magiccube5d-persistence-boundary' + authorTag = 'HyperTwistHigherDimensionalTrainingShell' + authoringManifestRelativePath = 'docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_map_manifest.json' + authoredViaGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode' + trainingShellTags = @( + 'phase6c', + 'family:magiccube5d', + 'host:dedicated-family-map', + 'projection:magiccube5d-5d-projection-distance-v1', + 'persistence:magiccube5d-persistence-boundary' + ) + } +) + +$Manifest = [ordered]@{ + manifestId = 'phase6c/dedicated-family-training-map-authoring' + manifestVersion = '2026.06.18' + authorTag = 'HyperTwistHigherDimensionalTrainingShell' + authoringScriptRelativePath = 'scripts/hypertwist_author_higher_dimensional_training_maps.py' + authoredThroughGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistCoachDashboardGameMode' + classicReferenceMapHashMd5 = $ClassicReferenceHashMd5 + entries = $ManifestEntries +} + +$Manifest | ConvertTo-Json -Depth 6 | Set-Content -Path $ManifestPath -Encoding UTF8 +Write-Host 'Higher-dimensional authored maps verified.' diff --git a/scripts/hypertwist_author_higher_dimensional_training_maps.py b/scripts/hypertwist_author_higher_dimensional_training_maps.py new file mode 100644 index 0000000..4c4eeaf --- /dev/null +++ b/scripts/hypertwist_author_higher_dimensional_training_maps.py @@ -0,0 +1,579 @@ +import hashlib +import json +import os +import traceback + +import unreal + + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +MAP_ROOT = "/Game/HyperTwistTraining/Maps" +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" +ENABLE_HEADLESS_TRAINING_SHELL_ACTOR = ( + os.getenv("HYPERTWIST_ENABLE_TRAINING_SHELL_ACTOR", "").strip().lower() + in {"1", "true", "yes"} +) +PRESERVED_LEVEL_FRAMEWORK_CLASS_NAMES = { + "Brush", + "DefaultPhysicsVolume", + "LevelScriptActor", + "WorldSettings", +} +MANIFEST_RELATIVE_PATH = os.path.join( + "docs", + "generated", + "higher_dimensional_training_maps", + "phase6c_dedicated_family_map_manifest.json", +) +MANIFEST_ABSOLUTE_PATH = os.path.join(PROJECT_ROOT, MANIFEST_RELATIVE_PATH) + +MAP_CONFIGS = ( + { + "map_kind": "magic120cell", + "family_key": "magic120cell", + "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_Magic120CellTraining", + "map_file_relative_path": os.path.join( + "UnrealHyperTwist", + "Content", + "HyperTwistTraining", + "Maps", + "L_HyperTwist_Magic120CellTraining.umap", + ), + "training_shell_id": "phase6c/magic120cell/dedicated-training-shell", + "title": "Magic120Cell dedicated training shell", + "summary": ( + "First-party authored dedicated-family training shell for the owned " + "Magic120Cell higher-dimensional activation lane." + ), + "activation_profile_id": "magic120cell-cleanroom-runtime-activation", + "host_surface_id": "phase6c/magic120cell/runtime-host-surface", + "launch_surface_id": "phase6c/magic120cell/dedicated-training-launch-surface", + "view_context_surface_id": "phase6c/magic120cell/dedicated-training-view-context-surface", + "session_surface_id": "phase6c/magic120cell/dedicated-training-session-surface", + "interactive_scene_surface_id": "phase6c/magic120cell/interactive-scene-surface", + "scene_context_id": "phase6c/magic120cell/interactive-scene-context", + "puzzle_id": "polychoron/magic120cell", + "runtime_mode_id": "magic120cell-full-color-runtime-v1", + "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": [ + "phase6c", + "family:magic120cell", + "host:dedicated-family-map", + "projection:magic120cell-4d-projection-distance-v1", + "persistence:magic120cell-persistence-boundary", + ], + }, + { + "map_kind": "magiccube5d", + "family_key": "magiccube5d", + "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_MagicCube5DTraining", + "map_file_relative_path": os.path.join( + "UnrealHyperTwist", + "Content", + "HyperTwistTraining", + "Maps", + "L_HyperTwist_MagicCube5DTraining.umap", + ), + "training_shell_id": "phase6c/magiccube5d/dedicated-training-shell", + "title": "MagicCube5D dedicated training shell", + "summary": ( + "First-party authored dedicated-family training shell for the owned " + "MagicCube5D higher-dimensional activation lane." + ), + "activation_profile_id": "magiccube5d-cleanroom-runtime-activation", + "host_surface_id": "phase6c/magiccube5d/runtime-host-surface", + "launch_surface_id": "phase6c/magiccube5d/dedicated-training-launch-surface", + "view_context_surface_id": "phase6c/magiccube5d/dedicated-training-view-context-surface", + "session_surface_id": "phase6c/magiccube5d/dedicated-training-session-surface", + "interactive_scene_surface_id": "phase6c/magiccube5d/interactive-scene-surface", + "scene_context_id": "phase6c/magiccube5d/interactive-scene-context", + "puzzle_id": "hypercube/magiccube5d/order3", + "runtime_mode_id": "magiccube5d-order3-runtime-v1", + "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": [ + "phase6c", + "family:magiccube5d", + "host:dedicated-family-map", + "projection:magiccube5d-5d-projection-distance-v1", + "persistence:magiccube5d-persistence-boundary", + ], + }, +) + + +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: + raise RuntimeError(f"Required class was not found: {class_path}") + return loaded_class + + +def ensure_directory(directory_path: str) -> None: + if not unreal.EditorAssetLibrary.does_directory_exist(directory_path): + if not unreal.EditorAssetLibrary.make_directory(directory_path): + raise RuntimeError(f"Failed to create content directory: {directory_path}") + + +def spawn_actor(actor_class, label: str, location: unreal.Vector, rotation: unreal.Rotator): + actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) + if actor_subsystem is None: + raise RuntimeError("EditorActorSubsystem was not available.") + + actor = actor_subsystem.spawn_actor_from_class(actor_class, location, rotation) + if actor is None: + raise RuntimeError(f"Failed to spawn actor: {label}") + + actor.set_actor_label(label) + actor.set_editor_property("tags", [unreal.Name(AUTHOR_TAG)]) + return actor + + +def build_map_object_path(map_asset_path: str) -> str: + asset_name = map_asset_path.rsplit("/", 1)[-1] + return f"{map_asset_path}.{asset_name}" + + +def map_package_exists(config) -> bool: + map_asset_path = config["map_asset_path"] + map_file_absolute_path = resolve_map_file_absolute_path(config) + map_object_path = build_map_object_path(map_asset_path) + return ( + os.path.exists(map_file_absolute_path) + or unreal.EditorAssetLibrary.does_asset_exist(map_asset_path) + or unreal.EditorAssetLibrary.does_asset_exist(map_object_path) + ) + + +def delete_existing_level(level_subsystem, config) -> bool: + map_asset_path = config["map_asset_path"] + if not map_package_exists(config): + return False + + if not level_subsystem.load_level(SAFE_TRANSIENT_LEVEL_PATH): + raise RuntimeError( + f"Failed to load transient level '{SAFE_TRANSIENT_LEVEL_PATH}' before rebuilding " + f"'{map_asset_path}'." + ) + + map_object_path = build_map_object_path(map_asset_path) + deleted_via_asset_api = False + for candidate_path in (map_object_path, map_asset_path): + if unreal.EditorAssetLibrary.does_asset_exist(candidate_path): + deleted_via_asset_api = unreal.EditorAssetLibrary.delete_asset(candidate_path) + if deleted_via_asset_api: + log(f"Deleted existing level asset {candidate_path} before rebuild") + break + + map_file_absolute_path = resolve_map_file_absolute_path(config) + if os.path.exists(map_file_absolute_path): + os.remove(map_file_absolute_path) + log(f"Removed existing level file {map_file_absolute_path} before rebuild") + return True + + if deleted_via_asset_api: + return True + + raise RuntimeError(f"Failed to delete existing level package: {map_asset_path}") + + +def recreate_level(level_subsystem, config) -> None: + map_asset_path = config["map_asset_path"] + delete_existing_level(level_subsystem, config) + + if not level_subsystem.new_level(map_asset_path): + raise RuntimeError(f"Failed to create level: {map_asset_path}") + + log(f"Created new level {map_asset_path}") + + +def should_preserve_existing_actor(actor) -> bool: + actor_class = actor.get_class() + actor_class_name = actor_class.get_name() if actor_class is not None else "" + return actor_class_name in PRESERVED_LEVEL_FRAMEWORK_CLASS_NAMES + + +def clear_existing_training_map_actors() -> None: + actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) + if actor_subsystem is None: + raise RuntimeError("EditorActorSubsystem was not available.") + + author_tags = {AUTHOR_TAG, LEGACY_CLASSIC_AUTHOR_TAG} + destroyed_tagged_count = 0 + destroyed_fallback_count = 0 + + for actor in actor_subsystem.get_all_level_actors(): + if actor is None: + continue + + if should_preserve_existing_actor(actor): + continue + + actor_tags = {str(tag) for tag in actor.tags} + if not actor_tags.isdisjoint(author_tags): + if actor_subsystem.destroy_actor(actor): + destroyed_tagged_count += 1 + continue + + if actor_subsystem.destroy_actor(actor): + destroyed_fallback_count += 1 + + log( + "Removed " + f"{destroyed_tagged_count} tagged and {destroyed_fallback_count} fallback actors " + "before reauthoring" + ) + + +def save_current_level_or_raise(level_subsystem, label: str) -> None: + if not level_subsystem.save_current_level(): + raise RuntimeError(f"Failed to save level after {label}.") + log(f"Saved current level after {label}") + + +def configure_world_settings(game_mode_class_path: str) -> None: + editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) + if editor_subsystem is None: + raise RuntimeError("UnrealEditorSubsystem was not available.") + + world = editor_subsystem.get_editor_world() + if world is None: + raise RuntimeError("Editor world was not available after loading the target level.") + + world_settings = world.get_world_settings() + if world_settings is None: + raise RuntimeError("World settings were not available for the target level.") + + 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, + f"HT_{config['family_key']}_DedicatedSurfaceAnchor", + config["player_start_location"], + unreal.Rotator(0.0, 0.0, 0.0), + ) + anchor_tags = [ + AUTHOR_TAG, + f"training-shell-id:{config['training_shell_id']}", + f"activation-profile:{config['activation_profile_id']}", + f"host-surface:{config['host_surface_id']}", + f"launch-surface:{config['launch_surface_id']}", + f"view-context-surface:{config['view_context_surface_id']}", + f"session-surface:{config['session_surface_id']}", + f"interactive-scene-surface:{config['interactive_scene_surface_id']}", + f"scene-context:{config['scene_context_id']}", + f"puzzle:{config['puzzle_id']}", + f"runtime-mode:{config['runtime_mode_id']}", + f"projection:{config['projection_profile_id']}", + f"persistence:{config['primary_persistence_boundary_id']}", + "ownership:dedicated-family", + ] + anchor_tags.extend(config["training_shell_tags"]) + player_start.set_editor_property( + "tags", + [unreal.Name(tag) for tag in dict.fromkeys(anchor_tags)], + ) + + +def ensure_training_shell_actor(config) -> None: + shell_class = require_class(TRAINING_SHELL_CLASS_PATH) + shell_actor = unreal.EditorLevelLibrary.spawn_actor_from_class( + shell_class, + config["shell_location"], + config["shell_rotation"], + ) + if shell_actor is None: + raise RuntimeError( + f"Failed to spawn dedicated training shell actor for {config['family_key']}." + ) + + shell_actor.set_actor_label(f"HT_{config['family_key']}_TrainingShell") + shell_actor.set_editor_property("family_key", config["family_key"]) + shell_actor.set_editor_property("training_shell_id", config["training_shell_id"]) + shell_actor.set_editor_property("title", config["title"]) + shell_actor.set_editor_property("summary", config["summary"]) + shell_actor.set_editor_property("map_asset_path", config["map_asset_path"]) + shell_actor.set_editor_property("activation_profile_id", config["activation_profile_id"]) + shell_actor.set_editor_property("host_surface_id", config["host_surface_id"]) + shell_actor.set_editor_property("launch_surface_id", config["launch_surface_id"]) + shell_actor.set_editor_property("view_context_surface_id", config["view_context_surface_id"]) + shell_actor.set_editor_property("session_surface_id", config["session_surface_id"]) + shell_actor.set_editor_property( + "interactive_scene_surface_id", + config["interactive_scene_surface_id"], + ) + shell_actor.set_editor_property("scene_context_id", config["scene_context_id"]) + shell_actor.set_editor_property("puzzle_id", config["puzzle_id"]) + shell_actor.set_editor_property("runtime_mode_id", config["runtime_mode_id"]) + shell_actor.set_editor_property("projection_profile_id", config["projection_profile_id"]) + shell_actor.set_editor_property( + "primary_persistence_boundary_id", + config["primary_persistence_boundary_id"], + ) + shell_actor.set_editor_property( + "authoring_manifest_relative_path", + MANIFEST_RELATIVE_PATH.replace("\\", "/"), + ) + shell_actor.set_editor_property("dedicated_family_ownership", True) + shell_actor.set_editor_property("training_shell_tags", config["training_shell_tags"]) + shell_tags = [ + AUTHOR_TAG, + f"training-shell-id:{config['training_shell_id']}", + f"activation-profile:{config['activation_profile_id']}", + f"host-surface:{config['host_surface_id']}", + f"launch-surface:{config['launch_surface_id']}", + f"view-context-surface:{config['view_context_surface_id']}", + f"session-surface:{config['session_surface_id']}", + f"interactive-scene-surface:{config['interactive_scene_surface_id']}", + f"scene-context:{config['scene_context_id']}", + f"puzzle:{config['puzzle_id']}", + f"runtime-mode:{config['runtime_mode_id']}", + f"projection:{config['projection_profile_id']}", + f"persistence:{config['primary_persistence_boundary_id']}", + "ownership:dedicated-family", + ] + shell_tags.extend(config["training_shell_tags"]) + shell_actor.set_editor_property( + "tags", + [unreal.Name(tag) for tag in dict.fromkeys(shell_tags)], + ) + try: + shell_actor.rerun_construction_scripts() + except Exception: + log( + "Training shell actor construction rerun was unavailable; " + "continuing with direct metadata state." + ) + + +def resolve_map_file_absolute_path(config) -> str: + return os.path.join(PROJECT_ROOT, config["map_file_relative_path"]) + + +def compute_md5(file_path: str) -> str: + digest = hashlib.md5() + with open(file_path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def ensure_manifest_directory() -> None: + os.makedirs(os.path.dirname(MANIFEST_ABSOLUTE_PATH), exist_ok=True) + + +def build_manifest_entries(): + entries = [] + for config in MAP_CONFIGS: + map_file_absolute_path = resolve_map_file_absolute_path(config) + if not os.path.exists(map_file_absolute_path): + continue + + entries.append( + { + "mapKind": config["map_kind"], + "familyKey": config["family_key"], + "mapAssetPath": config["map_asset_path"], + "mapFileRelativePath": config["map_file_relative_path"].replace("\\", "/"), + "mapHashMd5": compute_md5(map_file_absolute_path), + "trainingShellId": config["training_shell_id"], + "activationProfileId": config["activation_profile_id"], + "hostSurfaceId": config["host_surface_id"], + "launchSurfaceId": config["launch_surface_id"], + "viewContextSurfaceId": config["view_context_surface_id"], + "sessionSurfaceId": config["session_surface_id"], + "interactiveSceneSurfaceId": config["interactive_scene_surface_id"], + "sceneContextId": config["scene_context_id"], + "puzzleId": config["puzzle_id"], + "runtimeModeId": config["runtime_mode_id"], + "projectionProfileId": config["projection_profile_id"], + "primaryPersistenceBoundaryId": config["primary_persistence_boundary_id"], + "authorTag": AUTHOR_TAG, + "authoringManifestRelativePath": MANIFEST_RELATIVE_PATH.replace("\\", "/"), + "authoredViaGameModeClassPath": COACH_DASHBOARD_GAME_MODE_PATH, + "trainingShellTags": config["training_shell_tags"], + } + ) + return entries + + +def write_manifest() -> None: + ensure_manifest_directory() + classic_map_absolute_path = os.path.join( + PROJECT_ROOT, + "UnrealHyperTwist", + "Content", + "HyperTwistTraining", + "Maps", + "L_HyperTwist_ClassicTraining.umap", + ) + manifest = { + "manifestId": "phase6c/dedicated-family-training-map-authoring", + "manifestVersion": "2026.06.18", + "authorTag": AUTHOR_TAG, + "authoringScriptRelativePath": "scripts/hypertwist_author_higher_dimensional_training_maps.py", + "authoredThroughGameModeClassPath": COACH_DASHBOARD_GAME_MODE_PATH, + "classicReferenceMapHashMd5": ( + compute_md5(classic_map_absolute_path) + if os.path.exists(classic_map_absolute_path) + else "" + ), + "entries": build_manifest_entries(), + } + with open(MANIFEST_ABSOLUTE_PATH, "w", encoding="utf-8") as handle: + json.dump(manifest, handle, indent=2) + handle.write("\n") + log(f"Wrote higher-dimensional training-map manifest to {MANIFEST_ABSOLUTE_PATH}") + + +def author_map(config) -> None: + level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) + if level_subsystem is None: + raise RuntimeError("LevelEditorSubsystem was not available.") + + 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, + f"{config['family_key']} dedicated training-shell game mode", + ) + if ENABLE_HEADLESS_TRAINING_SHELL_ACTOR: + ensure_training_shell_actor(config) + 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" + ) + ensure_player_start(config) + log("Placed dedicated surface anchor") + save_current_level_or_raise( + level_subsystem, + f"{config['family_key']} dedicated shell surfaces", + ) + log(f"Finished authoring {config['map_asset_path']}") + + +def main() -> None: + ensure_directory(MAP_ROOT) + requested_map_kind = os.getenv("HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND", "").strip().lower() + write_manifest_requested = requested_map_kind in ("", "all", "both") + if requested_map_kind in ("", "all", "both"): + targets = MAP_CONFIGS + else: + targets = tuple( + config for config in MAP_CONFIGS if config["map_kind"] == requested_map_kind + ) + if not targets: + raise RuntimeError( + f"Unsupported HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND value: {requested_map_kind}" + ) + + for config in targets: + author_map(config) + + if write_manifest_requested: + write_manifest() + + log("Higher-dimensional dedicated-family authored maps are ready.") + + +if __name__ == "__main__": + try: + main() + except Exception as error: + unreal.log_error( + f"[HyperTwistHigherDimensionalMapAuthoring] {error}\n{traceback.format_exc()}" + ) + raise