593 lines
24 KiB
Python
593 lines
24 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"
|
|
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
|