11from ctypes import sizeof
2- from typing import List
2+ from pathlib import Path
3+ from typing import BinaryIO , Sequence
34
4- import numpy as np
5-
6- from .data import Psa
5+ from .data import Psa , PsaSectionName
76from ..shared .data import Section , PsxBone
7+ from ..shared .helpers import read_types
88
99
10- def _try_fix_cue4parse_issue_103 (sequences ) -> bool :
10+ def _try_fix_cue4parse_issue_103 (sequences : Sequence [ Psa . Sequence ] ) -> bool :
1111 # Detect if the file was exported from CUE4Parse prior to the fix for issue #103.
1212 # https://github.com/FabianFG/CUE4Parse/issues/103
1313 # The issue was that the frame_start_index was not being set correctly, and was always being set to the same value
@@ -24,65 +24,81 @@ def _try_fix_cue4parse_issue_103(sequences) -> bool:
2424 return False
2525
2626
27- class PsaReader (object ):
27+ def read_psa (fp : BinaryIO ):
28+ psa = Psa ()
29+ while fp .read (1 ):
30+ fp .seek (- 1 , 1 )
31+ section = Section .from_buffer_copy (fp .read (sizeof (Section )))
32+ match section .name :
33+ case PsaSectionName .ANIMHEAD :
34+ pass
35+ case PsaSectionName .BONENAMES :
36+ read_types (fp , PsxBone , section , psa .bones )
37+ case PsaSectionName .ANIMINFO :
38+ sequences : list [Psa .Sequence ] = []
39+ read_types (fp , Psa .Sequence , section , sequences )
40+ # Try to fix CUE4Parse bug, if necessary.
41+ _try_fix_cue4parse_issue_103 (sequences )
42+ for sequence in sequences :
43+ psa .sequences [sequence .name .decode ()] = sequence
44+ case PsaSectionName .ANIMKEYS :
45+ read_types (fp , Psa .Key , section , psa .keys )
46+ case PsaSectionName .SCALEKEYS :
47+ read_types (fp , Psa .ScaleKey , section , psa .scale_keys )
48+ case _:
49+ fp .seek (section .data_size * section .data_count , 1 )
50+ print (f'Unrecognized section in PSA: { section .name !r} ' )
51+ return psa
52+
53+
54+ class PsaReader :
2855 """
2956 This class reads the sequences and bone information immediately upon instantiation and holds onto a file handle.
57+
3058 The keyframe data is not read into memory upon instantiation due to its potentially very large size.
31- To read the key data for a particular sequence, call :read_sequence_keys.
59+
60+ To read the key data for a particular sequence, call `read_sequence_keys`.
3261 """
3362
34- def __init__ (self , path ):
35- self .keys_data_offset : int = 0
36- self .fp = open (path , 'rb' )
37- self .psa : Psa = self ._read (self .fp )
63+ def __init__ (self , fp : BinaryIO ):
64+ self ._keys_data_offset : int = 0
65+ self ._scale_keys_data_offset : int | None = None
66+ self ._fp = fp
67+ self ._psa : Psa = self ._read (self ._fp )
68+
69+ @staticmethod
70+ def from_path (path : str | Path ):
71+ return PsaReader (open (path , 'rb' ))
3872
3973 def __enter__ (self ):
4074 return self
4175
4276 def __exit__ (self , exc_type , exc_val , exc_tb ):
43- self .fp .close ()
77+ self ._fp .close ()
4478
4579 @property
4680 def bones (self ):
47- return self .psa .bones
81+ return self ._psa .bones
4882
4983 @property
5084 def sequences (self ):
51- return self .psa .sequences
85+ return self ._psa .sequences
5286
53- def read_sequence_data_matrix (self , sequence_name : str ) -> np .ndarray :
54- """
55- Reads and returns the data matrix for the given sequence.
56-
57- @param sequence_name: The name of the sequence.
58- @return: An FxBx7 matrix where F is the number of frames, B is the number of bones.
59- """
60- sequence = self .psa .sequences [sequence_name ]
61- keys = self .read_sequence_keys (sequence_name )
62- bone_count = len (self .bones )
63- matrix_size = sequence .frame_count , bone_count , 7
64- matrix = np .zeros (matrix_size )
65- keys_iter = iter (keys )
66- for frame_index in range (sequence .frame_count ):
67- for bone_index in range (bone_count ):
68- matrix [frame_index , bone_index , :] = list (next (keys_iter ).data )
69- return matrix
70-
71- def read_sequence_keys (self , sequence_name : str ) -> List [Psa .Key ]:
87+ def read_sequence_keys (self , sequence_name : str ) -> list [Psa .Key ]:
7288 """
7389 Reads and returns the key data for a sequence.
7490
7591 @param sequence_name: The name of the sequence.
7692 @return: A list of Psa.Keys.
7793 """
7894 # Set the file reader to the beginning of the keys data
79- sequence = self .psa .sequences [sequence_name ]
95+ sequence = self ._psa .sequences [sequence_name ]
8096 data_size = sizeof (Psa .Key )
81- bone_count = len (self .psa .bones )
97+ bone_count = len (self ._psa .bones )
8298 buffer_length = data_size * bone_count * sequence .frame_count
83- sequence_keys_offset = self .keys_data_offset + (sequence .frame_start_index * bone_count * data_size )
84- self .fp .seek (sequence_keys_offset , 0 )
85- buffer = self .fp .read (buffer_length )
99+ sequence_keys_offset = self ._keys_data_offset + (sequence .frame_start_index * bone_count * data_size )
100+ self ._fp .seek (sequence_keys_offset , 0 )
101+ buffer = self ._fp .read (buffer_length )
86102 offset = 0
87103 keys = []
88104 for _ in range (sequence .frame_count * bone_count ):
@@ -91,44 +107,72 @@ def read_sequence_keys(self, sequence_name: str) -> List[Psa.Key]:
91107 offset += data_size
92108 return keys
93109
94- @staticmethod
95- def _read_types (fp , data_class , section : Section , data ):
96- buffer_length = section .data_size * section .data_count
97- buffer = fp .read (buffer_length )
110+ def read_sequence_scale_keys (self , sequence_name : str ) -> list [Psa .ScaleKey ]:
111+ """
112+ Reads and returns the scale key data for a sequence.
113+
114+ @param sequence_name: The name of the sequence.
115+ @return: A list of Psa.ScaleKeys.
116+ """
117+ if self ._scale_keys_data_offset is None :
118+ # This PSA has no scale keys, return an empty list.
119+ return []
120+ sequence = self ._psa .sequences [sequence_name ]
121+ data_size = sizeof (Psa .ScaleKey )
122+ bone_count = len (self ._psa .bones )
123+ buffer_length = data_size * bone_count * sequence .frame_count
124+ if buffer_length == 0 :
125+ # In many cases, files are written with this section, but have no data (particularly out of FModel).
126+ # Therefore, simply return an empty array.
127+ return []
128+ sequence_scale_keys_offset = self ._scale_keys_data_offset + (sequence .frame_start_index * bone_count * data_size )
129+ self ._fp .seek (sequence_scale_keys_offset )
130+ buffer = self ._fp .read (buffer_length )
98131 offset = 0
99- for _ in range (section .data_count ):
100- data .append (data_class .from_buffer_copy (buffer , offset ))
101- offset += section .data_size
132+ scale_keys = []
133+ for _ in range (sequence .frame_count * bone_count ):
134+ scale_key = Psa .ScaleKey .from_buffer_copy (buffer , offset )
135+ scale_keys .append (scale_key )
136+ offset += data_size
137+ return scale_keys
102138
103- def _read (self , fp ) -> Psa :
139+ def _read (self , fp : BinaryIO ) -> Psa :
104140 psa = Psa ()
105141 while fp .read (1 ):
106142 fp .seek (- 1 , 1 )
107143 section = Section .from_buffer_copy (fp .read (sizeof (Section )))
108144 match section .name :
109- case b' ANIMHEAD' :
145+ case PsaSectionName . ANIMHEAD :
110146 pass
111- case b' BONENAMES' :
112- PsaReader . _read_types (fp , PsxBone , section , psa .bones )
113- case b' ANIMINFO' :
114- sequences = []
115- PsaReader . _read_types (fp , Psa .Sequence , section , sequences )
147+ case PsaSectionName . BONENAMES :
148+ read_types (fp , PsxBone , section , psa .bones )
149+ case PsaSectionName . ANIMINFO :
150+ sequences : list [ Psa . Sequence ] = []
151+ read_types (fp , Psa .Sequence , section , sequences )
116152 # Try to fix CUE4Parse bug, if necessary.
117153 _try_fix_cue4parse_issue_103 (sequences )
118154 for sequence in sequences :
119155 psa .sequences [sequence .name .decode ()] = sequence
120- case b' ANIMKEYS' :
156+ case PsaSectionName . ANIMKEYS :
121157 # Skip keys on this pass. We will keep this file open and read from it as needed.
122- self .keys_data_offset = fp .tell ()
158+ self ._keys_data_offset = fp .tell ()
159+ fp .seek (section .data_size * section .data_count , 1 )
160+ case PsaSectionName .SCALEKEYS :
161+ if section .data_count == 0 :
162+ # An empty SCALEKEYS section is common for exports from FModel, treat it as though it doesn't exist.
163+ continue
164+ # Skip scale keys on this pass. We will keep this file open and read from it as needed.
165+ self ._scale_keys_data_offset = fp .tell ()
123166 fp .seek (section .data_size * section .data_count , 1 )
124167 case _:
125168 fp .seek (section .data_size * section .data_count , 1 )
126- print (f'Unrecognized section in PSA: " { section .name } " ' )
169+ print (f'Unrecognized section in PSA: { section .name !r } ' )
127170 return psa
128171
129172
130173__all__ = [
131- 'PsaReader'
174+ 'PsaReader' ,
175+ 'read_psa'
132176]
133177
134178
0 commit comments