hypertwist/scripts/hypertwist_author_higher_dimensional_training_maps.py
2026-06-18 10:25:35 +00:00

579 lines
23 KiB
Python

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