diff --git a/io_export_psk_psa/__init__.py b/io_export_psk_psa/__init__.py index 53c00ac..044333d 100644 --- a/io_export_psk_psa/__init__.py +++ b/io_export_psk_psa/__init__.py @@ -16,55 +16,83 @@ if 'bpy' in locals(): importlib.reload(psk_data) importlib.reload(psk_builder) importlib.reload(psk_exporter) + importlib.reload(psk_importer) + importlib.reload(psk_reader) importlib.reload(psk_operator) importlib.reload(psa_data) importlib.reload(psa_builder) importlib.reload(psa_exporter) importlib.reload(psa_operator) + importlib.reload(psa_reader) + importlib.reload(psa_importer) else: # if i remove this line, it can be enabled just fine from .psk import data as psk_data from .psk import builder as psk_builder from .psk import exporter as psk_exporter + from .psk import reader as psk_reader + from .psk import importer as psk_importer from .psk import operator as psk_operator from .psa import data as psa_data from .psa import builder as psa_builder from .psa import exporter as psa_exporter from .psa import operator as psa_operator + from .psa import reader as psa_reader + from .psa import importer as psa_importer import bpy from bpy.props import IntProperty, CollectionProperty classes = [ psk_operator.PskExportOperator, + psk_operator.PskImportOperator, psa_operator.PsaExportOperator, + psa_operator.PsaImportOperator, psa_operator.PSA_UL_ActionList, - psa_operator.ActionListItem + psa_operator.PSA_UL_ImportActionList, + psa_operator.ActionListItem, + psa_operator.ImportActionListItem ] -def psk_menu_func(self, context): +def psk_export_menu_func(self, context): self.layout.operator(psk_operator.PskExportOperator.bl_idname, text ='Unreal PSK (.psk)') -def psa_menu_func(self, context): +def psk_import_menu_func(self, context): + self.layout.operator(psk_operator.PskImportOperator.bl_idname, text ='Unreal PSK (.psk)') + + +def psa_export_menu_func(self, context): self.layout.operator(psa_operator.PsaExportOperator.bl_idname, text='Unreal PSA (.psa)') +def psa_import_menu_func(self, context): + self.layout.operator(psa_operator.PsaImportOperator.bl_idname, text ='Unreal PSA (.psa)') + + def register(): for cls in classes: bpy.utils.register_class(cls) - bpy.types.TOPBAR_MT_file_export.append(psk_menu_func) - bpy.types.TOPBAR_MT_file_export.append(psa_menu_func) + bpy.types.TOPBAR_MT_file_export.append(psk_export_menu_func) + bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func) + bpy.types.TOPBAR_MT_file_export.append(psa_export_menu_func) + bpy.types.TOPBAR_MT_file_import.append(psa_import_menu_func) bpy.types.Scene.psa_action_list = CollectionProperty(type=psa_operator.ActionListItem) + bpy.types.Scene.psa_import_action_list = CollectionProperty(type=psa_operator.ImportActionListItem) bpy.types.Scene.psa_action_list_index = IntProperty(name='index for list??', default=0) + bpy.types.Scene.psa_import_action_list_index = IntProperty(name='index for list??', default=0) def unregister(): del bpy.types.Scene.psa_action_list_index + del bpy.types.Scene.psa_import_action_list_index del bpy.types.Scene.psa_action_list - bpy.types.TOPBAR_MT_file_export.remove(psa_menu_func) - bpy.types.TOPBAR_MT_file_export.remove(psk_menu_func) + del bpy.types.Scene.psa_import_action_list + bpy.types.TOPBAR_MT_file_export.remove(psk_export_menu_func) + bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func) + bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func) + bpy.types.TOPBAR_MT_file_import.remove(psa_import_menu_func) for cls in reversed(classes): bpy.utils.unregister_class(cls) diff --git a/io_export_psk_psa/data.py b/io_export_psk_psa/data.py index 91d9386..e48a6b2 100644 --- a/io_export_psk_psa/data.py +++ b/io_export_psk_psa/data.py @@ -17,6 +17,12 @@ class Quaternion(Structure): ('w', c_float), ] + def __iter__(self): + yield self.x + yield self.y + yield self.z + yield self.w + class Section(Structure): _fields_ = [ diff --git a/io_export_psk_psa/psa/data.py b/io_export_psk_psa/psa/data.py index adf744d..85e8344 100644 --- a/io_export_psk_psa/psa/data.py +++ b/io_export_psk_psa/psa/data.py @@ -22,7 +22,7 @@ class Psa(object): ('bone_count', c_int32), ('root_include', c_int32), ('compression_style', c_int32), - ('key_quotum', c_int32), # what the fuck is a quotum + ('key_quotum', c_int32), ('key_reduction', c_float), ('track_time', c_float), ('fps', c_float), diff --git a/io_export_psk_psa/psa/importer.py b/io_export_psk_psa/psa/importer.py new file mode 100644 index 0000000..9e79ffa --- /dev/null +++ b/io_export_psk_psa/psa/importer.py @@ -0,0 +1,15 @@ +import bpy +import bmesh +import mathutils +from .data import Psa + + +class PsaImporter(object): + def __init__(self): + pass + + def import_psa(self, psa: Psa, context): + print('importing yay') + print(psa.sequences) + for sequence in psa.sequences: + print(sequence.name, sequence.frame_start_index, sequence.frame_count) diff --git a/io_export_psk_psa/psa/operator.py b/io_export_psk_psa/psa/operator.py index 817b778..085c9f8 100644 --- a/io_export_psk_psa/psa/operator.py +++ b/io_export_psk_psa/psa/operator.py @@ -1,12 +1,23 @@ from bpy.types import Operator, Action, UIList, PropertyGroup -from bpy_extras.io_utils import ExportHelper +from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty from .builder import PsaBuilder, PsaBuilderOptions from .exporter import PsaExporter +from .reader import PsaReader +from .importer import PsaImporter import bpy import re +class ImportActionListItem(PropertyGroup): + action_name: StringProperty() + is_selected: BoolProperty(default=True) + + @property + def name(self): + return self.action_name + + class ActionListItem(PropertyGroup): action: PointerProperty(type=Action) is_selected: BoolProperty(default=False) @@ -16,6 +27,22 @@ class ActionListItem(PropertyGroup): return self.action.name +class PSA_UL_ImportActionList(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.alignment = 'LEFT' + layout.prop(item, 'is_selected', icon_only=True) + layout.label(text=item.action_name) + + # def filter_items(self, context, data, property): + # # TODO: returns two lists, apparently + # actions = getattr(data, property) + # flt_flags = [] + # flt_neworder = [] + # if self.filter_name: + # flt_flags = bpy.types.UI_UL_list.filter_items_by_name(self.filter_name, self.bitflag_filter_item, actions, 'name', reverse=self.use_filter_invert) + # return flt_flags, flt_neworder + + class PSA_UL_ActionList(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.alignment = 'LEFT' @@ -37,8 +64,8 @@ class PsaExportOperator(Operator, ExportHelper): bl_label = 'Export' __doc__ = 'PSA Exporter (.psa)' filename_ext = '.psa' - filter_glob : StringProperty(default='*.psa', options={'HIDDEN'}) - filepath : StringProperty( + filter_glob: StringProperty(default='*.psa', options={'HIDDEN'}) + filepath: StringProperty( name='File Path', description='File path used for exporting the PSA file', maxlen=1024, @@ -103,3 +130,47 @@ class PsaExportOperator(Operator, ExportHelper): exporter = PsaExporter(psk) exporter.export(self.filepath) return {'FINISHED'} + + +class PsaImportOperator(Operator, ImportHelper): + # TODO: list out the actions to be imported + bl_idname = 'import.psa' + bl_label = 'Import' + __doc__ = 'PSA Importer (.psa)' + filename_ext = '.psa' + filter_glob: StringProperty(default='*.psa', options={'HIDDEN'}) + filepath: StringProperty( + name='File Path', + description='File path used for importing the PSA file', + maxlen=1024, + default='') + + def invoke(self, context, event): + action_names = [] + try: + action_names = PsaReader().scan_sequence_names(self.filepath) + except IOError: + pass + + context.scene.psa_import_action_list.clear() + for action_name in action_names: + item = context.scene.psa_action_list.add() + item.action_name = action_name + item.is_selected = True + + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + def draw(self, context): + layout = self.layout + scene = context.scene + box = layout.box() + box.label(text='Actions', icon='ACTION') + row = box.row() + row.template_list('PSA_UL_ImportActionList', 'asd', scene, 'psa_import_action_list', scene, 'psa_import_action_list_index', rows=len(context.scene.psa_import_action_list)) + + def execute(self, context): + reader = PsaReader() + psa = reader.read(self.filepath) + PsaImporter().import_psa(psa, context) + return {'FINISHED'} diff --git a/io_export_psk_psa/psa/reader.py b/io_export_psk_psa/psa/reader.py new file mode 100644 index 0000000..8ad2611 --- /dev/null +++ b/io_export_psk_psa/psa/reader.py @@ -0,0 +1,53 @@ +from .data import * +from typing import AnyStr +import ctypes + + +class PsaReader(object): + + def __init__(self): + pass + + @staticmethod + def read_types(fp, data_class: ctypes.Structure, section: Section, data): + buffer_length = section.data_size * section.data_count + buffer = fp.read(buffer_length) + offset = 0 + for _ in range(section.data_count): + data.append(data_class.from_buffer_copy(buffer, offset)) + offset += section.data_size + + def scan_sequence_names(self, path) -> List[AnyStr]: + sequences = [] + with open(path, 'rb') as fp: + if fp.read(8) != b'ANIMINFO': + raise IOError('Unexpected file format') + fp.seek(0, 0) + while fp.read(1): + fp.seek(-1, 1) + section = Section.from_buffer_copy(fp.read(ctypes.sizeof(Section))) + if section.name == b'ANIMINFO': + PsaReader.read_types(fp, Psa.Sequence, section, sequences) + return [sequence.name for sequence in sequences] + else: + fp.seek(section.data_size * section.data_count, 1) + return [] + + def read(self, path) -> Psa: + psa = Psa() + with open(path, 'rb') as fp: + while fp.read(1): + fp.seek(-1, 1) + section = Section.from_buffer_copy(fp.read(ctypes.sizeof(Section))) + if section.name == b'ANIMHEAD': + pass + elif section.name == b'BONENAMES': + PsaReader.read_types(fp, Psa.Bone, section, psa.bones) + elif section.name == b'ANIMINFO': + PsaReader.read_types(fp, Psa.Sequence, section, psa.sequences) + elif section.name == b'ANIMKEYS': + PsaReader.read_types(fp, Psa.Key, section, psa.keys) + else: + raise RuntimeError(f'Unrecognized section "{section.name}"') + return psa +1 \ No newline at end of file diff --git a/io_export_psk_psa/psk/builder.py b/io_export_psk_psa/psk/builder.py index 88790a7..5a783cc 100644 --- a/io_export_psk_psa/psk/builder.py +++ b/io_export_psk_psa/psk/builder.py @@ -159,9 +159,9 @@ class PskBuilder(object): for f in object.data.loop_triangles: face = Psk.Face() face.material_index = material_indices[f.material_index] - face.wedge_index_1 = f.loops[2] + wedge_offset - face.wedge_index_2 = f.loops[1] + wedge_offset - face.wedge_index_3 = f.loops[0] + wedge_offset + face.wedge_indices[0] = f.loops[2] + wedge_offset + face.wedge_indices[1] = f.loops[1] + wedge_offset + face.wedge_indices[2] = f.loops[0] + wedge_offset face.smoothing_groups = poly_groups[f.polygon_index] psk.faces.append(face) # update the material index of the wedges diff --git a/io_export_psk_psa/psk/data.py b/io_export_psk_psa/psk/data.py index cf8d545..80fd441 100644 --- a/io_export_psk_psa/psk/data.py +++ b/io_export_psk_psa/psk/data.py @@ -25,9 +25,7 @@ class Psk(object): class Face(Structure): _fields_ = [ - ('wedge_index_1', c_int16), - ('wedge_index_2', c_int16), - ('wedge_index_3', c_int16), + ('wedge_indices', c_int16 * 3), ('material_index', c_int8), ('aux_material_index', c_int8), ('smoothing_groups', c_int32) diff --git a/io_export_psk_psa/psk/importer.py b/io_export_psk_psa/psk/importer.py new file mode 100644 index 0000000..df2da4b --- /dev/null +++ b/io_export_psk_psa/psk/importer.py @@ -0,0 +1,71 @@ +import bpy +import bmesh +import mathutils +from .data import Psk + + +class PskImporter(object): + def __init__(self): + pass + + def import_psk(self, psk: Psk, context): + # ARMATURE + armature_data = bpy.data.armatures.new('armature') + armature_object = bpy.data.objects.new('new_ao', armature_data) + context.scene.collection.objects.link(armature_object) + + try: + bpy.ops.object.mode_set(mode='OBJECT') + except: + pass + + armature_object.select_set(state=True) + bpy.context.view_layer.objects.active = armature_object + + bpy.ops.object.mode_set(mode='EDIT') + + for bone in psk.bones: + edit_bone = armature_data.edit_bones.new(bone.name.decode('utf-8')) + edit_bone.parent = armature_data.edit_bones[bone.parent_index] + edit_bone.head = (bone.location.x, bone.location.y, bone.location.z) + rotation = mathutils.Quaternion(*bone.rotation) + edit_bone.tail = edit_bone.head + (mathutils.Vector(0, 0, 1) @ rotation) + + # MESH + mesh_data = bpy.data.meshes.new('mesh') + mesh_object = bpy.data.objects.new('new_mo', mesh_data) + + # MATERIALS + for material in psk.materials: + bpy_material = bpy.data.materials.new(material.name.decode('utf-8')) + mesh_data.materials.append(bpy_material) + + bm = bmesh.new() + + # VERTICES + for point in psk.points: + bm.verts.new((point.x, point.y, point.z)) + + bm.verts.ensure_lookup_table() + + for face in psk.faces: + point_indices = [bm.verts[psk.wedges[i].point_index] for i in reversed(face.wedge_indices)] + bm_face = bm.faces.new(point_indices) + bm_face.material_index = face.material_index + + bm.to_mesh(mesh_data) + + # TEXTURE COORDINATES + data_index = 0 + uv_layer = mesh_data.uv_layers.new() + for face_index, face in enumerate(psk.faces): + face_wedges = [psk.wedges[i] for i in reversed(face.wedge_indices)] + for wedge in face_wedges: + uv_layer.data[data_index].uv = wedge.u, 1.0 - wedge.v + data_index += 1 + + bm.free() + + # TODO: weights (vertex grorups etc.) + + context.scene.collection.objects.link(mesh_object) diff --git a/io_export_psk_psa/psk/operator.py b/io_export_psk_psa/psk/operator.py index 0291385..1826550 100644 --- a/io_export_psk_psa/psk/operator.py +++ b/io_export_psk_psa/psk/operator.py @@ -1,8 +1,29 @@ from bpy.types import Operator -from bpy_extras.io_utils import ExportHelper +from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy.props import StringProperty, BoolProperty, FloatProperty from .builder import PskBuilder from .exporter import PskExporter +from .reader import PskReader +from .importer import PskImporter + + +class PskImportOperator(Operator, ImportHelper): + bl_idname = 'import.psk' + bl_label = 'Export' + __doc__ = 'PSK Importer (.psk)' + filename_ext = '.psk' + filter_glob: StringProperty(default='*.psk', options={'HIDDEN'}) + filepath: StringProperty( + name='File Path', + description='File path used for exporting the PSK file', + maxlen=1024, + default='') + + def execute(self, context): + reader = PskReader() + psk = reader.read(self.filepath) + PskImporter().import_psk(psk, context) + return {'FINISHED'} class PskExportOperator(Operator, ExportHelper): diff --git a/io_export_psk_psa/psk/reader.py b/io_export_psk_psa/psk/reader.py new file mode 100644 index 0000000..8f94c36 --- /dev/null +++ b/io_export_psk_psa/psk/reader.py @@ -0,0 +1,46 @@ +from .data import * +import ctypes + + +class PskReader(object): + + def __init__(self): + pass + + @staticmethod + def read_types(fp, data_class: ctypes.Structure, section: Section, data): + buffer_length = section.data_size * section.data_count + buffer = fp.read(buffer_length) + offset = 0 + for _ in range(section.data_count): + data.append(data_class.from_buffer_copy(buffer, offset)) + offset += section.data_size + + def read(self, path) -> Psk: + psk = Psk() + with open(path, 'rb') as fp: + while fp.read(1): + fp.seek(-1, 1) + section = Section.from_buffer_copy(fp.read(ctypes.sizeof(Section))) + if section.name == b'ACTRHEAD': + pass + elif section.name == b'PNTS0000': + PskReader.read_types(fp, Vector3, section, psk.points) + elif section.name == b'VTXW0000': + if section.data_size == ctypes.sizeof(Psk.Wedge16): + PskReader.read_types(fp, Psk.Wedge16, section, psk.wedges) + elif section.data_size == ctypes.sizeof(Psk.Wedge32): + PskReader.read_types(fp, Psk.Wedge32, section, psk.wedges) + else: + raise RuntimeError('Unrecognized wedge format') + elif section.name == b'FACE0000': + PskReader.read_types(fp, Psk.Face, section, psk.faces) + elif section.name == b'MATT0000': + PskReader.read_types(fp, Psk.Material, section, psk.materials) + elif section.name == b'REFSKELT': + PskReader.read_types(fp, Psk.Bone, section, psk.bones) + elif section.name == b'RAWWEIGHTS': + PskReader.read_types(fp, Psk.Weight, section, psk.weights) + else: + raise RuntimeError(f'Unrecognized section "{section.name}"') + return psk