From dd1ea683bb6f8d8cfc0bdaf46d03e21ab6b05a48 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Mon, 6 Oct 2025 17:48:51 -0700 Subject: [PATCH] Added bone mapping option to ignore trailing whitespace Some very old PSKs and PSAs have trailing spaces in the bone names instead of padding the buffer with null bytes. Trailing whitespace will now be ignored by default to maximize compatibility. --- io_scene_psk_psa/psa/import_/operators.py | 31 +++++++++++---- io_scene_psk_psa/psa/import_/properties.py | 13 ++++--- io_scene_psk_psa/psa/importer.py | 44 ++++++++++++++++------ 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/io_scene_psk_psa/psa/import_/operators.py b/io_scene_psk_psa/psa/import_/operators.py index 6fca9b5..7010a9c 100644 --- a/io_scene_psk_psa/psa/import_/operators.py +++ b/io_scene_psk_psa/psa/import_/operators.py @@ -8,7 +8,7 @@ from bpy_extras.io_utils import ImportHelper from .properties import PsaImportMixin, get_visible_sequences from ..config import read_psa_config -from ..importer import PsaImportOptions, import_psa +from ..importer import BoneMapping, PsaImportOptions, import_psa from ..reader import PsaReader @@ -188,7 +188,10 @@ def psa_import_options_from_property_group(pg: PsaImportMixin, sequence_names: I options.should_write_metadata = pg.should_write_metadata options.should_write_keyframes = pg.should_write_keyframes options.should_convert_to_samples = pg.should_convert_to_samples - options.bone_mapping_mode = pg.bone_mapping_mode + options.bone_mapping = BoneMapping( + is_case_sensitive=pg.bone_mapping_is_case_sensitive, + should_ignore_trailing_whitespace=pg.bone_mapping_should_ignore_trailing_whitespace + ) options.fps_source = pg.fps_source options.fps_custom = pg.fps_custom options.translation_scale = pg.translation_scale @@ -243,7 +246,10 @@ class PSA_OT_import_all(Operator, PsaImportMixin): options = PsaImportOptions( action_name_prefix=self.action_name_prefix, - bone_mapping_mode=self.bone_mapping_mode, + bone_mapping=BoneMapping( + is_case_sensitive=self.bone_mapping_is_case_sensitive, + should_ignore_trailing_whitespace=self.bone_mapping_should_ignore_trailing_whitespace + ), fps_custom=self.fps_custom, fps_source=self.fps_source, sequence_names=sequence_names, @@ -380,10 +386,14 @@ class PSA_OT_import(Operator, ImportHelper, PsaImportMixin): advanced_header.label(text='Advanced') if advanced_panel: - col = advanced_panel.column() - col.use_property_split = True - col.use_property_decorate = False - col.prop(self, 'bone_mapping_mode') + bone_mapping_header, bone_mapping_panel = layout.panel('bone_mapping_id', default_closed=False) + bone_mapping_header.label(text='Bone Mapping') + if bone_mapping_panel: + col = bone_mapping_panel.column() + col.use_property_split = True + col.use_property_decorate = False + col.prop(self, 'bone_mapping_is_case_sensitive') + col.prop(self, 'bone_mapping_should_ignore_trailing_whitespace') col = advanced_panel.column() col.use_property_split = True @@ -422,10 +432,15 @@ def draw_psa_import_options_no_panels(layout, pg: PsaImportMixin): col.use_property_decorate = False col.prop(pg, 'should_convert_to_samples') + col = layout.column(heading='Bone Mapping') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'bone_mapping_is_case_sensitive') + col.prop(pg, 'bone_mapping_should_ignore_trailing_whitespace') + col = layout.column() col.use_property_split = True col.use_property_decorate = False - col.prop(pg, 'bone_mapping_mode') col.prop(pg, 'translation_scale') col = layout.column(heading='Options') diff --git a/io_scene_psk_psa/psa/import_/properties.py b/io_scene_psk_psa/psa/import_/properties.py index 171c5b7..294fc42 100644 --- a/io_scene_psk_psa/psa/import_/properties.py +++ b/io_scene_psk_psa/psa/import_/properties.py @@ -80,12 +80,13 @@ class PsaImportMixin: description='Convert keyframes to read-only samples. ' 'Recommended if you do not plan on editing the actions directly' ) - bone_mapping_mode: EnumProperty( - name='Bone Mapping', - options=set(), - description='The method by which bones from the incoming PSA file are mapped to the armature', - items=bone_mapping_items, - default='CASE_INSENSITIVE' + bone_mapping_is_case_sensitive: BoolProperty( + default=False, + name='Case Sensitive' + ) + bone_mapping_should_ignore_trailing_whitespace: BoolProperty( + default=True, + name='Ignore Trailing Whitespace' ) fps_source: EnumProperty(name='FPS Source', items=fps_source_items) fps_custom: FloatProperty( diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index dc09d9e..6eb45c8 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -2,6 +2,7 @@ from typing import Sequence, Iterable, List, Optional, cast as typing_cast import bpy import numpy as np +import re from bpy.types import Armature, Context, FCurve, Object, Bone, PoseBone from mathutils import Vector, Quaternion @@ -9,11 +10,22 @@ from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION from .reader import PsaReader from ..shared.data import PsxBone +class BoneMapping: + def __init__(self, + is_case_sensitive: bool = False, + should_ignore_trailing_whitespace: bool = True + ): + self.is_case_sensitive = is_case_sensitive + # Ancient PSK and PSA exporters would, for some reason, pad the bone names with spaces + # instead of just writing null bytes, probably because the programmers were lazy. + # By default, we will ignore trailing whitespace when doing comparisons. + self.should_ignore_trailing_whitespace = should_ignore_trailing_whitespace + class PsaImportOptions(object): def __init__(self, action_name_prefix: str = '', - bone_mapping_mode: str = 'CASE_INSENSITIVE', + bone_mapping: BoneMapping = BoneMapping(), fps_custom: float = 30.0, fps_source: str = 'SEQUENCE', psa_config: PsaConfig = PsaConfig(), @@ -28,7 +40,7 @@ class PsaImportOptions(object): translation_scale: float = 1.0 ): self.action_name_prefix = action_name_prefix - self.bone_mapping_mode = bone_mapping_mode + self.bone_mapping = bone_mapping self.fps_custom = fps_custom self.fps_source = fps_source self.psa_config = psa_config @@ -78,20 +90,30 @@ class PsaImportResult: self.warnings: List[str] = [] -def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping_mode: str = 'EXACT') -> Optional[int]: +def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping: BoneMapping) -> Optional[int]: """ @param psa_bone_name: The name of the PSA bone. @param armature_bone_names: The names of the bones in the armature. - @param bone_mapping_mode: One of `['EXACT', 'CASE_INSENSITIVE']`. + @param bone_mapping: Bone mapping information. @return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists. """ + # Use regular expressions for bone name matching. + pattern = psa_bone_name + flags = 0 + + if bone_mapping.should_ignore_trailing_whitespace: + psa_bone_name = psa_bone_name.rstrip() + pattern += r'\s*' + + if not bone_mapping.is_case_sensitive: + flags = re.IGNORECASE + + pattern = re.compile(pattern, flags) + for armature_bone_index, armature_bone_name in enumerate(armature_bone_names): - if bone_mapping_mode == 'CASE_INSENSITIVE': - if armature_bone_name.lower() == psa_bone_name.lower(): - return armature_bone_index - else: - if armature_bone_name == psa_bone_name: - return armature_bone_index + if re.fullmatch(pattern, armature_bone_name): + return armature_bone_index + return None @@ -161,7 +183,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, for psa_bone_index, psa_bone in enumerate(psa_reader.bones): psa_bone_name: str = psa_bone.name.decode('windows-1252') - armature_bone_index = _get_armature_bone_index_for_psa_bone(psa_bone_name, armature_bone_names, options.bone_mapping_mode) + armature_bone_index = _get_armature_bone_index_for_psa_bone(psa_bone_name, armature_bone_names, options.bone_mapping) if armature_bone_index is not None: # Ensure that no other PSA bone has been mapped to this armature bone yet. if armature_bone_index not in armature_to_psa_bone_indices: