From 403e9a5cca36e3596fa8f0a61a958ca2ac3742f7 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Thu, 19 Oct 2023 16:06:03 -0700 Subject: [PATCH 01/14] Update README.md Clarified the need for manually assigning actions after importing PSAs. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2e6de7c..2452624 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ This Blender add-on allows you to import and export meshes and animations to and 3. Select the PSA file you want to import. 4. Select the sequences that you want to import and click `Import`. +> Note that in order to see the imported actions applied to your armature, you must use the [Dope Sheet](https://docs.blender.org/manual/en/latest/editors/dope_sheet/introduction.html) or [Nonlinear Animation](https://docs.blender.org/manual/en/latest/editors/nla/introduction.html) editors. + # FAQ ## Why are the mesh normals not accurate when importing a PSK extracted from [UE Viewer](https://www.gildor.org/en/projects/umodel)? If preserving the mesh normals of models is important for your workflow, it is *not recommended* to export PSK files from UE Viewer. This is because UE Viewer makes no attempt to reconstruct the original [smoothing groups](https://en.wikipedia.org/wiki/Smoothing_group). As a result, the normals of imported PSK files will be incorrect when imported into Blender and will need to be manually fixed. From 76440affdb795ce0d109dc87da93aae9a9cc603d Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 7 Nov 2023 18:33:18 -0800 Subject: [PATCH 02/14] Initial commit for handling the PSA config --- io_scene_psk_psa/__init__.py | 2 + io_scene_psk_psa/psa/config.py | 48 +++++++++++++++++++++++ io_scene_psk_psa/psa/import_/operators.py | 7 ++++ io_scene_psk_psa/psa/importer.py | 28 +++++++++---- 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 io_scene_psk_psa/psa/config.py diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index f9d5968..7092d7e 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -30,6 +30,7 @@ if 'bpy' in locals(): importlib.reload(psk_import_operators) importlib.reload(psa_data) + importlib.reload(psa_config) importlib.reload(psa_reader) importlib.reload(psa_writer) importlib.reload(psa_builder) @@ -55,6 +56,7 @@ else: from .psk.import_ import operators as psk_import_operators from .psa import data as psa_data + from .psa import config as psa_config from .psa import reader as psa_reader from .psa import writer as psa_writer from .psa import builder as psa_builder diff --git a/io_scene_psk_psa/psa/config.py b/io_scene_psk_psa/psa/config.py new file mode 100644 index 0000000..f9e1515 --- /dev/null +++ b/io_scene_psk_psa/psa/config.py @@ -0,0 +1,48 @@ +import re +from configparser import ConfigParser +from typing import Dict + +from .reader import PsaReader + +REMOVE_TRACK_LOCATION = (1 << 0) +REMOVE_TRACK_ROTATION = (1 << 1) + + +class PsaConfig: + def __init__(self): + self.sequence_bone_flags: Dict[str, Dict[int, int]] = dict() + + +def read_psa_config(psa_reader: PsaReader, file_path: str) -> PsaConfig: + psa_config = PsaConfig() + + config = ConfigParser() + config.read(file_path) + + psa_sequence_names = list(psa_reader.sequences.keys()) + lowercase_sequence_names = [sequence_name.lower() for sequence_name in psa_sequence_names] + + if config.has_section('RemoveTracks'): + for key, value in config.items('RemoveTracks'): + match = re.match(f'^(.+)\.(\d+)$', key) + sequence_name = match.group(1) + bone_index = int(match.group(2)) + + # Map the sequence name onto the actual sequence name in the PSA file. + try: + sequence_name = psa_sequence_names[lowercase_sequence_names.index(sequence_name.lower())] + except ValueError: + pass + + if sequence_name not in psa_config.sequence_bone_flags: + psa_config.sequence_bone_flags[sequence_name] = dict() + + match value: + case 'all': + psa_config.sequence_bone_flags[sequence_name][bone_index] = (REMOVE_TRACK_LOCATION | REMOVE_TRACK_ROTATION) + case 'trans': + psa_config.sequence_bone_flags[sequence_name][bone_index] = REMOVE_TRACK_LOCATION + case 'rot': + psa_config.sequence_bone_flags[sequence_name][bone_index] = REMOVE_TRACK_ROTATION + + return psa_config diff --git a/io_scene_psk_psa/psa/import_/operators.py b/io_scene_psk_psa/psa/import_/operators.py index 08af962..afa964d 100644 --- a/io_scene_psk_psa/psa/import_/operators.py +++ b/io_scene_psk_psa/psa/import_/operators.py @@ -1,10 +1,12 @@ import os +from pathlib import Path from bpy.props import StringProperty from bpy.types import Operator, Event, Context from bpy_extras.io_utils import ImportHelper from .properties import get_visible_sequences +from ..config import read_psa_config from ..importer import import_psa, PsaImportOptions from ..reader import PsaReader @@ -169,6 +171,11 @@ class PSA_OT_import(Operator, ImportHelper): options.fps_source = pg.fps_source options.fps_custom = pg.fps_custom + # Read the PSA config file if it exists. + config_path = Path(self.filepath).with_suffix('.config') + if config_path.exists(): + options.psa_config = read_psa_config(psa_reader, str(config_path)) + if len(sequence_names) == 0: self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected') return {'CANCELLED'} diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index c837e11..c4a66cc 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -6,6 +6,7 @@ import numpy from bpy.types import FCurve, Object, Context from mathutils import Vector, Quaternion +from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION from .data import Psa from .reader import PsaReader @@ -23,6 +24,7 @@ class PsaImportOptions(object): self.bone_mapping_mode = 'CASE_INSENSITIVE' self.fps_source = 'SEQUENCE' self.fps_custom: float = 30.0 + self.psa_config: PsaConfig = PsaConfig() class ImportBone(object): @@ -162,6 +164,11 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, sequence_name = sequence.name.decode('windows-1252') action_name = options.action_name_prefix + sequence_name + # Get the bone track flags for this sequence, or an empty dictionary if none exist. + sequence_bone_track_flags = dict() + if sequence_name in options.psa_config.sequence_bone_flags.keys(): + sequence_bone_track_flags = options.psa_config.sequence_bone_flags[sequence_name] + if options.should_overwrite and action_name in bpy.data.actions: action = bpy.data.actions[action_name] else: @@ -187,18 +194,21 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, # 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(): + bone_track_flags = sequence_bone_track_flags.get(psa_bone_index, 0) import_bone = import_bones[psa_bone_index] pose_bone = import_bone.pose_bone rotation_data_path = pose_bone.path_from_id('rotation_quaternion') location_data_path = pose_bone.path_from_id('location') + 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), # Qw - action.fcurves.new(rotation_data_path, index=1, action_group=pose_bone.name), # Qx - action.fcurves.new(rotation_data_path, index=2, action_group=pose_bone.name), # Qy - action.fcurves.new(rotation_data_path, index=3, action_group=pose_bone.name), # Qz - action.fcurves.new(location_data_path, index=0, action_group=pose_bone.name), # Lx - action.fcurves.new(location_data_path, index=1, action_group=pose_bone.name), # Ly - action.fcurves.new(location_data_path, index=2, action_group=pose_bone.name), # Lz + 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 ] # Read the sequence data matrix from the PSA. @@ -216,11 +226,15 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, # Write the keyframes out. fcurve_data = numpy.zeros(2 * sequence.frame_count, dtype=float) + + # Populate the keyframe time data. fcurve_data[0::2] = [x * keyframe_time_dilation for x in range(sequence.frame_count)] for bone_index, import_bone in enumerate(import_bones): if import_bone is None: continue for fcurve_index, fcurve in enumerate(import_bone.fcurves): + if fcurve is None: + continue fcurve_data[1::2] = sequence_data_matrix[:, bone_index, fcurve_index] fcurve.keyframe_points.add(sequence.frame_count) fcurve.keyframe_points.foreach_set('co', fcurve_data) From c8e5b8eb47bec84d461fb701f2e7c64a3311f0fc Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 10 Nov 2023 23:19:36 -0800 Subject: [PATCH 03/14] Fixed a runtime error when importing PSKs --- io_scene_psk_psa/psk/importer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index 809fbad..3d40c6e 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -227,7 +227,6 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult: for vertex_normal in psk.vertex_normals: normals.append(tuple(vertex_normal)) mesh_data.normals_split_custom_set_from_vertices(normals) - mesh_data.use_auto_smooth = True bm.normal_update() bm.free() From 9fe5aa03889094b7faa963a1b32a1e0def33e741 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 14 Nov 2023 00:50:20 -0800 Subject: [PATCH 04/14] Now using `shade_smooth` to smoothshade imported meshes (same as it did in the pre 4.0 versions) --- io_scene_psk_psa/psk/importer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index 3d40c6e..c701db9 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -222,11 +222,13 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult: # VERTEX NORMALS if psk.has_vertex_normals and options.should_import_vertex_normals: - mesh_data.polygons.foreach_set("use_smooth", [True] * len(mesh_data.polygons)) + mesh_data.polygons.foreach_set('use_smooth', [True] * len(mesh_data.polygons)) normals = [] for vertex_normal in psk.vertex_normals: normals.append(tuple(vertex_normal)) mesh_data.normals_split_custom_set_from_vertices(normals) + else: + mesh_data.shade_smooth() bm.normal_update() bm.free() From b23677f2911c9a631e6785c47b7785737fb69d64 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 14 Nov 2023 01:33:18 -0800 Subject: [PATCH 05/14] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2452624..2e17c48 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ -[![Blender](https://img.shields.io/badge/Blender->=3.4-blue?logo=blender&logoColor=white)](https://www.blender.org/download/ "Download Blender") +[![Blender](https://img.shields.io/badge/Blender->=2.9-blue?logo=blender&logoColor=white)](https://www.blender.org/download/ "Download Blender") [![GitHub release](https://img.shields.io/github/release/DarklightGames/io_scene_psk_psa?include_prereleases=&sort=semver&color=blue)](https://github.com/DarklightGames/io_scene_psk_psa/releases/) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L4L3853VR) This Blender add-on allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) used in many versions of the Unreal Engine. -> **NOTE**: This addon requires Blender 3.4+. If this is not available to you, install version [4.3.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/4.3.0), as it has a minimum Blender version of 2.9, but is no longer receiving new features. +| Blender Version | Addon Version | [LTS](https://en.wikipedia.org/wiki/Long-term_support) | +|-----------------|---------------|-----| +| 4.0 | [6.0.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/latest) | ✔ +| 3.4 - 3.6 | [5.0.6](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/5.0.6) | ✔ +| 2.9 - 3.3 | [4.3.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/4.3.0) | # Features * Full PSK/PSA import and export capabilities. From b1a6fb9dea3ff6048e8fa90ba91d51c8482c032c Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 14 Nov 2023 01:43:32 -0800 Subject: [PATCH 06/14] Updated README --- README.md | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5eabf8b..1176e23 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,15 @@ This Blender add-on allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) used in many versions of the Unreal Engine. -| Blender Version | Addon Version | [LTS](https://en.wikipedia.org/wiki/Long-term_support) | -|-----------------|---------------|-----| -| 4.0 | [6.0.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/latest) | ✔ -| 3.4 - 3.6 | [5.0.6](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/5.0.6) | ✔ -| 2.9 - 3.3 | [4.3.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/4.3.0) | +## Compatibility + +| Blender Version | Addon Version | Long Term Support | +|--------------------------------------------------------------|--------------------------------------------------------------------------------|-------------------| +| 4.0+ | [latest](https://github.com/DarklightGames/io_scene_psk_psa/releases/latest) | TBD | +| [3.4 - 3.6](https://www.blender.org/download/lts/3-6/) | [5.0.5](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/5.0.5) | ✅️ June 2025 | +| [2.93 - 3.3](https://www.blender.org/download/releases/3-3/) | [4.3.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/4.3.0) | ✅️ September 2024 | + +Bug fixes will be issued for legacy addon versions that are under [Blender's LTS maintenance period](https://www.blender.org/download/lts/). Once the LTS period has ended, legacy addon-on versions will no longer be supported by the maintainers of this repository, although we will accept pull requests for bug fixes. # Features * Full PSK/PSA import and export capabilities. @@ -20,16 +24,6 @@ This Blender add-on allows you to import and export meshes and animations to and * PSA sequences can be exported directly from actions or delineated using a scene's [timeline markers](https://docs.blender.org/manual/en/latest/animation/markers.html) or NLA track strips, allowing direct use of the [NLA](https://docs.blender.org/manual/en/latest/editors/nla/index.html) when creating sequences. * Manual re-ordering of material slots when exporting multiple mesh objects. -## Compatibility - -| Blender Version | Addon Version | LTS Support | -|--------------------------------------------------------------|--------------------------------------------------------------------------------|-------------------| -| 4.0+ | [latest](https://github.com/DarklightGames/io_scene_psk_psa/releases/latest) | TBD | -| [3.4 - 3.6](https://www.blender.org/download/lts/3-6/) | [5.0.5](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/5.0.5) | ✅️ June 2025 | -| [2.93 - 3.3](https://www.blender.org/download/releases/3-3/) | [4.3.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/4.3.0) | ✅️ September 2024 | - -Bug fixes will be issued for legacy addon versions that are under [Blender's LTS maintenance period](https://www.blender.org/download/lts/). Once the LTS period has ended, legacy addon-on versions will no longer be supported by the maintainers of this repository, although we will accept pull requests for bug fixes. - # Installation 1. Download the zip file for the latest version from the [releases](https://github.com/DarklightGames/io_export_psk_psa/releases) page. 2. Open Blender 4.0.0 or later. From 3cf10abe915848350536b41304a93a7a592aad1a Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Mon, 20 Nov 2023 13:39:26 -0800 Subject: [PATCH 07/14] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1176e23..99782e2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L4L3853VR) -This Blender add-on allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) used in many versions of the Unreal Engine. +This Blender addon allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) used in many versions of the Unreal Engine. ## Compatibility @@ -13,7 +13,7 @@ This Blender add-on allows you to import and export meshes and animations to and | [3.4 - 3.6](https://www.blender.org/download/lts/3-6/) | [5.0.5](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/5.0.5) | ✅️ June 2025 | | [2.93 - 3.3](https://www.blender.org/download/releases/3-3/) | [4.3.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/4.3.0) | ✅️ September 2024 | -Bug fixes will be issued for legacy addon versions that are under [Blender's LTS maintenance period](https://www.blender.org/download/lts/). Once the LTS period has ended, legacy addon-on versions will no longer be supported by the maintainers of this repository, although we will accept pull requests for bug fixes. +Bug fixes will be issued for legacy addon versions that are under [Blender's LTS maintenance period](https://www.blender.org/download/lts/). Once the LTS period has ended, legacy addon versions will no longer be supported by the maintainers of this repository, although we will accept pull requests for bug fixes. # Features * Full PSK/PSA import and export capabilities. From 68c7d93d6a5e603b60f9e3f3c20821e93e8fabea Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Wed, 22 Nov 2023 19:19:49 -0800 Subject: [PATCH 08/14] Incremented version to 6.1.0 --- io_scene_psk_psa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index 7092d7e..9f12fa4 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -3,7 +3,7 @@ from bpy.app.handlers import persistent bl_info = { "name": "PSK/PSA Importer/Exporter", "author": "Colin Basnett, Yurii Ti", - "version": (6, 0, 0), + "version": (6, 1, 0), "blender": (4, 0, 0), "description": "PSK/PSA Import/Export (.psk/.psa)", "warning": "", From d0fe7d978605b2f84dd772e2074c8513e0384973 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Wed, 22 Nov 2023 19:20:16 -0800 Subject: [PATCH 09/14] Added progress indicator when exporting PSK files --- io_scene_psk_psa/psk/builder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index 80b3fe4..623ae66 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -146,7 +146,9 @@ def build_psk(context, options: PskBuildOptions) -> PskBuildResult: psk_material.texture_index = len(psk.materials) psk.materials.append(psk_material) - for input_mesh_object in input_objects.mesh_objects: + context.window_manager.progress_begin(0, len(input_objects.mesh_objects)) + + for object_index, input_mesh_object in enumerate(input_objects.mesh_objects): # MATERIALS material_indices = [material_names.index(material_slot.material.name) for material_slot in input_mesh_object.material_slots] @@ -288,6 +290,10 @@ def build_psk(context, options: PskBuildOptions) -> PskBuildResult: bpy.data.meshes.remove(mesh_data) del mesh_data + context.window_manager.progress_update(object_index) + + context.window_manager.progress_end() + result.psk = psk return result From 0c11b326afb70444aa610b036e10a3626a8cae9e Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 16 Jan 2024 20:21:12 -0800 Subject: [PATCH 10/14] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 99782e2..a60f3f0 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,12 @@ Bug fixes will be issued for legacy addon versions that are under [Blender's LTS > Note that in order to see the imported actions applied to your armature, you must use the [Dope Sheet](https://docs.blender.org/manual/en/latest/editors/dope_sheet/introduction.html) or [Nonlinear Animation](https://docs.blender.org/manual/en/latest/editors/nla/introduction.html) editors. # FAQ + +## Why can't I see the animations imported from my PSA? +Simply importing an animation into the scene will not automatically apply the action to the armature. This is in part because a PSA can have multiple sequences imported from it, and also that it's generally bad form for importers to modify the scene when they don't need to. + +The PSA importer creates [Actions](https://docs.blender.org/manual/en/latest/animation/actions.html) for each of the selected sequences in the PSA. These actions can be applied to your armature via the [Action Editor](https://docs.blender.org/manual/en/latest/editors/dope_sheet/action.html) or [NLA Editor](https://docs.blender.org/manual/en/latest/editors/nla/index.html). + ## Why are the mesh normals not accurate when importing a PSK extracted from [UE Viewer](https://www.gildor.org/en/projects/umodel)? If preserving the mesh normals of models is important for your workflow, it is *not recommended* to export PSK files from UE Viewer. This is because UE Viewer makes no attempt to reconstruct the original [smoothing groups](https://en.wikipedia.org/wiki/Smoothing_group). As a result, the normals of imported PSK files will be incorrect when imported into Blender and will need to be manually fixed. From 82310d695ce9beb3e07beec180dfa17a9369ad49 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sat, 20 Jan 2024 15:25:00 -0800 Subject: [PATCH 11/14] Fix for parsing errors on .config files coming from UEViewer --- io_scene_psk_psa/psa/config.py | 58 ++++++++++++++++------ io_scene_psk_psa/psa/import_/operators.py | 22 +++++--- io_scene_psk_psa/psa/import_/properties.py | 6 +++ io_scene_psk_psa/psa/importer.py | 1 + 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/io_scene_psk_psa/psa/config.py b/io_scene_psk_psa/psa/config.py index f9e1515..184d59c 100644 --- a/io_scene_psk_psa/psa/config.py +++ b/io_scene_psk_psa/psa/config.py @@ -13,36 +13,66 @@ class PsaConfig: self.sequence_bone_flags: Dict[str, Dict[int, int]] = dict() +def _load_config_file(file_path: str) -> ConfigParser: + """ + UEViewer exports a dialect of INI files that is not compatible with Python's ConfigParser. + Specifically, it allows values in this format: + + [Section] + Key1 + Key2 + + This is not allowed in Python's ConfigParser, which requires a '=' character after each key name. + To work around this, we'll modify the file to add the '=' character after each key name if it is missing. + """ + with open(file_path, 'r') as f: + lines = f.read().split('\n') + + lines = [re.sub(r'^\s*(\w+)\s*$', r'\1=', line) for line in lines] + + contents = '\n'.join(lines) + + config = ConfigParser() + config.read_string(contents) + + return config + + +def _get_bone_flags_from_value(value: str) -> int: + match value: + case 'all': + return (REMOVE_TRACK_LOCATION | REMOVE_TRACK_ROTATION) + case 'trans': + return REMOVE_TRACK_LOCATION + case 'rot': + return REMOVE_TRACK_ROTATION + case _: + return 0 + + def read_psa_config(psa_reader: PsaReader, file_path: str) -> PsaConfig: psa_config = PsaConfig() - config = ConfigParser() - config.read(file_path) - - psa_sequence_names = list(psa_reader.sequences.keys()) - lowercase_sequence_names = [sequence_name.lower() for sequence_name in psa_sequence_names] + config = _load_config_file(file_path) if config.has_section('RemoveTracks'): for key, value in config.items('RemoveTracks'): match = re.match(f'^(.+)\.(\d+)$', key) sequence_name = match.group(1) - bone_index = int(match.group(2)) # Map the sequence name onto the actual sequence name in the PSA file. try: + psa_sequence_names = list(psa_reader.sequences.keys()) + lowercase_sequence_names = [sequence_name.lower() for sequence_name in psa_sequence_names] sequence_name = psa_sequence_names[lowercase_sequence_names.index(sequence_name.lower())] except ValueError: - pass + # Sequence name is not in the PSA file. + continue if sequence_name not in psa_config.sequence_bone_flags: psa_config.sequence_bone_flags[sequence_name] = dict() - match value: - case 'all': - psa_config.sequence_bone_flags[sequence_name][bone_index] = (REMOVE_TRACK_LOCATION | REMOVE_TRACK_ROTATION) - case 'trans': - psa_config.sequence_bone_flags[sequence_name][bone_index] = REMOVE_TRACK_LOCATION - case 'rot': - psa_config.sequence_bone_flags[sequence_name][bone_index] = REMOVE_TRACK_ROTATION + bone_index = int(match.group(2)) + psa_config.sequence_bone_flags[sequence_name][bone_index] = _get_bone_flags_from_value(value) return psa_config diff --git a/io_scene_psk_psa/psa/import_/operators.py b/io_scene_psk_psa/psa/import_/operators.py index afa964d..4b75a1a 100644 --- a/io_scene_psk_psa/psa/import_/operators.py +++ b/io_scene_psk_psa/psa/import_/operators.py @@ -158,6 +158,10 @@ class PSA_OT_import(Operator, ImportHelper): psa_reader = PsaReader(self.filepath) sequence_names = [x.action_name for x in pg.sequence_list if x.is_selected] + if len(sequence_names) == 0: + self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected') + return {'CANCELLED'} + options = PsaImportOptions() options.sequence_names = sequence_names options.should_use_fake_user = pg.should_use_fake_user @@ -171,14 +175,14 @@ class PSA_OT_import(Operator, ImportHelper): options.fps_source = pg.fps_source options.fps_custom = pg.fps_custom - # Read the PSA config file if it exists. - config_path = Path(self.filepath).with_suffix('.config') - if config_path.exists(): - options.psa_config = read_psa_config(psa_reader, str(config_path)) - - if len(sequence_names) == 0: - self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected') - return {'CANCELLED'} + if options.should_use_config_file: + # Read the PSA config file if it exists. + config_path = Path(self.filepath).with_suffix('.config') + if config_path.exists(): + try: + options.psa_config = read_psa_config(psa_reader, str(config_path)) + except Exception as e: + self.report({'WARNING'}, f'Failed to read PSA config file: {e}') result = import_psa(context, psa_reader, context.view_layer.objects.active, options) @@ -258,6 +262,8 @@ class PSA_OT_import(Operator, ImportHelper): col.use_property_decorate = False col.prop(pg, 'should_use_fake_user') col.prop(pg, 'should_stash') + col.prop(pg, 'should_use_config_file') + col.prop(pg, 'should_use_action_name_prefix') if pg.should_use_action_name_prefix: diff --git a/io_scene_psk_psa/psa/import_/properties.py b/io_scene_psk_psa/psa/import_/properties.py index b7e14f5..43dd375 100644 --- a/io_scene_psk_psa/psa/import_/properties.py +++ b/io_scene_psk_psa/psa/import_/properties.py @@ -32,6 +32,12 @@ class PSA_PG_import(PropertyGroup): description='Assign each imported action a fake user so that the data block is ' 'saved even it has no users', options=empty_set) + should_use_config_file: BoolProperty(default=True, name='Use Config File', + description='Use the .config file that is sometimes generated when the PSA ' + 'file is exported from UEViewer. This file contains ' + 'options that can be used to filter out certain bones tracks ' + 'from the imported actions', + options=empty_set) should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track', options=empty_set) diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index c4a66cc..5864645 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -24,6 +24,7 @@ class PsaImportOptions(object): self.bone_mapping_mode = 'CASE_INSENSITIVE' self.fps_source = 'SEQUENCE' self.fps_custom: float = 30.0 + self.should_use_config_file = True self.psa_config: PsaConfig = PsaConfig() From d0f64a6546dccec8035d97f981edf09ab98249c8 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sat, 20 Jan 2024 15:25:14 -0800 Subject: [PATCH 12/14] Incremented version to 6.1.1 --- io_scene_psk_psa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index 9f12fa4..aa72ddc 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -3,7 +3,7 @@ from bpy.app.handlers import persistent bl_info = { "name": "PSK/PSA Importer/Exporter", "author": "Colin Basnett, Yurii Ti", - "version": (6, 1, 0), + "version": (6, 1, 1), "blender": (4, 0, 0), "description": "PSK/PSA Import/Export (.psk/.psa)", "warning": "", From b4712295847fabb0ca3e989cfe7236add6e43dae Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sat, 20 Jan 2024 15:25:48 -0800 Subject: [PATCH 13/14] Rephrased description of PSA export option --- io_scene_psk_psa/psa/export/properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/psa/export/properties.py b/io_scene_psk_psa/psa/export/properties.py index 518adf9..59cd755 100644 --- a/io_scene_psk_psa/psa/export/properties.py +++ b/io_scene_psk_psa/psa/export/properties.py @@ -152,7 +152,7 @@ class PSA_PG_export(PropertyGroup): default=False, name='Enforce Bone Name Restrictions', description='Bone names restrictions will be enforced. Note that bone names without properly formatted names ' - 'cannot be referenced in scripts' + 'may not be able to be referenced in-engine' ) sequence_name_prefix: StringProperty(name='Prefix', options=empty_set) sequence_name_suffix: StringProperty(name='Suffix', options=empty_set) From 361a7f021859695b30ed2baeda47bbe994e6f4a2 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sat, 20 Jan 2024 15:31:26 -0800 Subject: [PATCH 14/14] Added the name of the imported PSK object to the info report --- io_scene_psk_psa/psk/import_/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/psk/import_/operators.py b/io_scene_psk_psa/psk/import_/operators.py index 1cabb22..e90b44c 100644 --- a/io_scene_psk_psa/psk/import_/operators.py +++ b/io_scene_psk_psa/psk/import_/operators.py @@ -113,7 +113,7 @@ class PSK_OT_import(Operator, ImportHelper): message += '\n'.join(result.warnings) self.report({'WARNING'}, message) else: - self.report({'INFO'}, f'PSK imported') + self.report({'INFO'}, f'PSK imported ({options.name})') return {'FINISHED'}