diff --git a/Dockerfile b/Dockerfile index 9b0f725..9dd2709 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,5 @@ FROM ubuntu:22.04 -ARG BLENDER_VERSION=4.4 - RUN apt-get update -y && \ apt-get install -y libxxf86vm-dev libxfixes3 libxi-dev libxkbcommon-x11-0 libgl1 libglx-mesa0 python3 python3-pip \ libxrender1 libsm6 @@ -10,6 +8,8 @@ RUN pip install --upgrade pip RUN pip install pytest-blender RUN pip install blender-downloader +ARG BLENDER_VERSION=5.0 + # Set BLENDER_EXECUTABLE and BLENDER_PYTHON as environment variables RUN BLENDER_EXECUTABLE=$(blender-downloader $BLENDER_VERSION --extract --remove-compressed --print-blender-executable) && \ BLENDER_PYTHON=$(pytest-blender --blender-executable "${BLENDER_EXECUTABLE}") && \ diff --git a/io_scene_psk_psa/blender_manifest.toml b/io_scene_psk_psa/blender_manifest.toml index 46f3596..9f5923c 100644 --- a/io_scene_psk_psa/blender_manifest.toml +++ b/io_scene_psk_psa/blender_manifest.toml @@ -1,13 +1,13 @@ schema_version = "1.0.0" id = "io_scene_psk_psa" -version = "8.2.4" +version = "9.0.0" name = "Unreal PSK/PSA (.psk/.psa)" tagline = "Import and export PSK and PSA files used in Unreal Engine" maintainer = "Colin Basnett " type = "add-on" website = "https://github.com/DarklightGames/io_scene_psk_psa/" tags = ["Game Engine", "Import-Export"] -blender_version_min = "4.4.0" +blender_version_min = "5.0.0" # Optional: maximum supported Blender version # blender_version_max = "5.1.0" license = [ diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index 774177a..2abeb0f 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -4,7 +4,7 @@ from .data import Psa from typing import Dict, List, Optional, Tuple from mathutils import Matrix, Quaternion, Vector -from ..shared.helpers import create_psx_bones, get_coordinate_system_transform +from ..shared.helpers import PsxBoneCollection, create_psx_bones, get_coordinate_system_transform class PsaBuildSequence: @@ -30,7 +30,7 @@ class PsaBuildOptions: self.animation_data: Optional[AnimData] = None self.sequences: List[PsaBuildSequence] = [] self.bone_filter_mode: str = 'ALL' - self.bone_collection_indices: List[PsaBoneCollectionIndex] = [] + self.bone_collection_indices: List[PsxBoneCollection] = [] self.sequence_name_prefix: str = '' self.sequence_name_suffix: str = '' self.scale = 1.0 diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 342f41c..9b1a3e8 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -18,6 +18,7 @@ from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions from ..writer import write_psa from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range, PsxBoneCollection from ...shared.ui import draw_bone_filter_mode +from ...shared.types import PSX_PG_action_export, PSX_PG_scene_export def get_sequences_propnames_from_source(sequence_source: str) -> Tuple[str, str]: @@ -453,7 +454,7 @@ class PSA_OT_export(Operator, ExportHelper): return {'RUNNING_MODAL'} def execute(self, context): - pg = getattr(context.scene, 'psa_export') + pg = typing_cast(PSA_PG_export, getattr(context.scene, 'psa_export')) # Populate the export sequence list. animation_data_object = get_animation_data_object(context) diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index 6eb45c8..616a285 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -214,6 +214,8 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, ) del armature_bone_names + assert armature_object.pose + # Create intermediate bone data for import operations. import_bones = [] psa_bone_names_to_import_bones = dict() @@ -277,7 +279,12 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, action = bpy.data.actions[action_name] else: action = bpy.data.actions.new(name=action_name) - action.slots.new('OBJECT', armature_object.name) + action_slot = action.slots.new('OBJECT', armature_object.name) + + # TODO: Wish there was a better way to do this. + action_slot = action.slots[f'OB{armature_object.name}'] + + assert action_slot # Calculate the target FPS. match options.fps_source: @@ -293,7 +300,22 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, if options.should_write_keyframes: # Remove existing f-curves. - action.fcurves.clear() + if len(action.layers) == 0: + layer = action.layers.new(armature_object.name) + else: + layer = action.layers[0] + + if len(layer.strips) == 0: + action_strip = layer.strips.new() + else: + action_strip = layer.strips[0] + + if len(action_strip.channelbags) == 0: + channelbag = action_strip.channelbags.new(action_slot) + else: + channelbag = action_strip.channelbags[0] + + channelbag.fcurves.clear() # Create f-curves for the rotation and location of each bone. for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items(): @@ -305,13 +327,13 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, add_rotation_fcurves = (bone_track_flags & REMOVE_TRACK_ROTATION) == 0 add_location_fcurves = (bone_track_flags & REMOVE_TRACK_LOCATION) == 0 import_bone.fcurves = [ - action.fcurves.new(rotation_data_path, index=0, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qw - action.fcurves.new(rotation_data_path, index=1, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qx - action.fcurves.new(rotation_data_path, index=2, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qy - action.fcurves.new(rotation_data_path, index=3, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qz - action.fcurves.new(location_data_path, index=0, action_group=pose_bone.name) if add_location_fcurves else None, # Lx - action.fcurves.new(location_data_path, index=1, action_group=pose_bone.name) if add_location_fcurves else None, # Ly - action.fcurves.new(location_data_path, index=2, action_group=pose_bone.name) if add_location_fcurves else None, # Lz + channelbag.fcurves.new(rotation_data_path, index=0, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qw + channelbag.fcurves.new(rotation_data_path, index=1, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qx + channelbag.fcurves.new(rotation_data_path, index=2, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qy + channelbag.fcurves.new(rotation_data_path, index=3, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qz + channelbag.fcurves.new(location_data_path, index=0, group_name=pose_bone.name) if add_location_fcurves else None, # Lx + channelbag.fcurves.new(location_data_path, index=1, group_name=pose_bone.name) if add_location_fcurves else None, # Ly + channelbag.fcurves.new(location_data_path, index=2, group_name=pose_bone.name) if add_location_fcurves else None, # Lz ] # Read the sequence data matrix from the PSA. @@ -375,7 +397,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, if animation_data is None: animation_data = armature_object.animation_data_create() for action in actions: - nla_track = armature_object.animation_data.nla_tracks.new() + nla_track = animation_data.nla_tracks.new() nla_track.name = action.name nla_track.mute = True nla_track.strips.new(name=action.name, start=0, action=action) diff --git a/io_scene_psk_psa/shared/helpers.py b/io_scene_psk_psa/shared/helpers.py index 6ad2116..57959a6 100644 --- a/io_scene_psk_psa/shared/helpers.py +++ b/io_scene_psk_psa/shared/helpers.py @@ -345,7 +345,10 @@ def create_psx_bones( coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis) coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion() - total_bone_count = sum(len(armature_object.data.bones) for armature_object in armature_objects) + total_bone_count = 0 + for armature_object in filter(lambda x: x.data is not None, armature_objects): + armature_data = typing_cast(Armature, armature_object.data) + total_bone_count += len(armature_data.bones) # Store the bone names to be exported for each armature object. armature_object_bone_names: Dict[Object, List[str]] = dict()