387 lines
14 KiB
Python
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
|