diff --git a/fast64_internal/sm64/animation/classes.py b/fast64_internal/sm64/animation/classes.py index 6ecf0c4f5..85b79ccb6 100644 --- a/fast64_internal/sm64/animation/classes.py +++ b/fast64_internal/sm64/animation/classes.py @@ -91,7 +91,7 @@ def to_c(self, is_dma_structure: bool): return text_data.getvalue() - def to_binary(self, start_address: int) -> bytearray: + def to_binary(self, start_address: int = 0) -> bytearray: data: bytearray = bytearray() value_table, indice_tables = create_tables([self]) @@ -300,12 +300,12 @@ def to_props(self, action, header, actor_name: str, use_custom_name: bool = True header.action = action - header.custom_name = self.reference if ( isinstance(self.reference, str) and self.reference != header.get_anim_name(actor_name, action) and use_custom_name ): + header.custom_name = self.reference header.override_name = True correct_frame_range = self.start_frame, self.loop_start, self.loop_end @@ -336,7 +336,7 @@ class SM64_Anim: headers: list[SM64_AnimHeader] = dataclasses.field(default_factory=list) action_name: str = None - def to_binary(self, is_dma: bool, start_address: int) -> bytearray: + def to_binary(self, is_dma: bool = False, start_address: int = 0) -> bytearray: data: bytearray = bytearray() ptrs: list[int] = [] @@ -395,8 +395,10 @@ def to_props(self, action, actor_name: str, remove_name_footer: bool = True, use main_header = self.headers[0] - if not self.action_name and main_header.reference and remove_name_footer: - self.action_name = main_header.reference.lstrip("anim_") + if not self.action_name and main_header.reference: + self.action_name = str(main_header.reference) + if remove_name_footer: + self.action_name = self.action_name.lstrip("anim_") if self.action_name: action.name = self.action_name @@ -482,7 +484,7 @@ def get_sets(self) -> tuple[list[SM64_AnimHeader], list[SM64_AnimData]]: headers_set.append(anim_header) return headers_set, data_set - def to_binary(self, is_dma: bool, start_address: int): + def to_binary(self, is_dma: bool = False, start_address: int = 0): # TODO: Handle dma exports data: bytearray = bytearray() ptrs: list[int] = [] @@ -491,7 +493,7 @@ def to_binary(self, is_dma: bool, start_address: int): # Calculate offsets and generate tables table_offset = start_address - table_length = len(self.elements) * 4 + table_length = len(self.elements) * 4 + 4 headers_offset = table_offset + table_length headers_length = len(headers_set) * HEADER_SIZE if data_set: @@ -505,21 +507,22 @@ def to_binary(self, is_dma: bool, start_address: int): ptrs.append(len(data)) header_offset = headers_offset + (headers_set.index(anim_header) * HEADER_SIZE) data.extend(header_offset.to_bytes(4, byteorder="big", signed=False)) + data.extend(bytearray([0x00] * 4)) # Add all the headers for anim_header in self.elements: if anim_header.data: - indice_offset = sum( + ptrs.extend([start_address + len(data) + 12, start_address + len(data) + 16]) + indice_offset = indice_tables_offset + sum( [len(indice_table.data) for indice_table in indice_tables[: data_set.index(anim_header.data)]] ) data.extend( anim_header.to_binary( - indice_tables_offset + indice_offset, + indice_offset, values_table_offset, ) ) else: - ptrs.append(len(data)) data.extend(anim_header.to_binary()) if data_set: @@ -527,7 +530,7 @@ def to_binary(self, is_dma: bool, start_address: int): data.extend(indice_table.to_binary()) data.extend(value_table.to_binary()) - return data + return data, ptrs def to_c_dma(self) -> list[os.PathLike, str]: files_data: dict[str, str] = {} diff --git a/fast64_internal/sm64/animation/importing.py b/fast64_internal/sm64/animation/importing.py index 116fdb6eb..ca6f27918 100644 --- a/fast64_internal/sm64/animation/importing.py +++ b/fast64_internal/sm64/animation/importing.py @@ -275,33 +275,23 @@ def import_binary_dma_animation( def import_binary_table( - rom_data: bytes, - address: int, - segment_data: dict[int, tuple[int, int]], + table_reader: RomReading, animations: dict[str, SM64_Anim], read_entire_table: bool, table_index: int, ignore_null: bool, table: SM64_AnimTable, ): - table_reader = RomReading(rom_data, address) for i in range(255): - ptr = table_reader.read_value(4, signed=False) + ptr = table_reader.read_ptr() if ptr == 0 and not ignore_null: if not read_entire_table: - raise PluginError("Table Index not in table.") + raise PluginError("Table index not in table.") break is_correct_index = i == table_index if read_entire_table or is_correct_index: - ptr_in_bytes: bytes = ptr.to_bytes(4, "big") - if ptr_in_bytes[0] not in segment_data: - raise PluginError( - f"Header at table index {i} located at {hex(table_reader.address)} does not belong to the current segment." - ) - header_address = decodeSegmentedAddr(ptr_in_bytes, segment_data) - - header = import_binary_header(rom_data, header_address, False, animations, segment_data) + header = import_binary_header(table_reader.data, ptr, False, animations, None) table.elements.append(header) if not read_entire_table and is_correct_index: @@ -311,7 +301,7 @@ def import_binary_table( def import_binary_animations( - rom_data: BufferedReader, + rom_data: RomReading, import_type: str, is_segmented_pointer: bool, address: int, @@ -327,7 +317,12 @@ def import_binary_animations( if import_type == "Table": import_binary_table( - rom_data, address, segment_data, animations, read_entire_table, table_index, ignore_null, table + rom_data=rom_data, + animations=animations, + table=table, + read_entire_table=read_entire_table, + table_index=table_index, + ignore_null=ignore_null, ) elif import_type == "DMA": import_binary_dma_animation(rom_data, address, animations, table, table_index if read_entire_table else None) @@ -338,29 +333,43 @@ def import_binary_animations( def import_insertable_binary_animations( - header_and_data: bytes, + insertable_data_reader: RomReading, animations: dict[str, SM64_Anim], + segment_data: Optional[dict[int, tuple[int, int]]] = None, + read_entire_table: bool = False, + table_index: int = 0, + ignore_null: bool = False, table: SM64_AnimTable = SM64_AnimTable(), ): - # TODO: Finish - header_reader = RomReading(header_and_data, 0) - data_type_num = header_reader.read_value(4, signed=False) - + data_type_num = insertable_data_reader.read_value(4, signed=False) if data_type_num not in insertableBinaryTypes.values(): raise PluginError(f"Unknown data type: {hex(data_type_num)}") - data_type = next(key for key, value in insertableBinaryTypes.items() if value == data_type_num) - if data_type not in {"Animation"}: - raise PluginError(f'Wrong data type "{data_type}".') - - data_size = header_reader.read_value(4, signed=False) - start_address = header_reader.read_value(4, signed=False) + data_size = insertable_data_reader.read_value(4, signed=False) + start_address = insertable_data_reader.read_value(4, signed=False) - pointer_count = header_reader.read_value(4, signed=False) + pointer_count = insertable_data_reader.read_value(4, signed=False) pointer_offsets = [] for _ in range(pointer_count): - pointer_offsets.append(header_reader.read_value(4, signed=False)) + pointer_offsets.append(insertable_data_reader.read_value(4, signed=False)) + insertable_data_reader.insertable_ptrs = pointer_offsets - actual_start = header_reader.address + start_address - data = header_and_data[actual_start : actual_start + data_size] + actual_start = insertable_data_reader.address + start_address + data_reader = insertable_data_reader.branch( + 0, + insertable_data_reader.data[actual_start : actual_start + data_size], + ) - import_binary_header(data, 0, False, animations) + data_type = next(key for key, value in insertableBinaryTypes.items() if value == data_type_num) + if data_type == "Animation": + import_binary_header(data_reader, 0, False, animations) + elif data_type == "Animation Table": + import_binary_table( + table_reader=data_reader, + animations=animations, + read_entire_table=read_entire_table, + table_index=table_index, + ignore_null=ignore_null, + table=table, + ) + else: + raise PluginError(f'Wrong animation data type "{data_type}".') diff --git a/fast64_internal/sm64/animation/operators.py b/fast64_internal/sm64/animation/operators.py index 2391840ec..e55400a5f 100644 --- a/fast64_internal/sm64/animation/operators.py +++ b/fast64_internal/sm64/animation/operators.py @@ -1,3 +1,5 @@ +import os + import bpy from bpy.utils import register_class, unregister_class from bpy.types import Context, Object, Scene, Operator @@ -14,19 +16,19 @@ PluginError, applyBasicTweaks, copyPropertyGroup, + path_checks, filepath_checks, toAlnum, raisePluginError, get_mode_set_from_context_mode, - applyRotation, writeInsertableFile, ) from ...utility_anim import stashActionInArmature from ..sm64_utility import import_rom_checks from ..sm64_level_parser import parseLevelAtPointer -from ..sm64_constants import level_pointers +from ..sm64_constants import level_pointers, insertableBinaryTypes -from .classes import DMATable, DMATableEntrie, SM64_Anim, SM64_AnimTable +from .classes import DMATable, DMATableEntrie, SM64_Anim, SM64_AnimTable, RomReading from .importing import ( import_binary_animations, import_binary_dma_animation, @@ -48,6 +50,11 @@ ) from .constants import marioAnimationNames +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..settings.properties import SM64_Properties + from .properties import SM64_AnimExportProps, SM64_AnimImportProps, SM64_AnimTableProps def emulate_no_loop(scene: Scene): export_props = scene.fast64.sm64.anim_export @@ -260,9 +267,9 @@ def execute_operator(self, context: Context): animation_operator_checks(context) scene = context.scene - sm64_props = scene.fast64.sm64 - export_props = sm64_props.anim_export - table_props = export_props.table + sm64_props: SM64_Properties = scene.fast64.sm64 + export_props: SM64_AnimExportProps = sm64_props.anim_export + table_props: SM64_AnimTableProps = export_props.table actions = table_props.actions armature_obj: Object = context.selected_objects[0] @@ -343,6 +350,10 @@ def execute_operator(self, context: Context): header_type, True, ) + elif sm64_props.export_type == "Insertable Binary": + data, ptrs = table.to_binary(export_props.is_binary_dma, 0) + path = abspath(os.path.join(export_props.directory_path, table_props.insertable_file_name)) + writeInsertableFile(path, insertableBinaryTypes["Animation Table"], ptrs, 0, data) else: raise PluginError(f"Unimplemented export type ({sm64_props.export_type})") @@ -525,44 +536,59 @@ def execute_operator(self, context): bpy.ops.object.mode_set(mode="OBJECT") animation_operator_checks(context, False) - sm64_props = context.scene.fast64.sm64 - export_props = sm64_props.anim_export - import_props = sm64_props.anim_import + scene = context.scene + sm64_props: SM64_Properties = scene.fast64.sm64 + export_props: SM64_AnimExportProps = sm64_props.anim_export + import_props: SM64_AnimImportProps = sm64_props.anim_import + table_props: SM64_AnimTableProps = export_props.table animations: dict[str, SM64_Anim] = {} table = SM64_AnimTable() - if import_props.import_type == "Binary": + if import_props.import_type == "Binary" or ( + import_props.import_type == "Insertable Binary" and import_props.insertable_read_from_rom + ): import_rom_path = abspath(sm64_props.import_rom) import_rom_checks(import_rom_path) - with open(import_rom_path, "rb") as rom_data: - import_binary_animations( - rom_data.read(), - import_props.binary_import_type, - import_props.is_segmented_address, - eval_num_from_str( - import_props.dma_table_address - if import_props.binary_import_type == "DMA" - else import_props.address - ), - parseLevelAtPointer(rom_data, level_pointers[import_props.level]).segmentData, - animations, - import_props.read_entire_table, - import_props.mario_or_table_index, - import_props.ignore_null, - table, - ) + with open(import_rom_path, "rb") as rom_file: + rom_data = rom_file.read() + segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData + else: + rom_data, segment_data = None, None + + if import_props.import_type == "Binary": + import_binary_animations( + rom_data, + import_props.binary_import_type, + import_props.is_segmented_address, + eval_num_from_str( + import_props.dma_table_address if import_props.binary_import_type == "DMA" else import_props.address + ), + segment_data, + animations, + import_props.read_entire_table, + import_props.mario_or_table_index, + import_props.ignore_null, + table, + ) elif import_props.import_type == "Insertable Binary": insertable_path = abspath(import_props.insertable_path) filepath_checks(insertable_path) - with open(insertable_path, "rb") as rom_data: + + with open(insertable_path, "rb") as insertable_file: import_insertable_binary_animations( - rom_data.read(), - animations, - table, + insertable_data_reader=RomReading(insertable_file.read(), 0, None, rom_data, segment_data), + animations=animations, + table=table, + segment_data=segment_data, + read_entire_table=import_props.read_entire_table, + table_index=import_props.mario_or_table_index, + ignore_null=import_props.ignore_null, ) elif import_props.import_type == "C": - import_c_animations(abspath(import_props.c_path), animations, table) + c_path = abspath(import_props.c_path) + path_checks(c_path) + import_c_animations(c_path, animations, table) for data in animations.values(): animation_data_to_blender( diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py index 29a916e9c..35b287934 100644 --- a/fast64_internal/sm64/animation/properties.py +++ b/fast64_internal/sm64/animation/properties.py @@ -636,7 +636,7 @@ def draw_props( if export_type == "Binary": prop_split(col, self, "start_address", "Start Address") prop_split(col, self, "end_address", "End Address") - elif export_type == "C" and draw_file_name: + elif draw_file_name: name_split = col.split(factor=0.5) name_split.prop(self, "override_file_name") if self.override_file_name: @@ -701,16 +701,20 @@ class SM64_AnimTableProps(PropertyGroup): expand_tab: BoolProperty(name="Table Export", default=True) update_table: BoolProperty( - name="Update Table On Action Export", description="Update table outside of table exports", default=True + name="Update Table On Action Export", + description="Update table outside of table exports", + default=True, ) + export_seperately: BoolProperty(name="Export All Seperately") + override_files_prop: BoolProperty(name="Override Table and Data Files") + elements: CollectionProperty(type=SM64_TableElement) + generate_enums: BoolProperty(name="Generate Enums") override_table_name: BoolProperty(name="Override Table Name") custom_table_name: StringProperty(name="Table Name", default="mario_anims") - export_seperately: BoolProperty(name="Export All Seperately") - override_files_prop: BoolProperty(name="Override Table and Data Files") - elements: CollectionProperty(type=SM64_TableElement) + insertable_file_name: StringProperty(name="Insertable File Name", default="toad.insertable") start_address: StringProperty(name="Start Address", default="0x00") end_address: StringProperty(name="End Address", default="0x00") @@ -894,6 +898,7 @@ def draw_element( icon="TRIA_DOWN" if table_element.expand_tab else "TRIA_RIGHT", text=f"{header.get_anim_name(actor_name, action)} Properties", ) + c_not_dma = export_type == "C" and not is_dma if table_element.expand_tab: action_props.draw_props( layout=prop_box, @@ -903,9 +908,9 @@ def draw_element( specific_variant=header_variant, draw_export_operation=False, draw_table_operations=False, - draw_names=export_type == "C" and not is_dma, - draw_references=export_type == "C" and not is_dma, - draw_file_name=export_type == "C" and not is_dma and self.export_seperately, + draw_names=c_not_dma, + draw_references=c_not_dma, + draw_file_name=(c_not_dma or export_type == "Insertable Binary") and self.export_seperately, actor_name=actor_name, generate_enums=self.generate_enums, is_dma=is_dma, @@ -948,9 +953,11 @@ def draw_props( self.draw_non_exclusive_settings(col, is_dma, export_type, actor_name) col.separator() - if export_type == "C" and not is_dma: + if export_type == "Insertable Binary" or (export_type == "C" and not is_dma): col.prop(self, "export_seperately") - if self.export_seperately: + if export_type == "Insertable Binary" and not self.export_seperately: + prop_split(col, self, "insertable_file_name", "File Name") + elif self.export_seperately: col.prop(self, "override_files_prop") if self.elements: @@ -1012,14 +1019,18 @@ class SM64_AnimExportProps(PropertyGroup): table: PointerProperty(type=SM64_AnimTableProps) - quick_read: BoolProperty(name="Quick read", default=True, description="May break constraint based rigs") + quick_read: BoolProperty( + name="Quick Data Read", default=True, description="Read fcurves directly, may break constraint based rigs" + ) + + directory_path: StringProperty(name="Directory Path", subtype="FILE_PATH") + dma_folder: StringProperty(name="DMA Folder", default="assets/anims/") use_dma_structure: BoolProperty( name="Use DMA Structure", description="When enabled, the Mario animation converter order is used (headers, indicies, values)", default=False, ) - custom_path: StringProperty(name="Directory", subtype="FILE_PATH") actor_name_prop: StringProperty(name="Name", default="mario") group_name: StringProperty( name="Group Name", @@ -1034,8 +1045,6 @@ class SM64_AnimExportProps(PropertyGroup): binary_overwrite_dma_entry: BoolProperty(name="Overwrite DMA Entry") is_binary_dma: BoolProperty(name="Is DMA", default=True) - insertable_path: StringProperty(name="Insertable Path", subtype="FILE_PATH") - @property def actor_name(self): return self.actor_name_prop if self.header_type != "DMA" else None @@ -1053,7 +1062,7 @@ def get_animation_paths(self, create_directories: bool = False): export_path, level_name = getPathAndLevel( custom_export, - self.custom_path, + self.directory_path, self.level_option, self.level_name, ) @@ -1142,7 +1151,7 @@ def draw_binary_settings(self, layout: UILayout, export_type: str): col.prop(self, "binary_level") if export_type == "Insertable Binary": - prop_split(col, self, "insertable_path", "Insertable Path") + prop_split(col, self, "insertable_path", "Path") def draw_c_settings(self, layout: UILayout): col = layout.column() @@ -1154,8 +1163,8 @@ def draw_c_settings(self, layout: UILayout): if self.header_type == "Custom": col.prop(self, "use_dma_structure") - col.prop(self, "custom_path") - if directory_ui_warnings(col, abspath(self.custom_path)): + col.prop(self, "directory_path") + if directory_ui_warnings(col, abspath(self.directory_path)): customExportWarning(col) elif self.header_type == "DMA": col.prop(self, "dma_folder") @@ -1190,6 +1199,9 @@ def draw_props(self, layout: UILayout, export_type: str): col.separator() col.prop(self, "quick_read") + prop_split(col, self, "directory_path", "Directory") + directory_ui_warnings(col, abspath(self.directory_path)) + col.separator() if not is_dma: @@ -1259,6 +1271,13 @@ def mario_or_table_index(self): else self.table_index ) + def draw_c(self, layout: UILayout): + col = layout.column() + prop_split(col, self, "c_path", "Path") + path_ui_warnings(col, abspath(self.c_path)) + col.prop(self, "remove_name_footer") + col.prop(self, "use_custom_name") + def draw_binary(self, layout: UILayout): col = layout.column() @@ -1286,18 +1305,26 @@ def draw_binary(self, layout: UILayout): prop_split(col, self, "table_index", "List Index") col.prop(self, "ignore_null") - def draw_c(self, layout: UILayout): - col = layout.column() - prop_split(col, self, "c_path", "Path") - path_ui_warnings(col, abspath(self.c_path)) - col.prop(self, "remove_name_footer") - col.prop(self, "use_custom_name") - def draw_insertable_binary(self, layout: UILayout): col = layout.column() prop_split(col, self, "insertable_path", "Path") path_ui_warnings(col, abspath(self.insertable_path)) - col.prop(self, "insertable_read_from_rom") + col.label(text="Type will be read from the data type of the files", icon="INFO") + col.separator() + + read_from_rom_box = col.box().column() + read_from_rom_box.prop(self, "insertable_read_from_rom") + if self.insertable_read_from_rom: + prop_split(read_from_rom_box, self, "level", "Level") + read_from_rom_box.prop(self, "is_segmented_address") + prop_split(read_from_rom_box, self, "address", "Address") + + table_box = col.box().column() + table_box.label(text="Table Imports") + table_box.prop(self, "read_entire_table") + if not self.read_entire_table: + prop_split(table_box, self, "table_index", "List Index") + table_box.prop(self, "ignore_null") def draw_props(self, layout: UILayout, binary_col_enabled: bool = True): col = layout.column() @@ -1312,9 +1339,9 @@ def draw_props(self, layout: UILayout, binary_col_enabled: bool = True): elif self.import_type == "Insertable Binary": self.draw_insertable_binary(col) + col.separator() col.operator(SM64_ImportAnim.bl_idname, icon="IMPORT") - if self.import_type == "Binary": - col.separator() + if self.import_type not in {"Binary", "C"}: col.operator(SM64_ImportAllMarioAnims.bl_idname, icon="IMPORT") diff --git a/fast64_internal/sm64/animation/utility.py b/fast64_internal/sm64/animation/utility.py index d1478fadd..598602cf3 100644 --- a/fast64_internal/sm64/animation/utility.py +++ b/fast64_internal/sm64/animation/utility.py @@ -1,11 +1,12 @@ import ast +import copy import dataclasses import math import bpy from bpy.types import Object, Armature -from ...utility import findStartBones, toAlnum, PluginError +from ...utility import findStartBones, PluginError, decodeSegmentedAddr from ..sm64_geolayout_bone import animatableBoneTypes @@ -13,12 +14,41 @@ class RomReading: """ Simple class that simplifies reading data continously from a starting address. + Accounts for insertable binary data. """ - def __init__(self, data: bytes, start_address: int): + def __init__( + self, + data: bytes, + start_address: int, + insertable_ptrs: list[int] | None = None, + rom_data: bytes | None = None, + segment_data: dict[int, tuple[int, int]] | None = None, + ): self.start_address = start_address self.address = start_address self.data = data + self.insertable_ptrs = insertable_ptrs + self.segment_data = segment_data + + def branch(self, start_address: int, data: bytes | None = None): + branch = copy.copy(self) + if data: + branch.data = data + branch.start_address = start_address + branch.address = start_address + return self + + def read_ptr(self): + in_bytes = self.data[self.address : self.address + 4] + self.address += 4 + ptr = int.from_bytes(in_bytes, "big", signed=False) + if not ptr in self.insertable_ptrs and self.segment_data: + ptr_in_bytes: bytes = ptr.to_bytes(4, "big") + if ptr_in_bytes[0] not in self.segment_data: + raise PluginError(f"Address {ptr} does not belong to the current segment.") + return decodeSegmentedAddr(ptr_in_bytes, self.segment_data) + return ptr def read_value(self, size, offset: int = None, signed=True): if offset: