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" MATERIAL_ROOT = "/Game/HyperTwistTraining/Materials" VERTEX_COLOR_MATERIAL_PATH = f"{MATERIAL_ROOT}/M_HT_ProjectionVertexColor" SAFE_TRANSIENT_LEVEL_PATH = "/Engine/Maps/Entry" AUTHOR_TAG = "HyperTwistVirtual4DTrainingMap" MANIFEST_RELATIVE_PATH = os.path.join( "docs", "generated", "virtual_4d_training_maps", "virtual_4d_dedicated_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 = ( { "order": 2, "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_MagicCube4D_2x2x2x2Training", "game_mode_class_path": ( "/Script/UnrealHyperTwist.HyperTwistMelindaProjectionGameMode" ), "projection_actor_class_path": ( "/Script/UnrealHyperTwist.HyperTwistMelindaProjectionActor" ), "runtime_profile": "melinda-cell-first-exact-runtime-v1", }, { "order": 3, "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_MagicCube4D_3x3x3x3Training", "game_mode_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionGameMode" ), "projection_actor_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor" ), "runtime_profile": "hypercube-3x3x3x3-runtime-v1", }, { "order": 4, "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_MagicCube4D_4x4x4x4Training", "game_mode_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual4444ProjectionGameMode" ), "projection_actor_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor" ), "runtime_profile": "hypercube-4x4x4x4-runtime-v1", }, { "order": 5, "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_MagicCube4D_5x5x5x5Training", "game_mode_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual5555ProjectionGameMode" ), "projection_actor_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor" ), "runtime_profile": "hypercube-5x5x5x5-runtime-v1", }, { "order": 6, "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_MagicCube4D_6x6x6x6Training", "game_mode_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual6666ProjectionGameMode" ), "projection_actor_class_path": ( "/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor" ), "runtime_profile": "hypercube-6x6x6x6-runtime-v1", }, ) def log(message: str) -> None: unreal.log(f"[HyperTwistVirtual4DMapAuthoring] {message}") 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 split_asset_path(asset_path: str): package_path, asset_name = asset_path.rsplit("/", 1) return package_path, asset_name 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 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 create_asset(asset_path: str, asset_class, factory): package_path, asset_name = split_asset_path(asset_path) ensure_directory(package_path) asset = unreal.AssetToolsHelpers.get_asset_tools().create_asset( asset_name, package_path, asset_class, factory, ) if asset is None: raise RuntimeError(f"Failed to create asset: {asset_path}") return asset def ensure_projection_material(): if unreal.EditorAssetLibrary.does_asset_exist(VERTEX_COLOR_MATERIAL_PATH): material = require_asset(VERTEX_COLOR_MATERIAL_PATH) else: material = create_asset( VERTEX_COLOR_MATERIAL_PATH, unreal.Material, unreal.MaterialFactoryNew(), ) vertex_color = unreal.MaterialEditingLibrary.create_material_expression( material, unreal.MaterialExpressionVertexColor, -560, -80, ) glow_strength = unreal.MaterialEditingLibrary.create_material_expression( material, unreal.MaterialExpressionScalarParameter, -560, 120, ) glow_strength.set_editor_property("parameter_name", "GlowStrength") glow_strength.set_editor_property("default_value", 1.15) emissive = unreal.MaterialEditingLibrary.create_material_expression( material, unreal.MaterialExpressionMultiply, -240, -20, ) unreal.MaterialEditingLibrary.connect_material_expressions( vertex_color, "RGB", emissive, "A", ) unreal.MaterialEditingLibrary.connect_material_expressions( glow_strength, "", emissive, "B", ) unreal.MaterialEditingLibrary.connect_material_property( emissive, "", unreal.MaterialProperty.MP_EMISSIVE_COLOR, ) unreal.MaterialEditingLibrary.connect_material_property( vertex_color, "A", unreal.MaterialProperty.MP_OPACITY, ) unreal.MaterialEditingLibrary.layout_material_expressions(material) material.set_editor_property("two_sided", True) material.set_editor_property("blend_mode", unreal.BlendMode.BLEND_TRANSLUCENT) material.set_editor_property( "shading_model", unreal.MaterialShadingModel.MSM_UNLIT, ) unreal.MaterialEditingLibrary.recompile_material(material) if not unreal.EditorAssetLibrary.save_loaded_asset(material, False): raise RuntimeError( f"Failed to save projection material: {VERTEX_COLOR_MATERIAL_PATH}" ) log(f"Verified projection material {VERTEX_COLOR_MATERIAL_PATH}") return material def map_file_relative_path(config) -> str: map_name = config["map_asset_path"].rsplit("/", 1)[-1] return os.path.join( "UnrealHyperTwist", "Content", "HyperTwistTraining", "Maps", f"{map_name}.umap", ) def map_file_absolute_path(config) -> str: return os.path.join(PROJECT_ROOT, map_file_relative_path(config)) def map_object_path(config) -> str: map_name = config["map_asset_path"].rsplit("/", 1)[-1] return f"{config['map_asset_path']}.{map_name}" def delete_existing_level(level_subsystem, config) -> None: map_path = config["map_asset_path"] map_file = map_file_absolute_path(config) if ( not os.path.exists(map_file) and not unreal.EditorAssetLibrary.does_asset_exist(map_path) and not unreal.EditorAssetLibrary.does_asset_exist(map_object_path(config)) ): return if not level_subsystem.load_level(SAFE_TRANSIENT_LEVEL_PATH): raise RuntimeError( f"Failed to load {SAFE_TRANSIENT_LEVEL_PATH} before replacing {map_path}." ) for candidate_path in (map_object_path(config), map_path): if unreal.EditorAssetLibrary.does_asset_exist(candidate_path): unreal.EditorAssetLibrary.delete_asset(candidate_path) if os.path.exists(map_file): os.remove(map_file) def configure_world_settings(config) -> 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() world_settings = world.get_world_settings() if world is not None else None if world_settings is None: raise RuntimeError("World settings were not available.") world_settings.set_editor_property( "default_game_mode", require_class(config["game_mode_class_path"]), ) def spawn_actor(actor_class, label: str, location, rotation): 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) return actor def place_map_anchor(config) -> None: order = config["order"] player_start = spawn_actor( unreal.PlayerStart, f"HT_Virtual4D_{order}x_PlayerStart", unreal.Vector(-720.0, 0.0, 180.0), unreal.Rotator(0.0, 0.0, 0.0), ) player_start.set_editor_property( "tags", [ unreal.Name(AUTHOR_TAG), unreal.Name(f"puzzle-order:{order}"), unreal.Name(f"runtime-profile:{config['runtime_profile']}"), unreal.Name("input:keyboard-mouse"), unreal.Name("projection-spawn:runtime-game-mode"), unreal.Name( f"projection-class:{config['projection_actor_class_path']}" ), unreal.Name("ownership:first-party-dedicated-map"), ], ) def validate_current_map(config) -> None: 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 validation.") actors_by_label = { actor.get_actor_label(): actor for actor in actor_subsystem.get_all_level_actors() if actor is not None } order = config["order"] expected_anchor_label = f"HT_Virtual4D_{order}x_PlayerStart" missing = {expected_anchor_label} - set(actors_by_label) if missing: raise RuntimeError( "Authored map is missing required actors: " + ", ".join(sorted(missing)) ) anchor_tags = { str(tag) for tag in actors_by_label[expected_anchor_label].tags } expected_anchor_tags = { AUTHOR_TAG, f"puzzle-order:{order}", f"runtime-profile:{config['runtime_profile']}", "input:keyboard-mouse", "projection-spawn:runtime-game-mode", f"projection-class:{config['projection_actor_class_path']}", "ownership:first-party-dedicated-map", } missing_tags = expected_anchor_tags - anchor_tags if missing_tags: raise RuntimeError( "Authored map anchor is missing required ownership tags: " + ", ".join(sorted(missing_tags)) ) if not unreal.EditorAssetLibrary.does_asset_exist(VERTEX_COLOR_MATERIAL_PATH): raise RuntimeError( f"Authored map projection material is missing: {VERTEX_COLOR_MATERIAL_PATH}" ) world = editor_subsystem.get_editor_world() world_settings = world.get_world_settings() if world is not None else None game_mode = ( world_settings.get_editor_property("default_game_mode") if world_settings is not None else None ) expected_game_mode_name = config["game_mode_class_path"].rsplit(".", 1)[-1] if game_mode is None or game_mode.get_name() != expected_game_mode_name: raise RuntimeError( f"Map {config['map_asset_path']} does not own {expected_game_mode_name}." ) 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 write_receipt(config) -> None: map_file = map_file_absolute_path(config) if not os.path.exists(map_file): raise RuntimeError(f"Authored map file was not saved: {map_file}") os.makedirs(AUTHORING_RECEIPT_DIRECTORY, exist_ok=True) order = config["order"] receipt_path = os.path.join( AUTHORING_RECEIPT_DIRECTORY, f"virtual_4d_{order}x_complete.json", ) receipt = { "schemaVersion": "hypertwist/virtual-4d-map-authoring-receipt/v1", "order": order, "mapAssetPath": config["map_asset_path"], "mapHashMd5": compute_md5(map_file), "gameModeClassPath": config["game_mode_class_path"], "projectionActorClassPath": config["projection_actor_class_path"], "projectionSpawnMode": "runtime-game-mode", "runtimeProfile": config["runtime_profile"], "validatedActorLabels": [ f"HT_Virtual4D_{order}x_PlayerStart", ], "authoringComplete": True, } with open(receipt_path, "w", encoding="utf-8") as handle: json.dump(receipt, handle, indent=2) handle.write("\n") def author_map(config) -> None: level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) if level_subsystem is None: raise RuntimeError("LevelEditorSubsystem was not available.") delete_existing_level(level_subsystem, config) if not level_subsystem.new_level(config["map_asset_path"]): raise RuntimeError(f"Failed to create level: {config['map_asset_path']}") configure_world_settings(config) # Procedural projection actors are runtime-owned. Spawning them under # UnrealEditor-Cmd + NullRHI can crash UE 5.7 before a map is certifiable. place_map_anchor(config) if not level_subsystem.save_current_level(): raise RuntimeError(f"Failed to save level: {config['map_asset_path']}") validate_current_map(config) write_receipt(config) log(f"Authored {config['map_asset_path']}") def write_manifest() -> None: entries = [] for config in MAP_CONFIGS: map_file = map_file_absolute_path(config) if not os.path.exists(map_file): raise RuntimeError(f"Cannot seal manifest; map is missing: {map_file}") order = config["order"] entries.append( { "order": order, "puzzleId": f"hypercube/{order}x{order}x{order}x{order}", "mapAssetPath": config["map_asset_path"], "mapFileRelativePath": map_file_relative_path(config).replace("\\", "/"), "mapHashMd5": compute_md5(map_file), "gameModeClassPath": config["game_mode_class_path"], "projectionActorClassPath": config["projection_actor_class_path"], "projectionSpawnMode": "runtime-game-mode", "runtimeProfile": config["runtime_profile"], "inputMode": "keyboard-mouse", "authorTag": AUTHOR_TAG, } ) os.makedirs(os.path.dirname(MANIFEST_ABSOLUTE_PATH), exist_ok=True) manifest = { "schemaVersion": "hypertwist/virtual-4d-dedicated-map-manifest/v1", "manifestVersion": "2026.07.23", "authoringScriptRelativePath": ( "scripts/hypertwist_author_virtual_4d_training_maps.py" ), "projectionMaterialAssetPath": VERTEX_COLOR_MATERIAL_PATH, "entries": entries, } with open(MANIFEST_ABSOLUTE_PATH, "w", encoding="utf-8") as handle: json.dump(manifest, handle, indent=2) handle.write("\n") log(f"Wrote manifest {MANIFEST_ABSOLUTE_PATH}") def all_map_files_exist() -> bool: return all(os.path.exists(map_file_absolute_path(config)) for config in MAP_CONFIGS) def main() -> None: ensure_directory(MAP_ROOT) ensure_directory(MATERIAL_ROOT) ensure_projection_material() requested_order = os.getenv("HYPERTWIST_VIRTUAL_4D_ORDER", "").strip() targets = MAP_CONFIGS if requested_order: try: order = int(requested_order) except ValueError as error: raise RuntimeError( f"Invalid HYPERTWIST_VIRTUAL_4D_ORDER: {requested_order}" ) from error targets = tuple(config for config in MAP_CONFIGS if config["order"] == order) if not targets: raise RuntimeError(f"Unsupported 4D puzzle order: {order}") for config in targets: author_map(config) if not requested_order or all_map_files_exist(): write_manifest() log("All requested virtual 4D training maps are ready.") if __name__ == "__main__": try: main() except Exception as error: unreal.log_error( f"[HyperTwistVirtual4DMapAuthoring] {error}\n{traceback.format_exc()}" ) raise