hypertwist/scripts/hypertwist_author_classic_cube_training_maps.py
2026-06-12 02:21:20 +00:00

387 lines
14 KiB
Python

import os
import traceback
import unreal
MAP_ROOT = "/Game/HyperTwistTraining/Maps"
MATERIAL_ROOT = "/Game/HyperTwistTraining/Materials"
CLASSIC_MAP_PATH = f"{MAP_ROOT}/L_HyperTwist_ClassicTraining"
FOLLOW_ALONG_MAP_PATH = f"{MAP_ROOT}/L_HyperTwist_FollowAlongTraining"
AUTHOR_TAG = "HyperTwistClassicCubeTrainingMap"
SAFE_TRANSIENT_LEVEL_PATH = "/Engine/Maps/Entry"
MASTER_MATERIAL_PATH = f"{MATERIAL_ROOT}/M_HT_ClassicCubeFaceMaster"
INTERNAL_MATERIAL_PATH = f"{MATERIAL_ROOT}/MI_HT_ClassicCube_Internal"
FACE_MATERIAL_SPECS = (
("MI_HT_ClassicCube_Up", unreal.LinearColor(1.0, 1.0, 1.0, 1.0)),
("MI_HT_ClassicCube_Down", unreal.LinearColor(1.0, 0.92, 0.1, 1.0)),
("MI_HT_ClassicCube_Front", unreal.LinearColor(0.0, 0.55, 0.2, 1.0)),
("MI_HT_ClassicCube_Back", unreal.LinearColor(0.0, 0.22, 0.75, 1.0)),
("MI_HT_ClassicCube_Left", unreal.LinearColor(0.92, 0.36, 0.02, 1.0)),
("MI_HT_ClassicCube_Right", unreal.LinearColor(0.78, 0.04, 0.04, 1.0)),
)
def log(message: str) -> None:
unreal.log(f"[HyperTwistClassicCubeMapAuthoring] {message}")
def warn(message: str) -> None:
unreal.log_warning(f"[HyperTwistClassicCubeMapAuthoring] {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 split_asset_path(asset_path: str):
package_path, asset_name = asset_path.rsplit("/", 1)
return package_path, asset_name
def create_asset(asset_path: str, asset_class, factory):
package_path, asset_name = split_asset_path(asset_path)
ensure_directory(package_path)
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
asset = 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_face_master_material():
if unreal.EditorAssetLibrary.does_asset_exist(MASTER_MATERIAL_PATH):
return require_asset(MASTER_MATERIAL_PATH)
material = create_asset(
MASTER_MATERIAL_PATH,
unreal.Material,
unreal.MaterialFactoryNew(),
)
material.set_editor_property("two_sided", False)
base_color = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionVectorParameter,
-640,
-120,
)
base_color.set_editor_property("parameter_name", "BaseColor")
base_color.set_editor_property("default_value", unreal.LinearColor(1.0, 1.0, 1.0, 1.0))
glow_strength = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionScalarParameter,
-640,
80,
)
glow_strength.set_editor_property("parameter_name", "GlowStrength")
glow_strength.set_editor_property("default_value", 0.0)
face_opacity = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionScalarParameter,
-640,
240,
)
face_opacity.set_editor_property("parameter_name", "FaceOpacity")
face_opacity.set_editor_property("default_value", 1.0)
emissive = unreal.MaterialEditingLibrary.create_material_expression(
material,
unreal.MaterialExpressionMultiply,
-280,
0,
)
unreal.MaterialEditingLibrary.connect_material_expressions(base_color, "", emissive, "A")
unreal.MaterialEditingLibrary.connect_material_expressions(glow_strength, "", emissive, "B")
unreal.MaterialEditingLibrary.connect_material_property(
base_color,
"",
unreal.MaterialProperty.MP_BASE_COLOR,
)
unreal.MaterialEditingLibrary.connect_material_property(
emissive,
"",
unreal.MaterialProperty.MP_EMISSIVE_COLOR,
)
unreal.MaterialEditingLibrary.layout_material_expressions(material)
unreal.MaterialEditingLibrary.recompile_material(material)
if not unreal.EditorAssetLibrary.save_loaded_asset(material, False):
raise RuntimeError(f"Failed to save face master material: {MASTER_MATERIAL_PATH}")
log(f"Created face master material {MASTER_MATERIAL_PATH}")
return material
def ensure_material_instance(asset_path: str, parent_material, color: unreal.LinearColor):
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
return require_asset(asset_path)
instance = create_asset(
asset_path,
unreal.MaterialInstanceConstant,
unreal.MaterialInstanceConstantFactoryNew(),
)
unreal.MaterialEditingLibrary.set_material_instance_parent(instance, parent_material)
unreal.MaterialEditingLibrary.set_material_instance_vector_parameter_value(
instance,
"BaseColor",
color,
)
unreal.MaterialEditingLibrary.set_material_instance_scalar_parameter_value(
instance,
"GlowStrength",
0.0,
)
unreal.MaterialEditingLibrary.set_material_instance_scalar_parameter_value(
instance,
"FaceOpacity",
1.0,
)
if not unreal.EditorAssetLibrary.save_loaded_asset(instance, False):
raise RuntimeError(f"Failed to save material instance: {asset_path}")
log(f"Created material instance {asset_path}")
return instance
def ensure_cube_material_family():
ensure_directory(MATERIAL_ROOT)
master_material = ensure_face_master_material()
face_materials = [
ensure_material_instance(f"{MATERIAL_ROOT}/{asset_name}", master_material, color)
for asset_name, color in FACE_MATERIAL_SPECS
]
internal_material = ensure_material_instance(
INTERNAL_MATERIAL_PATH,
master_material,
unreal.LinearColor(0.05, 0.05, 0.05, 1.0),
)
return face_materials, internal_material
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 ensure_floor_plane() -> None:
floor_actor = spawn_actor(
unreal.StaticMeshActor,
"HT_ClassicCubeTraining_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(unreal.Vector(14.0, 14.0, 1.0))
def ensure_directional_light() -> None:
directional_light = spawn_actor(
unreal.DirectionalLight,
"HT_ClassicCubeTraining_Sun",
unreal.Vector(-300.0, 150.0, 420.0),
unreal.Rotator(-42.0, -38.0, 0.0),
)
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", 9.0)
def ensure_skylight() -> None:
skylight = spawn_actor(
unreal.SkyLight,
"HT_ClassicCubeTraining_Sky",
unreal.Vector(0.0, 0.0, 220.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.2)
def ensure_reflection_capture() -> None:
reflection_capture = spawn_actor(
unreal.SphereReflectionCapture,
"HT_ClassicCubeTraining_Reflection",
unreal.Vector(0.0, 0.0, 140.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
reflection_capture.set_actor_scale3d(unreal.Vector(12.0, 12.0, 12.0))
def ensure_player_start() -> None:
spawn_actor(
unreal.PlayerStart,
"HT_ClassicCubeTraining_PlayerStart",
unreal.Vector(-540.0, 0.0, 170.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
def ensure_cube_actor():
cube_class = require_class("/Script/UnrealHyperTwist.HyperTwistClassicCubeActor")
cube_actor = spawn_actor(
cube_class,
"HT_ClassicCubeTraining_Cube",
unreal.Vector(0.0, 0.0, 0.0),
unreal.Rotator(0.0, 0.0, 0.0),
)
face_materials, internal_material = ensure_cube_material_family()
cube_actor.set_editor_property("face_materials", face_materials)
cube_actor.set_editor_property("internal_material", internal_material)
if hasattr(cube_actor, "reset_cube"):
cube_actor.reset_cube()
else:
warn("Classic cube actor did not expose reset_cube() to Python; relying on saved properties.")
return cube_actor
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 recreate_level(level_subsystem, map_asset_path: str) -> None:
if unreal.EditorAssetLibrary.does_asset_exist(map_asset_path):
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}'."
)
if not unreal.EditorAssetLibrary.delete_asset(map_asset_path):
raise RuntimeError(f"Failed to delete existing level: {map_asset_path}")
log(f"Deleted existing level {map_asset_path} before rebuild")
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 author_map(map_asset_path: str, game_mode_class_path: str) -> None:
level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
if level_subsystem is None:
raise RuntimeError("LevelEditorSubsystem was not available.")
recreate_level(level_subsystem, map_asset_path)
save_current_level_or_raise(level_subsystem, "level recreation")
configure_world_settings(game_mode_class_path)
save_current_level_or_raise(level_subsystem, "world settings")
ensure_floor_plane()
save_current_level_or_raise(level_subsystem, "floor placement")
ensure_directional_light()
save_current_level_or_raise(level_subsystem, "directional light placement")
ensure_skylight()
save_current_level_or_raise(level_subsystem, "skylight placement")
ensure_reflection_capture()
save_current_level_or_raise(level_subsystem, "reflection capture placement")
ensure_player_start()
save_current_level_or_raise(level_subsystem, "player start placement")
ensure_cube_actor()
save_current_level_or_raise(level_subsystem, "cube placement")
log(f"Finished authoring {map_asset_path}")
def main() -> None:
ensure_directory(MAP_ROOT)
ensure_directory(MATERIAL_ROOT)
ensure_cube_material_family()
requested_map_kind = os.getenv("HYPERTWIST_CLASSIC_CUBE_MAP_KIND", "").strip().lower()
if requested_map_kind in ("", "all", "both"):
targets = (
(
CLASSIC_MAP_PATH,
"/Script/UnrealHyperTwist.HyperTwistClassicCubeGameMode",
),
(
FOLLOW_ALONG_MAP_PATH,
"/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode",
),
)
elif requested_map_kind == "classic":
targets = (
(
CLASSIC_MAP_PATH,
"/Script/UnrealHyperTwist.HyperTwistClassicCubeGameMode",
),
)
elif requested_map_kind in ("followalong", "follow-along"):
targets = (
(
FOLLOW_ALONG_MAP_PATH,
"/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode",
),
)
else:
raise RuntimeError(
f"Unsupported HYPERTWIST_CLASSIC_CUBE_MAP_KIND value: {requested_map_kind}"
)
for map_asset_path, game_mode_class_path in targets:
author_map(map_asset_path, game_mode_class_path)
log("Classic cube authored maps are ready.")
if __name__ == "__main__":
try:
main()
except Exception as error:
unreal.log_error(
f"[HyperTwistClassicCubeMapAuthoring] {error}\n{traceback.format_exc()}"
)
raise