579 lines
23 KiB
Python
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
|