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" HIGHER_DIMENSIONAL_GAME_MODE_PATH = ( "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode" ) ENABLE_HEADLESS_TRAINING_SHELL_ACTOR = ( os.getenv("HYPERTWIST_ENABLE_TRAINING_SHELL_ACTOR", "").strip().lower() in {"1", "true", "yes"} ) 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) AUTHORING_RECEIPT_DIRECTORY = os.path.join( PROJECT_ROOT, "UnrealHyperTwist", "Saved", "HyperTwistMapAuthoring", ) 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), "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), "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_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_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 get_target_receipt_absolute_path(config) -> str: return os.path.join( AUTHORING_RECEIPT_DIRECTORY, f"phase6c_{config['map_kind']}_complete.json", ) def validate_authored_map(config): actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) if actor_subsystem is None or editor_subsystem is None: raise RuntimeError("Editor subsystems were unavailable during authored-map validation.") actor_labels = { actor.get_actor_label() for actor in actor_subsystem.get_all_level_actors() if actor is not None } expected_actor_labels = {f"HT_{config['family_key']}_DedicatedSurfaceAnchor"} missing_actor_labels = sorted(expected_actor_labels - actor_labels) if missing_actor_labels: raise RuntimeError( "Authored map is missing required presentation actors: " + ", ".join(missing_actor_labels) ) world = editor_subsystem.get_editor_world() world_settings = world.get_world_settings() if world is not None else None game_mode_class = ( world_settings.get_editor_property("default_game_mode") if world_settings is not None else None ) if game_mode_class is None or game_mode_class.get_name() != "HyperTwistHigherDimensionalTrainingGameMode": raise RuntimeError("Authored map does not own the dedicated higher-dimensional game mode.") return sorted(expected_actor_labels) def write_target_completion_receipt(config, actor_labels) -> None: map_file_absolute_path = resolve_map_file_absolute_path(config) if not os.path.exists(map_file_absolute_path): raise RuntimeError( f"Cannot write completion receipt because the authored map is missing: {map_file_absolute_path}" ) os.makedirs(AUTHORING_RECEIPT_DIRECTORY, exist_ok=True) receipt_path = get_target_receipt_absolute_path(config) receipt = { "schemaVersion": "hypertwist/phase6c-map-authoring-receipt/v1", "mapKind": config["map_kind"], "mapAssetPath": config["map_asset_path"], "mapHashMd5": compute_md5(map_file_absolute_path), "gameModeClassPath": HIGHER_DIMENSIONAL_GAME_MODE_PATH, "presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components", "validatedActorLabels": actor_labels, "authoringComplete": True, } with open(receipt_path, "w", encoding="utf-8") as handle: json.dump(receipt, handle, indent=2) handle.write("\n") log(f"Wrote hash-bound completion receipt to {receipt_path}") def ensure_manifest_directory() -> None: os.makedirs(os.path.dirname(MANIFEST_ABSOLUTE_PATH), exist_ok=True) 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": HIGHER_DIMENSIONAL_GAME_MODE_PATH, "runtimePresentationClassPath": TRAINING_SHELL_CLASS_PATH, "presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components", "canonicalRenderableElementCount": ( 120 if config["family_key"] == "magic120cell" else 242 ), "trainingShellTags": config["training_shell_tags"], } ) 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.07.19", "authorTag": AUTHOR_TAG, "authoringScriptRelativePath": "scripts/hypertwist_author_higher_dimensional_training_maps.py", "authoredThroughGameModeClassPath": HIGHER_DIMENSIONAL_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") save_current_level_or_raise(level_subsystem, "clean level recreation") configure_world_settings(HIGHER_DIMENSIONAL_GAME_MODE_PATH) log("Configured dedicated higher-dimensional runtime game mode") save_current_level_or_raise( level_subsystem, f"{config['family_key']} dedicated training-shell game mode", ) ensure_player_start(config) save_current_level_or_raise(level_subsystem, "dedicated surface-anchor placement") log( "Placed dedicated surface anchor; floor, lighting, and projection presentation " "are self-healing runtime-owned surfaces" ) if ENABLE_HEADLESS_TRAINING_SHELL_ACTOR: ensure_training_shell_actor(config) save_current_level_or_raise(level_subsystem, "dedicated training-shell actor placement") log("Placed dedicated training shell actor") else: log( "Skipped dedicated training shell actor on the headless authoring lane; " "the dedicated game mode owns deterministic runtime spawning" ) save_current_level_or_raise( level_subsystem, f"{config['family_key']} final dedicated shell surfaces", ) validated_actor_labels = validate_authored_map(config) write_target_completion_receipt(config, validated_actor_labels) log(f"Finished authoring {config['map_asset_path']}") 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