From 8dd43138c123da34a3c29ddbe27e5899d062c809 Mon Sep 17 00:00:00 2001 From: Lila Date: Mon, 30 Sep 2024 22:03:26 +0100 Subject: [PATCH 01/14] [SM64] Animations Rewrite/Rework --- __init__.py | 14 +- fast64_internal/f3d_material_converter.py | 12 +- fast64_internal/oot/oot_utility.py | 10 + fast64_internal/operators.py | 64 +- fast64_internal/sm64/__init__.py | 33 +- fast64_internal/sm64/animation/__init__.py | 15 + fast64_internal/sm64/animation/classes.py | 1006 +++++++++ fast64_internal/sm64/animation/constants.py | 88 + fast64_internal/sm64/animation/exporting.py | 1025 +++++++++ fast64_internal/sm64/animation/importing.py | 808 +++++++ fast64_internal/sm64/animation/operators.py | 346 +++ fast64_internal/sm64/animation/panels.py | 196 ++ fast64_internal/sm64/animation/properties.py | 1204 +++++++++++ fast64_internal/sm64/animation/utility.py | 165 ++ fast64_internal/sm64/settings/properties.py | 35 +- .../sm64/settings/repo_settings.py | 3 + fast64_internal/sm64/sm64_anim.py | 1119 ---------- fast64_internal/sm64/sm64_classes.py | 290 +++ fast64_internal/sm64/sm64_collision.py | 50 +- fast64_internal/sm64/sm64_constants.py | 1878 +++++++++++++++-- fast64_internal/sm64/sm64_f3d_writer.py | 50 +- fast64_internal/sm64/sm64_geolayout_writer.py | 70 +- fast64_internal/sm64/sm64_level_writer.py | 48 +- fast64_internal/sm64/sm64_objects.py | 191 +- fast64_internal/sm64/sm64_texscroll.py | 16 +- fast64_internal/sm64/sm64_utility.py | 270 ++- fast64_internal/sm64/tools/panels.py | 4 +- fast64_internal/utility.py | 84 +- fast64_internal/utility_anim.py | 115 +- piranha plant/toad.insertable | Bin 0 -> 38872 bytes pyproject.toml | 3 + toad.insertable | Bin 0 -> 25796 bytes 32 files changed, 7615 insertions(+), 1597 deletions(-) create mode 100644 fast64_internal/sm64/animation/__init__.py create mode 100644 fast64_internal/sm64/animation/classes.py create mode 100644 fast64_internal/sm64/animation/constants.py create mode 100644 fast64_internal/sm64/animation/exporting.py create mode 100644 fast64_internal/sm64/animation/importing.py create mode 100644 fast64_internal/sm64/animation/operators.py create mode 100644 fast64_internal/sm64/animation/panels.py create mode 100644 fast64_internal/sm64/animation/properties.py create mode 100644 fast64_internal/sm64/animation/utility.py delete mode 100644 fast64_internal/sm64/sm64_anim.py create mode 100644 fast64_internal/sm64/sm64_classes.py create mode 100644 piranha plant/toad.insertable create mode 100644 toad.insertable diff --git a/__init__.py b/__init__.py index d3e5c548c..280b88408 100644 --- a/__init__.py +++ b/__init__.py @@ -13,7 +13,7 @@ repo_settings_operators_unregister, ) -from .fast64_internal.sm64 import sm64_register, sm64_unregister +from .fast64_internal.sm64 import sm64_register, sm64_unregister, SM64_ActionProperty from .fast64_internal.sm64.sm64_constants import sm64_world_defaults from .fast64_internal.sm64.settings.properties import SM64_Properties from .fast64_internal.sm64.sm64_geolayout_bone import SM64_BoneProperties @@ -227,6 +227,14 @@ class Fast64_Properties(bpy.types.PropertyGroup): renderSettings: bpy.props.PointerProperty(type=Fast64RenderSettings_Properties, name="Fast64 Render Settings") +class Fast64_ActionProperties(bpy.types.PropertyGroup): + """ + Properties in Action.fast64. + """ + + sm64: bpy.props.PointerProperty(type=SM64_ActionProperty, name="SM64 Properties") + + class Fast64_BoneProperties(bpy.types.PropertyGroup): """ Properties in bone.fast64 (bpy.types.Bone) @@ -314,6 +322,7 @@ def draw(self, context): Fast64RenderSettings_Properties, ManualUpdatePreviewOperator, Fast64_Properties, + Fast64_ActionProperties, Fast64_BoneProperties, Fast64_ObjectProperties, F3D_GlobalSettingsPanel, @@ -457,7 +466,7 @@ def register(): bpy.types.Scene.fast64 = bpy.props.PointerProperty(type=Fast64_Properties, name="Fast64 Properties") bpy.types.Bone.fast64 = bpy.props.PointerProperty(type=Fast64_BoneProperties, name="Fast64 Bone Properties") bpy.types.Object.fast64 = bpy.props.PointerProperty(type=Fast64_ObjectProperties, name="Fast64 Object Properties") - + bpy.types.Action.fast64 = bpy.props.PointerProperty(type=Fast64_ActionProperties, name="Fast64 Action Properties") bpy.app.handlers.load_post.append(after_load) @@ -486,6 +495,7 @@ def unregister(): del bpy.types.Scene.fast64 del bpy.types.Bone.fast64 del bpy.types.Object.fast64 + del bpy.types.Action.fast64 repo_settings_operators_unregister() diff --git a/fast64_internal/f3d_material_converter.py b/fast64_internal/f3d_material_converter.py index fffb903da..992c2c221 100644 --- a/fast64_internal/f3d_material_converter.py +++ b/fast64_internal/f3d_material_converter.py @@ -186,16 +186,16 @@ def convertAllBSDFtoF3D(objs, renameUV): def convertBSDFtoF3D(obj, index, material, materialDict): if not material.use_nodes: newMaterial = createF3DMat(obj, preset="Shaded Solid", index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = material.diffuse_color + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.default_light_color = material.diffuse_color updateMatWithName(newMaterial, material, materialDict) elif "Principled BSDF" in material.node_tree.nodes: tex0Node = material.node_tree.nodes["Principled BSDF"].inputs["Base Color"] if len(tex0Node.links) == 0: newMaterial = createF3DMat(obj, preset=getDefaultMaterialPreset("Shaded Solid"), index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = tex0Node.default_value + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.default_light_color = tex0Node.default_value updateMatWithName(newMaterial, material, materialDict) else: if isinstance(tex0Node.links[0].from_node, bpy.types.ShaderNodeTexImage): @@ -213,8 +213,8 @@ def convertBSDFtoF3D(obj, index, material, materialDict): else: presetName = getDefaultMaterialPreset("Shaded Texture") newMaterial = createF3DMat(obj, preset=presetName, index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.tex0.tex = tex0Node.links[0].from_node.image + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.tex0.tex = tex0Node.links[0].from_node.image updateMatWithName(newMaterial, material, materialDict) else: print("Principled BSDF material does not have an Image Node attached to its Base Color.") diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/oot/oot_utility.py index 3173cc299..464430478 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/oot/oot_utility.py @@ -496,6 +496,16 @@ def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: st return filepath +def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: str) -> str: + if isCustomExport: + filepath = exportPath + else: + filepath = os.path.join( + ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, False), folderName + ".h" + ) + return filepath + + def ootGetPath(exportPath, isCustomExport, subPath, folderName, makeIfNotExists, useFolderForCustom): if isCustomExport: path = bpy.path.abspath(os.path.join(exportPath, (folderName if useFolderForCustom else ""))) diff --git a/fast64_internal/operators.py b/fast64_internal/operators.py index e4a2041cd..57d099397 100644 --- a/fast64_internal/operators.py +++ b/fast64_internal/operators.py @@ -1,8 +1,20 @@ -import bpy, mathutils, math -from bpy.types import Operator, Context, UILayout +from cProfile import Profile +from pstats import SortKey, Stats +from typing import Optional + +import bpy, mathutils +from bpy.types import Operator, Context, UILayout, EnumProperty from bpy.utils import register_class, unregister_class -from .utility import * -from .f3d.f3d_material import * + +from .utility import ( + cleanupTempMeshes, + get_mode_set_from_context_mode, + raisePluginError, + parentObject, + store_original_meshes, + store_original_mtx, +) +from .f3d.f3d_material import createF3DMat def addMaterialByName(obj, matName, preset): @@ -14,6 +26,9 @@ def addMaterialByName(obj, matName, preset): material.name = matName +PROFILE_ENABLED = False + + class OperatorBase(Operator): """Base class for operators, keeps track of context mode and sets it back after running execute_operator() and catches exceptions for raisePluginError()""" @@ -21,13 +36,19 @@ class OperatorBase(Operator): context_mode: str = "" icon = "NONE" + @classmethod + def is_enabled(cls, context: Context, **op_values): + return True + @classmethod def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, **op_values): """Op args are passed to the operator via setattr()""" icon = icon if icon else cls.icon + layout = layout.column() op = layout.operator(cls.bl_idname, icon=icon, text=text) for key, value in op_values.items(): setattr(op, key, value) + layout.enabled = cls.is_enabled(bpy.context, **op_values) return op def execute_operator(self, context: Context): @@ -40,7 +61,12 @@ def execute(self, context: Context): try: if self.context_mode and self.context_mode != starting_mode_set: bpy.ops.object.mode_set(mode=self.context_mode) - self.execute_operator(context) + if PROFILE_ENABLED: + with Profile() as profile: + self.execute_operator(context) + print(Stats(profile).strip_dirs().sort_stats(SortKey.CUMULATIVE).print_stats()) + else: + self.execute_operator(context) return {"FINISHED"} except Exception as exc: raisePluginError(self, exc) @@ -53,6 +79,34 @@ def execute(self, context: Context): bpy.ops.object.mode_set(mode=starting_mode_set) +class SearchEnumOperatorBase(OperatorBase): + bl_description = "Search Enum" + bl_label = "Search" + bl_property = None + bl_options = {"UNDO"} + + @classmethod + def draw_props(cls, layout: UILayout, data, prop: str, name: str): + row = layout.row() + if name: + row.label(text=name) + row.prop(data, prop, text="") + row.operator(cls.bl_idname, icon="VIEWZOOM", text="") + + def update_enum(self, context: Context): + raise NotImplementedError() + + def execute_operator(self, context: Context): + assert self.bl_property + self.report({"INFO"}, f"Selected: {getattr(self, self.bl_property)}") + self.update_enum(context) + context.region.tag_redraw() + + def invoke(self, context: Context, _): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + class AddWaterBox(OperatorBase): bl_idname = "object.add_water_box" bl_label = "Add Water Box" diff --git a/fast64_internal/sm64/__init__.py b/fast64_internal/sm64/__init__.py index 81fdbf596..a9b429989 100644 --- a/fast64_internal/sm64/__init__.py +++ b/fast64_internal/sm64/__init__.py @@ -1,3 +1,7 @@ +from bpy.types import PropertyGroup +from bpy.props import PointerProperty +from bpy.utils import register_class, unregister_class + from .settings import ( settings_props_register, settings_props_unregister, @@ -83,14 +87,23 @@ sm64_dl_writer_unregister, ) -from .sm64_anim import ( - sm64_anim_panel_register, - sm64_anim_panel_unregister, - sm64_anim_register, - sm64_anim_unregister, +from .animation import ( + anim_panel_register, + anim_panel_unregister, + anim_register, + anim_unregister, + SM64_ActionAnimProperty, ) +class SM64_ActionProperty(PropertyGroup): + """ + Properties in Action.fast64.sm64. + """ + + animation: PointerProperty(type=SM64_ActionAnimProperty, name="SM64 Properties") + + def sm64_panel_register(): settings_panels_register() tools_panels_register() @@ -103,7 +116,7 @@ def sm64_panel_register(): sm64_spline_panel_register() sm64_dl_writer_panel_register() sm64_dl_parser_panel_register() - sm64_anim_panel_register() + anim_panel_register() def sm64_panel_unregister(): @@ -118,12 +131,13 @@ def sm64_panel_unregister(): sm64_spline_panel_unregister() sm64_dl_writer_panel_unregister() sm64_dl_parser_panel_unregister() - sm64_anim_panel_unregister() + anim_panel_unregister() def sm64_register(register_panels: bool): tools_operators_register() tools_props_register() + anim_register() sm64_col_register() sm64_bone_register() sm64_cam_register() @@ -134,8 +148,8 @@ def sm64_register(register_panels: bool): sm64_spline_register() sm64_dl_writer_register() sm64_dl_parser_register() - sm64_anim_register() settings_props_register() + register_class(SM64_ActionProperty) if register_panels: sm64_panel_register() @@ -144,6 +158,7 @@ def sm64_register(register_panels: bool): def sm64_unregister(unregister_panels: bool): tools_operators_unregister() tools_props_unregister() + anim_unregister() sm64_col_unregister() sm64_bone_unregister() sm64_cam_unregister() @@ -154,8 +169,8 @@ def sm64_unregister(unregister_panels: bool): sm64_spline_unregister() sm64_dl_writer_unregister() sm64_dl_parser_unregister() - sm64_anim_unregister() settings_props_unregister() + unregister_class(SM64_ActionProperty) if unregister_panels: sm64_panel_unregister() diff --git a/fast64_internal/sm64/animation/__init__.py b/fast64_internal/sm64/animation/__init__.py new file mode 100644 index 000000000..0aca0cc02 --- /dev/null +++ b/fast64_internal/sm64/animation/__init__.py @@ -0,0 +1,15 @@ +from .operators import anim_ops_register, anim_ops_unregister +from .properties import anim_props_register, anim_props_unregister, SM64_ArmatureAnimProperties, SM64_ActionAnimProperty +from .panels import anim_panel_register, anim_panel_unregister +from .exporting import export_animation, export_animation_table +from .utility import get_anim_obj, is_obj_animatable + + +def anim_register(): + anim_ops_register() + anim_props_register() + + +def anim_unregister(): + anim_ops_unregister() + anim_props_unregister() diff --git a/fast64_internal/sm64/animation/classes.py b/fast64_internal/sm64/animation/classes.py new file mode 100644 index 000000000..52ca93bb7 --- /dev/null +++ b/fast64_internal/sm64/animation/classes.py @@ -0,0 +1,1006 @@ +from typing import Optional +from pathlib import Path +from enum import IntFlag +from io import StringIO +from copy import copy +import dataclasses +import numpy as np +import functools +import typing +import re + +from bpy.types import Action + +from ...f3d.f3d_parser import math_eval + +from ...utility import PluginError, cast_integer, encodeSegmentedAddr, intToHex +from ..sm64_constants import MAX_U16, SegmentData +from ..sm64_utility import CommentMatch, adjust_start_end +from ..sm64_classes import RomReader, DMATable, DMATableElement, IntArray + +from .constants import HEADER_STRUCT, HEADER_SIZE, TABLE_ELEMENT_PATTERN +from .utility import get_dma_header_name, get_dma_anim_name + + +@dataclasses.dataclass +class CArrayDeclaration: + name: str = "" + path: Path = Path("") + file_name: str = "" + values: list[str] | dict[str, str] = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class SM64_AnimPair: + values: np.ndarray[typing.Any, np.dtype[np.int16]] = dataclasses.field(compare=False) + + # Importing + address: int = 0 + end_address: int = 0 + + offset: int = 0 # For compressing + + def __post_init__(self): + assert self.values.size > 0, "values cannot be empty" + + def clean_frames(self): + mask = self.values != self.values[-1] + # Reverse the order, find the last element with the same value + index = np.argmax(mask[::-1]) + if index != 1: + self.values = self.values[: 1 if index == 0 else (-index + 1)] + return self + + def get_frame(self, frame: int): + return self.values[min(frame, len(self.values) - 1)] + + +@dataclasses.dataclass +class SM64_AnimData: + pairs: list[SM64_AnimPair] = dataclasses.field(default_factory=list) + indice_reference: str | int = "" + values_reference: str | int = "" + + # Importing + indices_file_name: str = "" + values_file_name: str = "" + value_end_address: int = 0 + indice_end_address: int = 0 + start_address: int = 0 + end_address: int = 0 + + @property + def key(self): + return (self.indice_reference, self.values_reference) + + def create_tables(self, start_address=-1): + indice_tables, value_tables = create_tables([self], start_address=start_address) + assert ( + len(value_tables) == 1 and len(indice_tables) == 1 + ), "Single animation data export should only return 1 of each table." + return indice_tables[0], value_tables[0] + + def to_c(self, dma: bool = False): + text_data = StringIO() + + indice_table, value_table = self.create_tables() + if dma: + indice_table.to_c(text_data, new_lines=2) + value_table.to_c(text_data) + else: + value_table.to_c(text_data, new_lines=2) + indice_table.to_c(text_data) + + return text_data.getvalue() + + def to_binary(self, start_address=-1): + indice_table, value_table = self.create_tables(start_address) + values_offset = len(indice_table.data) * 2 + + data = bytearray() + data.extend(indice_table.to_binary()) + data.extend(value_table.to_binary()) + return data, values_offset + + def read_binary(self, indices_reader: RomReader, values_reader: RomReader, bone_count: int): + print( + f"Reading pairs from indices table at {intToHex(indices_reader.address)}", + f"and values table at {intToHex(values_reader.address)}.", + ) + self.indice_reference = indices_reader.start_address + self.values_reference = values_reader.start_address + + # 3 pairs per bone + 3 for root translation of 2, each 2 bytes + indices_size = (((bone_count + 1) * 3) * 2) * 2 + indices_values = np.frombuffer(indices_reader.read_data(indices_size), dtype=">u2") + for i in range(0, len(indices_values), 2): + max_frame, offset = indices_values[i], indices_values[i + 1] + address, size = values_reader.start_address + (offset * 2), max_frame * 2 + + values = np.frombuffer(values_reader.read_data(size, address), dtype=">i2", count=max_frame) + self.pairs.append(SM64_AnimPair(values, address, address + size, offset).clean_frames()) + self.indice_end_address = indices_reader.address + self.value_end_address = max(pair.end_address for pair in self.pairs) + + self.start_address = min(self.indice_reference, self.values_reference) + self.end_address = max(self.indice_end_address, self.value_end_address) + return self + + def read_c(self, indice_decl: CArrayDeclaration, value_decl: CArrayDeclaration): + print(f'Reading data from "{indice_decl.name}" and "{value_decl.name}" c declarations.') + self.indices_file_name, self.values_file_name = indice_decl.file_name, value_decl.file_name + self.indice_reference, self.values_reference = indice_decl.name, value_decl.name + + indices_values = np.vectorize(lambda x: int(x, 0), otypes=[np.uint16])(indice_decl.values) + values_array = np.vectorize(lambda x: int(x, 0), otypes=[np.int16])(value_decl.values) + + for i in range(0, len(indices_values), 2): + max_frame, offset = indices_values[i], indices_values[i + 1] + self.pairs.append(SM64_AnimPair(values_array[offset : offset + max_frame], -1, -1, offset).clean_frames()) + return self + + +class SM64_AnimFlags(IntFlag): + prop: Optional[str] + + def __new__(cls, value, blender_prop: str | None = None): + obj = int.__new__(cls, value) + obj._value_, obj.prop = 1 << value, blender_prop + return obj + + ANIM_FLAG_NOLOOP = (0, "no_loop") + ANIM_FLAG_FORWARD = (1, "backwards") + ANIM_FLAG_2 = (2, "no_acceleration") + ANIM_FLAG_HOR_TRANS = (3, "only_vertical") + ANIM_FLAG_VERT_TRANS = (4, "only_horizontal") + ANIM_FLAG_5 = (5, "disabled") + ANIM_FLAG_6 = (6, "no_trans") + ANIM_FLAG_7 = 7 + + ANIM_FLAG_BACKWARD = (1, "backwards") # refresh 16 + + # hackersm64 + ANIM_FLAG_NO_ACCEL = (2, "no_acceleration") + ANIM_FLAG_DISABLED = (5, "disabled") + ANIM_FLAG_NO_TRANS = (6, "no_trans") + ANIM_FLAG_UNUSED = 7 + + @classmethod + @functools.cache + def all_flags(cls): + flags = SM64_AnimFlags(0) + for flag in cls.__members__.values(): + flags |= flag + return flags + + @classmethod + @functools.cache + def all_flags_with_prop(cls): + flags = SM64_AnimFlags(0) + for flag in cls.__members__.values(): + if flag.prop is not None: + flags |= flag + return flags + + @classmethod + @functools.cache + def props_to_flags(cls): + return {flag.prop: flag for flag in cls.__members__.values() if flag.prop is not None} + + @classmethod + @functools.cache + def flags_to_names(cls): + names: dict[SM64_AnimFlags, list[str]] = {} + for name, flag in cls.__members__.items(): + if flag in names: + names[flag].append(name) + else: + names[flag] = [name] + return names + + @property + @functools.cache + def names(self): + names: list[str] = [] + for flag, flag_names in SM64_AnimFlags.flags_to_names().items(): + if flag in self: + names.append("/".join(flag_names)) + if self & ~self.__class__.all_flags(): # flag value outside known flags + names.append("unknown bits") + return names + + @classmethod + @functools.cache + def evaluate(cls, value: str | int): + if isinstance(value, cls): # the value was already evaluated + return value + elif isinstance(value, str): + try: + value = cls(math_eval(value, cls)) + except Exception as exc: # pylint: disable=broad-except + print(f"Failed to evaluate flags {value}: {exc}") + if isinstance(value, int): # the value was fully evaluated + if isinstance(value, cls): + value = value.value + # cast to u16 for simplicity + return cls(cast_integer(value, 16, signed=False)) + else: # the value was not evaluated + return value + + +@dataclasses.dataclass +class SM64_AnimHeader: + reference: str | int = "" + flags: SM64_AnimFlags | str = SM64_AnimFlags(0) + trans_divisor: int = 0 + start_frame: int = 0 + loop_start: int = 0 + loop_end: int = 1 + bone_count: int = 0 + length: int = 0 + indice_reference: Optional[str | int] = None + values_reference: Optional[str | int] = None + data: Optional[SM64_AnimData] = None + + enum_name: str = "" + # Imports + file_name: str = "" + end_address: int = 0 + header_variant: int = 0 + table_index: int = 0 + action: Action | None = None + + @property + def data_key(self): + return (self.indice_reference, self.values_reference) + + @property + def flags_comment(self): + if isinstance(self.flags, SM64_AnimFlags): + return ", ".join(self.flags.names) + return "" + + @property + def c_flags(self): + return self.flags if isinstance(self.flags, str) else intToHex(self.flags.value, 2) + + def get_reference(self, override: Optional[str | int], expected_type: type, reference_name: str): + name = reference_name.replace("_", " ") + if override: + reference = override + elif self.data and getattr(self.data, reference_name): + reference = getattr(self.data, reference_name) + elif getattr(self, reference_name): + reference = getattr(self, reference_name) + else: + assert False, f"Unknown {name}" + + assert isinstance( + reference, expected_type + ), f"{name.capitalize()} must be a {expected_type},is instead {type(reference)}." + return reference + + def get_values_reference(self, override: Optional[str | int] = None, expected_type: type = str): + return self.get_reference(override, expected_type, "values_reference") + + def get_indice_reference(self, override: Optional[str | int] = None, expected_type: type = str): + return self.get_reference(override, expected_type, "indice_reference") + + def to_c(self, values_override: Optional[str] = None, indice_override: Optional[str] = None, dma=False): + assert not dma or isinstance( # assert if dma and flags are not SM64_AnimFlags + self.flags, SM64_AnimFlags + ), f"Flags must be SM64_AnimFlags for C DMA, is instead {type(self.flags)}" + return ( + f"static const struct Animation {self.reference}{'[]' if dma else ''} = {{\n" + + f"\t{self.c_flags}, // flags {self.flags_comment}\n" + f"\t{self.trans_divisor}, // animYTransDivisor\n" + f"\t{self.start_frame}, // startFrame\n" + f"\t{self.loop_start}, // loopStart\n" + f"\t{self.loop_end}, // loopEnd\n" + f"\tANIMINDEX_NUMPARTS({self.get_indice_reference(indice_override, str)}), // unusedBoneCount\n" + f"\t{self.get_values_reference(values_override, str)}, // values\n" + f"\t{self.get_indice_reference(indice_override, str)}, // index\n" + "\t0 // length\n" + "};\n" + ) + + def to_binary( + self, + values_override: Optional[int] = None, + indice_override: Optional[int] = None, + segment_data: SegmentData | None = None, + length=0, + ): + assert isinstance( + self.flags, SM64_AnimFlags + ), f"Flags must be SM64_AnimFlags for binary, is instead {type(self.flags)}" + values_address = self.get_values_reference(values_override, int) + indice_address = self.get_indice_reference(indice_override, int) + if segment_data: + values_address = int.from_bytes(encodeSegmentedAddr(values_address, segment_data), "big") + indice_address = int.from_bytes(encodeSegmentedAddr(indice_address, segment_data), "big") + + return HEADER_STRUCT.pack( + self.flags.value, + self.trans_divisor, + self.start_frame, + self.loop_start, + self.loop_end, + self.bone_count, + values_address, + indice_address, + length, + ) + + @staticmethod + def read_binary( + reader: RomReader, + read_headers: dict[str, "SM64_AnimHeader"], + dma: bool = False, + bone_count: Optional[int] = None, + table_index: Optional[int] = None, + ): + if str(reader.start_address) in read_headers: + return read_headers[str(reader.start_address)] + print(f"Reading animation header at {intToHex(reader.start_address)}.") + + header = SM64_AnimHeader() + read_headers[str(reader.start_address)] = header + header.reference = reader.start_address + + header.flags = SM64_AnimFlags.evaluate(reader.read_int(2, True)) # /*0x00*/ s16 flags; + header.trans_divisor = reader.read_int(2, True) # /*0x02*/ s16 animYTransDivisor; + header.start_frame = reader.read_int(2, True) # /*0x04*/ s16 startFrame; + header.loop_start = reader.read_int(2, True) # /*0x06*/ s16 loopStart; + header.loop_end = reader.read_int(2, True) # /*0x08*/ s16 loopEnd; + + # /*0x0A*/ s16 unusedBoneCount; (Unused in engine) + header.bone_count = reader.read_int(2, True) + if header.bone_count <= 0: + if bone_count is None: + raise PluginError( + "No bone count in header and no bone count passed in from target armature, cannot figure out" + ) + header.bone_count = bone_count + print("Old exports lack a defined bone count, invalid armatures won't be detected") + elif bone_count is not None and header.bone_count != bone_count: + raise PluginError( + f"Imported header's bone count is {header.bone_count} but object's is {bone_count}", + ) + + # /*0x0C*/ const s16 *values; + # /*0x10*/ const u16 *index; + if dma: + header.values_reference = reader.start_address + reader.read_int(4) + header.indice_reference = reader.start_address + reader.read_int(4) + else: + header.values_reference, header.indice_reference = reader.read_ptr(), reader.read_ptr() + header.length = reader.read_int(4) + + header.end_address = reader.address + 1 + header.table_index = len(read_headers) if table_index is None else table_index + + data = next( + (other_header.data for other_header in read_headers.values() if header.data_key == other_header.data_key), + None, + ) + if not data: + indices_reader = reader.branch(header.indice_reference) + values_reader = reader.branch(header.values_reference) + if indices_reader and values_reader: + data = SM64_AnimData().read_binary( + indices_reader, + values_reader, + header.bone_count, + ) + header.data = data + + return header + + @staticmethod + def read_c( + header_decl: CArrayDeclaration, + value_decls, + indices_decls, + read_headers: dict[str, "SM64_AnimHeader"], + table_index: Optional[int] = None, + ): + if header_decl.name in read_headers: + return read_headers[header_decl.name] + if len(header_decl.values) != 9: + raise ValueError(f"Header declarion has {len(header_decl.values)} values instead of 9.\n {header_decl}") + print(f'Reading header "{header_decl.name}" c declaration.') + header = SM64_AnimHeader() + read_headers[header_decl.name] = header + header.reference = header_decl.name + header.file_name = header_decl.file_name + + # Place the values into a dictionary, handles designated initialization + if isinstance(header_decl.values, list): + designated = {} + for value, var in zip( + header_decl.values, + [ + "flags", + "animYTransDivisor", + "startFrame", + "loopStart", + "loopEnd", + "unusedBoneCount", + "values", + "index", + "length", + ], + ): + designated[var] = value + else: + designated = header_decl.values + + # Read from the dict + header.flags = SM64_AnimFlags.evaluate(designated["flags"]) + header.trans_divisor = int(designated["animYTransDivisor"], 0) + header.start_frame = int(designated["startFrame"], 0) + header.loop_start = int(designated["loopStart"], 0) + header.loop_end = int(designated["loopEnd"], 0) + # bone_count = designated["unusedBoneCount"] + header.values_reference = designated["values"] + header.indice_reference = designated["index"] + + header.table_index = len(read_headers) if table_index is None else table_index + + data = next( + (other_header.data for other_header in read_headers.values() if header.data_key == other_header.data_key), + None, + ) + if not data: + indices_decl = next((indice for indice in indices_decls if indice.name == header.indice_reference), None) + value_decl = next((value for value in value_decls if value.name == header.values_reference), None) + if indices_decl and value_decl: + data = SM64_AnimData().read_c(indices_decl, value_decl) + header.data = data + + return header + + +@dataclasses.dataclass +class SM64_Anim: + data: SM64_AnimData | None = None + headers: list[SM64_AnimHeader] = dataclasses.field(default_factory=list) + file_name: str = "" + + # Imports + action_name: str = "" # Used for the blender action's name + action: Action | None = None # Used in the table class to prop function + + @property + def names(self) -> tuple[list[str], list[str]]: + names, enums = [], [] + for header in self.headers: + names.append(header.reference) + enums.append(header.enum_name) + return names, enums + + @property + def header_names(self) -> list[str]: + return self.names[0] + + @property + def enum_names(self) -> list[str]: + return self.names[1] + + def to_binary_dma(self): + assert self.data + headers: list[bytes] = [] + + indice_offset = HEADER_SIZE * len(self.headers) + anim_data, values_offset = self.data.to_binary() + for header in self.headers: + header_data = header.to_binary( + indice_offset + values_offset, indice_offset, length=indice_offset + len(anim_data) + ) + headers.append(header_data) + indice_offset -= HEADER_SIZE + return headers, anim_data + + def to_binary(self, start_address: int = 0, segment_data: SegmentData | None = None): + data: bytearray = bytearray() + ptrs: list[int] = [] + if self.data: + anim_data, values_offset = self.data.to_binary() + indice_offset = start_address + (HEADER_SIZE * len(self.headers)) + values_offset = indice_offset + values_offset + else: + anim_data = bytearray() + indice_offset = values_offset = None + for header in self.headers: + if self.data: + ptrs.extend([start_address + len(data) + 12, start_address + len(data) + 16]) + header_data = header.to_binary( + values_offset, + indice_offset, + segment_data, + ) + data.extend(header_data) + + data.extend(anim_data) + return data, ptrs + + def headers_to_c(self, dma: bool) -> str: + text_data = StringIO() + for header in self.headers: + text_data.write(header.to_c(dma=dma)) + text_data.write("\n") + return text_data.getvalue() + + def to_c(self, dma: bool): + text_data = StringIO() + c_headers = self.headers_to_c(dma) + if dma: + text_data.write(c_headers) + text_data.write("\n") + if self.data: + text_data.write(self.data.to_c(dma)) + text_data.write("\n") + if not dma: + text_data.write(c_headers) + return text_data.getvalue() + + +@dataclasses.dataclass +class SM64_AnimTableElement: + reference: str | int | None = None + header: SM64_AnimHeader | None = None + + # C exporting + enum_name: str = "" + reference_start: int = -1 + reference_end: int = -1 + enum_start: int = -1 + enum_end: int = -1 + enum_val: str = "" + + @property + def c_name(self): + if self.reference: + return self.reference + return "" + + @property + def c_reference(self): + if self.reference: + return f"&{self.reference}" + return "NULL" + + @property + def enum_c(self): + if self.enum_val: + return f"{self.enum_name} = {self.enum_val}" + return self.enum_name + + @property + def data(self): + return self.header.data if self.header else None + + def to_c(self, designated: bool): + if designated and self.enum_name: + return f"[{self.enum_name}] = {self.c_reference}," + else: + return f"{self.c_reference}," + + +@dataclasses.dataclass +class SM64_AnimTable: + reference: str | int = "" + enum_list_reference: str = "" + enum_list_delimiter: str = "" + file_name: str = "" + elements: list[SM64_AnimTableElement] = dataclasses.field(default_factory=list) + # Importing + end_address: int = 0 + # C exporting + values_reference: str = "" + start: int = -1 + end: int = -1 + enum_list_start: int = -1 + enum_list_end: int = -1 + + @property + def names(self) -> tuple[list[str], list[str]]: + names, enums = [], [] + for element in self.elements: + names.append(element.c_name) + enums.append(element.enum_name) + return names, enums + + @property + def header_names(self) -> list[str]: + return self.names[0] + + @property + def enum_names(self) -> list[str]: + return self.names[1] + + @property + def header_data_sets(self) -> tuple[list[SM64_AnimHeader], list[SM64_AnimData]]: + # Remove duplicates of data and headers, keep order by using a list + data_set = [] + headers_set = [] + for element in self.elements: + if element.data and not element.data in data_set: + data_set.append(element.data) + if element.header and not element.header in headers_set: + headers_set.append(element.header) + return headers_set, data_set + + @property + def header_set(self) -> list[SM64_AnimHeader]: + return self.header_data_sets[0] + + @property + def has_null_delimiter(self): + return bool(self.elements and self.elements[-1].reference is None) + + def get_seperate_anims(self): + print("Getting seperate animations from table.") + anims: list[SM64_Anim] = [] + headers_set, headers_added = self.header_set, [] + for header in headers_set: + if header in headers_added: + continue + ordered_headers: list[SM64_AnimHeader] = [] + variant = 0 + for other_header in headers_set: + if other_header.data == header.data: + other_header.header_variant = variant + ordered_headers.append(other_header) + headers_added.append(other_header) + variant += 1 + + anims.append(SM64_Anim(header.data, ordered_headers, header.file_name)) + return anims + + def get_seperate_anims_dma(self) -> list[SM64_Anim]: + print("Getting seperate DMA animations from table.") + + anims = [] + header_nums = [] + included_headers: list[SM64_AnimHeader] = [] + data = None + # For creating duplicates + data_already_added = [] + headers_already_added = [] + + for i, element in enumerate(self.elements): + assert element.header, f"Header in table element {i} is not set." + assert element.data, f"Data in table element {i} is not set." + header_nums.append(i) + + header, data = element.header, element.data + if header in headers_already_added: + print(f"Made duplicate of header {i}.") + header = copy(header) + header.reference = get_dma_header_name(i) + headers_already_added.append(header) + + included_headers.append(header) + + # If not at the end of the list and the next element doesn´t have different data + if (i < len(self.elements) - 1) and self.elements[i + 1].data is data: + continue + + name = get_dma_anim_name(header_nums) + file_name = f"{name}.inc.c" + if data in data_already_added: + print(f"Made duplicate of header {i}'s data.") + data = copy(data) + data_already_added.append(data) + + data.indice_reference, data.values_reference = f"{name}_indices", f"{name}_values" + # Normal names are possible (order goes by line and file) but would break convention + for i, included_header in enumerate(included_headers): + included_header.file_name = file_name + included_header.indice_reference = data.indice_reference + included_header.values_reference = data.values_reference + included_header.data = data + included_header.header_variant = i + anims.append(SM64_Anim(data, included_headers, file_name)) + + header_nums.clear() + included_headers = [] + + return anims + + def to_binary_dma(self): + dma_table = DMATable() + for animation in self.get_seperate_anims_dma(): + headers, data = animation.to_binary_dma() + end_offset = len(dma_table.data) + (HEADER_SIZE * len(headers)) + len(data) + for header in headers: + offset = len(dma_table.data) + size = end_offset - offset + dma_table.entries.append(DMATableElement(offset, size)) + dma_table.data.extend(header) + dma_table.data.extend(data) + return dma_table.to_binary() + + def to_combined_binary(self, table_address=0, data_address=-1, segment_data: SegmentData | None = None): + table_data: bytearray = bytearray() + data: bytearray = bytearray() + ptrs: list[int] = [] + headers_set, data_set = self.header_data_sets + + # Pre calculate offsets + table_length = len(self.elements) * 4 + if data_address == -1: + data_address = table_address + table_length + + headers_length = len(headers_set) * HEADER_SIZE + indice_tables, value_tables = create_tables(data_set, self.values_reference, data_address + headers_length) + + # Add the animation table + for i, element in enumerate(self.elements): + if element.header: + ptrs.append(table_address + len(table_data)) + header_offset = data_address + (headers_set.index(element.header) * HEADER_SIZE) + if segment_data: + table_data.extend(encodeSegmentedAddr(header_offset, segment_data)) + else: + table_data.extend(header_offset.to_bytes(4, byteorder="big")) + continue + if element.reference is None: + table_data.extend(0x0.to_bytes(4, byteorder="big")) + continue + assert isinstance(element.reference, int), f"Reference at element {i} is not an int." + table_data.extend(element.reference.to_bytes(4, byteorder="big")) + + for anim_header in headers_set: # Add the headers + if not anim_header.data: + data.extend(anim_header.to_binary()) + continue + ptrs.extend([data_address + len(data) + 12, data_address + len(data) + 16]) + data.extend(anim_header.to_binary(segment_data=segment_data)) + + for table in indice_tables + value_tables: + data.extend(table.to_binary()) + + return table_data, data, ptrs + + def data_and_headers_to_c(self, dma: bool): + files_data: dict[str, str] = {} + animation: SM64_Anim + for animation in self.get_seperate_anims_dma() if dma else self.get_seperate_anims(): + files_data[animation.file_name] = animation.to_c(dma=dma) + return files_data + + def data_and_headers_to_c_combined(self): + text_data = StringIO() + headers_set, data_set = self.header_data_sets + if data_set: + indice_tables, value_tables = create_tables(data_set, self.values_reference) + for table in value_tables + indice_tables: + table.to_c(text_data, new_lines=2) + for anim_header in headers_set: + text_data.write(anim_header.to_c()) + text_data.write("\n") + + return text_data.getvalue() + + def read_binary( + self, + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + size: Optional[int] = None, + ): + print(f"Reading table at address {reader.start_address}.") + self.elements.clear() + self.reference = reader.start_address + + range_size = size or 300 + if table_index is not None: + range_size = min(range_size, table_index + 1) + for i in range(range_size): + ptr = reader.read_ptr() + if size is None and ptr == 0: # If no specified size and ptr is NULL, break + self.elements.append(SM64_AnimTableElement()) + break + elif table_index is not None and i != table_index: + continue # Skip entries until table_index if specified + + header_reader = reader.branch(ptr) + if header_reader is None: + self.elements.append(SM64_AnimTableElement(ptr)) + else: + try: + header = SM64_AnimHeader.read_binary( + header_reader, + read_headers, + False, + bone_count, + i, + ) + except Exception as exc: + raise PluginError(f"Failed to read header in table element {i}: {str(exc)}") from exc + self.elements.append(SM64_AnimTableElement(ptr, header)) + + if table_index is not None: # Break if table_index is specified + break + else: + if table_index is not None: + raise PluginError(f"Table index {table_index} not found in table.") + if size is None: + raise PluginError(f"Iterated through {range_size} elements and no NULL was found.") + self.end_address = reader.address + return self + + def read_dma_binary( + self, + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + ): + dma_table = DMATable() + dma_table.read_binary(reader) + self.reference = reader.start_address + if table_index is not None: + assert table_index >= 0 and table_index < len( + dma_table.entries + ), f"Index {table_index} outside of defined table ({len(dma_table.entries)} entries)." + entrie = dma_table.entries[table_index] + header_reader = reader.branch(entrie.address) + if header_reader is None: + raise PluginError("Failed to branch into DMA entrie's address") + return SM64_AnimHeader.read_binary( + header_reader, + read_headers, + True, + bone_count, + table_index, + ) + + for i, entrie in enumerate(dma_table.entries): + header_reader = reader.branch(entrie.address) + try: + if not header_reader: + raise PluginError("Failed to branch to header's address") + header = SM64_AnimHeader.read_binary(header_reader, read_headers, True, bone_count, i) + except Exception as exc: + raise PluginError(f"Failed to read header in table element {i}: {str(exc)}") from exc + self.elements.append(SM64_AnimTableElement(reader.start_address, header)) + self.end_address = dma_table.end_address + return self + + def read_c( + self, + c_data: str, + start: int, + end: int, + comment_map: list[CommentMatch], + read_headers: dict[str, SM64_AnimHeader], + header_decls: list[CArrayDeclaration], + values_decls: list[CArrayDeclaration], + indices_decls: list[CArrayDeclaration], + ): + table_start, table_end = adjust_start_end(start, end, comment_map) + self.start, self.end = table_start, table_end + + for i, element_match in enumerate(re.finditer(TABLE_ELEMENT_PATTERN, c_data[start:end])): + enum, element, null = ( + element_match.group("enum"), + element_match.group("element"), + element_match.group("null"), + ) + if enum is None and element is None and null is None: # comment + continue + header = None + if element is not None: + header_decl = next((header for header in header_decls if header.name == element), None) + if header_decl: + header = SM64_AnimHeader.read_c( + header_decl, + values_decls, + indices_decls, + read_headers, + i, + ) + element_start, element_end = adjust_start_end( + table_start + element_match.start(), table_start + element_match.end(), comment_map + ) + self.elements.append( + SM64_AnimTableElement( + element, + enum_name=enum, + reference_start=element_start - table_start, + reference_end=element_end - table_start, + header=header, + ) + ) + + +def create_tables(anims_data: list[SM64_AnimData], values_name="", start_address=-1): + """ + Can generate multiple indices table with only one value table (or multiple if needed), + which improves compression (this feature is used in table exports). + Update the animation data with the correct references. + Returns: indice_tables, value_tables (in that order) + """ + + def add_data(values_table: IntArray, size: int, anim_data: SM64_AnimData, values_address: int): + data = values_table.data + for pair in anim_data.pairs: + pair_values = pair.values + if len(pair_values) >= MAX_U16: + raise PluginError( + f"Pair frame count ({len(pair_values)}) is higher than the 16 bit max ({MAX_U16}). Too many frames." + ) + + # It's never worth it to find an existing offset for values bigger than 1 frame. + # From my (@Lilaa3) testing, the only improvement in Mario resulted in just 286 bytes saved. + offset = None + if len(pair_values) == 1: + indices = np.isin(data[:size], pair_values[0]).nonzero()[0] + offset = indices[0] if indices.size > 0 else None + + if offset is None: # no existing offset found + offset = size + size = offset + len(pair_values) + if size > MAX_U16: # exceeded limit, but we may be able to recover with a new table + return -1, None + data[offset:size] = pair_values + pair.offset = offset + + # build indice table + indice_values = np.empty((len(anim_data.pairs), 2), np.uint16) + for i, pair in enumerate(anim_data.pairs): + indice_values[i] = [len(pair.values), pair.offset] # Use calculated offsets + indice_values = indice_values.reshape(-1) + indice_table = IntArray(indice_values, str(anim_data.indice_reference), 6, -6) + + if values_address == -1: + anim_data.values_reference = value_table.name + else: + anim_data.values_reference = values_address + return size, indice_table + + indice_tables: list[IntArray] = [] + value_tables: list[IntArray] = [] + + values_name = values_name or str(anims_data[0].values_reference) + indices_address = start_address + if start_address != -1: + for anim_data in anims_data: + anim_data.indice_reference = indices_address + indices_address += len(anim_data.pairs) * 2 * 2 + values_address = indices_address + + print("Generating compressed value table and offsets.") + # opt: this is the max size possible, prevents tons of allocations and only about 65 kb + value_table = IntArray(np.empty(MAX_U16, np.int16), values_name, 8) + size = 0 + value_tables.append(value_table) + i = 0 # we can´t use enumarate, as we may repeat + while i < len(anims_data): + anim_data = anims_data[i] + + size_before_add = size + size, indice_table = add_data(value_table, size, anim_data, values_address) + if size != -1: # sucefully added the data to the value table + assert indice_table is not None + indice_tables.append(indice_table) + i += 1 # do the next animation + else: # Could not add to the value table + if size_before_add == 0: # If the table was empty, it is simply invalid + raise PluginError(f"Index table cannot fit into value table of 16 bit max size ({MAX_U16}).") + else: # try again with a fresh value table + value_table.data.resize(size_before_add, refcheck=False) + if start_address != -1: + values_address += size_before_add * 2 + value_table = IntArray(np.empty(MAX_U16, np.int16), f"{values_name}_{len(value_tables)}", 9) + value_tables.append(value_table) + size = 0 # reset size + # don't increment i, redo + value_table.data.resize(size, refcheck=False) + + return indice_tables, value_tables diff --git a/fast64_internal/sm64/animation/constants.py b/fast64_internal/sm64/animation/constants.py new file mode 100644 index 000000000..7ea4bbdc4 --- /dev/null +++ b/fast64_internal/sm64/animation/constants.py @@ -0,0 +1,88 @@ +import struct +import re + +from ...utility import intToHex +from ..sm64_constants import ACTOR_PRESET_INFO, ActorPresetInfo + +HEADER_STRUCT = struct.Struct(">h h h h h h I I I") +HEADER_SIZE = HEADER_STRUCT.size + +TABLE_ELEMENT_PATTERN = re.compile( # strict but only in the sense that it requires valid c code + r""" + (?:\[\s*(?P\w+)\s*\]\s*=\s*)? # Don´t capture brackets or equal, works with nums + (?:(?:&\s*(?P\w+))|(?PNULL)) # Capture element or null, element requires & + (?:\s*,|) # allow no comma, techinically not correct but no other method works + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_PATTERN = re.compile( + r""" + const\s+struct\s*Animation\s*\*const\s*(?P\w+)\s* + (?:\[.*?\])? # Optional size, don´t capture + \s*=\s*\{ + (?P[\s\S]*) # Capture any character including new lines + (?=\}\s*;) # Look ahead for the end + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_ENUM_PATTERN = re.compile( # strict but only in the sense that it requires valid c code + r""" + (?P\w+)\s* + (?:\s*=\s*(?P\w+)\s*)? + (?=,|) # lookahead, allow no comma, techinically not correct but no other method works + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_ENUM_LIST_PATTERN = re.compile( + r""" + enum\s*(?P\w+)\s*\{ + (?P[\s\S]*) # Capture any character including new lines, lazy + (?=\}\s*;) + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +enumAnimExportTypes = [ + ("Actor", "Actor Data", "Includes are added to a group in actors/"), + ("Level", "Level Data", "Includes are added to a specific level in levels/"), + ( + "DMA", + "DMA (Mario)", + "No headers or includes are genarated. Mario animation converter order is used (headers, indicies, values)", + ), + ("Custom", "Custom Path", "Exports to a specific path"), +] + +enum_anim_import_types = [ + ("C", "C", "Import a decomp folder or a specific animation"), + ("Binary", "Binary", "Import from ROM"), + ("Insertable Binary", "Insertable Binary", "Import from an insertable binary file"), +] + +enum_anim_binary_import_types = [ + ("DMA", "DMA (Mario)", "Import a DMA animation from a DMA table from a ROM"), + ("Table", "Table", "Import animations from an animation table from a ROM"), + ("Animation", "Animation", "Import one animation from a ROM"), +] + + +enum_animated_behaviours = [("Custom", "Custom Behavior", "Custom"), ("", "Presets", "")] +enum_anim_tables = [("Custom", "Custom", "Custom"), ("", "Presets", "")] +for actor_name, preset_info in ACTOR_PRESET_INFO.items(): + if not preset_info.animation: + continue + behaviours = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.behaviours) + enum_animated_behaviours.extend( + [(intToHex(address), name, intToHex(address)) for name, address in behaviours.items()] + ) + tables = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.address) + enum_anim_tables.extend( + [(name, name, f"{intToHex(address)}, {preset_info.level}") for name, address in tables.items()] + ) diff --git a/fast64_internal/sm64/animation/exporting.py b/fast64_internal/sm64/animation/exporting.py new file mode 100644 index 000000000..dadae5413 --- /dev/null +++ b/fast64_internal/sm64/animation/exporting.py @@ -0,0 +1,1025 @@ +from typing import TYPE_CHECKING, Optional +from pathlib import Path +import os +import typing +import numpy as np + +import bpy +from bpy.types import Object, Action, PoseBone, Context +from bpy.path import abspath +from mathutils import Euler, Quaternion + +from ...utility import ( + PluginError, + bytesToHex, + encodeSegmentedAddr, + decodeSegmentedAddr, + get64bitAlignedAddr, + getPathAndLevel, + getExportDir, + intToHex, + applyBasicTweaks, + toAlnum, + directory_path_checks, +) +from ...utility_anim import stashActionInArmature + +from ..sm64_constants import BEHAVIOR_COMMANDS, BEHAVIOR_EXITS, defaultExtendSegment4, level_pointers +from ..sm64_utility import ( + ModifyFoundDescriptor, + find_descriptor_in_text, + get_comment_map, + to_include_descriptor, + write_includes, + update_actor_includes, + int_from_str, + write_or_delete_if_found, +) +from ..sm64_classes import BinaryExporter, RomReader, InsertableBinaryData +from ..sm64_level_parser import parseLevelAtPointer +from ..sm64_rom_tweaks import ExtendBank0x04 + +from .classes import ( + SM64_Anim, + SM64_AnimHeader, + SM64_AnimData, + SM64_AnimPair, + SM64_AnimTable, + SM64_AnimTableElement, +) +from .importing import import_enums, import_tables, update_table_with_table_enum +from .utility import ( + get_anim_owners, + get_anim_actor_name, + anim_name_to_enum_name, + get_selected_action, + get_action_props, + duplicate_name, +) +from .constants import HEADER_SIZE + +if TYPE_CHECKING: + from .properties import ( + SM64_ActionAnimProperty, + SM64_AnimHeaderProperties, + SM64_ArmatureAnimProperties, + SM64_AnimTableElementProperties, + ) + from ..settings.properties import SM64_Properties + from ..sm64_objects import SM64_CombinedObjectProperties + + +def trim_duplicates_vectorized(arr2d: np.ndarray) -> list: + """ + Similar to the old removeTrailingFrames(), but using numpy vectorization. + Remove trailing duplicate elements along the last axis of a 2D array. + One dimensional example of this in SM64_AnimPair.clean_frames + """ + # Get the last element of each sub-array along the last axis + last_elements = arr2d[:, -1] + mask = arr2d != last_elements[:, None] + # Reverse the order, find the last element with the same value + trim_indices = np.argmax(mask[:, ::-1], axis=1) + # return list(arr2d) # uncomment to test large sizes + return [ + sub_array if index == 1 else sub_array[: 1 if index == 0 else (-index + 1)] + for sub_array, index in zip(arr2d, trim_indices) + ] + + +def get_entire_fcurve_data( + action: Action, + anim_owner: PoseBone | Object, + prop: str, + max_frame: int, + values: np.ndarray[tuple[typing.Any, typing.Any], np.dtype[np.float32]], +): + data_path = anim_owner.path_from_id(prop) + + default_values = list(getattr(anim_owner, prop)) + populated = [False] * len(default_values) + + for fcurve in action.fcurves: + if fcurve.data_path == data_path: + array_index = fcurve.array_index + for frame in range(max_frame): + values[array_index, frame] = fcurve.evaluate(frame) + populated[array_index] = True + + for i, is_populated in enumerate(populated): + if not is_populated: + values[i] = np.full(values[i].size, default_values[i]) + + return values + + +def read_quick(actions, max_frames, anim_owners, trans_values, rot_values): + def to_xyz(row): + euler = Euler(row, mode) + return [euler.x, euler.y, euler.z] + + for action, max_frame, action_trans, action_rot in zip(actions, max_frames, trans_values, rot_values): + quats = np.empty((4, max_frame), dtype=np.float32) + + get_entire_fcurve_data(action, anim_owners[0], "location", max_frame, action_trans) + + for bone_index, anim_owner in enumerate(anim_owners): + mode = anim_owner.rotation_mode + prop = {"QUATERNION": "rotation_quaternion", "AXIS_ANGLE": "rotation_axis_angle"}.get( + mode, "rotation_euler" + ) + + index = bone_index * 3 + if mode == "QUATERNION": + get_entire_fcurve_data(action, anim_owner, prop, max_frame, quats) + action_rot[index : index + 3] = np.apply_along_axis( + lambda row: Quaternion(row).to_euler(), 1, quats.T + ).T + elif mode == "AXIS_ANGLE": + get_entire_fcurve_data(action, anim_owner, prop, max_frame, quats) + action_rot[index : index + 3] = np.apply_along_axis( + lambda row: list(Quaternion(row[1:], row[0]).to_euler()), 1, quats.T + ).T + else: + get_entire_fcurve_data(action, anim_owner, prop, max_frame, action_rot[index : index + 3]) + if mode != "XYZ": + action_rot[index : index + 3] = np.apply_along_axis(to_xyz, -1, action_rot[index : index + 3].T).T + + +def read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, is_owner_obj): + pre_export_frame = bpy.context.scene.frame_current + pre_export_action = obj.animation_data.action + was_playing = bpy.context.screen.is_animation_playing + + try: + if bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() # if an animation is being played, stop it + for action, action_trans, action_rot, max_frame in zip(actions, trans_values, rot_values, max_frames): + print(f'Reading animation data from action "{action.name}".') + obj.animation_data.action = action + for frame in range(max_frame): + bpy.context.scene.frame_set(frame) + + for bone_index, anim_owner in enumerate(anim_owners): + if is_owner_obj: + local_matrix = anim_owner.matrix_local + else: + local_matrix = obj.convert_space( + pose_bone=anim_owner, matrix=anim_owner.matrix, from_space="POSE", to_space="LOCAL" + ) + if bone_index == 0: + action_trans[0:3, frame] = list(local_matrix.to_translation()) + index = bone_index * 3 + action_rot[index : index + 3, frame] = list(local_matrix.to_euler()) + finally: + obj.animation_data.action = pre_export_action + bpy.context.scene.frame_set(pre_export_frame) + if was_playing != bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() + + +def get_animation_pairs( + sm64_scale: float, actions: list[Action], obj: Object, quick_read=False +) -> dict[Action, list[SM64_AnimPair]]: + anim_owners = get_anim_owners(obj) + is_owner_obj = isinstance(obj.type == "MESH", Object) + + if len(anim_owners) == 0: + raise PluginError(f'No animation bones in armature "{obj.name}"') + + if len(actions) < 1: + return {} + + max_frames = [get_action_props(action).get_max_frame(action) for action in actions] + trans_values = [np.zeros((3, max_frame), dtype=np.float32) for max_frame in max_frames] + rot_values = [np.zeros((len(anim_owners) * 3, max_frame), dtype=np.float32) for max_frame in max_frames] + + if quick_read: + read_quick(actions, max_frames, anim_owners, trans_values, rot_values) + else: + read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, is_owner_obj) + + action_pairs = {} + for action, action_trans, action_rot in zip(actions, trans_values, rot_values): + action_trans = trim_duplicates_vectorized(np.round(action_trans * sm64_scale).astype(np.int16)) + action_rot = trim_duplicates_vectorized(np.round(np.degrees(action_rot) * (2**16 / 360.0)).astype(np.int16)) + + pairs = [SM64_AnimPair(values) for values in action_trans] + pairs.extend([SM64_AnimPair(values) for values in action_rot]) + action_pairs[action] = pairs + + return action_pairs + + +def to_header_class( + header_props: "SM64_AnimHeaderProperties", + bone_count: int, + data: SM64_AnimData | None, + action: Action, + values_reference: int | str, + indice_reference: int | str, + dma: bool, + export_type: str, + table_index: Optional[int] = None, + actor_name="mario", + gen_enums=False, + file_name="anim_00.inc.c", +): + header = SM64_AnimHeader() + header.reference = header_props.get_name(actor_name, action, dma) + if gen_enums: + header.enum_name = header_props.get_enum(actor_name, action) + + header.flags = header_props.get_flags(not (export_type.endswith("Binary") or dma)) + header.trans_divisor = header_props.trans_divisor + header.start_frame, header.loop_start, header.loop_end = header_props.get_loop_points(action) + header.values_reference = values_reference + header.indice_reference = indice_reference + header.bone_count = bone_count + header.table_index = header_props.table_index if table_index is None else table_index + header.file_name = file_name + header.data = data + return header + + +def to_data_class(pairs: list[SM64_AnimPair], data_name="anim_00", file_name: str = "anim_00.inc.c"): + return SM64_AnimData(pairs, f"{data_name}_indices", f"{data_name}_values", file_name, file_name) + + +def to_animation_class( + action_props: "SM64_ActionAnimProperty", + action: Action, + obj: Object, + blender_to_sm64_scale: float, + quick_read: bool, + export_type: str, + dma: bool, + actor_name="mario", + gen_enums=False, +) -> SM64_Anim: + can_reference = not dma + animation = SM64_Anim() + animation.file_name = action_props.get_file_name(action, export_type, dma) + + if can_reference and action_props.reference_tables: + if export_type.endswith("Binary"): + values_reference, indice_reference = int_from_str(action_props.values_address), int( + action_props.indices_address, 0 + ) + else: + values_reference, indice_reference = action_props.values_table, action_props.indices_table + else: + pairs = get_animation_pairs(blender_to_sm64_scale, [action], obj, quick_read)[action] + animation.data = to_data_class(pairs, action_props.get_name(action, dma), animation.file_name) + values_reference = animation.data.values_reference + indice_reference = animation.data.indice_reference + bone_count = len(get_anim_owners(obj)) + for header_props in action_props.headers: + animation.headers.append( + to_header_class( + header_props=header_props, + bone_count=bone_count, + data=animation.data, + action=action, + values_reference=values_reference, + indice_reference=indice_reference, + dma=dma, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + file_name=animation.file_name, + table_index=None, + ) + ) + + return animation + + +def to_table_element_class( + element_props: "SM64_AnimTableElementProperties", + header_dict: dict["SM64_AnimHeaderProperties", SM64_AnimHeader], + data_dict: dict[Action, SM64_AnimData], + action_pairs: dict[Action, list[SM64_AnimPair]], + bone_count: int, + table_index: int, + dma: bool, + export_type: str, + actor_name="mario", + gen_enums=False, + prev_enums: dict[str, int] | None = None, +): + prev_enums = prev_enums or {} + use_addresses, can_reference = export_type.endswith("Binary"), not dma + element = SM64_AnimTableElement() + + enum = None + if gen_enums: + enum = element_props.get_enum(can_reference, actor_name, prev_enums) + element.enum_name = enum + + if can_reference and element_props.reference: + reference = int_from_str(element_props.header_address) if use_addresses else element_props.header_name + element.reference = reference + if reference == "": + raise PluginError("Header is not set.") + if enum == "": + raise PluginError("Enum name is not set.") + return element + + # Not reference + header_props, action = element_props.get_header(can_reference), element_props.get_action(can_reference) + if not action: + raise PluginError("Action is not set.") + if not header_props: + raise PluginError("Header is not set.") + if enum == "": + raise PluginError("Enum name is not set.") + + action_props = get_action_props(action) + if can_reference and action_props.reference_tables: + data = None + if use_addresses: + values_reference, indice_reference = ( + int_from_str(action_props.values_address), + int_from_str(action_props.indices_address), + ) + else: + values_reference, indice_reference = action_props.values_table, action_props.indices_table + else: + if action in action_pairs and action not in data_dict: + data_dict[action] = to_data_class( + action_pairs[action], + action_props.get_name(action, dma), + action_props.get_file_name(action, export_type, dma), + ) + data = data_dict[action] + values_reference, indice_reference = data.values_reference, data.indice_reference + + if header_props not in header_dict: + header_dict[header_props] = to_header_class( + header_props=header_props, + bone_count=bone_count, + data=data, + action=action, + values_reference=values_reference, + indice_reference=indice_reference, + dma=dma, + export_type=export_type, + table_index=table_index, + actor_name=actor_name, + gen_enums=gen_enums, + file_name=action_props.get_file_name(action, export_type), + ) + + element.header = header_dict[header_props] + element.reference = element.header.reference + return element + + +def to_table_class( + anim_props: "SM64_ArmatureAnimProperties", + obj: Object, + blender_to_sm64_scale: float, + quick_read: bool, + dma: bool, + export_type: str, + actor_name="mario", + gen_enums=False, +) -> SM64_AnimTable: + can_reference = not dma + table = SM64_AnimTable( + anim_props.get_table_name(actor_name), + anim_props.get_enum_name(actor_name), + anim_props.get_enum_end(actor_name), + anim_props.get_table_file_name(actor_name, export_type), + values_reference=toAlnum(f"anim_{actor_name}_values"), + ) + + header_dict: dict[SM64_AnimHeaderProperties, SM64_AnimHeader] = {} + + bone_count = len(get_anim_owners(obj)) + action_pairs = get_animation_pairs( + blender_to_sm64_scale, + [action for action in anim_props.actions if not (can_reference and get_action_props(action).reference_tables)], + obj, + quick_read, + ) + data_dict = {} + + prev_enums = {} + element_props: SM64_AnimTableElementProperties + for i, element_props in enumerate(anim_props.elements): + try: + table.elements.append( + to_table_element_class( + element_props=element_props, + header_dict=header_dict, + data_dict=data_dict, + action_pairs=action_pairs, + bone_count=bone_count, + table_index=i, + dma=dma, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + prev_enums=prev_enums, + ) + ) + except Exception as exc: + raise PluginError(f"Table element {i}: {exc}") from exc + if not dma and anim_props.null_delimiter: + table.elements.append(SM64_AnimTableElement(enum_name=table.enum_list_delimiter)) + return table + + +def update_includes( + combined_props: "SM64_CombinedObjectProperties", + header_dir: Path, + actor_name, + update_table: bool, +): + data_includes = [Path("anims/data.inc.c")] + header_includes = [] + if update_table: + data_includes.append(Path("anims/table.inc.c")) + header_includes.append(Path("anim_header.h")) + update_actor_includes( + combined_props.export_header_type, + combined_props.actor_group_name, + header_dir, + actor_name, + combined_props.export_level_name, + data_includes, + header_includes, + ) + + +def update_anim_header(path: Path, table_name: str, gen_enums: bool, override_files: bool): + to_add = [ + ModifyFoundDescriptor( + f"extern const struct Animation *const {table_name}[];", + rf"extern\h*const\h*struct\h*Animation\h?\*const\h*{table_name}\[.*?\]\h*?;", + ) + ] + if gen_enums: + to_add.append(to_include_descriptor(Path("anims/table_enum.h"))) + if write_or_delete_if_found(path, to_add, create_new=override_files): + print(f"Updated animation header {path}") + + +def update_enum_file(path: Path, override_files: bool, table: SM64_AnimTable): + text, comment_map = "", [] + existing_file = path.exists() and not override_files + if existing_file: + text, comment_map = get_comment_map(path.read_text()) + + if table.enum_list_start == -1 and table.enum_list_end == -1: # create new enum list + if text and text[-1] not in {"\n", "\r"}: + text += "\n" + table.enum_list_start = len(text) + text += f"enum {table.enum_list_reference} {{\n" + table.enum_list_end = len(text) + text += "};\n" + + content = text[table.enum_list_start : table.enum_list_end] + for i, element in enumerate(table.elements): + if element.enum_start == -1 or element.enum_end == -1: + content += f"\t{element.enum_c},\n" + if existing_file: + print(f"Added enum list entrie {element.enum_c}.") + continue + + old_text = content[element.enum_start : element.enum_end] + if old_text != element.enum_c: + content = content[: element.enum_start] + element.enum_c + content[element.enum_end :] + if existing_file: + print(f'Replaced "{old_text}" with "{element.enum_c}".') + # acccount for changed size + size_increase = len(element.enum_c) - len(old_text) + for next_element in table.elements[i + 1 :]: + if next_element.enum_start != -1 and next_element.enum_end != -1: + next_element.enum_start += size_increase + next_element.enum_end += size_increase + if not existing_file: + print(f"Creating enum list file at {path}.") + text = text[: table.enum_list_start] + content + text[table.enum_list_end :] + path.write_text(text) + + +def update_table_file( + table: SM64_AnimTable, + table_path: Path, + add_null_delimiter: bool, + override_files: bool, + gen_enums: bool, + designated: bool, + enum_list_path: Path, +): + assert isinstance(table.reference, str) and table.reference, "Invalid table reference" + + text, comment_less, enum_text, comment_map = "", "", "", [] + existing_file = table_path.exists() and not override_files + if existing_file: + text = table_path.read_text() + comment_less, comment_map = get_comment_map(text) + + # add include if not already there + descriptor = to_include_descriptor(Path("table_enum.h")) + if gen_enums and len(find_descriptor_in_text(descriptor, comment_less, comment_map)) == 0: + text = '#include "table_enum.h"\n' + text + + # First, find existing tables + tables = import_tables(comment_less, table_path, comment_map, table.reference) + enum_tables = [] + if gen_enums: + assert isinstance(table.enum_list_reference, str) and table.enum_list_reference + enum_text, enum_comment_less, enum_comment_map = "", "", [] + if enum_list_path.exists() and not override_files: + enum_text = enum_list_path.read_text() + enum_comment_less, enum_comment_map = get_comment_map(enum_text) + enum_tables = import_enums(enum_comment_less, enum_list_path, enum_comment_map, table.enum_list_reference) + if len(enum_tables) > 1: + raise PluginError(f'Duplicate enum list "{table.enum_list_reference}"') + + if len(tables) > 1: + raise PluginError(f'Duplicate animation table "{table.reference}"') + elif len(tables) == 1: + existing_table = tables[0] + if gen_enums: + if enum_tables: # apply enum table names to existing unset enums + update_table_with_table_enum(existing_table, enum_tables[0]) + table.enum_list_reference, table.enum_list_start, table.enum_list_end = ( + existing_table.enum_list_reference, + existing_table.enum_list_start, + existing_table.enum_list_end, + ) + + # Figure out enums on existing enum-less elements + prev_enums = {name: 0 for name in existing_table.enum_names} + for i, element in enumerate(existing_table.elements): + if element.enum_name: + continue + if not element.reference: + if i == len(existing_table.elements) - 1: + element.enum_name = duplicate_name(table.enum_list_delimiter, prev_enums) + else: + element.enum_name = duplicate_name( + anim_name_to_enum_name(f"{existing_table.reference}_NULL"), prev_enums + ) + continue + element.enum_name = duplicate_name( + next( + (enum for name, enum in zip(*table.names) if enum and name == element.reference), + anim_name_to_enum_name(element.reference), + ), + prev_enums, + ) + + new_elements = existing_table.elements.copy() + has_null_delimiter = existing_table.has_null_delimiter + for element in table.elements: + if element.c_name in existing_table.header_names and ( + not gen_enums or element.enum_name in existing_table.enum_names + ): + continue + if has_null_delimiter: + new_elements[-1].reference = element.reference + new_elements[-1].enum_name = element.enum_name + has_null_delimiter = False + else: + new_elements.append(element) + table.elements = new_elements + table.start, table.end = (existing_table.start, existing_table.end) + else: # create new table + if text and text[-1] not in {"\n", "\r"}: + text += "\n" + table.start = len(text) + text += f"const struct Animation *const {table.reference}[] = {{\n" + table.end = len(text) + text += "};\n" + + if add_null_delimiter and not table.has_null_delimiter: # add null delimiter if not present or replaced + table.elements.append(SM64_AnimTableElement(enum_name=table.enum_list_delimiter)) + + if gen_enums: + update_enum_file(enum_list_path, override_files, table) + + content = text[table.start : table.end] + for i, element in enumerate(table.elements): + element_text = element.to_c(designated and gen_enums) + if element.reference_start == -1 or element.reference_end == -1: + content += f"\t{element_text}\n" + if existing_file: + print(f"Added table entrie {element_text}.") + continue + + # update existing region instead + old_text = content[element.reference_start : element.reference_end] + if old_text != element_text: + content = content[: element.reference_start] + element_text + content[element.reference_end :] + if existing_file: + print(f'Replaced "{old_text}" with "{element_text}".') + + size_increase = len(element_text) - len(old_text) + if size_increase == 0: + continue + for next_element in table.elements[i + 1 :]: # acccount for changed size + if next_element.reference_start != -1 and next_element.reference_end != -1: + next_element.reference_start += size_increase + next_element.reference_end += size_increase + + if not existing_file: + print(f"Creating table file at {table_path}.") + text = text[: table.start] + content + text[table.end :] + table_path.write_text(text) + + +def update_data_file(path: Path, anim_file_names: list[str], override_files: bool = False): + includes = [Path(file_name) for file_name in anim_file_names] + if write_includes(path, includes, create_new=override_files): + print(f"Updating animation data file includes at {path}") + + +def update_behaviour_binary( + binary_exporter: BinaryExporter, address: int, table_address: bytes, beginning_animation: int +): + load_set = False + animate_set = False + exited = False + while not exited and not (load_set and animate_set): + command_index = int.from_bytes(binary_exporter.read(1, address), "big") + name, size = BEHAVIOR_COMMANDS[command_index] + print(name, intToHex(address)) + if name in BEHAVIOR_EXITS: + exited = True + if name == "LOAD_ANIMATIONS": + ptr_address = address + 4 + print( + f"Found LOAD_ANIMATIONS at {intToHex(address)}, " + f"replacing ptr {bytesToHex(binary_exporter.read(4, ptr_address))} " + f"at {intToHex(ptr_address)} with {bytesToHex(table_address)}" + ) + binary_exporter.write(table_address, ptr_address) + load_set = True + elif name == "ANIMATE": + value_address = address + 1 + print( + f"Found ANIMATE at {intToHex(address)}, " + f"replacing value {int.from_bytes(binary_exporter.read(1, value_address), 'big')} " + f"at {intToHex(value_address)} with {beginning_animation}" + ) + binary_exporter.write(beginning_animation.to_bytes(1, "big"), value_address) + animate_set = True + address += 4 * size + if exited: + if not load_set: + raise IndexError("Could not find LOAD_ANIMATIONS command") + if not animate_set: + print("Could not find ANIMATE command") + + +def export_animation_table_binary( + binary_exporter: BinaryExporter, + anim_props: "SM64_ArmatureAnimProperties", + table: SM64_AnimTable, + is_dma: bool, + level_option: str, + extend_bank_4: bool, +): + if is_dma: + data = table.to_binary_dma() + binary_exporter.write_to_range( + get64bitAlignedAddr(int_from_str(anim_props.dma_address)), int_from_str(anim_props.dma_end_address), data + ) + return + + level_parsed = parseLevelAtPointer(binary_exporter.rom_file_output, level_pointers[level_option]) + segment_data = level_parsed.segmentData + if extend_bank_4: + ExtendBank0x04(binary_exporter.rom_file_output, segment_data, defaultExtendSegment4) + + address = get64bitAlignedAddr(int_from_str(anim_props.address)) + end_address = int_from_str(anim_props.end_address) + + if anim_props.write_data_seperately: # Write the data and the table into seperate address range + data_address = get64bitAlignedAddr(int_from_str(anim_props.data_address)) + data_end_address = int_from_str(anim_props.data_end_address) + table_data, data = table.to_combined_binary(address, data_address, segment_data)[:2] + binary_exporter.write_to_range(address, end_address, table_data) + binary_exporter.write_to_range(data_address, data_end_address, data) + else: # Write table then the data in one address range + table_data, data = table.to_combined_binary(address, -1, segment_data)[:2] + binary_exporter.write_to_range(address, end_address, table_data + data) + if anim_props.update_behavior: + update_behaviour_binary( + binary_exporter, + decodeSegmentedAddr(anim_props.behavior_address.to_bytes(4, "big"), segment_data), + encodeSegmentedAddr(address, segment_data), + int_from_str(anim_props.beginning_animation), + ) + + +def export_animation_table_insertable(table: SM64_AnimTable, is_dma: bool, directory: Path): + directory_path_checks(directory, "Empty directory path.") + path = directory / table.file_name + if is_dma: + data = table.to_binary_dma() + InsertableBinaryData("Animation DMA Table", data).write(path) + else: + table_data, data, ptrs = table.to_combined_binary() + InsertableBinaryData("Animation Table", table_data + data, 0, ptrs).write(path) + + +def create_and_get_paths( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + decomp: Path, +): + anim_directory = geo_directory = header_directory = None + if anim_props.is_dma: + if combined_props.export_header_type == "Custom": + geo_directory = Path(abspath(combined_props.custom_export_path)) + anim_directory = Path(abspath(combined_props.custom_export_path), anim_props.dma_folder) + else: + anim_directory = Path(decomp, anim_props.dma_folder) + else: + export_path, level_name = getPathAndLevel( + combined_props.is_actor_custom_export, + combined_props.actor_custom_path, + combined_props.export_level_name, + combined_props.level_name, + ) + header_directory, _tex_dir = getExportDir( + combined_props.is_actor_custom_export, + export_path, + combined_props.export_header_type, + level_name, + texDir="", + dirName=actor_name, + ) + header_directory = Path(bpy.path.abspath(header_directory)) + geo_directory = header_directory / actor_name + anim_directory = geo_directory / "anims" + + for path in (anim_directory, geo_directory, header_directory): + if path is not None and not os.path.exists(path): + os.makedirs(path, exist_ok=True) + return (anim_directory, geo_directory, header_directory) + + +def export_animation_table_c( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + table: SM64_AnimTable, + decomp: Path, + actor_name: str, + designated: bool, +): + if not combined_props.is_actor_custom_export: + applyBasicTweaks(decomp) + anim_directory, geo_directory, header_directory = create_and_get_paths( + anim_props, combined_props, actor_name, decomp + ) + + print("Creating all animation C data") + if anim_props.export_seperately or anim_props.is_dma: + files_data = table.data_and_headers_to_c(anim_props.is_dma) + print("Saving all generated data files") + for file_name, file_data in files_data.items(): + (anim_directory / file_name).write_text(file_data) + print(file_name) + if not anim_props.is_dma: + update_data_file( + anim_directory / "data.inc.c", + list(files_data.keys()), + anim_props.override_files, + ) + else: + result = table.data_and_headers_to_c_combined() + print("Saving generated data file") + (anim_directory / "data.inc.c").write_text(result) + print("All animation data files exported.") + if anim_props.is_dma: # Don´t create an actual table and or update includes for dma exports + return + assert geo_directory and header_directory and isinstance(table.reference, str) + + header_path = geo_directory / "anim_header.h" + update_anim_header(header_path, table.reference, anim_props.gen_enums, anim_props.override_files) + update_table_file( + table=table, + table_path=anim_directory / "table.inc.c", + add_null_delimiter=anim_props.null_delimiter, + gen_enums=anim_props.gen_enums, + designated=designated, + enum_list_path=anim_directory / "table_enum.h", + override_files=anim_props.override_files, + ) + update_includes(combined_props, header_directory, actor_name, True) + + +def export_animation_binary( + binary_exporter: BinaryExporter, + animation: SM64_Anim, + action_props: "SM64_ActionAnimProperty", + anim_props: "SM64_ArmatureAnimProperties", + bone_count: int, + level_option: str, + extend_bank_4: bool, +): + if anim_props.is_dma: + dma_address = int_from_str(anim_props.dma_address) + print("Reading DMA table from ROM") + table = SM64_AnimTable().read_dma_binary( + reader=RomReader(rom_file=binary_exporter.rom_file_output, start_address=dma_address), + read_headers={}, + table_index=None, + bone_count=bone_count, + ) + empty_data = SM64_AnimData() + for header in animation.headers: + while header.table_index >= len(table.elements): + table.elements.append(SM64_AnimTableElement(header=SM64_AnimHeader(data=empty_data))) + table.elements[header.table_index] = SM64_AnimTableElement(header=header) + print("Converting to binary data") + data = table.to_binary_dma() + binary_exporter.write_to_range(dma_address, int_from_str(anim_props.dma_end_address), data) + return + level_parsed = parseLevelAtPointer(binary_exporter.rom_file_output, level_pointers[level_option]) + segment_data = level_parsed.segmentData + if extend_bank_4: + ExtendBank0x04(binary_exporter.rom_file_output, segment_data, defaultExtendSegment4) + + animation_address = get64bitAlignedAddr(int_from_str(action_props.start_address)) + animation_end_address = int_from_str(action_props.end_address) + + data = animation.to_binary(animation_address, segment_data)[0] + binary_exporter.write_to_range( + animation_address, + animation_end_address, + data, + ) + table_address = get64bitAlignedAddr(int_from_str(anim_props.address)) + if anim_props.update_table: + for i, header in enumerate(animation.headers): + element_address = table_address + (4 * header.table_index) + binary_exporter.seek(element_address) + binary_exporter.write(encodeSegmentedAddr(animation_address + (i * HEADER_SIZE), segment_data)) + if anim_props.update_behavior: + update_behaviour_binary( + binary_exporter, + decodeSegmentedAddr(anim_props.behavior_address.to_bytes(4, "big"), segment_data), + encodeSegmentedAddr(table_address, segment_data), + int_from_str(anim_props.beginning_animation), + ) + + +def export_animation_insertable(animation: SM64_Anim, is_dma: bool, directory: Path): + data, ptrs = animation.to_binary(is_dma) + InsertableBinaryData("Animation", data, 0, ptrs).write(directory / animation.file_name) + + +def export_animation_c( + animation: SM64_Anim, + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + decomp: Path, + actor_name: str, + designated: bool, +): + if not combined_props.is_actor_custom_export: + applyBasicTweaks(decomp) + anim_directory, geo_directory, header_directory = create_and_get_paths( + anim_props, combined_props, actor_name, decomp + ) + + (anim_directory / animation.file_name).write_text(animation.to_c(anim_props.is_dma)) + + if anim_props.is_dma: # Don´t create an actual table and don´t update includes for dma exports + return + + table_name = anim_props.get_table_name(actor_name) + + if anim_props.update_table: + update_anim_header(geo_directory / "anim_header.h", table_name, anim_props.gen_enums, False) + update_table_file( + table=SM64_AnimTable( + table_name, + enum_list_reference=anim_props.get_enum_name(actor_name), + enum_list_delimiter=anim_props.get_enum_end(actor_name), + elements=[ + SM64_AnimTableElement(header.reference, header, header.enum_name) for header in animation.headers + ], + ), + table_path=anim_directory / "table.inc.c", + add_null_delimiter=anim_props.null_delimiter, + gen_enums=anim_props.gen_enums, + designated=designated, + enum_list_path=anim_directory / "table_enum.h", + override_files=False, + ) + update_data_file(anim_directory / "data.inc.c", [animation.file_name]) + update_includes(combined_props, header_directory, actor_name, anim_props.update_table) + + +def export_animation(context: Context, obj: Object): + scene = context.scene + sm64_props: SM64_Properties = scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + actor_name: str = get_anim_actor_name(context) + + action = get_selected_action(obj) + action_props = get_action_props(action) + stashActionInArmature(obj, action) + bone_count = len(get_anim_owners(obj)) + + try: + animation = to_animation_class( + action_props=action_props, + action=action, + obj=obj, + blender_to_sm64_scale=sm64_props.blender_to_sm64_scale, + quick_read=combined_props.quick_anim_read, + export_type=sm64_props.export_type, + dma=anim_props.is_dma, + actor_name=actor_name, + gen_enums=not sm64_props.binary_export and anim_props.gen_enums, + ) + except Exception as exc: + raise PluginError(f"Failed to generate animation class. {exc}") from exc + if sm64_props.export_type == "C": + export_animation_c( + animation, anim_props, combined_props, sm64_props.abs_decomp_path, actor_name, sm64_props.designated + ) + elif sm64_props.export_type == "Insertable Binary": + export_animation_insertable(animation, anim_props.is_dma, Path(abspath(combined_props.insertable_directory))) + elif sm64_props.export_type == "Binary": + with BinaryExporter( + Path(abspath(sm64_props.export_rom)), Path(abspath(sm64_props.output_rom)) + ) as binary_exporter: + export_animation_binary( + binary_exporter, + animation, + action_props, + anim_props, + bone_count, + combined_props.binary_level, + sm64_props.extend_bank_4, + ) + else: + raise NotImplementedError(f"Export type {sm64_props.export_type} is not implemented") + + +def export_animation_table(context: Context, obj: Object): + bpy.ops.object.mode_set(mode="OBJECT") + + scene = context.scene + sm64_props: SM64_Properties = scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + actor_name: str = get_anim_actor_name(context) + + print("Stashing all actions in table") + for action in anim_props.actions: + stashActionInArmature(obj, action) + + if len(anim_props.elements) == 0: + raise PluginError("Empty animation table") + + try: + print("Reading table data from fast64") + table = to_table_class( + anim_props=anim_props, + obj=obj, + blender_to_sm64_scale=sm64_props.blender_to_sm64_scale, + quick_read=combined_props.quick_anim_read, + dma=anim_props.is_dma, + export_type=sm64_props.export_type, + actor_name=actor_name, + gen_enums=not anim_props.is_dma and not sm64_props.binary_export and anim_props.gen_enums, + ) + except Exception as exc: + raise PluginError(f"Failed to generate table class. {exc}") from exc + + print("Exporting table data") + if sm64_props.export_type == "C": + export_animation_table_c( + anim_props, combined_props, table, sm64_props.abs_decomp_path, actor_name, sm64_props.designated + ) + elif sm64_props.export_type == "Insertable Binary": + export_animation_table_insertable(table, anim_props.is_dma, Path(abspath(combined_props.insertable_directory))) + elif sm64_props.export_type == "Binary": + with BinaryExporter( + Path(abspath(sm64_props.export_rom)), Path(abspath(sm64_props.output_rom)) + ) as binary_exporter: + export_animation_table_binary( + binary_exporter, + anim_props, + table, + anim_props.is_dma, + combined_props.binary_level, + sm64_props.extend_bank_4, + ) + else: + raise NotImplementedError(f"Export type {sm64_props.export_type} is not implemented") diff --git a/fast64_internal/sm64/animation/importing.py b/fast64_internal/sm64/animation/importing.py new file mode 100644 index 000000000..21237a57d --- /dev/null +++ b/fast64_internal/sm64/animation/importing.py @@ -0,0 +1,808 @@ +from typing import TYPE_CHECKING, Optional +from pathlib import Path +import dataclasses +import functools +import os +import re +import numpy as np + +import bpy +from bpy.path import abspath +from bpy.types import Object, Action, Context, PoseBone +from mathutils import Quaternion + +from ...f3d.f3d_parser import math_eval +from ...utility import PluginError, decodeSegmentedAddr, filepath_checks, path_checks, intToHex +from ...utility_anim import create_basic_action + +from ..sm64_constants import AnimInfo, level_pointers +from ..sm64_level_parser import parseLevelAtPointer +from ..sm64_utility import CommentMatch, get_comment_map, adjust_start_end, import_rom_checks +from ..sm64_classes import RomReader + +from .utility import ( + animation_operator_checks, + get_action_props, + get_anim_owners, + get_scene_anim_props, + get_anim_actor_name, + anim_name_to_enum_name, + table_name_to_enum, +) +from .classes import ( + SM64_Anim, + CArrayDeclaration, + SM64_AnimHeader, + SM64_AnimTable, + SM64_AnimTableElement, +) +from .constants import ACTOR_PRESET_INFO, TABLE_ENUM_LIST_PATTERN, TABLE_ENUM_PATTERN, TABLE_PATTERN + +if TYPE_CHECKING: + from .properties import ( + SM64_AnimImportProperties, + SM64_ArmatureAnimProperties, + SM64_AnimHeaderProperties, + SM64_ActionAnimProperty, + SM64_AnimTableElementProperties, + ) + from ..settings.properties import SM64_Properties + + +def get_preset_anim_name_list(preset_name: str): + assert preset_name in ACTOR_PRESET_INFO, "Selected preset not in actor presets" + preset = ACTOR_PRESET_INFO[preset_name] + assert preset.animation is not None and isinstance( + preset.animation, AnimInfo + ), "Selected preset's actor has not animation information" + return preset.animation.names + + +def flip_euler(euler: np.ndarray) -> np.ndarray: + euler = euler.copy() + euler[1] = -euler[1] + euler += np.pi + return euler + + +def naive_flip_diff(a1: np.ndarray, a2: np.ndarray) -> np.ndarray: + diff = a1 - a2 + mask = np.abs(diff) > np.pi + return a2 + mask * np.sign(diff) * 2 * np.pi + + +@dataclasses.dataclass +class FramesHolder: + frames: np.ndarray = dataclasses.field(default_factory=list) + + def populate_action(self, action: Action, pose_bone: PoseBone, path: str): + for property_index in range(3): + f_curve = action.fcurves.new( + data_path=pose_bone.path_from_id(path), + index=property_index, + action_group=pose_bone.name, + ) + for time, frame in enumerate(self.frames): + f_curve.keyframe_points.insert(time, frame[property_index], options={"FAST"}) + + +def euler_to_quaternion(euler_angles: np.ndarray): + """ + Fast vectorized euler to quaternion function, euler_angles is an array of shape (-1, 3) + """ + phi = euler_angles[:, 0] + theta = euler_angles[:, 1] + psi = euler_angles[:, 2] + + half_phi = phi / 2.0 + half_theta = theta / 2.0 + half_psi = psi / 2.0 + + cos_half_phi = np.cos(half_phi) + sin_half_phi = np.sin(half_phi) + cos_half_theta = np.cos(half_theta) + sin_half_theta = np.sin(half_theta) + cos_half_psi = np.cos(half_psi) + sin_half_psi = np.sin(half_psi) + + q_w = cos_half_phi * cos_half_theta * cos_half_psi + sin_half_phi * sin_half_theta * sin_half_psi + q_x = sin_half_phi * cos_half_theta * cos_half_psi - cos_half_phi * sin_half_theta * sin_half_psi + q_y = cos_half_phi * sin_half_theta * cos_half_psi + sin_half_phi * cos_half_theta * sin_half_psi + q_z = cos_half_phi * cos_half_theta * sin_half_psi - sin_half_phi * sin_half_theta * cos_half_psi + + quaternions = np.vstack((q_w, q_x, q_y, q_z)).T # shape (-1, 4) + return quaternions + + +@dataclasses.dataclass +class RotationFramesHolder(FramesHolder): + @property + def quaternion(self): + return euler_to_quaternion(self.frames) # We make this code path as optiomal as it can be + + def get_euler(self, order: str): + if order == "XYZ": + return self.frames + return [Quaternion(x).to_euler(order) for x in self.quaternion] + + @property + def axis_angle(self): + result = [] + for x in self.quaternion: + x = Quaternion(x).to_axis_angle() + result.append([x[1]] + list(x[0])) + return result + + def populate_action(self, action: Action, pose_bone: PoseBone): + rotation_mode = pose_bone.rotation_mode + rotation_mode_name = { + "QUATERNION": "rotation_quaternion", + "AXIS_ANGLE": "rotation_axis_angle", + }.get(rotation_mode, "rotation_euler") + data_path = pose_bone.path_from_id(rotation_mode_name) + + size = 4 + if rotation_mode == "QUATERNION": + rotations = self.quaternion + elif rotation_mode == "AXIS_ANGLE": + rotations = self.axis_angle + else: + rotations = self.get_euler(rotation_mode) + size = 3 + for property_index in range(size): + f_curve = action.fcurves.new( + data_path=data_path, + index=property_index, + action_group=pose_bone.name, + ) + for frame, rotation in enumerate(rotations): + f_curve.keyframe_points.insert(frame, rotation[property_index], options={"FAST"}) + + +@dataclasses.dataclass +class IntermidiateAnimationBone: + translation: FramesHolder = dataclasses.field(default_factory=FramesHolder) + rotation: RotationFramesHolder = dataclasses.field(default_factory=RotationFramesHolder) + + def read_pairs(self, pairs: list["SM64_AnimPair"]): + pair_count = len(pairs) + max_length = max(len(pair.values) for pair in pairs) + result = np.empty((max_length, pair_count), dtype=np.int16) + + for i, pair in enumerate(pairs): + current_length = len(pair.values) + result[:current_length, i] = pair.values + result[current_length:, i] = pair.values[-1] + return result + + def read_translation(self, pairs: list["SM64_AnimPair"], scale: float): + self.translation.frames = self.read_pairs(pairs) / scale + + def continuity_filter(self, frames: np.ndarray) -> np.ndarray: + if len(frames) <= 1: + return frames + + # There is no way to fully vectorize this function + prev = frames[0] + for frame, euler in enumerate(frames): + euler = naive_flip_diff(prev, euler) + flipped_euler = naive_flip_diff(prev, flip_euler(euler)) + if np.all((prev - flipped_euler) ** 2 < (prev - euler) ** 2): + euler = flipped_euler + frames[frame] = prev = euler + + return frames + + def read_rotation(self, pairs: list["SM64_AnimPair"], continuity_filter: bool): + frames = self.read_pairs(pairs).astype(np.uint16).astype(np.float32) + frames *= 360.0 / (2**16) + frames = np.radians(frames) + if continuity_filter: + frames = self.continuity_filter(frames) + self.rotation.frames = frames + + def populate_action(self, action: Action, pose_bone: PoseBone): + self.translation.populate_action(action, pose_bone, "location") + self.rotation.populate_action(action, pose_bone) + + +def from_header_class( + header_props: "SM64_AnimHeaderProperties", + header: SM64_AnimHeader, + action: Action, + actor_name: str, + use_custom_name: bool, +): + if isinstance(header.reference, str) and header.reference != header_props.get_name(actor_name, action): + header_props.custom_name = header.reference + if use_custom_name: + header_props.use_custom_name = True + if header.enum_name and header.enum_name != header_props.get_enum(actor_name, action): + header_props.custom_enum = header.enum_name + header_props.use_custom_enum = True + + correct_loop_points = header.start_frame, header.loop_start, header.loop_end + header_props.start_frame, header_props.loop_start, header_props.loop_end = correct_loop_points + if correct_loop_points != header_props.get_loop_points(action): # check if auto loop points don´t match + header_props.use_manual_loop = True + + header_props.trans_divisor = header.trans_divisor + header_props.set_flags(header.flags) + + header_props.table_index = header.table_index + + +def from_anim_class( + action_props: "SM64_ActionAnimProperty", + action: Action, + animation: SM64_Anim, + actor_name: str, + use_custom_name: bool, + import_type: str, +): + main_header = animation.headers[0] + is_from_binary = import_type.endswith("Binary") + + if animation.action_name: + action_name = animation.action_name + elif main_header.file_name: + action_name = main_header.file_name.removesuffix(".c").removesuffix(".inc") + elif is_from_binary: + action_name = intToHex(main_header.reference) + + action.name = action_name.removeprefix("anim_") + print(f'Populating action "{action.name}" properties.') + + indice_reference, values_reference = main_header.indice_reference, main_header.values_reference + if is_from_binary: + action_props.indices_address, action_props.values_address = intToHex(indice_reference), intToHex( + values_reference + ) + else: + action_props.indices_table, action_props.values_table = indice_reference, values_reference + + if animation.data: + file_name = animation.data.indices_file_name + action_props.custom_max_frame = max([1] + [len(x.values) for x in animation.data.pairs]) + if action_props.get_max_frame(action) != action_props.custom_max_frame: + action_props.use_custom_max_frame = True + else: + file_name = main_header.file_name + action_props.reference_tables = True + if file_name: + action_props.custom_file_name = file_name + if use_custom_name and action_props.get_file_name(action, import_type) != action_props.custom_file_name: + action_props.use_custom_file_name = True + if is_from_binary: + start_addresses = [x.reference for x in animation.headers] + end_addresses = [x.end_address for x in animation.headers] + if animation.data: + start_addresses.append(animation.data.start_address) + end_addresses.append(animation.data.end_address) + + action_props.start_address = intToHex(min(start_addresses)) + action_props.end_address = intToHex(max(end_addresses)) + + print("Populating header properties.") + for i, header in enumerate(animation.headers): + if i: + action_props.header_variants.add() + header_props = action_props.headers[-1] + header.action = action # Used in table class to prop + from_header_class(header_props, header, action, actor_name, use_custom_name) + + action_props.update_variant_numbers() + + +def from_table_element_class( + element_props: "SM64_AnimTableElementProperties", + element: SM64_AnimTableElement, + use_custom_name: bool, + actor_name: str, + prev_enums: dict[str, int], +): + if element.header: + assert element.header.action + element_props.set_variant(element.header.action, element.header.header_variant) + else: + element_props.reference = True + + if isinstance(element.reference, int): + element_props.header_address = intToHex(element.reference) + else: + element_props.header_name = element.c_name + element_props.header_address = intToHex(0) + + if element.enum_name: + element_props.custom_enum = element.enum_name + if use_custom_name and element.enum_name != element_props.get_enum(True, actor_name, prev_enums): + element_props.use_custom_enum = True + + +def from_anim_table_class( + anim_props: "SM64_ArmatureAnimProperties", + table: SM64_AnimTable, + clear_table: bool, + use_custom_name: bool, + actor_name: str, +): + if clear_table: + anim_props.elements.clear() + anim_props.null_delimiter = table.has_null_delimiter + + prev_enums: dict[str, int] = {} + for i, element in enumerate(table.elements): + if anim_props.null_delimiter and i == len(table.elements) - 1: + break + anim_props.elements.add() + from_table_element_class(anim_props.elements[-1], element, use_custom_name, actor_name, prev_enums) + + if isinstance(table.reference, int): # Binary + anim_props.dma_address = intToHex(table.reference) + anim_props.dma_end_address = intToHex(table.end_address) + anim_props.address = intToHex(table.reference) + anim_props.end_address = intToHex(table.end_address) + + # Data + start_addresses = [] + end_addresses = [] + for element in table.elements: + if element.header and element.header.data: + start_addresses.append(element.header.data.start_address) + end_addresses.append(element.header.data.end_address) + if start_addresses and end_addresses: + anim_props.write_data_seperately = True + anim_props.data_address = intToHex(min(start_addresses)) + anim_props.data_end_address = intToHex(max(end_addresses)) + elif isinstance(table.reference, str) and table.reference: # C + if use_custom_name: + anim_props.custom_table_name = table.reference + if anim_props.get_table_name(actor_name) != anim_props.custom_table_name: + anim_props.use_custom_table_name = True + + +def animation_import_to_blender( + obj: Object, + blender_to_sm64_scale: float, + anim_import: SM64_Anim, + actor_name: str, + use_custom_name: bool, + import_type: str, + force_quaternion: bool, + continuity_filter: bool, +): + action = create_basic_action(obj, "") + try: + if anim_import.data: + print("Converting pairs to intermidiate data.") + bones = get_anim_owners(obj) + bones_data: list[IntermidiateAnimationBone] = [] + pairs = anim_import.data.pairs + for pair_num in range(3, len(pairs), 3): + bone = IntermidiateAnimationBone() + if pair_num == 3: + bone.read_translation(pairs[0:3], blender_to_sm64_scale) + bone.read_rotation(pairs[pair_num : pair_num + 3], continuity_filter) + bones_data.append(bone) + print("Populating action keyframes.") + for pose_bone, bone_data in zip(bones, bones_data): + if force_quaternion: + pose_bone.rotation_mode = "QUATERNION" + bone_data.populate_action(action, pose_bone) + + from_anim_class(get_action_props(action), action, anim_import, actor_name, use_custom_name, import_type) + return action + except PluginError as exc: + bpy.data.actions.remove(action) + raise exc + + +def update_table_with_table_enum(table: SM64_AnimTable, enum_table: SM64_AnimTable): + for element, enum_element in zip(table.elements, enum_table.elements): + if element.enum_name: + enum_element = next( + ( + other_enum_element + for other_enum_element in enum_table.elements + if element.enum_name == other_enum_element.enum_name + ), + enum_element, + ) + element.enum_name = enum_element.enum_name + element.enum_val = enum_element.enum_val + element.enum_start = enum_element.enum_start + element.enum_end = enum_element.enum_end + table.enum_list_reference = enum_table.enum_list_reference + table.enum_list_start = enum_table.enum_list_start + table.enum_list_end = enum_table.enum_list_end + + +def import_enums(c_data: str, path: Path, comment_map: list[CommentMatch], specific_name=""): + tables = [] + for list_match in re.finditer(TABLE_ENUM_LIST_PATTERN, c_data): + name, content = list_match.group("name"), list_match.group("content") + if name is None and content is None: # comment + continue + if specific_name and name != specific_name: + continue + list_start, list_end = adjust_start_end(c_data.find(content, list_match.start()), list_match.end(), comment_map) + content = c_data[list_start:list_end] + table = SM64_AnimTable( + file_name=path.name, + enum_list_reference=name, + enum_list_start=list_start, + enum_list_end=list_end, + ) + for element_match in re.finditer(TABLE_ENUM_PATTERN, content): + name, num = (element_match.group("name"), element_match.group("num")) + if name is None and num is None: # comment + continue + enum_start, enum_end = adjust_start_end( + list_start + element_match.start(), list_start + element_match.end(), comment_map + ) + table.elements.append( + SM64_AnimTableElement( + enum_name=name, enum_val=num, enum_start=enum_start - list_start, enum_end=enum_end - list_start + ) + ) + tables.append(table) + return tables + + +def import_tables( + c_data: str, + path: Path, + comment_map: list[CommentMatch], + specific_name="", + header_decls: Optional[list[CArrayDeclaration]] = None, + values_decls: Optional[list[CArrayDeclaration]] = None, + indices_decls: Optional[list[CArrayDeclaration]] = None, +): + read_headers = {} + header_decls, values_decls, indices_decls = ( + header_decls or [], + values_decls or [], + indices_decls or [], + ) + tables: list[SM64_AnimTable] = [] + for table_match in re.finditer(TABLE_PATTERN, c_data): + table_elements = [] + name, content = table_match.group("name"), table_match.group("content") + if name is None and content is None: # comment + continue + if specific_name and name != specific_name: + continue + + table = SM64_AnimTable(name, file_name=path.name, elements=table_elements) + table.read_c( + c_data, + c_data.find(content, table_match.start()), + table_match.end(), + comment_map, + read_headers, + header_decls, + values_decls, + indices_decls, + ) + tables.append(table) + return tables + + +DECL_PATTERN = re.compile( + r"(static\s+const\s+struct\s+Animation|static\s+const\s+u16|static\s+const\s+s16)\s+" + r"(\w+)\s*?(?:\[.*?\])?\s*?=\s*?\{(.*?)\s*?\};", + re.DOTALL, +) +VALUE_SPLIT_PATTERN = re.compile(r"\s*(?:(?:\.(?P\w+)|\[\s*(?P.*?)\s*\])\s*=\s*)?(?P.+?)(?:,|\Z)") + + +def find_decls(c_data: str, path: Path, decl_list: dict[str, list[CArrayDeclaration]]): + """At this point a generilized c parser would be better""" + matches = DECL_PATTERN.findall(c_data) + for decl_type, name, value_text in matches: + values = [] + for match in VALUE_SPLIT_PATTERN.finditer(value_text): + var, designator, val = match.group("var"), match.group("designator"), match.group("val") + assert val is not None + if designator is not None: + designator = math_eval(designator, object()) + if isinstance(designator, int): + if isinstance(values, dict): + raise PluginError("Invalid mix of designated initializers") + first_val = values[0] if values else "0" + values.extend([first_val] * (designator + 1 - len(values))) + else: + if not values: + values = {} + elif isinstance(values, list): + raise PluginError("Invalid mix of designated initializers") + values[designator] = val + elif var is not None: + if not values: + values = {} + elif isinstance(values, list): + raise PluginError("Mix of designated and positional variable assignment") + values[var] = val + else: + if isinstance(values, dict): + raise PluginError("Mix of designated and positional variable assignment") + values.append(val) + decl_list[decl_type].append(CArrayDeclaration(name, path, path.name, values)) + + +def import_c_animations(path: Path) -> tuple[SM64_AnimTable | None, dict[str, SM64_AnimHeader]]: + path_checks(path) + if path.is_file(): + file_paths = [path] + elif path.is_dir(): + file_paths = sorted([f for f in path.rglob("*") if f.suffix in {".c", ".h"}]) + else: + raise PluginError("Path is neither a file or a folder but it exists, somehow.") + + print("Reading from:\n" + "\n".join([f.name for f in file_paths])) + c_files = {file_path: get_comment_map(file_path.read_text()) for file_path in file_paths} + + decl_lists = {"static const struct Animation": [], "static const u16": [], "static const s16": []} + header_decls, indices_decls, value_decls = ( + decl_lists["static const struct Animation"], + decl_lists["static const u16"], + decl_lists["static const s16"], + ) + tables: list[SM64_AnimTable] = [] + enum_lists: list[SM64_AnimTable] = [] + for file_path, (comment_less, _comment_map) in c_files.items(): + find_decls(comment_less, file_path, decl_lists) + for file_path, (comment_less, comment_map) in c_files.items(): + tables.extend(import_tables(comment_less, file_path, comment_map, "", header_decls, value_decls, indices_decls)) + enum_lists.extend(import_enums(comment_less, file_path, comment_map)) + + if len(tables) > 1: + raise ValueError("More than 1 table declaration") + elif len(tables) == 1: + table: SM64_AnimTable = tables[0] + if enum_lists: + enum_table = next( # find enum with the same name or use the first + ( + enum_table + for enum_table in enum_lists + if enum_table.reference == table_name_to_enum(table.reference) + ), + enum_lists[0], + ) + update_table_with_table_enum(table, enum_table) + read_headers = {header.reference: header for header in table.header_set} + return table, read_headers + else: + read_headers: dict[str, SM64_AnimHeader] = {} + for table_index, header_decl in enumerate(sorted(header_decls, key=lambda h: h.name)): + SM64_AnimHeader().read_c(header_decl, value_decls, indices_decls, read_headers, table_index) + return None, read_headers + + +def import_binary_animations( + data_reader: RomReader, + import_type: str, + read_headers: dict[str, SM64_AnimHeader], + table: SM64_AnimTable, + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + table_size: Optional[int] = None, +): + if import_type == "Table": + table.read_binary(data_reader, read_headers, table_index, bone_count, table_size) + elif import_type == "DMA": + table.read_dma_binary(data_reader, read_headers, table_index, bone_count) + elif import_type == "Animation": + SM64_AnimHeader.read_binary( + data_reader, + read_headers, + False, + bone_count, + table_size, + ) + else: + raise PluginError("Unimplemented binary import type.") + + +def import_insertable_binary_animations( + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table: SM64_AnimTable, + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + table_size: Optional[int] = None, +): + if reader.insertable.data_type == "Animation": + SM64_AnimHeader.read_binary( + reader, + read_headers, + False, + bone_count, + ) + elif reader.insertable.data_type == "Animation Table": + table.read_binary(reader, read_headers, table_index, bone_count, table_size) + elif reader.insertable.data_type == "Animation DMA Table": + table.read_dma_binary(reader, read_headers, table_index, bone_count) + + +def import_animations(context: Context): + animation_operator_checks(context, False) + + scene = context.scene + obj: Object = context.object + sm64_props: SM64_Properties = scene.fast64.sm64 + import_props: SM64_AnimImportProperties = sm64_props.animation.importing + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + + update_table_preset(import_props, context) + + read_headers: dict[str, SM64_AnimHeader] = {} + table = SM64_AnimTable() + + print("Reading animation data.") + + if import_props.binary: + rom_path = Path(abspath(import_props.rom if import_props.rom else sm64_props.import_rom)) + binary_args = ( + read_headers, + table, + import_props.table_index, + None if import_props.ignore_bone_count else len(get_anim_owners(obj)), + import_props.table_size, + ) + if import_props.import_type == "Binary": + import_rom_checks(rom_path) + address = import_props.address + with rom_path.open("rb") as rom_file: + if import_props.binary_import_type == "DMA": + segment_data = None + else: + segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData + if import_props.is_segmented_address: + address = decodeSegmentedAddr(address.to_bytes(4, "big"), segment_data) + import_binary_animations( + RomReader(rom_file, start_address=address, segment_data=segment_data), + import_props.binary_import_type, + *binary_args, + ) + elif import_props.import_type == "Insertable Binary": + insertable_path = Path(abspath(import_props.path)) + filepath_checks(insertable_path) + with insertable_path.open("rb") as insertable_file: + if import_props.read_from_rom: + import_rom_checks(rom_path) + with rom_path.open("rb") as rom_file: + segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData + import_insertable_binary_animations( + RomReader(rom_file, insertable_file=insertable_file, segment_data=segment_data), + *binary_args, + ) + else: + import_insertable_binary_animations(RomReader(insertable_file=insertable_file), *binary_args) + elif import_props.import_type == "C": + table, read_headers = import_c_animations(Path(abspath(import_props.path))) + table = table or SM64_AnimTable() + else: + raise NotImplementedError(f"Unimplemented animation import type {import_props.import_type}") + + if not table.elements: + print("No table was read. Automatically creating table.") + table.elements = [SM64_AnimTableElement(header=header) for header in read_headers.values()] + seperate_anims = table.get_seperate_anims() + + actor_name: str = get_anim_actor_name(context) + if import_props.use_preset and import_props.preset in ACTOR_PRESET_INFO: + preset_animation_names = get_preset_anim_name_list(import_props.preset) + for animation in seperate_anims: + if len(animation.headers) == 0: + continue + names, indexes = [], [] + for header in animation.headers: + if header.table_index >= len(preset_animation_names): + continue + name = preset_animation_names[header.table_index] + header.enum_name = header.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}") + names.append(name) + indexes.append(str(header.table_index)) + animation.action_name = f"{'/'.join(indexes)} - {'/'.join(names)}" + for i, element in enumerate(table.elements[: len(preset_animation_names)]): + name = preset_animation_names[i] + element.enum_name = element.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}") + + print("Importing animations into blender.") + actions = [] + for animation in seperate_anims: + actions.append( + animation_import_to_blender( + obj, + sm64_props.blender_to_sm64_scale, + animation, + actor_name, + import_props.use_custom_name, + import_props.import_type, + import_props.force_quaternion, + import_props.continuity_filter if not import_props.force_quaternion else True, + ) + ) + + if import_props.run_decimate: + print("Decimating imported actions's fcurves") + old_area = bpy.context.area.type + old_action = obj.animation_data.action + try: + if obj.type == "ARMATURE": + bpy.ops.object.posemode_toggle() # Select all bones + bpy.ops.pose.select_all(action="SELECT") + + bpy.context.area.type = "GRAPH_EDITOR" + for action in actions: + print(f"Decimating {action.name}.") + obj.animation_data.action = action + bpy.ops.graph.select_all(action="SELECT") + bpy.ops.graph.decimate(mode="ERROR", factor=1, remove_error_margin=import_props.decimate_margin) + finally: + bpy.context.area.type = old_area + obj.animation_data.action = old_action + + if import_props.binary: + anim_props.is_dma = import_props.binary_import_type == "DMA" + if table: + print("Importing animation table into properties.") + from_anim_table_class(anim_props, table, import_props.clear_table, import_props.use_custom_name, actor_name) + + +@functools.cache +def cached_enum_from_import_preset(preset: str): + animation_names = get_preset_anim_name_list(preset) + enum_items: list[tuple[str, str, str, int]] = [] + enum_items.append(("Custom", "Custom", "Pick your own animation index", 0)) + if animation_names: + enum_items.append(("", "Presets", "", 1)) + for i, name in enumerate(animation_names): + enum_items.append((str(i), f"{i} - {name}", f'"{preset}" Animation {i}', i + 2)) + return enum_items + + +def get_enum_from_import_preset(_import_props: "SM64_AnimImportProperties", context): + try: + return cached_enum_from_import_preset(get_scene_anim_props(context).importing.preset) + except Exception as exc: # pylint: disable=broad-except + print(str(exc)) + return [("Custom", "Custom", "Pick your own animation index", 0)] + + +def update_table_preset(import_props: "SM64_AnimImportProperties", context): + if not import_props.use_preset: + return + + preset = ACTOR_PRESET_INFO[import_props.preset] + assert preset.animation is not None and isinstance( + preset.animation, AnimInfo + ), "Selected preset's actor has not animation information" + + if import_props.preset_animation == "": + # If the previously selected animation isn't in this preset, select animation 0 + import_props.preset_animation = "0" + + # C + decomp_path = import_props.decomp_path if import_props.decomp_path else context.scene.fast64.sm64.decomp_path + directory = preset.animation.directory if preset.animation.directory else f"{preset.decomp_path}/anims" + import_props.path = os.path.join(decomp_path, directory) + + # Binary + import_props.ignore_bone_count = preset.animation.ignore_bone_count + import_props.level = preset.level + if preset.animation.dma: + import_props.dma_table_address = intToHex(preset.animation.address) + import_props.binary_import_type = "DMA" + import_props.is_segmented_address_prop = False + else: + import_props.table_address = intToHex(preset.animation.address) + import_props.binary_import_type = "Table" + import_props.is_segmented_address_prop = True + + if preset.animation.size is None: + import_props.check_null = True + else: + import_props.check_null = False + import_props.table_size_prop = preset.animation.size diff --git a/fast64_internal/sm64/animation/operators.py b/fast64_internal/sm64/animation/operators.py new file mode 100644 index 000000000..8f0e6426e --- /dev/null +++ b/fast64_internal/sm64/animation/operators.py @@ -0,0 +1,346 @@ +from typing import TYPE_CHECKING + +import bpy +from bpy.utils import register_class, unregister_class +from bpy.types import Context, Scene, Action +from bpy.props import EnumProperty, StringProperty, IntProperty +from bpy.app.handlers import persistent + +from ...operators import OperatorBase, SearchEnumOperatorBase +from ...utility import copyPropertyGroup +from ...utility_anim import get_action + +from .importing import import_animations, get_enum_from_import_preset +from .exporting import export_animation, export_animation_table +from .utility import ( + animation_operator_checks, + get_action_props, + get_anim_obj, + get_scene_anim_props, + get_anim_props, + get_anim_actor_name, +) +from .constants import enum_anim_tables, enum_animated_behaviours + +if TYPE_CHECKING: + from .properties import SM64_AnimProperties, SM64_AnimHeaderProperties + + +@persistent +def emulate_no_loop(scene: Scene): + if scene.gameEditorMode != "SM64": + return + anim_props: SM64_AnimProperties = scene.fast64.sm64.animation + played_action: Action = anim_props.played_action + if not played_action: + return + if not bpy.context.screen.is_animation_playing or anim_props.played_header >= len( + get_action_props(played_action).headers + ): + anim_props.played_action = None + return + + frame = scene.frame_current + header_props = get_action_props(played_action).headers[anim_props.played_header] + _start, loop_start, end = header_props.get_loop_points(played_action) + if header_props.backwards: + if frame < loop_start: + if header_props.no_loop: + scene.frame_set(loop_start) + else: + scene.frame_set(end - 1) + elif frame >= end: + if header_props.no_loop: + scene.frame_set(end - 1) + else: + scene.frame_set(loop_start) + + +class SM64_PreviewAnim(OperatorBase): + bl_idname = "scene.sm64_preview_animation" + bl_label = "Preview Animation" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "PLAY" + + played_header: IntProperty(name="Header", min=0, default=0) + played_action: StringProperty(name="Action") + + def execute_operator(self, context): + animation_operator_checks(context) + played_action = get_action(self.played_action) + scene = context.scene + anim_props = scene.fast64.sm64.animation + + context.object.animation_data.action = played_action + action_props = get_action_props(played_action) + + if self.played_header >= len(action_props.headers): + raise ValueError("Invalid Header Index") + header_props: SM64_AnimHeaderProperties = action_props.headers[self.played_header] + start_frame = header_props.get_loop_points(played_action)[0] + scene.frame_set(start_frame) + scene.render.fps = 30 + + if bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() # in case it was already playing, stop it + bpy.ops.screen.animation_play() + + anim_props.played_header = self.played_header + anim_props.played_action = played_action + + +class SM64_AnimTableOps(OperatorBase): + bl_idname = "scene.sm64_table_operations" + bl_label = "Table Operations" + bl_description = "Move, remove, clear or add table elements" + bl_options = {"UNDO"} + + index: IntProperty() + op_name: StringProperty() + action_name: StringProperty() + header_variant: IntProperty() + + @classmethod + def is_enabled(cls, context: Context, op_name: str, index: int, **_kwargs): + table_elements = get_anim_props(context).elements + if op_name == "MOVE_UP" and index == 0: + return False + elif op_name == "MOVE_DOWN" and index >= len(table_elements) - 1: + return False + elif op_name == "CLEAR" and len(table_elements) == 0: + return False + return True + + def execute_operator(self, context): + table_elements = get_anim_props(context).elements + if self.op_name == "MOVE_UP": + table_elements.move(self.index, self.index - 1) + elif self.op_name == "MOVE_DOWN": + table_elements.move(self.index, self.index + 1) + elif self.op_name == "ADD": + if self.index != -1: + table_element = table_elements[self.index] + table_elements.add() + if self.action_name: # set based on action variant + table_elements[-1].set_variant(bpy.data.actions[self.action_name], self.header_variant) + elif self.index != -1: # copy from table + copyPropertyGroup(table_element, table_elements[-1]) + if self.index != -1: + table_elements.move(len(table_elements) - 1, self.index + 1) + elif self.op_name == "ADD_ALL": + action = bpy.data.actions[self.action_name] + for header_variant in range(len(get_action_props(action).headers)): + table_elements.add() + table_elements[-1].set_variant(action, header_variant) + elif self.op_name == "REMOVE": + table_elements.remove(self.index) + elif self.op_name == "CLEAR": + table_elements.clear() + else: + raise NotImplementedError(f"Unimplemented table op {self.op_name}") + + +class SM64_AnimVariantOps(OperatorBase): + bl_idname = "scene.sm64_header_variant_operations" + bl_label = "Header Variant Operations" + bl_description = "Move, remove, clear or add variants" + bl_options = {"UNDO"} + + index: IntProperty() + op_name: StringProperty() + action_name: StringProperty() + + @classmethod + def is_enabled(cls, context: Context, action_name: str, op_name: str, index: int, **_kwargs): + action_props = get_action_props(get_action(action_name)) + headers = action_props.headers + if op_name == "REMOVE" and index == 0: + return False + elif op_name == "MOVE_UP" and index <= 0: + return False + elif op_name == "MOVE_DOWN" and index >= len(headers) - 1: + return False + elif op_name == "CLEAR" and len(headers) <= 1: + return False + return True + + def execute_operator(self, context): + action = get_action(self.action_name) + action_props = get_action_props(action) + headers = action_props.headers + variants = action_props.header_variants + variant_position = self.index - 1 + if self.op_name == "MOVE_UP": + if self.index - 1 == 0: + variants.add() + copyPropertyGroup(headers[0], variants[-1]) + copyPropertyGroup(headers[self.index], headers[0]) + copyPropertyGroup(variants[-1], headers[self.index]) + variants.remove(len(variants) - 1) + else: + variants.move(variant_position, variant_position - 1) + elif self.op_name == "MOVE_DOWN": + if self.index == 0: + variants.add() + copyPropertyGroup(headers[0], variants[-1]) + copyPropertyGroup(headers[1], headers[0]) + copyPropertyGroup(variants[-1], headers[1]) + variants.remove(len(variants) - 1) + else: + variants.move(variant_position, variant_position + 1) + elif self.op_name == "ADD": + variants.add() + added_variant = variants[-1] + + copyPropertyGroup(action_props.headers[self.index], added_variant) + variants.move(len(variants) - 1, variant_position + 1) + action_props.update_variant_numbers() + added_variant.action = action + added_variant.expand_tab = True + added_variant.use_custom_name = False + added_variant.use_custom_enum = False + added_variant.custom_name = added_variant.get_name(get_anim_actor_name(context), action) + elif self.op_name == "REMOVE": + variants.remove(variant_position) + elif self.op_name == "CLEAR": + variants.clear() + else: + raise NotImplementedError(f"Unimplemented table op {self.op_name}") + action_props.update_variant_numbers() + + +class SM64_AddNLATracksToTable(OperatorBase): + bl_idname = "scene.sm64_add_nla_tracks_to_table" + bl_label = "Add Existing NLA Tracks To Animation Table" + bl_description = "Adds all NLA tracks in the selected armature to the animation table" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "NLA" + + @classmethod + def poll(cls, context): + if get_anim_obj(context) is None or get_anim_obj(context).animation_data is None: + return False + actions = get_anim_props(context).actions + for track in context.object.animation_data.nla_tracks: + for strip in track.strips: + if strip.action is not None and strip.action not in actions: + return True + return False + + def execute_operator(self, context): + assert self.__class__.poll(context) + anim_props = get_anim_props(context) + for track in context.object.animation_data.nla_tracks: + for strip in track.strips: + action = strip.action + if action is None or action in anim_props.actions: + continue + for header_variant in range(len(get_action_props(action).headers)): + anim_props.elements.add() + anim_props.elements[-1].set_variant(action, header_variant) + + +class SM64_ExportAnimTable(OperatorBase): + bl_idname = "scene.sm64_export_anim_table" + bl_label = "Export Animation Table" + bl_description = "Exports the animation table of the selected armature" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "EXPORT" + + @classmethod + def poll(cls, context): + return get_anim_obj(context) is not None + + def execute_operator(self, context): + animation_operator_checks(context) + export_animation_table(context, context.object) + self.report({"INFO"}, "Exported animation table successfully!") + + +class SM64_ExportAnim(OperatorBase): + bl_idname = "scene.sm64_export_anim" + bl_label = "Export Individual Animation" + bl_description = "Exports the select action of the selected armature" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION" + + @classmethod + def poll(cls, context): + return get_anim_obj(context) is not None + + def execute_operator(self, context): + animation_operator_checks(context) + export_animation(context, context.object) + self.report({"INFO"}, "Exported animation successfully!") + + +class SM64_ImportAnim(OperatorBase): + bl_idname = "scene.sm64_import_anim" + bl_label = "Import Animation(s)" + bl_description = "Imports animations into the call context's animation propreties, scene or object" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "IMPORT" + + def execute_operator(self, context): + import_animations(context) + + +class SM64_SearchAnimPresets(SearchEnumOperatorBase): + bl_idname = "scene.search_mario_anim_enum_operator" + bl_property = "preset_animation" + + preset_animation: EnumProperty(items=get_enum_from_import_preset) + + def update_enum(self, context: Context): + get_scene_anim_props(context).importing.preset_animation = self.preset_animation + + +class SM64_SearchAnimTablePresets(SearchEnumOperatorBase): + bl_idname = "scene.search_anim_table_enum_operator" + bl_property = "preset" + + preset: EnumProperty(items=enum_anim_tables) + + def update_enum(self, context: Context): + get_scene_anim_props(context).importing.preset = self.preset + + +class SM64_SearchAnimatedBhvs(SearchEnumOperatorBase): + bl_idname = "scene.search_animated_behavior_enum_operator" + bl_property = "behaviour" + + behaviour: EnumProperty(items=enum_animated_behaviours) + + def update_enum(self, context: Context): + get_anim_props(context).behaviour = self.behaviour + + +classes = ( + SM64_ExportAnimTable, + SM64_ExportAnim, + SM64_PreviewAnim, + SM64_AnimTableOps, + SM64_AnimVariantOps, + SM64_AddNLATracksToTable, + SM64_ImportAnim, + SM64_SearchAnimPresets, + SM64_SearchAnimatedBhvs, + SM64_SearchAnimTablePresets, +) + + +def anim_ops_register(): + for cls in classes: + register_class(cls) + + bpy.app.handlers.frame_change_pre.append(emulate_no_loop) + + +def anim_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/panels.py b/fast64_internal/sm64/animation/panels.py new file mode 100644 index 000000000..a43ff268e --- /dev/null +++ b/fast64_internal/sm64/animation/panels.py @@ -0,0 +1,196 @@ +from typing import TYPE_CHECKING + +from bpy.utils import register_class, unregister_class +from bpy.types import Context + +from ...utility_anim import is_action_stashed, CreateAnimData, AddBasicAction, StashAction +from ...panels import SM64_Panel + +from .utility import ( + get_action_props, + get_anim_actor_name, + get_anim_props, + get_selected_action, + dma_structure_context, + get_anim_obj, +) +from .operators import SM64_ExportAnim, SM64_ExportAnimTable, SM64_AddNLATracksToTable + +if TYPE_CHECKING: + from ..settings.properties import SM64_Properties + from ..sm64_objects import SM64_CombinedObjectProperties + from .properties import SM64_AnimImportProperties + + +# Base +class AnimationPanel(SM64_Panel): + bl_label = "SM64 Animation Inspector" + goal = "Object/Actor/Anim" + + +# Base panels +class SceneAnimPanel(AnimationPanel): + bl_idname = "SM64_PT_anim" + bl_parent_id = bl_idname + + +class ObjAnimPanel(AnimationPanel): + bl_idname = "OBJECT_PT_SM64_anim" + bl_context = "object" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_parent_id = bl_idname + + +# Main tab +class SceneAnimPanelMain(SceneAnimPanel): + bl_parent_id = "" + + def draw(self, context): + col = self.layout.column() + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + + if sm64_props.export_type == "C": + col.prop(sm64_props, "designated", text="Designated Initialization for Tables") + else: + combined_props.draw_anim_props(col, sm64_props.export_type, dma_structure_context(context)) + SM64_ExportAnimTable.draw_props(col) + if get_anim_obj(context) is None: + col.box().label(text="No selected armature/animated object") + else: + col.box().label(text=f'Armature "{context.object.name}"') + + +class ObjAnimPanelMain(ObjAnimPanel): + bl_parent_id = "OBJECT_PT_context_object" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None + + def draw(self, context): + sm64_props: SM64_Properties = context.scene.fast64.sm64 + get_anim_props(context).draw_props( + self.layout, sm64_props.export_type, sm64_props.combined_export.export_header_type + ) + + +# Action tab + + +class AnimationPanelAction(AnimationPanel): + bl_label = "Action Inspector" + + def draw(self, context): + col = self.layout.column() + + if context.object.animation_data is None: + col.box().label(text="Select object has no animation data") + CreateAnimData.draw_props(col) + action = None + else: + col.prop(context.object.animation_data, "action", text="Selected Action") + action = get_selected_action(context.object, False) + if action is None: + AddBasicAction.draw_props(col) + return + + if not is_action_stashed(context.object, action): + warn_col = col.column() + StashAction.draw_props(warn_col, action=action.name) + warn_col.alert = True + + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + if sm64_props.export_type != "C": + SM64_ExportAnim.draw_props(col) + + export_seperately = get_anim_props(context).export_seperately + if sm64_props.export_type == "C": + export_seperately = export_seperately or combined_props.export_single_action + elif sm64_props.export_type == "Insertable Binary": + export_seperately = True + get_action_props(action).draw_props( + layout=col, + action=action, + specific_variant=None, + in_table=False, + updates_table=get_anim_props(context).update_table, + export_seperately=export_seperately, + export_type=sm64_props.export_type, + actor_name=get_anim_actor_name(context), + gen_enums=get_anim_props(context).gen_enums, + dma=dma_structure_context(context), + ) + + +class SceneAnimPanelAction(AnimationPanelAction, SceneAnimPanel): + bl_idname = "SM64_PT_anim_panel_action" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None and SceneAnimPanel.poll(context) + + +class ObjAnimPanelAction(AnimationPanelAction, ObjAnimPanel): + bl_idname = "OBJECT_PT_SM64_anim_action" + + +class ObjAnimPanelTable(ObjAnimPanel): + bl_label = "Table" + bl_idname = "OBJECT_PT_SM64_anim_table" + + def draw(self, context): + if SM64_AddNLATracksToTable.poll(context): + SM64_AddNLATracksToTable.draw_props(self.layout) + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + get_anim_props(context).draw_table( + self.layout, sm64_props.export_type, get_anim_actor_name(context), combined_props.export_bhv + ) + + +# Importing tab + + +class AnimationPanelImport(AnimationPanel): + bl_label = "Importing" + import_panel = True + + def draw(self, context): + sm64_props: SM64_Properties = context.scene.fast64.sm64 + importing: SM64_AnimImportProperties = sm64_props.animation.importing + importing.draw_props(self.layout, sm64_props.import_rom, sm64_props.decomp_path) + + +class SceneAnimPanelImport(SceneAnimPanel, AnimationPanelImport): + bl_idname = "SM64_PT_anim_panel_import" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None and AnimationPanelImport.poll(context) + + +class ObjAnimPanelImport(ObjAnimPanel, AnimationPanelImport): + bl_idname = "OBJECT_PT_SM64_anim_panel_import" + + +classes = ( + ObjAnimPanelMain, + ObjAnimPanelTable, + ObjAnimPanelAction, + SceneAnimPanelMain, + SceneAnimPanelAction, + SceneAnimPanelImport, +) + + +def anim_panel_register(): + for cls in classes: + register_class(cls) + + +def anim_panel_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py new file mode 100644 index 000000000..4dd9f5bb1 --- /dev/null +++ b/fast64_internal/sm64/animation/properties.py @@ -0,0 +1,1204 @@ +import os + +import bpy +from bpy.types import PropertyGroup, Action, UILayout, Scene, Context +from bpy.utils import register_class, unregister_class +from bpy.props import ( + BoolProperty, + StringProperty, + EnumProperty, + IntProperty, + FloatProperty, + CollectionProperty, + PointerProperty, +) +from bpy.path import abspath, clean_name + +from ...utility import ( + decompFolderMessage, + directory_ui_warnings, + run_and_draw_errors, + path_ui_warnings, + draw_and_check_tab, + multilineLabel, + prop_split, + intToHex, + upgrade_old_prop, + toAlnum, +) +from ...utility_anim import getFrameInterval + +from ..sm64_utility import import_rom_ui_warnings, int_from_str, string_int_prop, string_int_warning +from ..sm64_constants import MAX_U16, MIN_S16, MAX_S16, level_enums + +from .operators import ( + OperatorBase, + SM64_PreviewAnim, + SM64_AnimTableOps, + SM64_AnimVariantOps, + SM64_ImportAnim, + SM64_SearchAnimPresets, + SM64_SearchAnimatedBhvs, + SM64_SearchAnimTablePresets, +) +from .constants import enum_anim_import_types, enum_anim_binary_import_types, enum_animated_behaviours, enum_anim_tables +from .classes import SM64_AnimFlags +from .utility import ( + dma_structure_context, + get_action_props, + get_dma_anim_name, + get_dma_header_name, + is_obj_animatable, + anim_name_to_enum_name, + action_name_to_enum_name, + duplicate_name, + table_name_to_enum, +) +from .importing import get_enum_from_import_preset, update_table_preset + + +def draw_custom_or_auto(holder, layout: UILayout, prop: str, default: str, factor=0.5, **kwargs): + use_custom_prop = "use_custom_" + prop + name_split = layout.split(factor=factor) + name_split.prop(holder, use_custom_prop, **kwargs) + if getattr(holder, use_custom_prop): + name_split.prop(holder, "custom_" + prop, text="") + else: + prop_size_label(name_split, text=default, icon="LOCKED") + + +def draw_forced(layout: UILayout, holder, prop: str, forced: bool): + row = layout.row(align=True) if forced else layout.column() + if forced: + prop_size_label(row, text="", icon="LOCKED") + row.alignment = "LEFT" + row.enabled = not forced + row.prop(holder, prop, invert_checkbox=not getattr(holder, prop) if forced else False) + + +def prop_size_label(layout: UILayout, **label_args): + box = layout.box() + box.scale_y = 0.5 + box.label(**label_args) + return box + + +def draw_list_op(layout: UILayout, op_cls: OperatorBase, op_name: str, index=-1, text="", icon="", **op_args): + col = layout.column() + icon = icon or {"MOVE_UP": "TRIA_UP", "MOVE_DOWN": "TRIA_DOWN", "CLEAR": "TRASH"}.get(op_name) or op_name + return op_cls.draw_props(col, icon, text, index=index, op_name=op_name, **op_args) + + +def draw_list_ops(layout: UILayout, op_cls: OperatorBase, index: int, **op_args): + layout.label(text=str(index)) + ops = ("MOVE_UP", "MOVE_DOWN", "ADD", "REMOVE") + for op_name in ops: + draw_list_op(layout, op_cls, op_name, index, **op_args) + + +def set_if_different(owner, prop: str, value): + if getattr(owner, prop) != value: + setattr(owner, prop, value) + + +def on_flag_update(self: "SM64_AnimHeaderProperties", context: Context): + use_int = context.scene.fast64.sm64.binary_export or dma_structure_context(context) + self.set_flags(self.get_flags(not use_int), set_custom=not self.use_custom_flags) + + +class SM64_AnimHeaderProperties(PropertyGroup): + expand_tab_in_action: BoolProperty(name="Header Properties", default=True) + header_variant: IntProperty(name="Header Variant Number", min=0) + + use_custom_name: BoolProperty(name="Name") + custom_name: StringProperty(name="Name", default="anim_00") + use_custom_enum: BoolProperty(name="Enum") + custom_enum: StringProperty(name="Enum", default="ANIM_00") + use_manual_loop: BoolProperty(name="Manual Loop Points") + start_frame: IntProperty(name="Start", min=0, max=MAX_S16) + loop_start: IntProperty(name="Loop Start", min=0, max=MAX_S16) + loop_end: IntProperty(name="End", min=0, max=MAX_S16) + trans_divisor: IntProperty( + name="Translation Divisor", + description="(animYTransDivisor)\n" + "If set to 0, the translation multiplier will be 1. " + "Otherwise, the translation multiplier is determined by " + "dividing the object's translation dividend (animYTrans) by this divisor", + min=MIN_S16, + max=MAX_S16, + ) + use_custom_flags: BoolProperty(name="Set Custom Flags") + custom_flags: StringProperty(name="Flags", default="ANIM_NO_LOOP", update=on_flag_update) + # Some flags are inverted in the ui for readability, descriptions match ui behavior + no_loop: BoolProperty( + name="No Loop", + description="(ANIM_FLAG_NOLOOP)\n" + "When disabled, the animation will not repeat from the loop start after reaching the loop " + "end frame", + update=on_flag_update, + ) + backwards: BoolProperty( + name="Loop Backwards", + description="(ANIM_FLAG_FORWARD/ANIM_FLAG_BACKWARD)\n" + "When enabled, the animation will loop (or stop if looping is disabled) after reaching " + "the loop start frame.\n" + "Tipically used with animations which use acceleration to play an animation backwards", + update=on_flag_update, + ) + no_acceleration: BoolProperty( + name="No Acceleration", + description="(ANIM_FLAG_NO_ACCEL/ANIM_FLAG_2)\n" + "When disabled, acceleration will not be used when calculating which animation frame is " + "next", + update=on_flag_update, + ) + disabled: BoolProperty( + name="No Shadow Translation", + description="(ANIM_FLAG_DISABLED/ANIM_FLAG_5)\n" + "When disabled, the animation translation will not be applied to shadows", + update=on_flag_update, + ) + only_vertical: BoolProperty( + name="Only Vertical Translation", + description="(ANIM_FLAG_HOR_TRANS)\n" + "When enabled, only the animation vertical translation will be applied during rendering (takes priority over no translation and only horizontal)\n" + "(shadows included), the horizontal translation will still be exported and included", + update=on_flag_update, + ) + only_horizontal: BoolProperty( + name="Only Horizontal Translation", + description="(ANIM_FLAG_VERT_TRANS)\n" + "When enabled, only the animation horizontal translation will be applied during rendering (takes priority over no translation)\n" + "(shadows included) the vertical translation will still be exported and included", + update=on_flag_update, + ) + no_trans: BoolProperty( + name="No Translation", + description="(ANIM_FLAG_NO_TRANS/ANIM_FLAG_6)\n" + "When disabled, the animation translation will not be used during rendering\n" + "(shadows included), the translation will still be exported and included", + update=on_flag_update, + ) + # Binary + table_index: IntProperty(name="Table Index", min=0) + + def get_flags(self, allow_str: bool) -> SM64_AnimFlags | str: + if self.use_custom_flags: + result = SM64_AnimFlags.evaluate(self.custom_flags) + if not allow_str and isinstance(result, str): + raise ValueError("Failed to evaluate custom flags") + return result + value = SM64_AnimFlags(0) + for prop, flag in SM64_AnimFlags.props_to_flags().items(): + if getattr(self, prop, False): + value |= flag + return value + + @property + def int_flags(self): + return self.get_flags(allow_str=False) + + def set_flags(self, value: SM64_AnimFlags | str, set_custom=True): + if isinstance(value, SM64_AnimFlags): # the value was fully evaluated + for prop, flag in SM64_AnimFlags.props_to_flags().items(): # set prop flags + set_if_different(self, prop, flag in value) + if set_custom: + if value not in SM64_AnimFlags.all_flags_with_prop(): # if a flag does not have a prop + set_if_different(self, "use_custom_flags", True) + set_if_different(self, "custom_flags", intToHex(value, 2)) + elif isinstance(value, str): + if set_custom: + set_if_different(self, "custom_flags", value) + set_if_different(self, "use_custom_flags", True) + else: # invalid + raise ValueError(f"Invalid type: {value}") + + @property + def manual_loop_range(self) -> tuple[int, int, int]: + if self.use_manual_loop: + return (self.start_frame, self.loop_start, self.loop_end) + + def get_loop_points(self, action: Action): + if self.use_manual_loop: + return self.manual_loop_range + loop_start, loop_end = getFrameInterval(action) + return (0, loop_start, loop_end + 1) + + def get_name(self, actor_name: str, action: Action, dma=False) -> str: + if dma: + return get_dma_header_name(self.table_index) + elif self.use_custom_name: + return self.custom_name + elif self.header_variant == 0: + return toAlnum(f"{actor_name}_anim_{action.name}") + else: + main_header_name = get_action_props(action).headers[0].get_name(actor_name, action, dma) + return toAlnum(f"{main_header_name}_{self.header_variant}") + + def get_enum(self, actor_name: str, action: Action) -> str: + if self.use_custom_enum: + return self.custom_enum + elif self.use_custom_name: + return anim_name_to_enum_name(self.get_name(actor_name, action)) + elif self.header_variant == 0: + clean_name = action_name_to_enum_name(action.name) + return anim_name_to_enum_name(f"{actor_name}_anim_{clean_name}") + else: + main_enum = get_action_props(action).headers[0].get_enum(actor_name, action) + return f"{main_enum}_{self.header_variant}" + + def draw_flag_props(self, layout: UILayout, use_int_flags: bool = False): + col = layout.column() + custom_split = col.split() + custom_split.prop(self, "use_custom_flags") + if self.use_custom_flags: + custom_split.prop(self, "custom_flags", text="") + if use_int_flags: + run_and_draw_errors(col, self.get_flags, False) + return + else: + prop_size_label(custom_split, text=intToHex(self.int_flags, 2), icon="LOCKED") + # Draw flag toggles + row = col.row(align=True) + row.prop(self, "no_loop", invert_checkbox=True, text="Loop", toggle=1) + row.prop(self, "backwards", toggle=1) + row.prop(self, "no_acceleration", invert_checkbox=True, text="Acceleration", toggle=1) + if self.no_acceleration and self.backwards: + col.label(text="Backwards has no porpuse without acceleration.", icon="INFO") + + trans_row = col.row(align=True) + no_row = trans_row.row() + no_row.enabled = not self.only_vertical and not self.only_horizontal + no_row.prop(self, "no_trans", invert_checkbox=True, text="Translate", toggle=1) + + vert_row = trans_row.row() + vert_row.prop(self, "only_vertical", text="Only Vertical", toggle=1) + + hor_row = trans_row.row() + hor_row.enabled = not self.only_vertical + hor_row.prop(self, "only_horizontal", text="Only Horizontal", toggle=1) + if self.only_vertical and self.only_horizontal: + multilineLabel( + layout=col, + text='"Only Vertical" takes priority, only vertical\n translation will be used.', + icon="INFO", + ) + if (self.only_vertical or self.only_horizontal) and self.no_trans: + multilineLabel( + layout=col, + text='"Only Horizontal" and "Only Vertical" take\n priority over no translation.', + icon="INFO", + ) + + disabled_row = trans_row.row() + disabled_row.enabled = not self.no_trans and not self.only_vertical + disabled_row.prop(self, "disabled", invert_checkbox=True, text="Shadow", toggle=1) + + def draw_frame_range(self, layout: UILayout, action: Action): + split = layout.split() + split.prop(self, "use_manual_loop") + if self.use_manual_loop: + split = layout.split() + split.prop(self, "start_frame") + split.prop(self, "loop_start") + split.prop(self, "loop_end") + else: + start, loop_start, end = self.get_loop_points(action) + prop_size_label(split, text=f"Start {start}, Loop Start {loop_start}, End {end}", icon="LOCKED") + + def draw_names(self, layout: UILayout, action: Action, actor_name: str, gen_enums: bool, dma: bool): + col = layout.column() + if gen_enums: + draw_custom_or_auto(self, col, "enum", self.get_enum(actor_name, action)) + draw_custom_or_auto(self, col, "name", self.get_name(actor_name, action, dma)) + + def draw_props( + self, + layout: UILayout, + action: Action, + in_table: bool, + updates_table: bool, + dma: bool, + export_type: str, + actor_name: str, + gen_enums: bool, + ): + col = layout.column() + split = col.split() + preview_op = SM64_PreviewAnim.draw_props(split) + preview_op.played_header = self.header_variant + preview_op.played_action = action.name + if not in_table: # Don´t show index or name in table props + draw_list_op( + split, + SM64_AnimTableOps, + "ADD", + text="Add To Table", + icon="LINKED", + action_name=action.name, + header_variant=self.header_variant, + ) + if (export_type == "C" and dma) or (export_type == "Binary" and updates_table): + prop_split(col, self, "table_index", "Table Index") + if not dma and export_type == "C": + self.draw_names(col, action, actor_name, gen_enums, dma) + col.separator() + + prop_split(col, self, "trans_divisor", "Translation Divisor") + self.draw_frame_range(col, action) + self.draw_flag_props(col, use_int_flags=dma or export_type.endswith("Binary")) + + +class SM64_ActionAnimProperty(PropertyGroup): + """Properties in Action.fast64.sm64.animation""" + + header: PointerProperty(type=SM64_AnimHeaderProperties) + variants_tab: BoolProperty(name="Header Variants") + header_variants: CollectionProperty(type=SM64_AnimHeaderProperties) + use_custom_file_name: BoolProperty(name="File Name") + custom_file_name: StringProperty(name="File Name", default="anim_00.inc.c") + use_custom_max_frame: BoolProperty(name="Max Frame") + custom_max_frame: IntProperty(name="Max Frame", min=1, max=MAX_U16, default=1) + reference_tables: BoolProperty(name="Reference Tables") + indices_table: StringProperty(name="Indices Table", default="anim_00_indices") + values_table: StringProperty(name="Value Table", default="anim_00_values") + # Binary, toad anim 0 for defaults + indices_address: StringProperty(name="Indices Table", default=intToHex(0x00A42150)) + values_address: StringProperty(name="Value Table", default=intToHex(0x00A40CC8)) + start_address: StringProperty(name="Start Address", default=intToHex(0x00A40CC8)) + end_address: StringProperty(name="End Address", default=intToHex(0x00A42265)) + + @property + def headers(self) -> list[SM64_AnimHeaderProperties]: + return [self.header] + list(self.header_variants) + + @property + def dma_name(self): + return get_dma_anim_name([header.table_index for header in self.headers]) + + def get_name(self, action: Action, dma=False) -> str: + if dma: + return self.dma_name + return toAlnum(f"anim_{action.name}") + + def get_file_name(self, action: Action, export_type: str, dma=False) -> str: + if not export_type in {"C", "Insertable Binary"}: + return "" + if export_type == "C" and dma: + return f"{self.dma_name}.inc.c" + elif self.use_custom_file_name: + return self.custom_file_name + else: + name = clean_name(f"anim_{action.name}", replace=" ") + return name + (".inc.c" if export_type == "C" else ".insertable") + + def get_max_frame(self, action: Action) -> int: + if self.use_custom_max_frame: + return self.custom_max_frame + loop_ends: list[int] = [getFrameInterval(action)[1]] + header_props: SM64_AnimHeaderProperties + for header_props in self.headers: + loop_ends.append(header_props.get_loop_points(action)[2]) + + return max(loop_ends) + + def update_variant_numbers(self): + for i, variant in enumerate(self.headers): + variant.header_variant = i + + def draw_variants( + self, + layout: UILayout, + action: Action, + dma: bool, + actor_name: str, + header_args: list, + ): + col = layout.column() + op_row = col.row() + op_row.label(text=f"Header Variants ({len(self.headers)})", icon="NLA") + draw_list_op(op_row, SM64_AnimVariantOps, "CLEAR", action_name=action.name) + + for i, header_props in enumerate(self.headers): + if i != 0: + col.separator() + + row = col.row() + if draw_and_check_tab( + row, + header_props, + "expand_tab_in_action", + header_props.get_name(actor_name, action, dma), + ): + header_props.draw_props(col, *header_args) + op_row = row.row() + op_row.alignment = "RIGHT" + draw_list_ops(op_row, SM64_AnimVariantOps, i, action_name=action.name) + + def draw_references(self, layout: UILayout, is_binary: bool = False): + col = layout.column() + col.prop(self, "reference_tables") + if not self.reference_tables: + return + if is_binary: + string_int_prop(col, self, "indices_address", "Indices Table") + string_int_prop(col, self, "values_address", "Value Table") + else: + prop_split(col, self, "indices_table", "Indices Table") + prop_split(col, self, "values_table", "Value Table") + + def draw_props( + self, + layout: UILayout, + action: Action, + specific_variant: int | None, + in_table: bool, + updates_table: bool, + export_seperately: bool, + export_type: str, + actor_name: str, + gen_enums: bool, + dma: bool, + ): + # Args to pass to the headers + header_args = (action, in_table, updates_table, dma, export_type, actor_name, gen_enums) + + col = layout.column() + if specific_variant is not None: + col.label(text="Action Properties", icon="ACTION") + if not in_table: + draw_list_op( + col, + SM64_AnimTableOps, + "ADD_ALL", + text="Add All Variants To Table", + icon="LINKED", + action_name=action.name, + ) + col.separator() + + if export_type == "Binary" and not dma: + string_int_prop(col, self, "start_address", "Start Address") + string_int_prop(col, self, "end_address", "End Address") + if export_type != "Binary" and (export_seperately or not in_table): + if not dma or export_type == "Insertable Binary": # not c dma or insertable + text = "File Name" + if not in_table and not export_seperately: + text = "File Name (individual action export)" + draw_custom_or_auto(self, col, "file_name", self.get_file_name(action, export_type), text=text) + elif not in_table: # C DMA forced auto name + split = col.split(factor=0.5) + split.label(text="File Name") + file_name = self.get_file_name(action, export_type, dma) + prop_size_label(split, text=file_name, icon="LOCKED") + if dma or not self.reference_tables: # DMA tables don´t allow references + draw_custom_or_auto(self, col, "max_frame", str(self.get_max_frame(action))) + if not dma: + self.draw_references(col, is_binary=export_type.endswith("Binary")) + col.separator() + + if specific_variant is not None: + if specific_variant < 0 or specific_variant >= len(self.headers): + col.box().label(text="Header variant does not exist.", icon="ERROR") + else: + col.label(text="Variant Properties", icon="NLA") + self.headers[specific_variant].draw_props(col, *header_args) + else: + self.draw_variants(col, action, dma, actor_name, header_args) + + +class SM64_AnimTableElementProperties(PropertyGroup): + expand_tab: BoolProperty() + action_prop: PointerProperty(name="Action", type=Action) + variant: IntProperty(name="Variant", min=0) + reference: BoolProperty(name="Reference") + # Toad example + header_name: StringProperty(name="Header Reference", default="toad_seg6_anim_0600B66C") + header_address: StringProperty(name="Header Reference", default=intToHex(0x0600B75C)) + use_custom_enum: BoolProperty(name="Enum") + custom_enum: StringProperty(name="Enum Name") + + def get_enum(self, can_reference: bool, actor_name: str, prev_enums: dict[str, int]): + """Updates prev_enums""" + enum = "" + if self.use_custom_enum: + self.custom_enum: str + enum = self.custom_enum + elif can_reference and self.reference: + enum = duplicate_name(anim_name_to_enum_name(self.header_name), prev_enums) + else: + action, header = self.get_action_header(can_reference) + if header and action: + enum = duplicate_name(header.get_enum(actor_name, action), prev_enums) + return enum + + def get_action_header(self, can_reference: bool): + self.variant: int + self.action_prop: Action + if (not can_reference or not self.reference) and self.action_prop: + headers = get_action_props(self.action_prop).headers + if self.variant < len(headers): + return (self.action_prop, headers[self.variant]) + return (None, None) + + def get_action(self, can_reference: bool) -> Action | None: + return self.get_action_header(can_reference)[0] + + def get_header(self, can_reference: bool) -> SM64_AnimHeaderProperties | None: + return self.get_action_header(can_reference)[1] + + def set_variant(self, action: Action, variant: int): + self.action_prop = action + self.variant = variant + + def draw_reference( + self, layout: UILayout, export_type: str = "C", gen_enums: bool = False, prev_enums: dict[str, int] = None + ): + if export_type.endswith("Binary"): + string_int_prop(layout, self, "header_address", "Header Address") + return + split = layout.split() + if gen_enums: + draw_custom_or_auto(self, split, "enum", self.get_enum(True, "", prev_enums), factor=0.3) + split.prop(self, "header_name", text="") + + def draw_props( + self, + row: UILayout, # left side of the row for table ops + prop_layout: UILayout, + index: int, + dma: bool, + updates_table: bool, + export_seperately: bool, + export_type: str, + gen_enums: bool, + actor_name: str, + prev_enums: dict[str, int], + ): + can_reference = not dma + col = prop_layout.column() + if can_reference: + reference_row = row.row() + reference_row.alignment = "LEFT" + reference_row.prop(self, "reference") + if self.reference: + self.draw_reference(col, export_type, gen_enums, prev_enums) + return + action_row = row.row() + action_row.alignment = "EXPAND" + action_row.prop(self, "action_prop", text="") + + if not self.action_prop: + col.box().label(text="Header´s action does not exist.", icon="ERROR") + return + action = self.action_prop + action_props = get_action_props(action) + + variant_split = col.split(factor=0.3) + variant_split.prop(self, "variant") + + if 0 <= self.variant < len(action_props.headers): + header_props = self.get_header(can_reference) + if dma: + name = get_dma_header_name(index) + else: + name = header_props.get_name(actor_name, action, dma) + if gen_enums: + draw_custom_or_auto( + self, + variant_split, + "enum", + self.get_enum(can_reference, actor_name, prev_enums), + factor=0.3, + ) + tab_name = name + (f" (Variant {self.variant})" if self.variant > 0 else "") + if not draw_and_check_tab(col, self, "expand_tab", tab_name): + return + + action_props.draw_props( + layout=col, + action=action, + specific_variant=self.variant, + in_table=True, + updates_table=updates_table, + export_seperately=export_seperately, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + dma=dma, + ) + + +class SM64_AnimImportProperties(PropertyGroup): + run_decimate: BoolProperty(name="Run Decimate (Allowed Change)", default=True) + decimate_margin: FloatProperty( + name="Error Margin", + default=0.025, + min=0.0, + max=0.025, + description="Use blender's builtin decimate (allowed change) operator to clean up all the " + "keyframes, generally the better option compared to clean keyframes but can be slow", + ) + + continuity_filter: BoolProperty(name="Continuity Filter", default=True) + force_quaternion: BoolProperty( + name="Force Quaternions", + description="Changes bones to quaternion rotation mode, can break existing actions", + ) + + clear_table: BoolProperty(name="Clear Table On Import", default=True) + import_type: EnumProperty(items=enum_anim_import_types, name="Import Type", default="C") + preset: bpy.props.EnumProperty( + items=enum_anim_tables, + name="Preset", + update=update_table_preset, + default="Mario", + ) + decomp_path: StringProperty(name="Decomp Path", subtype="FILE_PATH", default="") + binary_import_type: EnumProperty( + items=enum_anim_binary_import_types, + name="Binary Import Type", + default="Table", + ) + read_entire_table: BoolProperty(name="Read Entire Table", default=True) + check_null: BoolProperty(name="Check NULL Delimiter", default=True) + table_size_prop: IntProperty(name="Size", min=1) + table_index_prop: IntProperty(name="Index", min=0) + ignore_bone_count: BoolProperty( + name="Ignore bone count", + description="The armature bone count won´t be used when importing, a safety check will be skipped and old " + "fast64 animations won´t import, needed to import bowser's broken animation", + ) + preset_animation: EnumProperty(name="Preset Animation", items=get_enum_from_import_preset) + + rom: StringProperty(name="Import ROM", subtype="FILE_PATH") + table_address: StringProperty(name="Address", default=intToHex(0x0600FC48)) # Toad + animation_address: StringProperty(name="Address", default=intToHex(0x0600B75C)) + is_segmented_address_prop: BoolProperty(name="Is Segmented Address", default=True) + level: EnumProperty(items=level_enums, name="Level", default="IC") + dma_table_address: StringProperty(name="DMA Table Address", default="0x4EC000") + + read_from_rom: BoolProperty( + name="Read From Import ROM", + description="When enabled, the importer will read from the import ROM given an " + "address not included in the insertable file's defined pointers", + ) + + path: StringProperty(name="Path", subtype="FILE_PATH", default="anims/") + use_custom_name: BoolProperty(name="Use Custom Name", default=True) + + @property + def binary(self) -> bool: + return self.import_type.endswith("Binary") + + @property + def table_index(self): + if self.read_entire_table: + return + elif self.preset_animation == "Custom" or not self.use_preset: + return self.table_index_prop + else: + return int_from_str(self.preset_animation) + + @property + def address(self): + if self.import_type != "Binary": + return + elif self.binary_import_type == "DMA": + return int_from_str(self.dma_table_address) + elif self.binary_import_type == "Table": + return int_from_str(self.table_address) + else: + return int_from_str(self.animation_address) + + @property + def is_segmented_address(self): + if self.import_type != "Binary": + return + return ( + self.is_segmented_address_prop + if self.import_type == "Binary" and self.binary_import_type in {"Table", "Animation"} + else False + ) + + @property + def table_size(self): + return None if self.check_null else self.table_size_prop + + @property + def use_preset(self): + return self.import_type != "Insertable Binary" and self.preset != "Custom" + + def upgrade_old_props(self, scene: Scene): + upgrade_old_prop( + self, + "animation_address", + scene, + "animStartImport", + fix_forced_base_16=True, + ) + upgrade_old_prop(self, "is_segmented_address_prop", scene, "animIsSegPtr") + upgrade_old_prop(self, "level", scene, "levelAnimImport") + upgrade_old_prop(self, "table_index_prop", scene, "animListIndexImport") + if scene.pop("isDMAImport", False): + self.binary_import_type = "DMA" + elif scene.pop("animIsAnimList", True): + self.binary_import_type = "Table" + + def draw_clean_up(self, layout: UILayout): + col = layout.column() + col.prop(self, "run_decimate") + if self.run_decimate: + prop_split(col, self, "decimate_margin", "Error Margin") + col.box().label(text="While very useful and stable, it can be very slow", icon="INFO") + col.separator() + + row = col.row() + row.prop(self, "force_quaternion") + continuity_row = row.row() + continuity_row.enabled = not self.force_quaternion + continuity_row.prop( + self, + "continuity_filter", + text="Continuity Filter" + (" (Always on)" if self.force_quaternion else ""), + invert_checkbox=not self.continuity_filter if self.force_quaternion else False, + ) + + def draw_path(self, layout: UILayout): + prop_split(layout, self, "path", "Directory or File Path") + path_ui_warnings(layout, abspath(self.path)) + + def draw_c(self, layout: UILayout, decomp: os.PathLike = ""): + col = layout.column() + if self.preset == "Custom": + self.draw_path(col) + else: + col.label(text="Uses scene decomp path by default", icon="INFO") + prop_split(col, self, "decomp_path", "Decomp Path") + directory_ui_warnings(col, abspath(self.decomp_path or decomp)) + col.prop(self, "use_custom_name") + + def draw_import_rom(self, layout: UILayout, import_rom: os.PathLike = ""): + col = layout.column() + col.label(text="Uses scene import ROM by default", icon="INFO") + prop_split(col, self, "rom", "Import ROM") + return import_rom_ui_warnings(col, abspath(self.rom or import_rom)) + + def draw_table_settings(self, layout: UILayout): + row = layout.row(align=True) + left_row = row.row(align=True) + left_row.alignment = "LEFT" + left_row.prop(self, "read_entire_table") + left_row.prop(self, "check_null") + right_row = row.row(align=True) + right_row.alignment = "EXPAND" + if not self.read_entire_table: + right_row.prop(self, "table_index_prop", text="Index") + elif not self.check_null: + right_row.prop(self, "table_size_prop") + + def draw_binary(self, layout: UILayout, import_rom: os.PathLike): + col = layout.column() + self.draw_import_rom(col, import_rom) + col.separator() + + if self.preset != "Custom": + split = col.split() + split.prop(self, "read_entire_table") + if not self.read_entire_table: + SM64_SearchAnimPresets.draw_props(split, self, "preset_animation", "") + if self.preset_animation == "Custom": + split.prop(self, "table_index_prop", text="Index") + return + col.prop(self, "ignore_bone_count") + prop_split(col, self, "binary_import_type", "Animation Type") + if self.binary_import_type == "DMA": + string_int_prop(col, self, "dma_table_address", "DMA Table Address") + split = col.split() + split.prop(self, "read_entire_table") + if not self.read_entire_table: + split.prop(self, "table_index_prop", text="Index") + return + + split = col.split() + split.prop(self, "is_segmented_address_prop") + if self.binary_import_type == "Table": + split.prop(self, "table_address", text="") + string_int_warning(col, self.table_address) + elif self.binary_import_type == "Animation": + split.prop(self, "animation_address", text="") + string_int_warning(col, self.animation_address) + prop_split(col, self, "level", "Level") + if self.binary_import_type == "Table": # Draw settings after level + self.draw_table_settings(col) + + def draw_insertable_binary(self, layout: UILayout, import_rom: os.PathLike): + col = layout.column() + self.draw_path(col) + col.separator() + + col.label(text="Animation type will be read from the files", icon="INFO") + + table_box = col.column() + table_box.label(text="Table Imports", icon="ANIM") + self.draw_table_settings(table_box) + col.separator() + + col.prop(self, "read_from_rom") + if self.read_from_rom: + self.draw_import_rom(col, import_rom) + prop_split(col, self, "level", "Level") + + col.prop(self, "ignore_bone_count") + + def draw_props(self, layout: UILayout, import_rom: os.PathLike = "", decomp: os.PathLike = ""): + col = layout.column() + + prop_split(col, self, "import_type", "Type") + + if self.import_type in {"C", "Binary"}: + SM64_SearchAnimTablePresets.draw_props(col, self, "preset", "Preset") + col.separator() + + if self.import_type == "C": + self.draw_c(col, decomp) + elif self.binary: + if self.import_type == "Binary": + self.draw_binary(col, import_rom) + elif self.import_type == "Insertable Binary": + self.draw_insertable_binary(col, import_rom) + col.separator() + + self.draw_clean_up(col) + col.prop(self, "clear_table") + SM64_ImportAnim.draw_props(col) + + +class SM64_AnimProperties(PropertyGroup): + version: IntProperty(name="SM64_AnimProperties Version", default=0) + cur_version = 1 # version after property migration + + played_header: IntProperty(min=0) + played_action: PointerProperty(name="Action", type=Action) + + importing: PointerProperty(type=SM64_AnimImportProperties) + + def upgrade_old_props(self, scene: Scene): + self.importing.upgrade_old_props(scene) + + # Export + loop = scene.pop("loopAnimation", None) + start_address = scene.pop("animExportStart", None) + end_address = scene.pop("animExportEnd", None) + + for action in bpy.data.actions: + action_props: SM64_ActionAnimProperty = get_action_props(action) + action_props.header: SM64_AnimHeaderProperties + if loop is not None: + action_props.header.set_flags(SM64_AnimFlags(0) if loop else SM64_AnimFlags.ANIM_FLAG_NOLOOP) + if start_address is not None: + action_props.start_address = intToHex(int(start_address, 16)) + if end_address is not None: + action_props.end_address = intToHex(int(end_address, 16)) + + insertable_path = scene.pop("animInsertableBinaryPath", "") + is_dma = scene.pop("loopAnimation", None) + update_table = scene.pop("animExportStart", None) + update_behavior = scene.pop("animExportEnd", None) + beginning_animation = scene.pop("animListIndexExport", None) + for obj in bpy.data.objects: + if not is_obj_animatable(obj): + continue + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + if is_dma is not None: + anim_props.is_dma = is_dma + if update_table is not None: + anim_props.update_table = update_table + if update_behavior is not None: + anim_props.update_behavior = update_behavior + if beginning_animation is not None: + anim_props.beginning_animation = beginning_animation + if insertable_path is not None: # Ignores directory + anim_props.use_custom_file_name = True + anim_props.custom_file_name = os.path.split(insertable_path)[0] + + # Deprecated: + # - addr 0x27 was a pointer to a load anim cmd that would be used to update table pointers + # the actual table pointer is used instead + # - addr 0x28 was a pointer to a animate cmd that would be updated to the beggining + # animation a behavior script pointer is used instead so both load an animate can be updated + # easily without much thought + + self.version = 1 + + def upgrade_changed_props(self, scene): + if self.version != self.cur_version: + self.upgrade_old_props(scene) + self.version = SM64_AnimProperties.cur_version + + +class SM64_ArmatureAnimProperties(PropertyGroup): + version: IntProperty(name="SM64_AnimProperties Version", default=0) + cur_version = 1 # version after property migration + + is_dma: BoolProperty(name="Is DMA Export") + dma_folder: StringProperty(name="DMA Folder", default="assets/anims/") + update_table: BoolProperty( + name="Update Table On Action Export", + description="Update table outside of table exports", + default=True, + ) + + # Table + elements: CollectionProperty(type=SM64_AnimTableElementProperties) + + export_seperately_prop: BoolProperty(name="Export All Seperately") + write_data_seperately: BoolProperty(name="Write Data Seperately") + null_delimiter: BoolProperty(name="Add Null Delimiter") + override_files_prop: BoolProperty(name="Override Table and Data Files", default=True) + gen_enums: BoolProperty(name="Generate Enums", default=True) + use_custom_table_name: BoolProperty(name="Table Name") + custom_table_name: StringProperty(name="Table Name", default="mario_anims") + # Binary, Toad animation table example + data_address: StringProperty( + name="Data Address", + default=intToHex(0x00A3F7E0), + ) + data_end_address: StringProperty( + name="Data End", + default=intToHex(0x00A466C0), + ) + address: StringProperty(name="Table Address", default=intToHex(0x00A46738)) + end_address: StringProperty(name="Table End", default=intToHex(0x00A4675C)) + update_behavior: BoolProperty(name="Update Behavior", default=True) + behaviour: bpy.props.EnumProperty(items=enum_animated_behaviours, default=intToHex(0x13002EF8)) + behavior_address_prop: StringProperty(name="Behavior Address", default=intToHex(0x13002EF8)) + beginning_animation: StringProperty(name="Begining Animation", default="0x00") + # Mario animation table + dma_address: StringProperty(name="DMA Table Address", default=intToHex(0x4EC000)) + dma_end_address: StringProperty(name="DMA Table End", default=intToHex(0x4EC000 + 0x8DC20)) + + use_custom_file_name: BoolProperty(name="File Name") + custom_file_name: StringProperty(name="File Name", default="toad.insertable") + + @property + def behavior_address(self) -> int: + if self.behaviour == "Custom": + return int_from_str(self.behavior_address_prop) + return int_from_str(self.behaviour) + + @property + def export_seperately(self): + return self.is_dma or self.export_seperately_prop + + @property + def override_files(self) -> bool: + return not self.export_seperately or self.override_files_prop + + @property + def actions(self) -> list[Action]: + actions = [] + for element_props in self.elements: + action = element_props.get_action(not self.is_dma) + if action and action not in actions: + actions.append(action) + return actions + + def get_table_name(self, actor_name: str) -> str: + if self.use_custom_table_name: + return self.custom_table_name + return f"{actor_name}_anims" + + def get_enum_name(self, actor_name: str): + return table_name_to_enum(self.get_table_name(actor_name)) + + def get_enum_end(self, actor_name: str): + table_name = self.get_table_name(actor_name) + return f"{table_name.upper()}_END" + + def get_table_file_name(self, actor_name: str, export_type: str) -> str: + if not export_type in {"C", "Insertable Binary"}: + return "" + elif export_type == "Insertable Binary": + if self.use_custom_file_name: + return self.custom_file_name + return clean_name(actor_name + ("_dma_table" if self.is_dma else "_table")) + ".insertable" + else: + return "table.inc.c" + + def draw_element( + self, + layout: UILayout, + index: int, + table_element: SM64_AnimTableElementProperties, + export_type: str, + actor_name: str, + prev_enums: dict[str, int], + ): + col = layout.column() + row = col.row() + left_row = row.row() + left_row.alignment = "EXPAND" + op_row = row.row() + op_row.alignment = "RIGHT" + draw_list_ops(op_row, SM64_AnimTableOps, index) + + table_element.draw_props( + left_row, + col, + index, + self.is_dma, + self.update_table, + self.export_seperately, + export_type, + export_type == "C" and self.gen_enums and not self.is_dma, + actor_name, + prev_enums, + ) + + def draw_table(self, layout: UILayout, export_type: str, actor_name: str, bhv_export: bool): + col = layout.column() + + if self.is_dma: + if export_type == "Binary": + string_int_prop(col, self, "dma_address", "DMA Table Address") + string_int_prop(col, self, "dma_end_address", "DMA Table End") + elif export_type == "C": + multilineLabel( + col, + "The export will follow the vanilla DMA naming\n" + "conventions (anim_xx.inc.c, anim_xx, anim_xx_values, etc).", + icon="INFO", + ) + else: + if export_type == "C": + draw_custom_or_auto(self, col, "table_name", self.get_table_name(actor_name)) + col.prop(self, "gen_enums") + if self.gen_enums: + multilineLabel( + col.box(), + f"Enum List Name: {self.get_enum_name(actor_name)}\n" + f"End Enum: {self.get_enum_end(actor_name)}", + ) + col.separator() + col.prop(self, "export_seperately_prop") + draw_forced(col, self, "override_files_prop", not self.export_seperately) + if bhv_export: + prop_split(col, self, "beginning_animation", "Beginning Animation") + elif export_type == "Binary": + string_int_prop(col, self, "address", "Table Address") + string_int_prop(col, self, "end_address", "Table End") + + box = col.box().column() + box.prop(self, "update_behavior") + if self.update_behavior: + multilineLabel( + box, + "Will update the LOAD_ANIMATIONS and ANIMATE commands.\n" + "Does not raise an error if there is no ANIMATE command", + "INFO", + ) + SM64_SearchAnimatedBhvs.draw_props(box, self, "behaviour", "Behaviour") + if self.behaviour == "Custom": + prop_split(box, self, "behavior_address_prop", "Behavior Address") + prop_split(box, self, "beginning_animation", "Beginning Animation") + + col.prop(self, "write_data_seperately") + if self.write_data_seperately: + string_int_prop(col, self, "data_address", "Data Address") + string_int_prop(col, self, "data_end_address", "Data End") + col.prop(self, "null_delimiter") + if export_type == "Insertable Binary": + draw_custom_or_auto(self, col, "file_name", self.get_table_file_name(actor_name, export_type)) + + col.separator() + + op_row = col.row() + op_row.label( + text="Headers " + (f"({len(self.elements)})" if self.elements else "(Empty)"), + icon="NLA", + ) + draw_list_op(op_row, SM64_AnimTableOps, "ADD") + draw_list_op(op_row, SM64_AnimTableOps, "CLEAR") + + if not self.elements: + return + + box = col.box().column() + actions_dups: dict[Action, list[int]] = {} + if self.is_dma: + actions_repeats: dict[Action, list[int]] = {} # possible dups + last_action = None + for i, element_props in enumerate(self.elements): + action: Action = element_props.get_action(can_reference=False) + if action != last_action: + if action in actions_repeats: + actions_repeats[action].append(i) + if action not in actions_dups: + actions_dups[action] = actions_repeats[action] + else: + actions_repeats[action] = [i] + last_action = action + + if actions_dups: + lines = [f'Action "{a.name}", Headers: {i}' for a, i in actions_dups.items()] + warn_box = box.box() + warn_box.alert = True + multilineLabel( + warn_box, + "In DMA tables, headers for each action must be \n" + "in one sequence or the data will be duplicated.\n" + "This will be handeled automatically but is undesirable.\n" + f'Data duplicate{"s" if len(actions_dups) > 1 else ""} in:\n' + "\n".join(lines), + "INFO", + ) + + prev_enums = {} + element_props: SM64_AnimTableElementProperties + for i, element_props in enumerate(self.elements): + if i != 0: + box.separator() + element_box = box.column() + action = element_props.get_action(not self.is_dma) + if action in actions_dups: + other_actions = [j for j in actions_dups[action] if j != i] + element_box.box().label(text=f"Action duplicates at {other_actions}") + self.draw_element(element_box, i, element_props, export_type, actor_name, prev_enums) + + def draw_c_settings(self, layout: UILayout, header_type: str): + col = layout.column() + if self.is_dma: + prop_split(col, self, "dma_folder", "Folder", icon="FILE_FOLDER") + if header_type == "Custom": + col.label(text="This folder will be relative to your custom path") + else: + decompFolderMessage(col) + return + + def draw_props(self, layout: UILayout, export_type: str, header_type: str): + col = layout.column() + col.prop(self, "is_dma") + if export_type == "C": + self.draw_c_settings(col, header_type) + elif export_type == "Binary" and not self.is_dma: + col.prop(self, "update_table") + + +classes = ( + SM64_AnimHeaderProperties, + SM64_AnimTableElementProperties, + SM64_ActionAnimProperty, + SM64_AnimImportProperties, + SM64_AnimProperties, + SM64_ArmatureAnimProperties, +) + + +def anim_props_register(): + for cls in classes: + register_class(cls) + + +def anim_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/utility.py b/fast64_internal/sm64/animation/utility.py new file mode 100644 index 000000000..b8f18efb9 --- /dev/null +++ b/fast64_internal/sm64/animation/utility.py @@ -0,0 +1,165 @@ +from typing import TYPE_CHECKING +import functools +import re + +from bpy.types import Context, Object, Action, PoseBone + +from ...utility import findStartBones, PluginError, toAlnum +from ..sm64_geolayout_bone import animatableBoneTypes + +if TYPE_CHECKING: + from .properties import SM64_ActionAnimProperty, SM64_AnimProperties, SM64_ArmatureAnimProperties + + +def is_obj_animatable(obj: Object) -> bool: + if obj.type == "ARMATURE" or (obj.type == "MESH" and obj.geo_cmd_static in animatableBoneTypes): + return True + return False + + +def get_anim_obj(context: Context) -> Object | None: + obj = context.object + if obj is None and len(context.selected_objects) > 0: + obj = context.selected_objects[0] + if obj is not None and is_obj_animatable(obj): + return obj + + +def animation_operator_checks(context: Context, requires_animation=True, specific_obj: Object | None = None): + if specific_obj is None: + if len(context.selected_objects) > 1: + raise PluginError("Multiple objects selected at once.") + obj = get_anim_obj(context) + else: + obj = specific_obj + if is_obj_animatable(obj): + raise PluginError(f'Selected object "{obj.name}" is not an armature.') + if requires_animation and obj.animation_data is None: + raise PluginError(f'Armature "{obj.name}" has no animation data.') + + +def get_selected_action(obj: Object, raise_exc=True) -> Action: + assert obj is not None + if not is_obj_animatable(obj): + if raise_exc: + raise ValueError(f'Object "{obj.name}" is not animatable in SM64.') + elif obj.animation_data is not None and obj.animation_data.action is not None: + return obj.animation_data.action + if raise_exc: + raise ValueError(f'No action selected in object "{obj.name}".') + + +def get_anim_owners(obj: Object): + """Get SM64 animation bones from an armature or return the obj if it's an animated cmd mesh""" + + def check_children(children: list[Object] | None): + if children is None: + return + for child in children: + if child.geo_cmd_static in animatableBoneTypes: + raise PluginError("Cannot have child mesh with animation, use an armature") + check_children(child.children) + + if obj.type == "MESH": # Object will be treated as a bone + if obj.geo_cmd_static in animatableBoneTypes: + check_children(obj.children) + return [obj] + else: + raise PluginError("Mesh is not animatable") + + assert obj.type == "ARMATURE", "Obj is neither mesh or armature" + + bones_to_process: list[str] = findStartBones(obj) + current_bone = obj.data.bones[bones_to_process[0]] + anim_bones: list[PoseBone] = [] + + # Get animation bones in order + while len(bones_to_process) > 0: + bone_name = bones_to_process[0] + current_bone = obj.data.bones[bone_name] + current_pose_bone = obj.pose.bones[bone_name] + bones_to_process = bones_to_process[1:] + + # Only handle 0x13 bones for animation + if current_bone.geo_cmd in animatableBoneTypes: + anim_bones.append(current_pose_bone) + + # Traverse children in alphabetical order. + children_names = sorted([bone.name for bone in current_bone.children]) + bones_to_process = children_names + bones_to_process + + return anim_bones + + +def num_to_padded_hex(num: int): + hex_str = hex(num)[2:].upper() # remove the '0x' prefix + return hex_str.zfill(2) + + +@functools.cache +def get_dma_header_name(index: int): + return f"anim_{num_to_padded_hex(index)}" + + +def get_dma_anim_name(header_indices: list[int]): + return f'anim_{"_".join([f"{num_to_padded_hex(num)}" for num in header_indices])}' + + +@functools.cache +def action_name_to_enum_name(action_name: str) -> str: + return re.sub(r"^_(\d+_)+(?=\w)", "", toAlnum(action_name), flags=re.MULTILINE) + + +@functools.cache +def anim_name_to_enum_name(anim_name: str) -> str: + enum_name = anim_name.upper() + enum_name: str = re.sub(r"(?<=_)_|_$", "", toAlnum(enum_name), flags=re.MULTILINE) + if anim_name == enum_name: + enum_name = f"{enum_name}_ENUM" + return enum_name + + +def duplicate_name(name: str, existing_names: dict[str, int]) -> str: + """Updates existing_names""" + current_num = existing_names.get(name) + if current_num is None: + existing_names[name] = 0 + elif name != "": + current_num += 1 + existing_names[name] = current_num + return f"{name}_{current_num}" + return name + + +def table_name_to_enum(name: str): + return name.title().replace("_", "") + + +def get_action_props(action: Action) -> "SM64_ActionAnimProperty": + return action.fast64.sm64.animation + + +def get_scene_anim_props(context: Context) -> "SM64_AnimProperties": + return context.scene.fast64.sm64.animation + + +def get_anim_props(context: Context) -> "SM64_ArmatureAnimProperties": + obj = get_anim_obj(context) + assert obj is not None + return obj.fast64.sm64.animation + + +def get_anim_actor_name(context: Context) -> str | None: + sm64_props = context.scene.fast64.sm64 + if sm64_props.export_type == "C" and sm64_props.combined_export.export_anim: + return toAlnum(sm64_props.combined_export.obj_name_anim) + elif context.object: + return sm64_props.combined_export.filter_name(toAlnum(context.object.name), True) + else: + return None + + +def dma_structure_context(context: Context) -> bool: + if get_anim_obj(context) is None: + return False + return get_anim_props(context).is_dma diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 471ad617d..591e343e2 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import bpy from bpy.types import PropertyGroup, UILayout, Context from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty, PointerProperty @@ -6,11 +7,12 @@ from bpy.utils import register_class, unregister_class from ...render_settings import on_update_render_settings -from ...utility import directory_path_checks, directory_ui_warnings, prop_split, upgrade_old_prop +from ...utility import directory_path_checks, directory_ui_warnings, prop_split, upgrade_old_prop, get_first_set_prop from ..sm64_constants import defaultExtendSegment4 from ..sm64_objects import SM64_CombinedObjectProperties from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings from ..tools import SM64_AddrConvProperties +from ..animation.properties import SM64_AnimProperties from .constants import ( enum_refresh_versions, @@ -22,17 +24,17 @@ def decomp_path_update(self, context: Context): fast64_settings = context.scene.fast64.settings - if fast64_settings.repo_settings_path: + if fast64_settings.repo_settings_path and Path(abspath(fast64_settings.repo_settings_path)).exists(): return - directory_path_checks(abspath(self.decomp_path)) - fast64_settings.repo_settings_path = os.path.join(abspath(self.decomp_path), "fast64.json") + directory_path_checks(self.abs_decomp_path) + fast64_settings.repo_settings_path = str(self.abs_decomp_path / "fast64.json") class SM64_Properties(PropertyGroup): """Global SM64 Scene Properties found under scene.fast64.sm64""" version: IntProperty(name="SM64_Properties Version", default=0) - cur_version = 3 # version after property migration + cur_version = 4 # version after property migration # UI Selection show_importing_menus: BoolProperty(name="Show Importing Menus", default=False) @@ -80,10 +82,21 @@ class SM64_Properties(PropertyGroup): name="Matstack Fix", description="Exports account for matstack fix requirements", ) + # could be used for other properties outside animation + designated: BoolProperty( + name="Designated Initialization for Animation Tables", + description="Extremely recommended but must be off when compiling with IDO.", + ) + + animation: PointerProperty(type=SM64_AnimProperties) @property def binary_export(self): - return self.export_type in ["Binary", "Insertable Binary"] + return self.export_type in {"Binary", "Insertable Binary"} + + @property + def abs_decomp_path(self) -> Path: + return Path(abspath(self.decomp_path)) @staticmethod def upgrade_changed_props(): @@ -107,10 +120,13 @@ def upgrade_changed_props(): "custom_level_name": {"levelName", "geoLevelName", "colLevelName", "animLevelName"}, "non_decomp_level": {"levelCustomExport"}, "export_header_type": {"geoExportHeaderType", "colExportHeaderType", "animExportHeaderType"}, + "binary_level": {"levelAnimExport"}, + # as the others binary props get carried over to here we need to update the cur_version again } for scene in bpy.data.scenes: sm64_props: SM64_Properties = scene.fast64.sm64 sm64_props.address_converter.upgrade_changed_props(scene) + sm64_props.animation.upgrade_changed_props(scene) if sm64_props.version == SM64_Properties.cur_version: continue upgrade_old_prop( @@ -131,6 +147,11 @@ def upgrade_changed_props(): combined_props = scene.fast64.sm64.combined_export for new, old in old_export_props_to_new.items(): upgrade_old_prop(combined_props, new, scene, old) + + insertable_directory = get_first_set_prop(scene, "animInsertableBinaryPath") + if insertable_directory is not None: # Ignores file name + combined_props.insertable_directory = os.path.split(insertable_directory)[1] + sm64_props.version = SM64_Properties.cur_version def draw_props(self, layout: UILayout, show_repo_settings: bool = True): @@ -149,7 +170,7 @@ def draw_props(self, layout: UILayout, show_repo_settings: bool = True): col.prop(self, "extend_bank_4") elif not self.binary_export: prop_split(col, self, "decomp_path", "Decomp Path") - directory_ui_warnings(col, abspath(self.decomp_path)) + directory_ui_warnings(col, self.abs_decomp_path) col.separator() if not self.binary_export: diff --git a/fast64_internal/sm64/settings/repo_settings.py b/fast64_internal/sm64/settings/repo_settings.py index f74b7eb3d..c8d42a501 100644 --- a/fast64_internal/sm64/settings/repo_settings.py +++ b/fast64_internal/sm64/settings/repo_settings.py @@ -22,6 +22,7 @@ def save_sm64_repo_settings(scene: Scene): data["compression_format"] = sm64_props.compression_format data["force_extended_ram"] = sm64_props.force_extended_ram data["matstack_fix"] = sm64_props.matstack_fix + data["designated"] = sm64_props.designated return data @@ -42,6 +43,7 @@ def load_sm64_repo_settings(scene: Scene, data: dict[str, Any]): sm64_props.compression_format = data.get("compression_format", sm64_props.compression_format) sm64_props.force_extended_ram = data.get("force_extended_ram", sm64_props.force_extended_ram) sm64_props.matstack_fix = data.get("matstack_fix", sm64_props.matstack_fix) + sm64_props.designated = data.get("designated", sm64_props.designated) def draw_repo_settings(scene: Scene, layout: UILayout): @@ -54,5 +56,6 @@ def draw_repo_settings(scene: Scene, layout: UILayout): prop_split(col, sm64_props, "refresh_version", "Refresh (Function Map)") col.prop(sm64_props, "force_extended_ram") col.prop(sm64_props, "matstack_fix") + col.prop(sm64_props, "designated") col.label(text="See Fast64 repo settings for general settings", icon="INFO") diff --git a/fast64_internal/sm64/sm64_anim.py b/fast64_internal/sm64/sm64_anim.py deleted file mode 100644 index 0aa570cb0..000000000 --- a/fast64_internal/sm64/sm64_anim.py +++ /dev/null @@ -1,1119 +0,0 @@ -import bpy, os, copy, shutil, mathutils, math -from bpy.utils import register_class, unregister_class -from ..panels import SM64_Panel -from .sm64_level_parser import parseLevelAtPointer -from .sm64_rom_tweaks import ExtendBank0x04 -from .sm64_geolayout_bone import animatableBoneTypes - -from ..utility import ( - CData, - PluginError, - ValueFrameData, - raisePluginError, - encodeSegmentedAddr, - decodeSegmentedAddr, - getExportDir, - toAlnum, - writeIfNotFound, - get64bitAlignedAddr, - writeInsertableFile, - getFrameInterval, - findStartBones, - saveTranslationFrame, - saveQuaternionFrame, - removeTrailingFrames, - applyRotation, - getPathAndLevel, - applyBasicTweaks, - tempName, - bytesToHex, - prop_split, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, - writeBoxExportType, - stashActionInArmature, - enumExportHeaderType, -) - -from .sm64_constants import ( - bank0Segment, - insertableBinaryTypes, - level_pointers, - defaultExtendSegment4, - level_enums, - enumLevelNames, - marioAnimations, -) - -from .sm64_utility import export_rom_checks, import_rom_checks - -sm64_anim_types = {"ROTATE", "TRANSLATE"} - - -class SM64_Animation: - def __init__(self, name): - self.name = name - self.header = None - self.indices = SM64_ShortArray(name + "_indices", False) - self.values = SM64_ShortArray(name + "_values", True) - - def get_ptr_offsets(self, isDMA): - return [12, 16] if not isDMA else [] - - def to_binary(self, segmentData, isDMA, startAddress): - return ( - self.header.to_binary(segmentData, isDMA, startAddress) + self.indices.to_binary() + self.values.to_binary() - ) - - def to_c(self): - data = CData() - data.header = "extern const struct Animation *const " + self.name + "[];\n" - data.source = self.values.to_c() + "\n" + self.indices.to_c() + "\n" + self.header.to_c() + "\n" - return data - - -class SM64_ShortArray: - def __init__(self, name, signed): - self.name = name - self.shortData = [] - self.signed = signed - - def to_binary(self): - data = bytearray(0) - for short in self.shortData: - # All euler values have been pre-converted to positive values, so don't care about signed. - data += short.to_bytes(2, "big", signed=False) - return data - - def to_c(self): - data = "static const " + ("s" if self.signed else "u") + "16 " + self.name + "[] = {\n\t" - wrapCounter = 0 - for short in self.shortData: - data += "0x" + format(short, "04X") + ", " - wrapCounter += 1 - if wrapCounter > 8: - data += "\n\t" - wrapCounter = 0 - data += "\n};\n" - return data - - -class SM64_AnimationHeader: - def __init__( - self, - name, - repetitions, - marioYOffset, - frameInterval, - nodeCount, - transformValuesStart, - transformIndicesStart, - animSize, - ): - self.name = name - self.repetitions = repetitions - self.marioYOffset = marioYOffset - self.frameInterval = frameInterval - self.nodeCount = nodeCount - self.transformValuesStart = transformValuesStart - self.transformIndicesStart = transformIndicesStart - self.animSize = animSize # DMA animations only - - self.transformIndices = [] - - # presence of segmentData indicates DMA. - def to_binary(self, segmentData, isDMA, startAddress): - if isDMA: - transformValuesStart = self.transformValuesStart - transformIndicesStart = self.transformIndicesStart - else: - transformValuesStart = self.transformValuesStart + startAddress - transformIndicesStart = self.transformIndicesStart + startAddress - - data = bytearray(0) - data.extend(self.repetitions.to_bytes(2, byteorder="big")) - data.extend(self.marioYOffset.to_bytes(2, byteorder="big")) # y offset, only used for mario - data.extend([0x00, 0x00]) # unknown, common with secondary anims, variable length animations? - data.extend(int(round(self.frameInterval[0])).to_bytes(2, byteorder="big")) - data.extend(int(round(self.frameInterval[1] - 1)).to_bytes(2, byteorder="big")) - data.extend(self.nodeCount.to_bytes(2, byteorder="big")) - if not isDMA: - data.extend(encodeSegmentedAddr(transformValuesStart, segmentData)) - data.extend(encodeSegmentedAddr(transformIndicesStart, segmentData)) - data.extend(bytearray([0x00] * 6)) - else: - data.extend(transformValuesStart.to_bytes(4, byteorder="big")) - data.extend(transformIndicesStart.to_bytes(4, byteorder="big")) - data.extend(self.animSize.to_bytes(4, byteorder="big")) - data.extend(bytearray([0x00] * 2)) - return data - - def to_c(self): - data = ( - "static const struct Animation " - + self.name - + " = {\n" - + "\t" - + str(self.repetitions) - + ",\n" - + "\t" - + str(self.marioYOffset) - + ",\n" - + "\t0,\n" - + "\t" - + str(int(round(self.frameInterval[0]))) - + ",\n" - + "\t" - + str(int(round(self.frameInterval[1] - 1))) - + ",\n" - + "\tANIMINDEX_NUMPARTS(" - + self.name - + "_indices),\n" - + "\t" - + self.name - + "_values,\n" - + "\t" - + self.name - + "_indices,\n" - + "\t0,\n" - + "};\n" - ) - return data - - -class SM64_AnimIndexNode: - def __init__(self, x, y, z): - self.x = x - self.y = y - self.z = z - - -class SM64_AnimIndex: - def __init__(self, numFrames, startOffset): - self.startOffset = startOffset - self.numFrames = numFrames - - -def getLastKeyframeTime(keyframes): - last = keyframes[0].co[0] - for keyframe in keyframes: - if keyframe.co[0] > last: - last = keyframe.co[0] - return last - - -# add definition to groupN.h -# add data/table includes to groupN.c (bin_id?) -# add data/table files -def exportAnimationC(armatureObj, loopAnim, dirPath, dirName, groupName, customExport, headerType, levelName): - dirPath, texDir = getExportDir(customExport, dirPath, headerType, levelName, "", dirName) - - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, dirName + "_anim") - animName = armatureObj.animation_data.action.name - - geoDirPath = os.path.join(dirPath, toAlnum(dirName)) - if not os.path.exists(geoDirPath): - os.mkdir(geoDirPath) - - animDirPath = os.path.join(geoDirPath, "anims") - if not os.path.exists(animDirPath): - os.mkdir(animDirPath) - - animsName = dirName + "_anims" - animFileName = "anim_" + toAlnum(animName) + ".inc.c" - animPath = os.path.join(animDirPath, animFileName) - - data = sm64_anim.to_c() - outFile = open(animPath, "w", newline="\n") - outFile.write(data.source) - outFile.close() - - headerPath = os.path.join(geoDirPath, "anim_header.h") - headerFile = open(headerPath, "w", newline="\n") - headerFile.write("extern const struct Animation *const " + animsName + "[];\n") - headerFile.close() - - # write to data.inc.c - dataFilePath = os.path.join(animDirPath, "data.inc.c") - if not os.path.exists(dataFilePath): - dataFile = open(dataFilePath, "w", newline="\n") - dataFile.close() - writeIfNotFound(dataFilePath, '#include "' + animFileName + '"\n', "") - - # write to table.inc.c - tableFilePath = os.path.join(animDirPath, "table.inc.c") - - # if table doesn´t exist, create one - if not os.path.exists(tableFilePath): - tableFile = open(tableFilePath, "w", newline="\n") - tableFile.write("const struct Animation *const " + animsName + "[] = {\n\tNULL,\n};\n") - tableFile.close() - - stringData = "" - with open(tableFilePath, "r") as f: - stringData = f.read() - - # if animation header isn´t already in the table then add it. - if sm64_anim.header.name not in stringData: - # search for the NULL value which represents the end of the table - # (this value is not present in vanilla animation tables) - footerIndex = stringData.rfind("\tNULL,\n") - - # if the null value cant be found, look for the end of the array - if footerIndex == -1: - footerIndex = stringData.rfind("};") - - # if that can´t be found then throw an error. - if footerIndex == -1: - raise PluginError("Animation table´s footer does not seem to exist.") - - stringData = stringData[:footerIndex] + "\tNULL,\n" + stringData[footerIndex:] - - stringData = stringData[:footerIndex] + f"\t&{sm64_anim.header.name},\n" + stringData[footerIndex:] - - with open(tableFilePath, "w") as f: - f.write(stringData) - - if not customExport: - if headerType == "Actor": - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/anims/data.inc.c"', "") - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/anims/table.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + dirName + '/anim_header.h"', "#endif") - elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/anims/data.inc.c"', "") - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/anims/table.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + dirName + '/anim_header.h"', "\n#endif" - ) - - -def exportAnimationBinary(romfile, exportRange, armatureObj, DMAAddresses, segmentData, isDMA, loopAnim): - startAddress = get64bitAlignedAddr(exportRange[0]) - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, armatureObj.name) - - animData = sm64_anim.to_binary(segmentData, isDMA, startAddress) - - if startAddress + len(animData) > exportRange[1]: - raise PluginError( - "Size too big: Data ends at " - + hex(startAddress + len(animData)) - + ", which is larger than the specified range." - ) - - romfile.seek(startAddress) - romfile.write(animData) - - addrRange = (startAddress, startAddress + len(animData)) - - if not isDMA: - animTablePointer = get64bitAlignedAddr(startAddress + len(animData)) - romfile.seek(animTablePointer) - romfile.write(encodeSegmentedAddr(startAddress, segmentData)) - return addrRange, animTablePointer - else: - if DMAAddresses is not None: - romfile.seek(DMAAddresses["entry"]) - romfile.write((startAddress - DMAAddresses["start"]).to_bytes(4, byteorder="big")) - romfile.seek(DMAAddresses["entry"] + 4) - romfile.write(len(animData).to_bytes(4, byteorder="big")) - return addrRange, None - - -def exportAnimationInsertableBinary(filepath, armatureObj, isDMA, loopAnim): - startAddress = get64bitAlignedAddr(0) - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, armatureObj.name) - segmentData = copy.copy(bank0Segment) - - animData = sm64_anim.to_binary(segmentData, isDMA, startAddress) - - if startAddress + len(animData) > 0xFFFFFF: - raise PluginError( - "Size too big: Data ends at " - + hex(startAddress + len(animData)) - + ", which is larger than the specified range." - ) - - writeInsertableFile( - filepath, insertableBinaryTypes["Animation"], sm64_anim.get_ptr_offsets(isDMA), startAddress, animData - ) - - -def exportAnimationCommon(armatureObj, loopAnim, name): - if armatureObj.animation_data is None or armatureObj.animation_data.action is None: - raise PluginError("No active animation selected.") - - anim = armatureObj.animation_data.action - stashActionInArmature(armatureObj, anim) - - sm64_anim = SM64_Animation(toAlnum(name + "_" + anim.name)) - - nodeCount = len(armatureObj.data.bones) - - frame_start, frame_last = getFrameInterval(anim) - - translationData, armatureFrameData = convertAnimationData( - anim, - armatureObj, - frame_start=frame_start, - frame_count=(frame_last - frame_start + 1), - ) - - repetitions = 0 if loopAnim else 1 - marioYOffset = 0x00 # ??? Seems to be this value for most animations - - transformValuesOffset = 0 - headerSize = 0x1A - transformIndicesStart = headerSize # 0x18 if including animSize? - - # all node rotations + root translation - # *3 for each property (xyz) and *4 for entry size - # each keyframe stored as 2 bytes - # transformValuesStart = transformIndicesStart + (nodeCount + 1) * 3 * 4 - transformValuesStart = transformIndicesStart - - for translationFrameProperty in translationData: - frameCount = len(translationFrameProperty.frames) - sm64_anim.indices.shortData.append(frameCount) - sm64_anim.indices.shortData.append(transformValuesOffset) - if (transformValuesOffset) > 2**16 - 1: - raise PluginError("Animation is too large.") - transformValuesOffset += frameCount - transformValuesStart += 4 - for value in translationFrameProperty.frames: - sm64_anim.values.shortData.append( - int.from_bytes(value.to_bytes(2, "big", signed=True), byteorder="big", signed=False) - ) - - for boneFrameData in armatureFrameData: - for boneFrameDataProperty in boneFrameData: - frameCount = len(boneFrameDataProperty.frames) - sm64_anim.indices.shortData.append(frameCount) - sm64_anim.indices.shortData.append(transformValuesOffset) - if (transformValuesOffset) > 2**16 - 1: - raise PluginError("Animation is too large.") - transformValuesOffset += frameCount - transformValuesStart += 4 - for value in boneFrameDataProperty.frames: - sm64_anim.values.shortData.append(value) - - animSize = headerSize + len(sm64_anim.indices.shortData) * 2 + len(sm64_anim.values.shortData) * 2 - - sm64_anim.header = SM64_AnimationHeader( - sm64_anim.name, - repetitions, - marioYOffset, - [frame_start, frame_last + 1], - nodeCount, - transformValuesStart, - transformIndicesStart, - animSize, - ) - - return sm64_anim - - -def convertAnimationData(anim, armatureObj, *, frame_start, frame_count): - bonesToProcess = findStartBones(armatureObj) - currentBone = armatureObj.data.bones[bonesToProcess[0]] - animBones = [] - - # Get animation bones in order - while len(bonesToProcess) > 0: - boneName = bonesToProcess[0] - currentBone = armatureObj.data.bones[boneName] - currentPoseBone = armatureObj.pose.bones[boneName] - bonesToProcess = bonesToProcess[1:] - - # Only handle 0x13 bones for animation - if currentBone.geo_cmd in animatableBoneTypes: - animBones.append(boneName) - - # Traverse children in alphabetical order. - childrenNames = sorted([bone.name for bone in currentBone.children]) - bonesToProcess = childrenNames + bonesToProcess - - # list of boneFrameData, which is [[x frames], [y frames], [z frames]] - translationData = [ValueFrameData(0, i, []) for i in range(3)] - armatureFrameData = [ - [ValueFrameData(i, 0, []), ValueFrameData(i, 1, []), ValueFrameData(i, 2, [])] for i in range(len(animBones)) - ] - - currentFrame = bpy.context.scene.frame_current - for frame in range(frame_start, frame_start + frame_count): - bpy.context.scene.frame_set(frame) - rootPoseBone = armatureObj.pose.bones[animBones[0]] - - translation = ( - mathutils.Matrix.Scale(bpy.context.scene.fast64.sm64.blender_to_sm64_scale, 4) @ rootPoseBone.matrix_basis - ).decompose()[0] - saveTranslationFrame(translationData, translation) - - for boneIndex in range(len(animBones)): - boneName = animBones[boneIndex] - currentBone = armatureObj.data.bones[boneName] - currentPoseBone = armatureObj.pose.bones[boneName] - - rotationValue = (currentBone.matrix.to_4x4().inverted() @ currentPoseBone.matrix).to_quaternion() - if currentBone.parent is not None: - rotationValue = ( - currentBone.matrix.to_4x4().inverted() - @ currentPoseBone.parent.matrix.inverted() - @ currentPoseBone.matrix - ).to_quaternion() - - # rest pose local, compared to current pose local - - saveQuaternionFrame(armatureFrameData[boneIndex], rotationValue) - - bpy.context.scene.frame_set(currentFrame) - removeTrailingFrames(translationData) - for frameData in armatureFrameData: - removeTrailingFrames(frameData) - - return translationData, armatureFrameData - - -def getNextBone(boneStack, armatureObj): - if len(boneStack) == 0: - raise PluginError("More bones in animation than on armature.") - bone = armatureObj.data.bones[boneStack[0]] - boneStack = boneStack[1:] - boneStack = sorted([child.name for child in bone.children]) + boneStack - - # Only return 0x13 bone - while armatureObj.data.bones[bone.name].geo_cmd not in animatableBoneTypes: - if len(boneStack) == 0: - raise PluginError("More bones in animation than on armature.") - bone = armatureObj.data.bones[boneStack[0]] - boneStack = boneStack[1:] - boneStack = sorted([child.name for child in bone.children]) + boneStack - - return bone, boneStack - - -def importAnimationToBlender(romfile, startAddress, armatureObj, segmentData, isDMA, animName): - boneStack = findStartBones(armatureObj) - startBoneName = boneStack[0] - if armatureObj.data.bones[startBoneName].geo_cmd not in animatableBoneTypes: - startBone, boneStack = getNextBone(boneStack, armatureObj) - startBoneName = startBone.name - boneStack = [startBoneName] + boneStack - - animationHeader, armatureFrameData = readAnimation(animName, romfile, startAddress, segmentData, isDMA) - - if len(armatureFrameData) > len(armatureObj.data.bones) + 1: - raise PluginError("More bones in animation than on armature.") - - # bpy.context.scene.render.fps = 30 - bpy.context.scene.frame_end = animationHeader.frameInterval[1] - anim = bpy.data.actions.new(animName) - - isRootTranslation = True - # boneFrameData = [[x keyframes], [y keyframes], [z keyframes]] - # len(armatureFrameData) should be = number of bones - # property index = 0,1,2 (aka x,y,z) - for boneFrameData in armatureFrameData: - if isRootTranslation: - for propertyIndex in range(3): - fcurve = anim.fcurves.new( - data_path='pose.bones["' + startBoneName + '"].location', - index=propertyIndex, - action_group=startBoneName, - ) - for frame in range(len(boneFrameData[propertyIndex])): - fcurve.keyframe_points.insert(frame, boneFrameData[propertyIndex][frame]) - isRootTranslation = False - else: - bone, boneStack = getNextBone(boneStack, armatureObj) - for propertyIndex in range(3): - fcurve = anim.fcurves.new( - data_path='pose.bones["' + bone.name + '"].rotation_euler', - index=propertyIndex, - action_group=bone.name, - ) - for frame in range(len(boneFrameData[propertyIndex])): - fcurve.keyframe_points.insert(frame, boneFrameData[propertyIndex][frame]) - - if armatureObj.animation_data is None: - armatureObj.animation_data_create() - - stashActionInArmature(armatureObj, anim) - armatureObj.animation_data.action = anim - - -def readAnimation(name, romfile, startAddress, segmentData, isDMA): - animationHeader = readAnimHeader(name, romfile, startAddress, segmentData, isDMA) - - print("Frames: " + str(animationHeader.frameInterval[1]) + " / Nodes: " + str(animationHeader.nodeCount)) - - animationHeader.transformIndices = readAnimIndices( - romfile, animationHeader.transformIndicesStart, animationHeader.nodeCount - ) - - armatureFrameData = [] # list of list of frames - - # sm64 space -> blender space -> pose space - # BlenderToSM64: YZX (set rotation mode of bones) - # SM64toBlender: ZXY (set anim keyframes and model armature) - # new bones should extrude in +Y direction - - # handle root translation - boneFrameData = [[], [], []] - rootIndexNode = animationHeader.transformIndices[0] - boneFrameData[0] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.x) - ] - boneFrameData[1] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.y) - ] - boneFrameData[2] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.z) - ] - armatureFrameData.append(boneFrameData) - - # handle rotations - for boneIndexNode in animationHeader.transformIndices[1:]: - boneFrameData = [[], [], []] - - # Transforming SM64 space to Blender space - boneFrameData[0] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.x) - ] - boneFrameData[1] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.y) - ] - boneFrameData[2] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.z) - ] - - armatureFrameData.append(boneFrameData) - - return (animationHeader, armatureFrameData) - - -def getKeyFramesRotation(romfile, transformValuesStart, boneIndex): - ptrToValue = transformValuesStart + boneIndex.startOffset - romfile.seek(ptrToValue) - - keyframes = [] - for frame in range(boneIndex.numFrames): - romfile.seek(ptrToValue + frame * 2) - value = int.from_bytes(romfile.read(2), "big") * 360 / (2**16) - keyframes.append(math.radians(value)) - - return keyframes - - -def getKeyFramesTranslation(romfile, transformValuesStart, boneIndex): - ptrToValue = transformValuesStart + boneIndex.startOffset - romfile.seek(ptrToValue) - - keyframes = [] - for frame in range(boneIndex.numFrames): - romfile.seek(ptrToValue + frame * 2) - keyframes.append( - int.from_bytes(romfile.read(2), "big", signed=True) / bpy.context.scene.fast64.sm64.blender_to_sm64_scale - ) - - return keyframes - - -def readAnimHeader(name, romfile, startAddress, segmentData, isDMA): - frameInterval = [0, 0] - - romfile.seek(startAddress + 0x00) - numRepeats = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x02) - marioYOffset = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x06) - frameInterval[0] = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x08) - frameInterval[1] = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x0A) - numNodes = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x0C) - transformValuesOffset = int.from_bytes(romfile.read(4), "big") - if isDMA: - transformValuesStart = startAddress + transformValuesOffset - else: - transformValuesStart = decodeSegmentedAddr(transformValuesOffset.to_bytes(4, byteorder="big"), segmentData) - - romfile.seek(startAddress + 0x10) - transformIndicesOffset = int.from_bytes(romfile.read(4), "big") - if isDMA: - transformIndicesStart = startAddress + transformIndicesOffset - else: - transformIndicesStart = decodeSegmentedAddr(transformIndicesOffset.to_bytes(4, byteorder="big"), segmentData) - - romfile.seek(startAddress + 0x14) - animSize = int.from_bytes(romfile.read(4), "big") - - return SM64_AnimationHeader( - name, numRepeats, marioYOffset, frameInterval, numNodes, transformValuesStart, transformIndicesStart, animSize - ) - - -def readAnimIndices(romfile, ptrAddress, nodeCount): - indices = [] - - # Handle root transform - rootPosIndex = readTransformIndex(romfile, ptrAddress) - indices.append(rootPosIndex) - - # Handle rotations - for i in range(nodeCount): - rotationIndex = readTransformIndex(romfile, ptrAddress + (i + 1) * 12) - indices.append(rotationIndex) - - return indices - - -def readTransformIndex(romfile, startAddress): - x = readValueIndex(romfile, startAddress + 0) - y = readValueIndex(romfile, startAddress + 4) - z = readValueIndex(romfile, startAddress + 8) - - return SM64_AnimIndexNode(x, y, z) - - -def readValueIndex(romfile, startAddress): - romfile.seek(startAddress) - numFrames = int.from_bytes(romfile.read(2), "big") - romfile.seek(startAddress + 2) - - # multiply 2 because value is the index in array of shorts (???) - startOffset = int.from_bytes(romfile.read(2), "big") * 2 - # print(str(hex(startAddress)) + ": " + str(numFrames) + " " + str(startOffset)) - return SM64_AnimIndex(numFrames, startOffset) - - -def writeAnimation(romfile, startAddress, segmentData): - pass - - -def writeAnimHeader(romfile, startAddress, segmentData): - pass - - -class SM64_ExportAnimMario(bpy.types.Operator): - bl_idname = "object.sm64_export_anim" - bl_label = "Export Animation" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileOutput = None - tempROM = None - try: - if len(context.selected_objects) == 0 or not isinstance( - context.selected_objects[0].data, bpy.types.Armature - ): - raise PluginError("Armature not selected.") - if len(context.selected_objects) > 1: - raise PluginError("Multiple objects selected, make sure to select only one.") - armatureObj = context.selected_objects[0] - if context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - try: - # Rotate all armatures 90 degrees - applyRotation([armatureObj], math.radians(90), "X") - - if context.scene.fast64.sm64.export_type == "C": - exportPath, levelName = getPathAndLevel( - context.scene.animCustomExport, - context.scene.animExportPath, - context.scene.animLevelName, - context.scene.animLevelOption, - ) - if not context.scene.animCustomExport: - applyBasicTweaks(exportPath) - exportAnimationC( - armatureObj, - context.scene.loopAnimation, - exportPath, - bpy.context.scene.animName, - bpy.context.scene.animGroupName, - context.scene.animCustomExport, - context.scene.animExportHeaderType, - levelName, - ) - self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.export_type == "Insertable Binary": - exportAnimationInsertableBinary( - bpy.path.abspath(context.scene.animInsertableBinaryPath), - armatureObj, - context.scene.isDMAExport, - context.scene.loopAnimation, - ) - self.report({"INFO"}, "Success! Animation at " + context.scene.animInsertableBinaryPath) - else: - export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) - tempROM = tempName(context.scene.fast64.sm64.output_rom) - romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") - shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) - romfileExport.close() - romfileOutput = open(bpy.path.abspath(tempROM), "rb+") - - # Note actual level doesn't matter for Mario, since he is in all of them - levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelAnimExport]) - segmentData = levelParsed.segmentData - if context.scene.fast64.sm64.extend_bank_4: - ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) - - DMAAddresses = None - if context.scene.animOverwriteDMAEntry: - DMAAddresses = {} - DMAAddresses["start"] = int(context.scene.DMAStartAddress, 16) - DMAAddresses["entry"] = int(context.scene.DMAEntryAddress, 16) - - addrRange, nonDMAListPtr = exportAnimationBinary( - romfileOutput, - [int(context.scene.animExportStart, 16), int(context.scene.animExportEnd, 16)], - bpy.context.active_object, - DMAAddresses, - segmentData, - context.scene.isDMAExport, - context.scene.loopAnimation, - ) - - if not context.scene.isDMAExport: - segmentedPtr = encodeSegmentedAddr(addrRange[0], segmentData) - if context.scene.setAnimListIndex: - romfileOutput.seek(int(context.scene.addr_0x27, 16) + 4) - segAnimPtr = romfileOutput.read(4) - virtAnimPtr = decodeSegmentedAddr(segAnimPtr, segmentData) - romfileOutput.seek(virtAnimPtr + 4 * context.scene.animListIndexExport) - romfileOutput.write(segmentedPtr) - if context.scene.overwrite_0x28: - romfileOutput.seek(int(context.scene.addr_0x28, 16) + 1) - romfileOutput.write(bytearray([context.scene.animListIndexExport])) - else: - segmentedPtr = None - - romfileOutput.close() - if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): - os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) - - if not context.scene.isDMAExport: - if context.scene.setAnimListIndex: - self.report( - {"INFO"}, - "Sucess! Animation table at " - + hex(virtAnimPtr) - + ", animation at (" - + hex(addrRange[0]) - + ", " - + hex(addrRange[1]) - + ") " - + "(Seg. " - + bytesToHex(segmentedPtr) - + ").", - ) - else: - self.report( - {"INFO"}, - "Sucess! Animation at (" - + hex(addrRange[0]) - + ", " - + hex(addrRange[1]) - + ") " - + "(Seg. " - + bytesToHex(segmentedPtr) - + ").", - ) - else: - self.report( - {"INFO"}, "Success! Animation at (" + hex(addrRange[0]) + ", " + hex(addrRange[1]) + ")." - ) - - applyRotation([armatureObj], math.radians(-90), "X") - except Exception as e: - applyRotation([armatureObj], math.radians(-90), "X") - - if romfileOutput is not None: - romfileOutput.close() - if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): - os.remove(bpy.path.abspath(tempROM)) - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ExportAnimPanel(SM64_Panel): - bl_idname = "SM64_PT_export_anim" - bl_label = "SM64 Animation Exporter" - goal = "Object/Actor/Anim" - - # called every frame - def draw(self, context): - col = self.layout.column() - propsAnimExport = col.operator(SM64_ExportAnimMario.bl_idname) - - col.prop(context.scene, "loopAnimation") - - if context.scene.fast64.sm64.export_type == "C": - col.prop(context.scene, "animCustomExport") - if context.scene.animCustomExport: - col.prop(context.scene, "animExportPath") - prop_split(col, context.scene, "animName", "Name") - customExportWarning(col) - else: - prop_split(col, context.scene, "animExportHeaderType", "Export Type") - prop_split(col, context.scene, "animName", "Name") - if context.scene.animExportHeaderType == "Actor": - prop_split(col, context.scene, "animGroupName", "Group Name") - elif context.scene.animExportHeaderType == "Level": - prop_split(col, context.scene, "animLevelOption", "Level") - if context.scene.animLevelOption == "Custom": - prop_split(col, context.scene, "animLevelName", "Level Name") - - decompFolderMessage(col) - writeBox = makeWriteInfoBox(col) - writeBoxExportType( - writeBox, - context.scene.animExportHeaderType, - context.scene.animName, - context.scene.animLevelName, - context.scene.animLevelOption, - ) - - elif context.scene.fast64.sm64.export_type == "Insertable Binary": - col.prop(context.scene, "isDMAExport") - col.prop(context.scene, "animInsertableBinaryPath") - else: - col.prop(context.scene, "isDMAExport") - if context.scene.isDMAExport: - col.prop(context.scene, "animOverwriteDMAEntry") - if context.scene.animOverwriteDMAEntry: - prop_split(col, context.scene, "DMAStartAddress", "DMA Start Address") - prop_split(col, context.scene, "DMAEntryAddress", "DMA Entry Address") - else: - col.prop(context.scene, "setAnimListIndex") - if context.scene.setAnimListIndex: - prop_split(col, context.scene, "addr_0x27", "27 Command Address") - prop_split(col, context.scene, "animListIndexExport", "Anim List Index") - col.prop(context.scene, "overwrite_0x28") - if context.scene.overwrite_0x28: - prop_split(col, context.scene, "addr_0x28", "28 Command Address") - col.prop(context.scene, "levelAnimExport") - col.separator() - prop_split(col, context.scene, "animExportStart", "Start Address") - prop_split(col, context.scene, "animExportEnd", "End Address") - - -class SM64_ImportAnimMario(bpy.types.Operator): - bl_idname = "object.sm64_import_anim" - bl_label = "Import Animation" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileSrc = None - try: - import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) - romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - try: - levelParsed = parseLevelAtPointer(romfileSrc, level_pointers[context.scene.levelAnimImport]) - segmentData = levelParsed.segmentData - - animStart = int(context.scene.animStartImport, 16) - if context.scene.animIsSegPtr: - animStart = decodeSegmentedAddr(animStart.to_bytes(4, "big"), segmentData) - - if not context.scene.isDMAImport and context.scene.animIsAnimList: - romfileSrc.seek(animStart + 4 * context.scene.animListIndexImport) - actualPtr = romfileSrc.read(4) - animStart = decodeSegmentedAddr(actualPtr, segmentData) - - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - armatureObj = context.active_object - if armatureObj.type != "ARMATURE": - raise PluginError("Armature not selected.") - - importAnimationToBlender( - romfileSrc, animStart, armatureObj, segmentData, context.scene.isDMAImport, "sm64_anim" - ) - romfileSrc.close() - self.report({"INFO"}, "Success!") - except Exception as e: - if romfileSrc is not None: - romfileSrc.close() - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ImportAllMarioAnims(bpy.types.Operator): - bl_idname = "object.sm64_import_mario_anims" - bl_label = "Import All Mario Animations" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileSrc = None - try: - import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) - romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - try: - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - armatureObj = context.active_object - if armatureObj.type != "ARMATURE": - raise PluginError("Armature not selected.") - - for adress, animName in marioAnimations: - importAnimationToBlender(romfileSrc, adress, armatureObj, {}, context.scene.isDMAImport, animName) - - romfileSrc.close() - self.report({"INFO"}, "Success!") - except Exception as e: - if romfileSrc is not None: - romfileSrc.close() - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ImportAnimPanel(SM64_Panel): - bl_idname = "SM64_PT_import_anim" - bl_label = "SM64 Animation Importer" - goal = "Object/Actor/Anim" - import_panel = True - - # called every frame - def draw(self, context): - col = self.layout.column() - propsAnimImport = col.operator(SM64_ImportAnimMario.bl_idname) - propsMarioAnimsImport = col.operator(SM64_ImportAllMarioAnims.bl_idname) - - col.prop(context.scene, "isDMAImport") - if not context.scene.isDMAImport: - col.prop(context.scene, "animIsAnimList") - if context.scene.animIsAnimList: - prop_split(col, context.scene, "animListIndexImport", "Anim List Index") - - prop_split(col, context.scene, "animStartImport", "Start Address") - col.prop(context.scene, "animIsSegPtr") - col.prop(context.scene, "levelAnimImport") - - -sm64_anim_classes = ( - SM64_ExportAnimMario, - SM64_ImportAnimMario, - SM64_ImportAllMarioAnims, -) - -sm64_anim_panels = ( - SM64_ImportAnimPanel, - SM64_ExportAnimPanel, -) - - -def sm64_anim_panel_register(): - for cls in sm64_anim_panels: - register_class(cls) - - -def sm64_anim_panel_unregister(): - for cls in sm64_anim_panels: - unregister_class(cls) - - -def sm64_anim_register(): - for cls in sm64_anim_classes: - register_class(cls) - - bpy.types.Scene.animStartImport = bpy.props.StringProperty(name="Import Start", default="4EC690") - bpy.types.Scene.animExportStart = bpy.props.StringProperty(name="Start", default="11D8930") - bpy.types.Scene.animExportEnd = bpy.props.StringProperty(name="End", default="11FFF00") - bpy.types.Scene.isDMAImport = bpy.props.BoolProperty(name="Is DMA Animation", default=True) - bpy.types.Scene.isDMAExport = bpy.props.BoolProperty(name="Is DMA Animation") - bpy.types.Scene.DMAEntryAddress = bpy.props.StringProperty(name="DMA Entry Address", default="4EC008") - bpy.types.Scene.DMAStartAddress = bpy.props.StringProperty(name="DMA Start Address", default="4EC000") - bpy.types.Scene.levelAnimImport = bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") - bpy.types.Scene.levelAnimExport = bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") - bpy.types.Scene.loopAnimation = bpy.props.BoolProperty(name="Loop Animation", default=True) - bpy.types.Scene.setAnimListIndex = bpy.props.BoolProperty(name="Set Anim List Entry", default=True) - bpy.types.Scene.overwrite_0x28 = bpy.props.BoolProperty(name="Overwrite 0x28 behaviour command", default=True) - bpy.types.Scene.addr_0x27 = bpy.props.StringProperty(name="0x27 Command Address", default="21CD00") - bpy.types.Scene.addr_0x28 = bpy.props.StringProperty(name="0x28 Command Address", default="21CD08") - bpy.types.Scene.animExportPath = bpy.props.StringProperty(name="Directory", subtype="FILE_PATH") - bpy.types.Scene.animOverwriteDMAEntry = bpy.props.BoolProperty(name="Overwrite DMA Entry") - bpy.types.Scene.animInsertableBinaryPath = bpy.props.StringProperty(name="Filepath", subtype="FILE_PATH") - bpy.types.Scene.animIsSegPtr = bpy.props.BoolProperty(name="Is Segmented Address", default=False) - bpy.types.Scene.animIsAnimList = bpy.props.BoolProperty(name="Is Anim List", default=True) - bpy.types.Scene.animListIndexImport = bpy.props.IntProperty(name="Anim List Index", min=0, max=255) - bpy.types.Scene.animListIndexExport = bpy.props.IntProperty(name="Anim List Index", min=0, max=255) - bpy.types.Scene.animName = bpy.props.StringProperty(name="Name", default="mario") - bpy.types.Scene.animGroupName = bpy.props.StringProperty(name="Group Name", default="group0") - bpy.types.Scene.animWriteHeaders = bpy.props.BoolProperty(name="Write Headers For Actor", default=True) - bpy.types.Scene.animCustomExport = bpy.props.BoolProperty(name="Custom Export Path") - bpy.types.Scene.animExportHeaderType = bpy.props.EnumProperty( - items=enumExportHeaderType, name="Header Export", default="Actor" - ) - bpy.types.Scene.animLevelName = bpy.props.StringProperty(name="Level", default="bob") - bpy.types.Scene.animLevelOption = bpy.props.EnumProperty(items=enumLevelNames, name="Level", default="bob") - - -def sm64_anim_unregister(): - for cls in reversed(sm64_anim_classes): - unregister_class(cls) - - del bpy.types.Scene.animStartImport - del bpy.types.Scene.animExportStart - del bpy.types.Scene.animExportEnd - del bpy.types.Scene.levelAnimImport - del bpy.types.Scene.levelAnimExport - del bpy.types.Scene.isDMAImport - del bpy.types.Scene.isDMAExport - del bpy.types.Scene.DMAStartAddress - del bpy.types.Scene.DMAEntryAddress - del bpy.types.Scene.loopAnimation - del bpy.types.Scene.setAnimListIndex - del bpy.types.Scene.overwrite_0x28 - del bpy.types.Scene.addr_0x27 - del bpy.types.Scene.addr_0x28 - del bpy.types.Scene.animExportPath - del bpy.types.Scene.animOverwriteDMAEntry - del bpy.types.Scene.animInsertableBinaryPath - del bpy.types.Scene.animIsSegPtr - del bpy.types.Scene.animIsAnimList - del bpy.types.Scene.animListIndexImport - del bpy.types.Scene.animListIndexExport - del bpy.types.Scene.animName - del bpy.types.Scene.animGroupName - del bpy.types.Scene.animWriteHeaders - del bpy.types.Scene.animCustomExport - del bpy.types.Scene.animExportHeaderType - del bpy.types.Scene.animLevelName - del bpy.types.Scene.animLevelOption diff --git a/fast64_internal/sm64/sm64_classes.py b/fast64_internal/sm64/sm64_classes.py new file mode 100644 index 000000000..48c165b50 --- /dev/null +++ b/fast64_internal/sm64/sm64_classes.py @@ -0,0 +1,290 @@ +from io import BufferedReader, StringIO +from typing import BinaryIO +from pathlib import Path +import dataclasses +import shutil +import struct +import os +import numpy as np + +from ..utility import intToHex, decodeSegmentedAddr, PluginError, toAlnum +from .sm64_constants import insertableBinaryTypes, SegmentData +from .sm64_utility import export_rom_checks, temp_file_path + + +@dataclasses.dataclass +class InsertableBinaryData: + data_type: str = "" + data: bytearray = dataclasses.field(default_factory=bytearray) + start_address: int = 0 + ptrs: list[int] = dataclasses.field(default_factory=list) + + def write(self, path: Path): + path.write_bytes(self.to_binary()) + + def to_binary(self): + data = bytearray() + data.extend(insertableBinaryTypes[self.data_type].to_bytes(4, "big")) # 0-4 + data.extend(len(self.data).to_bytes(4, "big")) # 4-8 + data.extend(self.start_address.to_bytes(4, "big")) # 8-12 + data.extend(len(self.ptrs).to_bytes(4, "big")) # 12-16 + for ptr in self.ptrs: # 16-(16 + len(ptr) * 4) + data.extend(ptr.to_bytes(4, "big")) + data.extend(self.data) + return data + + def read(self, file: BufferedReader, expected_type: list = None): + print(f"Reading insertable binary data from {file.name}") + reader = RomReader(file) + type_num = reader.read_int(4) + if type_num not in insertableBinaryTypes.values(): + raise ValueError(f"Unknown data type: {intToHex(type_num)}") + self.data_type = next(k for k, v in insertableBinaryTypes.items() if v == type_num) + if expected_type and self.data_type not in expected_type: + raise ValueError(f"Unexpected data type: {self.data_type}") + + data_size = reader.read_int(4) + self.start_address = reader.read_int(4) + pointer_count = reader.read_int(4) + self.ptrs = [] + for _ in range(pointer_count): + self.ptrs.append(reader.read_int(4)) + + actual_start = reader.address + self.start_address + self.data = reader.read_data(data_size, actual_start) + return self + + +@dataclasses.dataclass +class RomReader: + """ + Helper class that simplifies reading data continously from a starting address. + Can read insertable binary files, in which it can also read data from ROM if provided. + """ + + rom_file: BufferedReader = None + insertable_file: BufferedReader = None + start_address: int = 0 + segment_data: SegmentData = dataclasses.field(default_factory=dict) + insertable: InsertableBinaryData = None + address: int = dataclasses.field(init=False) + + def __post_init__(self): + self.address = self.start_address + if self.insertable_file and not self.insertable: + self.insertable = InsertableBinaryData().read(self.insertable_file) + assert self.insertable or self.rom_file + + def branch(self, start_address=-1): + start_address = self.address if start_address == -1 else start_address + if self.read_int(1, specific_address=start_address) is None: + if self.insertable and self.rom_file: + return RomReader(self.rom_file, start_address=start_address, segment_data=self.segment_data) + return None + return RomReader( + self.rom_file, + self.insertable_file, + start_address, + self.segment_data, + self.insertable, + ) + + def skip(self, size: int): + self.address += size + + def read_data(self, size=-1, specific_address=-1): + if specific_address == -1: + address = self.address + self.skip(size) + else: + address = specific_address + + if self.insertable: + data = self.insertable.data[address : address + size] + else: + self.rom_file.seek(address) + data = self.rom_file.read(size) + if size > 0 and not data: + raise IndexError(f"Value at {intToHex(address)} not present in data.") + return data + + def read_ptr(self, specific_address=-1): + address = self.address if specific_address == -1 else specific_address + ptr = self.read_int(4, specific_address=specific_address) + if self.insertable and address in self.insertable.ptrs: + return ptr + if ptr and self.segment_data: + return decodeSegmentedAddr(ptr.to_bytes(4, "big"), self.segment_data) + return ptr + + def read_int(self, size=4, signed=False, specific_address=-1): + return int.from_bytes(self.read_data(size, specific_address), "big", signed=signed) + + def read_float(self, size=4, specific_address=-1): + return struct.unpack(">f", self.read_data(size, specific_address))[0] + + def read_str(self, specific_address=-1): + ptr = self.read_ptr() if specific_address == -1 else specific_address + if not ptr: + return None + branch = self.branch(ptr) + text_data = bytearray() + while True: + byte = branch.read_data(1) + if byte == b"\x00" or not byte: + break + text_data.append(ord(byte)) + text = text_data.decode("utf-8") + return text + + +@dataclasses.dataclass +class BinaryExporter: + export_rom: Path + output_rom: Path + rom_file_output: BinaryIO = dataclasses.field(init=False) + temp_rom: Path = dataclasses.field(init=False) + + @property + def tell(self): + return self.rom_file_output.tell() + + def __enter__(self): + export_rom_checks(self.export_rom) + print(f"Binary export started, exporting to {self.output_rom}") + self.temp_rom = temp_file_path(self.output_rom) + print(f'Copying "{self.export_rom}" to temporary file "{self.temp_rom}".') + shutil.copy(self.export_rom, self.temp_rom) + self.rom_file_output = self.temp_rom.open("rb+") + return self + + def write_to_range(self, start_address: int, end_address: int, data: bytes | bytearray): + address_range_str = f"[{intToHex(start_address)}, {intToHex(end_address)}]" + if end_address < start_address: + raise PluginError(f"Start address is higher than the end address: {address_range_str}") + if start_address + len(data) > end_address: + raise PluginError( + f"Data ({len(data) / 1000.0} kb) does not fit in range {address_range_str} " + f"({(end_address - start_address) / 1000.0} kb).", + ) + print(f"Writing {len(data) / 1000.0} kb to {address_range_str} ({(end_address - start_address) / 1000.0} kb))") + self.write(data, start_address) + + def seek(self, offset: int, whence: int = 0): + self.rom_file_output.seek(offset, whence) + + def read(self, n=-1, offset=-1): + if offset != -1: + self.seek(offset) + return self.rom_file_output.read(n) + + def write(self, s: bytes, offset=-1): + if offset != -1: + self.seek(offset) + return self.rom_file_output.write(s) + + def __exit__(self, exc_type, exc_value, traceback): + if self.temp_rom.exists(): + print(f"Closing temporary file {self.temp_rom}.") + self.rom_file_output.close() + else: + raise FileNotFoundError(f"Temporary file {self.temp_rom} does not exist?") + if exc_value: + print("Deleting temporary file because of exception.") + os.remove(self.temp_rom) + print("Type:", exc_type, "\nValue:", exc_value, "\nTraceback:", traceback) + else: + print(f"Moving temporary file to {self.output_rom}.") + if os.path.exists(self.output_rom): + os.remove(self.output_rom) + self.temp_rom.rename(self.output_rom) + + +@dataclasses.dataclass +class DMATableElement: + offset: int = 0 + size: int = 0 + address: int = 0 + end_address: int = 0 + + +@dataclasses.dataclass +class DMATable: + address_place_holder: int = 0 + entries: list[DMATableElement] = dataclasses.field(default_factory=list) + data: bytearray = dataclasses.field(default_factory=bytearray) + address: int = 0 + end_address: int = 0 + + def to_binary(self): + print( + f"Generating DMA table with {len(self.entries)} entries", + f"and {len(self.data)} bytes of data", + ) + data = bytearray() + data.extend(len(self.entries).to_bytes(4, "big", signed=False)) + data.extend(self.address_place_holder.to_bytes(4, "big", signed=False)) + + entries_offset = 8 + entries_length = len(self.entries) * 8 + entrie_data_offset = entries_offset + entries_length + + for entrie in self.entries: + offset = entrie_data_offset + entrie.offset + data.extend(offset.to_bytes(4, "big", signed=False)) + data.extend(entrie.size.to_bytes(4, "big", signed=False)) + data.extend(self.data) + + return data + + def read_binary(self, reader: RomReader): + print("Reading DMA table at", intToHex(reader.start_address)) + self.address = reader.start_address + + num_entries = reader.read_int(4) # numEntries + self.address_place_holder = reader.read_int(4) # addrPlaceholder + + table_size = 0 + for _ in range(num_entries): + offset = reader.read_int(4) + size = reader.read_int(4) + address = self.address + offset + self.entries.append(DMATableElement(offset, size, address, address + size)) + end_of_entry = offset + size + if end_of_entry > table_size: + table_size = end_of_entry + self.end_address = self.address + table_size + print(f"Found {len(self.entries)} DMA entries") + return self + + +@dataclasses.dataclass +class IntArray: + data: np.ndarray + name: str = "" + wrap: int = 6 + wrap_start: int = 0 # -6 To replicate decomp animation index table formatting + + def to_binary(self): + return self.data.astype(">i2").tobytes() + + def to_c(self, c_data: StringIO | None = None, new_lines=1): + assert self.name, "Array must have a name" + data = self.data + byte_count = data.itemsize + data_type = f"{'s' if data.dtype == np.int16 else 'u'}{byte_count * 8}" + print(f'Generating {data_type} array "{self.name}" with {len(self.data)} elements') + + c_data = c_data or StringIO() + c_data.write(f"// {len(self.data)}\n") + c_data.write(f"static const {data_type} {toAlnum(self.name)}[] = {{\n\t") + i = self.wrap_start + for value in self.data: + c_data.write(f"{intToHex(value, byte_count, False)}, ") + i += 1 + if i >= self.wrap: + c_data.write("\n\t") + i = 0 + + c_data.write("\n};" + ("\n" * new_lines)) + return c_data diff --git a/fast64_internal/sm64/sm64_collision.py b/fast64_internal/sm64/sm64_collision.py index 6e2907f0c..5f342bd27 100644 --- a/fast64_internal/sm64/sm64_collision.py +++ b/fast64_internal/sm64/sm64_collision.py @@ -1,3 +1,4 @@ +from pathlib import Path import bpy, shutil, os, math, mathutils from bpy.utils import register_class, unregister_class from io import BytesIO @@ -8,7 +9,7 @@ insertableBinaryTypes, defaultExtendSegment4, ) -from .sm64_utility import export_rom_checks +from .sm64_utility import export_rom_checks, to_include_descriptor, update_actor_includes, write_or_delete_if_found from .sm64_objects import SM64_Area, start_process_sm64_objects from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 @@ -23,8 +24,6 @@ get64bitAlignedAddr, prop_split, getExportDir, - writeIfNotFound, - deleteIfFound, duplicateHierarchy, cleanupDuplicatedObjects, writeInsertableFile, @@ -331,31 +330,26 @@ def exportCollisionC( cDefFile.write(cDefine) cDefFile.close() - if headerType == "Actor": - # Write to group files - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"') - writeIfNotFound(groupPathH, '\n#include "' + name + '/collision_header.h"', "\n#endif") - - elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"') - writeIfNotFound(groupPathH, '\n#include "levels/' + levelName + "/" + name + '/collision_header.h"', "\n#endif") + data_includes = [Path("collision.inc.c")] + if writeRoomsFile: + data_includes.append(Path("rooms.inc.c")) + update_actor_includes( + headerType, groupName, Path(dirPath), name, levelName, data_includes, [Path("collision_header.h")] + ) + if not writeRoomsFile: # TODO: Could be done better + if headerType == "Actor": + group_path_c = Path(dirPath) / f"{groupName}.c" + write_or_delete_if_found(group_path_c, to_remove=[to_include_descriptor(Path(name) / "rooms.inc.c")]) + elif headerType == "Level": + group_path_c = Path(dirPath) / "leveldata.c" + write_or_delete_if_found( + group_path_c, + to_remove=[ + to_include_descriptor( + Path(name) / "rooms.inc.c", Path("levels") / levelName / name / "rooms.inc.c" + ), + ], + ) return cDefine diff --git a/fast64_internal/sm64/sm64_constants.py b/fast64_internal/sm64/sm64_constants.py index fd1893770..226b250dc 100644 --- a/fast64_internal/sm64/sm64_constants.py +++ b/fast64_internal/sm64/sm64_constants.py @@ -1,3 +1,6 @@ +import dataclasses +from typing import Any, Iterable, TypeVar + # RAM address used in evaluating switch for hatless Mario marioHatSwitch = 0x80277740 marioLowPolySwitch = 0x80277150 @@ -27,6 +30,28 @@ "metal": 0x9EC, } +NULL = 0x00000000 + +MIN_U8 = 0 +MAX_U8 = (2**8) - 1 + +MIN_S8 = -(2**7) +MAX_S8 = (2**7) - 1 + +MIN_S16 = -(2**15) +MAX_S16 = (2**15) - 1 + +MIN_U16 = 0 +MAX_U16 = 2**16 - 1 + +MIN_S32 = -(2**31) +MAX_S32 = 2**31 - 1 + +MIN_U32 = 0 +MAX_U32 = 2**32 - 1 + +SegmentData = dict[int, tuple[int, int]] + commonGeolayoutPointers = { "Dorrie": [2039136, "HMC"], "Bowser": [1809204, "BFB"], @@ -294,7 +319,14 @@ def __init__(self, geoAddr, level, switchDict): "TTM": 0x2AC2EC, } -insertableBinaryTypes = {"Display List": 0, "Geolayout": 1, "Animation": 2, "Collision": 3} +insertableBinaryTypes = { + "Display List": 0, + "Geolayout": 1, + "Animation": 2, + "Collision": 3, + "Animation Table": 4, + "Animation DMA Table": 5, +} enumBehaviourPresets = [ ("Custom", "Custom", "Custom"), ("1300407c", "1 Up", "1 Up"), @@ -2114,6 +2146,7 @@ def __init__(self, geoAddr, level, switchDict): ("Custom", "Custom", "Custom"), ] + # groups you can use for the combined object export groups_obj_export = [ ("common0", "common0", "chuckya, boxes, blue coin switch"), @@ -2139,219 +2172,1640 @@ def __init__(self, geoAddr, level, switchDict): ("Custom", "Custom", "Custom"), ] -marioAnimations = [ - # ( Adress, "Animation name" ), - (5162640, "0 - Slow ledge climb up"), - (5165520, "1 - Fall over backwards"), - (5165544, "2 - Backward air kb"), - (5172396, "3 - Dying on back"), - (5177044, "4 - Backflip"), - (5179584, "5 - Climbing up pole"), - (5185656, "6 - Grab pole short"), - (5186824, "7 - Grab pole swing part 1"), - (5186848, "8 - Grab pole swing part 2"), - (5191920, "9 - Handstand idle"), - (5194740, "10 - Handstand jump"), - (5194764, "11 - Start handstand"), - (5188592, "12 - Return from handstand"), - (5196388, "13 - Idle on pole"), - (5197436, "14 - A pose"), - (5197792, "15 - Skid on ground"), - (5197816, "16 - Stop skid"), - (5199596, "17 - Crouch from fast longjump"), - (5201048, "18 - Crouch from a slow longjump"), - (5202644, "19 - Fast longjump"), - (5204600, "20 - Slow longjump"), - (5205980, "21 - Airborne on stomach"), - (5207188, "22 - Walk with light object"), - (5211916, "23 - Run with light object"), - (5215136, "24 - Slow walk with light object"), - (5219864, "25 - Shivering and warming hands"), - (5225496, "26 - Shivering return to idle "), - (5226920, "27 - Shivering"), - (5230056, "28 - Climb down on ledge"), - (5231112, "29 - Credits - Waving"), - (5232768, "30 - Credits - Look up"), - (5234576, "31 - Credits - Return from look up"), - (5235700, "32 - Credits - Raising hand"), - (5243100, "33 - Credits - Lowering hand"), - (5245988, "34 - Credits - Taking off cap"), - (5248016, "35 - Credits - Start walking and look up"), - (5256508, "36 - Credits - Look back then run"), - (5266160, "37 - Final Bowser - Raise hand and spin"), - (5274456, "38 - Final Bowser - Wing cap take off"), - (5282084, "39 - Credits - Peach sign"), - (5291340, "40 - Stand up from lava boost"), - (5292628, "41 - Fire/Lava burn"), - (5293488, "42 - Wing cap flying"), - (5295016, "43 - Hang on owl"), - (5296876, "44 - Land on stomach"), - (5296900, "45 - Air forward kb"), - (5302796, "46 - Dying on stomach"), - (5306100, "47 - Suffocating"), - (5313796, "48 - Coughing"), - (5319500, "49 - Throw catch key"), - (5330436, "50 - Dying fall over"), - (5338604, "51 - Idle on ledge"), - (5341720, "52 - Fast ledge grab"), - (5343296, "53 - Hang on ceiling"), - (5347276, "54 - Put cap on"), - (5351252, "55 - Take cap off then on"), - (5358356, "56 - Quickly put cap on"), - (5359476, "57 - Head stuck in ground"), - (5372172, "58 - Ground pound landing"), - (5372824, "59 - Triple jump ground-pound"), - (5374304, "60 - Start ground-pound"), - (5374328, "61 - Ground-pound"), - (5375380, "62 - Bottom stuck in ground"), - (5387148, "63 - Idle with light object"), - (5390520, "64 - Jump land with light object"), - (5391892, "65 - Jump with light object"), - (5392704, "66 - Fall land with light object"), - (5393936, "67 - Fall with light object"), - (5394296, "68 - Fall from sliding with light object"), - (5395224, "69 - Sliding on bottom with light object"), - (5395248, "70 - Stand up from sliding with light object"), - (5396716, "71 - Riding shell"), - (5397832, "72 - Walking"), - (5403208, "73 - Forward flip"), - (5404784, "74 - Jump riding shell"), - (5405676, "75 - Land from double jump"), - (5407340, "76 - Double jump fall"), - (5408288, "77 - Single jump"), - (5408312, "78 - Land from single jump"), - (5411044, "79 - Air kick"), - (5412900, "80 - Double jump rise"), - (5413596, "81 - Start forward spinning"), - (5414876, "82 - Throw light object"), - (5416032, "83 - Fall from slide kick"), - (5418280, "84 - Bend kness riding shell"), - (5419872, "85 - Legs stuck in ground"), - (5431416, "86 - General fall"), - (5431440, "87 - General land"), - (5433276, "88 - Being grabbed"), - (5434636, "89 - Grab heavy object"), - (5437964, "90 - Slow land from dive"), - (5441520, "91 - Fly from cannon"), - (5442516, "92 - Moving right while hanging"), - (5444052, "93 - Moving left while hanging"), - (5445472, "94 - Missing cap"), - (5457860, "95 - Pull door walk in"), - (5463196, "96 - Push door walk in"), - (5467492, "97 - Unlock door"), - (5480428, "98 - Start reach pocket"), - (5481448, "99 - Reach pocket"), - (5483352, "100 - Stop reach pocket"), - (5484876, "101 - Ground throw"), - (5486852, "102 - Ground kick"), - (5489076, "103 - First punch"), - (5489740, "104 - Second punch"), - (5490356, "105 - First punch fast"), - (5491396, "106 - Second punch fast"), - (5492732, "107 - Pick up light object"), - (5493948, "108 - Pushing"), - (5495508, "109 - Start riding shell"), - (5497072, "110 - Place light object"), - (5498484, "111 - Forward spinning"), - (5498508, "112 - Backward spinning"), - (5498884, "113 - Breakdance"), - (5501240, "114 - Running"), - (5501264, "115 - Running (unused)"), - (5505884, "116 - Soft back kb"), - (5508004, "117 - Soft front kb"), - (5510172, "118 - Dying in quicksand"), - (5515096, "119 - Idle in quicksand"), - (5517836, "120 - Move in quicksand"), - (5528568, "121 - Electrocution"), - (5532480, "122 - Shocked"), - (5533160, "123 - Backward kb"), - (5535796, "124 - Forward kb"), - (5538372, "125 - Idle heavy object"), - (5539764, "126 - Stand against wall"), - (5544580, "127 - Side step left"), - (5548480, "128 - Side step right"), - (5553004, "129 - Start sleep idle"), - (5557588, "130 - Start sleep scratch"), - (5563636, "131 - Start sleep yawn"), - (5568648, "132 - Start sleep sitting"), - (5573680, "133 - Sleep idle"), - (5574280, "134 - Sleep start laying"), - (5577460, "135 - Sleep laying"), - (5579300, "136 - Dive"), - (5579324, "137 - Slide dive"), - (5580860, "138 - Ground bonk"), - (5584116, "139 - Stop slide light object"), - (5587364, "140 - Slide kick"), - (5588288, "141 - Crouch from slide kick"), - (5589652, "142 - Slide motionless"), - (5589676, "143 - Stop slide"), - (5591572, "144 - Fall from slide"), - (5592860, "145 - Slide"), - (5593404, "146 - Tiptoe"), - (5599280, "147 - Twirl land"), - (5600160, "148 - Twirl"), - (5600516, "149 - Start twirl"), - (5601072, "150 - Stop crouching"), - (5602028, "151 - Start crouching"), - (5602720, "152 - Crouching"), - (5605756, "153 - Crawling"), - (5613048, "154 - Stop crawling"), - (5613968, "155 - Start crawling"), - (5614876, "156 - Summon star"), - (5620036, "157 - Return star approach door"), - (5622256, "158 - Backwards water kb"), - (5626540, "159 - Swim with object part 1"), - (5627592, "160 - Swim with object part 2"), - (5628260, "161 - Flutter kick with object"), - (5629456, "162 - Action end with object in water"), - (5631180, "163 - Stop holding object in water"), - (5634048, "164 - Holding object in water"), - (5635976, "165 - Drowning part 1"), - (5641400, "166 - Drowning part 2"), - (5646324, "167 - Dying in water"), - (5649660, "168 - Forward kb in water"), - (5653848, "169 - Falling from water"), - (5655852, "170 - Swimming part 1"), - (5657100, "171 - Swimming part 2"), - (5658128, "172 - Flutter kick"), - (5660112, "173 - Action end in water"), - (5662248, "174 - Pick up object in water"), - (5663480, "175 - Grab object in water part 2"), - (5665916, "176 - Grab object in water part 1"), - (5666632, "177 - Throw object in water"), - (5669328, "178 - Idle in water"), - (5671428, "179 - Star dance in water"), - (5678200, "180 - Return from in water star dance"), - (5680324, "181 - Grab bowser"), - (5680348, "182 - Swing bowser"), - (5682008, "183 - Release bowser"), - (5685264, "184 - Holding bowser"), - (5686316, "185 - Heavy throw"), - (5688660, "186 - Walk panting"), - (5689924, "187 - Walk with heavy object"), - (5694332, "188 - Turning part 1"), - (5694356, "189 - Turning part 2"), - (5696160, "190 - Side flip land"), - (5697196, "191 - Side flip"), - (5699408, "192 - Triple jump land"), - (5702136, "193 - Triple jump"), - (5704880, "194 - First person"), - (5710580, "195 - Idle head left"), - (5712800, "196 - Idle head right"), - (5715020, "197 - Idle head center"), - (5717240, "198 - Handstand left"), - (5719184, "199 - Handstand right"), - (5722304, "200 - Wake up from sleeping"), - (5724228, "201 - Wake up from laying"), - (5726444, "202 - Start tiptoeing"), - (5728720, "203 - Slide jump"), - (5728744, "204 - Start wallkick"), - (5730404, "205 - Star dance"), - (5735864, "206 - Return from star dance"), - (5737600, "207 - Forwards spinning flip"), - (5740584, "208 - Triple jump fly"), +BEHAVIOR_EXITS = [ + "RETURN", + "GOTO", + "END_LOOP", + "BREAK", + "BREAK_UNUSED", + "DEACTIVATE", ] +BEHAVIOR_COMMANDS = [ + # Name, Size + ("BEGIN", 1), # bhv_cmd_begin + ("DELAY", 1), # bhv_cmd_delay + ("CALL", 1), # bhv_cmd_call + ("RETURN", 1), # bhv_cmd_return + ("GOTO", 1), # bhv_cmd_goto + ("BEGIN_REPEAT", 1), # bhv_cmd_begin_repeat + ("END_REPEAT", 1), # bhv_cmd_end_repeat + ("END_REPEAT_CONTINUE", 1), # bhv_cmd_end_repeat_continue + ("BEGIN_LOOP", 1), # bhv_cmd_begin_loop + ("END_LOOP", 1), # bhv_cmd_end_loop + ("BREAK", 1), # bhv_cmd_break + ("BREAK_UNUSED", 1), # bhv_cmd_break_unused + ("CALL_NATIVE", 2), # bhv_cmd_call_native + ("ADD_FLOAT", 1), # bhv_cmd_add_float + ("SET_FLOAT", 1), # bhv_cmd_set_float + ("ADD_INT", 1), # bhv_cmd_add_int + ("SET_INT", 1), # bhv_cmd_set_int + ("OR_INT", 1), # bhv_cmd_or_int + ("BIT_CLEAR", 1), # bhv_cmd_bit_clear + ("SET_INT_RAND_RSHIFT", 2), # bhv_cmd_set_int_rand_rshift + ("SET_RANDOM_FLOAT", 2), # bhv_cmd_set_random_float + ("SET_RANDOM_INT", 2), # bhv_cmd_set_random_int + ("ADD_RANDOM_FLOAT", 2), # bhv_cmd_add_random_float + ("ADD_INT_RAND_RSHIFT", 2), # bhv_cmd_add_int_rand_rshift + ("NOP_1", 1), # bhv_cmd_nop_1 + ("NOP_2", 1), # bhv_cmd_nop_2 + ("NOP_3", 1), # bhv_cmd_nop_3 + ("SET_MODEL", 1), # bhv_cmd_set_model + ("SPAWN_CHILD", 3), # bhv_cmd_spawn_child + ("DEACTIVATE", 1), # bhv_cmd_deactivate + ("DROP_TO_FLOOR", 1), # bhv_cmd_drop_to_floor + ("SUM_FLOAT", 1), # bhv_cmd_sum_float + ("SUM_INT", 1), # bhv_cmd_sum_int + ("BILLBOARD", 1), # bhv_cmd_billboard + ("HIDE", 1), # bhv_cmd_hide + ("SET_HITBOX", 2), # bhv_cmd_set_hitbox + ("NOP_4", 1), # bhv_cmd_nop_4 + ("DELAY_VAR", 1), # bhv_cmd_delay_var + ("BEGIN_REPEAT_UNUSED", 1), # bhv_cmd_begin_repeat_unused + ("LOAD_ANIMATIONS", 2), # bhv_cmd_load_animations + ("ANIMATE", 1), # bhv_cmd_animate + ("SPAWN_CHILD_WITH_PARAM", 3), # bhv_cmd_spawn_child_with_param + ("LOAD_COLLISION_DATA", 2), # bhv_cmd_load_collision_data + ("SET_HITBOX_WITH_OFFSET", 3), # bhv_cmd_set_hitbox_with_offset + ("SPAWN_OBJ", 3), # bhv_cmd_spawn_obj + ("SET_HOME", 1), # bhv_cmd_set_home + ("SET_HURTBOX", 2), # bhv_cmd_set_hurtbox + ("SET_INTERACT_TYPE", 2), # bhv_cmd_set_interact_type + ("SET_OBJ_PHYSICS", 5), # bhv_cmd_set_obj_physics + ("SET_INTERACT_SUBTYPE", 2), # bhv_cmd_set_interact_subtype + ("SCALE", 1), # bhv_cmd_scale + ("PARENT_BIT_CLEAR", 2), # bhv_cmd_parent_bit_clear + ("ANIMATE_TEXTURE", 1), # bhv_cmd_animate_texture + ("DISABLE_RENDERING", 1), # bhv_cmd_disable_rendering + ("SET_INT_UNUSED", 2), # bhv_cmd_set_int_unused + ("SPAWN_WATER_DROPLET", 2), # bhv_cmd_spawn_water_droplet +] + +T = TypeVar("T") +DictOrVal = T | dict[str, T] | None +ListOrVal = T | list[T] | None + + +def as_list(val: ListOrVal[Any]) -> list[T]: + if isinstance(val, Iterable): + return list(val) + if val is None: + return [] + return [val] + + +def as_dict(val: DictOrVal[T], name: str = "") -> dict[str, T]: + """If val is a dict, returns it, otherwise returns {name: member}""" + if isinstance(val, dict): + return val + elif val is not None: + return {name: val} + return {} + + +def validate_dict(val: DictOrVal, val_type: type): + return all(isinstance(k, str) and isinstance(v, val_type) for k, v in as_dict(val).items()) + + +def validate_list(val: ListOrVal, val_type: type): + return all(isinstance(v, val_type) for v in as_list(val)) + + +@dataclasses.dataclass +class AnimInfo: + address: int + behaviours: DictOrVal[int] = dataclasses.field(default_factory=dict) + size: int | None = None # None means the size can be determined from the NULL delimiter + ignore_bone_count: bool = False + dma: bool = False + directory: str | None = None + names: list[str] = dataclasses.field(default_factory=list) + + def __post_init__(self): + assert isinstance(self.address, int) + assert validate_dict(self.behaviours, int) + assert self.size is None or isinstance(self.size, int) + assert isinstance(self.ignore_bone_count, bool) + assert isinstance(self.dma, bool) + assert self.directory is None or isinstance(self.directory, str) + assert validate_list(self.names, str) + + +@dataclasses.dataclass +class ModelIDInfo: + number: int + enum: str + + def __post_init__(self): + assert isinstance(self.number, int) + assert isinstance(self.enum, str) + + +@dataclasses.dataclass +class DisplaylistInfo: + address: int + # Displaylists are compressed, so their c name can´t be fetched from func_map like geolayouts + c_name: str + + def __post_init__(self): + assert isinstance(self.address, int) + assert isinstance(self.c_name, str) + + +@dataclasses.dataclass +class ModelInfo: + model_id: ListOrVal[ModelIDInfo] = dataclasses.field(default_factory=list) + geolayout: int | None = None + displaylist: DisplaylistInfo | None = None + + def __post_init__(self): + self.model_id = as_list(self.model_id) + assert validate_list(self.model_id, ModelIDInfo) + assert validate_list(self.geolayout, int) + assert validate_list(self.displaylist, DisplaylistInfo) + + +@dataclasses.dataclass +class CollisionInfo: + address: int + c_name: str + + def __post_init__(self): + assert isinstance(self.address, int) + assert isinstance(self.c_name, str) + + +@dataclasses.dataclass +class ActorPresetInfo: + decomp_path: str = None + level: str | None = None + group: str | None = None + animation: DictOrVal[AnimInfo] = dataclasses.field(default_factory=dict) + models: DictOrVal[ModelInfo] = dataclasses.field(default_factory=dict) + collision: DictOrVal[CollisionInfo] = dataclasses.field(default_factory=dict) + + def __post_init__(self): + assert self.decomp_path is not None and isinstance(self.decomp_path, str) + assert self.group is None or isinstance(self.group, str) + assert validate_dict(self.animation, AnimInfo) + assert validate_dict(self.models, ModelInfo) + assert validate_dict(self.collision, CollisionInfo) + group_to_level = { + "common0": "HH", + "common1": "HH", + "group0": "HH", + "group1": "WF", + "group2": "LLL", + "group3": "BOB", + "group4": "JRB", + "group5": "SSL", + "group6": "TTM", + "group7": "CCM", + "group8": "VC", + "group9": "HH", + "group10": "CG", + "group11": "THI", + "group12": "BFB", + "group13": "WDW", + "group14": "BOB", + "group15": "IC", + "group16": "CCM", + "group17": "HMC", + } + if self.level is None and self.group is not None: + self.level = group_to_level[self.group] + assert isinstance(self.level, str) + + @staticmethod + def get_member_as_dict(name: str, member: DictOrVal[T]): + return as_dict(member, name) + + +ACTOR_PRESET_INFO = { + "Amp": ActorPresetInfo( + decomp_path="actors/amp", + group="common0", + animation=AnimInfo( + address=0x8004034, + behaviours={"Circling Amp": 0x13003388, "Homing Amp": 0x13003354}, + names=["Moving"], + ignore_bone_count=True, + ), + models=ModelInfo(model_id=ModelIDInfo(0xC2, "MODEL_AMP"), geolayout=0xF000028), + ), + "Bird": ActorPresetInfo( + decomp_path="actors/bird", + group="group10", + animation=AnimInfo( + address=0x50009E8, + behaviours={"Bird": 0x13005354, "End Birds 1": 0x1300565C, "End Birds 2": 0x13005680}, + names=["Flying", "Gliding"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BIRDS"), geolayout=0xC000000), + ), + "Blargg": ActorPresetInfo( + decomp_path="actors/blargg", + group="group2", + animation=AnimInfo(address=0x500616C, names=["Idle", "Bite"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BLARGG"), geolayout=0xC000240), + ), + "Blue Coin Switch": ActorPresetInfo( + decomp_path="actors/blue_coin_switch", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x8C, "MODEL_BLUE_COIN_SWITCH"), geolayout=0xF000000), + collision=CollisionInfo(address=0x8000E98, c_name="blue_coin_switch_seg8_collision_08000E98"), + ), + "Blue Fish": ActorPresetInfo( + decomp_path="actors/blue_fish", + group="common1", + animation=AnimInfo(address=0x301C2B0, behaviours=0x13001B2C, names=["Swimming", "Diving"]), + models={ + "Fish": ModelInfo(model_id=ModelIDInfo(0xB9, "MODEL_FISH"), geolayout=0x16000C44), + "Fish (With Shadow)": ModelInfo(model_id=ModelIDInfo(0xBA, "MODEL_FISH_SHADOW"), geolayout=0x16000BEC), + }, + ), + "Bobomb": ActorPresetInfo( + decomp_path="actors/bobomb", + group="common0", + animation=AnimInfo( + address=0x802396C, + behaviours={"Bobomb": 0x13003174, "Bobomb Buddy": 0x130031DC, "Bobomb Buddy (Opens Cannon)": 0x13003228}, + names=["Walking", "Strugling"], + ), + models={ + "Bobomb": ModelInfo(model_id=ModelIDInfo(0xBC, "MODEL_BLACK_BOBOMB"), geolayout=0xF0007B8), + "Bobomb Buddy": ModelInfo(model_id=ModelIDInfo(0xC3, "MODEL_BOBOMB_BUDDY"), geolayout=0xF0008F4), + }, + ), + "Bowser Bomb": ActorPresetInfo( + decomp_path="actors/bomb", + group="group12", + models=ModelInfo( + model_id=[ModelIDInfo(0x65, "MODEL_BOWSER_BOMB_CHILD_OBJ"), ModelIDInfo(0xB3, "MODEL_BOWSER_BOMB")], + geolayout=0xD000BBC, + ), + ), + "Boo": ActorPresetInfo( + decomp_path="actors/boo", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BOO"), geolayout=0xC000224), + ), + "Boo (Inside Castle)": ActorPresetInfo( + decomp_path="actors/boo_castle", + group="group15", + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_BOO_CASTLE"), geolayout=0xD0005B0), + ), + "Bookend": ActorPresetInfo( + decomp_path="actors/book", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_BOOKEND"), geolayout=0xC0000C0), + ), + "Bookend Part": ActorPresetInfo( + decomp_path="actors/bookend", + group="group9", + animation=AnimInfo(address=0x5002540, behaviours=0x1300506C, names=["Opening Mouth", "Bite", "Closed"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_BOOKEND_PART"), geolayout=0xC000000), + ), + "Metal Ball": ActorPresetInfo( + decomp_path="actors/bowling_ball", + group="common0", + models={ + "Bowling Ball": ModelInfo(model_id=ModelIDInfo(0xB4, "MODEL_BOWLING_BALL"), geolayout=0xF000640), + "Trajectory Marker Ball": ModelInfo( + model_id=ModelIDInfo(0xE1, "MODEL_TRAJECTORY_MARKER_BALL"), geolayout=0xF00066C + ), + }, + ), + "Bowser": ActorPresetInfo( + decomp_path="actors/bowser", + group="group12", + animation=AnimInfo( + address=0x60577E0, + behaviours=0x13001850, + size=27, + ignore_bone_count=True, + names=[ + "Stand Up", + "Stand Up (Unused)", + "Shaking", + "Grabbed", + "Broken Animation (Unused)", + "Fall Down", + "Fire Breath", + "Jump", + "Jump Stop", + "Jump Start", + "Dance", + "Fire Breath Up", + "Idle", + "Slow Gait", + "Look Down Stop Walk", + "Look Up Start Walk", + "Flip Down", + "Lay Down", + "Run Start", + "Run", + "Run Stop", + "Run Slip", + "Fire Breath Quick", + "Edge Move", + "Edge Stop", + "Flip", + "Stand Up From Flip", + ], + ), + models={ + "Bowser": ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_BOWSER"), geolayout=0xD000AC4), + "Bowser (No Shadow)": ModelInfo(model_id=ModelIDInfo(0x69, "MODEL_BOWSER_NO_SHADOW"), geolayout=0xD000B40), + }, + ), + "Bowser Flame": ActorPresetInfo( + decomp_path="actors/bowser_flame", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_BOWSER_FLAMES"), geolayout=0xD000000), + ), + "Bowser Key": ActorPresetInfo( + decomp_path="actors/bowser_key", + group="common1", + animation=AnimInfo( + address=0x30172D0, + behaviours={"Bowser Key": 0x13001BB4, "Bowser Key (Cutscene)": 0x13001BD4}, + size=2, + names=["Unlock Door", "Course Exit"], + ), + models={ + "Bowser Key (Cutscene)": ModelInfo( + model_id=ModelIDInfo(0xC8, "MODEL_BOWSER_KEY_CUTSCENE"), geolayout=0x16000AB0 + ), + "Bowser Key": ModelInfo(model_id=ModelIDInfo(0xCC, "MODEL_BOWSER_KEY"), geolayout=0x16000A84), + }, + ), + "Breakable Box": ActorPresetInfo( + decomp_path="actors/breakable_box", + group="common0", + models={ + "Breakable Box": ModelInfo(model_id=ModelIDInfo(0x81, "MODEL_BREAKABLE_BOX"), geolayout=0xF0005D0), + "Breakable Box (Small)": ModelInfo( + model_id=ModelIDInfo(0x82, "MODEL_BREAKABLE_BOX_SMALL"), geolayout=0xF000610 + ), + }, + collision=CollisionInfo(address=0x8012D70, c_name="breakable_box_seg8_collision_08012D70"), + ), + "Bub": ActorPresetInfo( + decomp_path="actors/bub", + group="group13", + animation=AnimInfo(address=0x6012354, behaviours=0x1300220C, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_BUB"), geolayout=0xD00038C), + ), + "Bubba": ActorPresetInfo( + decomp_path="actors/bubba", + group="group11", + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_BUBBA"), geolayout=0xC000000), + ), + "Bubble": ActorPresetInfo( + decomp_path="actors/bubble", + group="group0", + models={ + "Bubble": ModelInfo(model_id=ModelIDInfo(0xA8, "MODEL_BUBBLE"), geolayout=0x17000000), + "Bubble Marble": ModelInfo(model_id=ModelIDInfo(0xAA, "MODEL_PURPLE_MARBLE"), geolayout=0x1700001C), + }, + ), + "Bullet Bill": ActorPresetInfo( + decomp_path="actors/bullet_bill", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BULLET_BILL"), geolayout=0xC000264), + ), + "Bully": ActorPresetInfo( + decomp_path="actors/bully", + group="group2", + animation=AnimInfo( + address=0x500470C, + behaviours={"Bully": 0x13003660, "Bully (With Minions)": 0x13003694, "Bully (Small)": 0x1300362C}, + names=["Patrol", "Chase", "Falling over (Unused)", "Knockback"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_BULLY"), geolayout=0xC000000), + ), + "Burn Smoke": ActorPresetInfo( + decomp_path="actors/burn_smoke", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x94, "MODEL_BURN_SMOKE"), geolayout=0x17000084), + ), + "Butterfly": ActorPresetInfo( + decomp_path="actors/butterfly", + group="common1", + animation=AnimInfo( + address=0x30056B0, + behaviours={"Butterfly": 0x130033BC, "Triplet Butterfly": 0x13005598}, + size=2, + names=["Flying", "Resting"], + ), + models=ModelInfo(model_id=ModelIDInfo(0xBB, "MODEL_BUTTERFLY"), geolayout=0x160000A8), + ), + "Cannon Barrel": ActorPresetInfo( + decomp_path="actors/cannon_barrel", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x7F, "MODEL_CANNON_BARREL"), geolayout=0xF0001C0), + ), + "Cannon Base": ActorPresetInfo( + decomp_path="actors/cannon_base", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x80, "MODEL_CANNON_BASE"), geolayout=0xF0001A8), + ), + "Cannon Lid": ActorPresetInfo( + decomp_path="actors/cannon_lid", + group="common0", + collision=CollisionInfo(address=0x8004950, c_name="cannon_lid_seg8_collision_08004950"), + models=ModelInfo( + model_id=ModelIDInfo(0xC9, "MODEL_DL_CANNON_LID"), + displaylist=DisplaylistInfo(0x80048E0, "cannon_lid_seg8_dl_080048E0"), + ), + ), + "Cap Switch": ActorPresetInfo( + decomp_path="actors/capswitch", + group="group8", + models={ + "Cap Switch": ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_CAP_SWITCH"), geolayout=0xC000048), + "Cap Switch (Exclamation)": ModelInfo( + model_id=ModelIDInfo(0x54, "MODEL_CAP_SWITCH_EXCLAMATION"), + displaylist=DisplaylistInfo(0x5002E00, "cap_switch_exclamation_seg5_dl_05002E00"), + ), + "Cap Switch (Base)": ModelInfo( + model_id=ModelIDInfo(0x56, "MODEL_CAP_SWITCH_BASE"), + displaylist=DisplaylistInfo(0x5003120, "cap_switch_base_seg5_dl_05003120"), + ), + }, + collision={ + "Cap Switch (Base)": CollisionInfo(address=0x50033D0, c_name="capswitch_collision_050033D0"), + "Cap Switch (Top)": CollisionInfo(address=0x5003448, c_name="capswitch_collision_05003448"), + }, + ), + "Chain Ball": ActorPresetInfo( # also known as metallic ball + decomp_path="actors/chain_ball", + group="group14", + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_METALLIC_BALL"), geolayout=0xD0005D0), + ), + "Chain Chomp": ActorPresetInfo( + decomp_path="actors/chain_chomp", + group="group14", + animation=AnimInfo(address=0x6025178, behaviours=0x1300478C, names=["Chomping"]), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_CHAIN_CHOMP"), geolayout=0xD0005EC), + ), + "Haunted Chair": ActorPresetInfo( + decomp_path="actors/chair", + group="group9", + animation=AnimInfo(address=0x5005784, behaviours=0x13004FD4, names=["Default Pose"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_HAUNTED_CHAIR"), geolayout=0xC0000D8), + ), + "Checkerboard Platform": ActorPresetInfo( + decomp_path="actors/checkerboard_platform", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0xCA, "MODEL_CHECKERBOARD_PLATFORM"), geolayout=0xF0004E4), + collision=CollisionInfo(address=0x800D710, c_name="checkerboard_platform_seg8_collision_0800D710"), + ), + "Chilly Chief": ActorPresetInfo( + decomp_path="actors/chilly_chief", + group="group16", + animation=AnimInfo( + address=0x6003994, + behaviours={"Chilly Chief (Small)": 0x130036C8, "Chilly Chief (Big)": 0x13003700}, + names=["Patrol", "Chase", "Falling over (Unused)", "Knockback"], + ), + models={ + "Chilly Chief (Small)": ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_CHILL_BULLY"), geolayout=0x6003754), + "Chilly Chief (Big)": ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_BIG_CHILL_BULLY"), geolayout=0x6003874), + }, + ), + "Chuckya": ActorPresetInfo( + decomp_path="actors/chuckya", + group="common0", + animation=AnimInfo( + address=0x800C070, + behaviours=0x13000528, + names=["Grab Mario", "Holding Mario", "Being Held", "Throwing", "Moving", "Balancing/Idle (Unused)"], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDF, "MODEL_CHUCKYA"), geolayout=0xF0001D8), + ), + "Clam Shell": ActorPresetInfo( + decomp_path="actors/clam", + group="group4", + animation=AnimInfo(address=0x5001744, behaviours=0x13005440, names=["Close", "Open"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_CLAM_SHELL"), geolayout=0xC000000), + ), + "Coin": ActorPresetInfo( + decomp_path="actors/coin", + group="common1", + models={ + "Yellow Coin": ModelInfo(model_id=ModelIDInfo(0x74, "MODEL_YELLOW_COIN"), geolayout=0x1600013C), + "Yellow Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0x75, "MODEL_YELLOW_COIN_NO_SHADOW"), geolayout=0x160001A0 + ), + "Blue Coin": ModelInfo(model_id=ModelIDInfo(0x76, "MODEL_BLUE_COIN"), geolayout=0x16000200), + "Blue Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0x77, "MODEL_BLUE_COIN_NO_SHADOW"), geolayout=0x16000264 + ), + "Red Coin": ModelInfo(model_id=ModelIDInfo(0xD7, "MODEL_RED_COIN"), geolayout=0x160002C4), + "Red Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0xD8, "MODEL_RED_COIN_NO_SHADOW"), geolayout=0x16000328 + ), + }, + ), + "Cyan Fish": ActorPresetInfo( + decomp_path="actors/cyan_fish", + group="group13", + animation=AnimInfo(address=0x600E264, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_CYAN_FISH"), geolayout=0xD000324), + ), + "Dirt": ActorPresetInfo( + decomp_path="actors/dirt", + group="common1", + models={ + "Dirt": ModelInfo(model_id=ModelIDInfo(0x8A, "MODEL_DIRT_ANIMATION"), geolayout=0x16000ED4), + "(Unused) Cartoon Start": ModelInfo(model_id=ModelIDInfo(0x8B, "MODEL_CARTOON_STAR"), geolayout=0x16000F24), + }, + ), + "Door": ActorPresetInfo( + decomp_path="actors/door", + group="common1", + animation=AnimInfo( + address=0x30156C0, + behaviours=0x13000B0C, + names=[ + "Closed", + "Open and Close", + "Open and Close (Slower?)", + "Open and Close (Slower? Last 10 frames)", + "Open and Close (Last 10 frames)", + ], + ignore_bone_count=True, + ), + models={ + "Castle Door": ModelInfo( + model_id=[ + ModelIDInfo(0x26, "MODEL_CASTLE_GROUNDS_CASTLE_DOOR"), + ModelIDInfo(0x26, "MODEL_CASTLE_CASTLE_DOOR"), + ModelIDInfo(0x1C, "MODEL_CASTLE_CASTLE_DOOR_UNUSED"), + ], + geolayout=0x160003A8, + ), + "Cabin Door": ModelInfo(model_id=ModelIDInfo(0x27, "MODEL_CCM_CABIN_DOOR"), geolayout=0x1600043C), + "Wooden Door": ModelInfo( + model_id=[ + ModelIDInfo(0x1D, "MODEL_CASTLE_WOODEN_DOOR_UNUSED"), + ModelIDInfo(0x1D, "MODEL_HMC_WOODEN_DOOR"), + ModelIDInfo(0x27, "MODEL_CASTLE_WOODEN_DOOR"), + ModelIDInfo(0x27, "MODEL_COURTYARD_WOODEN_DOOR"), + ], + geolayout=0x160004D0, + ), + "Wooden Door 2": ModelInfo(geolayout=0x16000564), + "Metal Door": ModelInfo( + model_id=[ + ModelIDInfo(0x1F, "MODEL_HMC_METAL_DOOR"), + ModelIDInfo(0x29, "MODEL_CASTLE_METAL_DOOR"), + ModelIDInfo(0x29, "MODEL_CASTLE_GROUNDS_METAL_DOOR"), + ], + geolayout=0x16000618, + ), + "Hazy Maze Door": ModelInfo(model_id=ModelIDInfo(0x20, "MODEL_HMC_HAZY_MAZE_DOOR"), geolayout=0x1600068C), + "Haunted Door": ModelInfo(model_id=ModelIDInfo(0x1D, "MODEL_BBH_HAUNTED_DOOR"), geolayout=0x16000720), + "Castle Door (0 Star)": ModelInfo( + model_id=ModelIDInfo(0x22, "MODEL_CASTLE_DOOR_0_STARS"), geolayout=0x160007B4 + ), + "Castle Door (1 Star)": ModelInfo( + model_id=ModelIDInfo(0x23, "MODEL_CASTLE_DOOR_1_STAR"), geolayout=0x16000868 + ), + "Castle Door (3 Star)": ModelInfo( + model_id=ModelIDInfo(0x24, "MODEL_CASTLE_DOOR_3_STARS"), geolayout=0x1600091C + ), + "Key Door": ModelInfo(model_id=ModelIDInfo(0x25, "MODEL_CASTLE_KEY_DOOR"), geolayout=0x160009D0), + }, + ), + "Dorrie": ActorPresetInfo( + decomp_path="actors/dorrie", + group="group17", + animation=AnimInfo( + address=0x600F638, behaviours=0x13004F90, size=3, names=["Idle", "Moving", "Lower and Raise Head"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_DORRIE"), geolayout=0xD000230), + ), + "Exclamation Box": ActorPresetInfo( + decomp_path="actors/exclamation_box", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x89, "MODEL_EXCLAMATION_BOX"), geolayout=0xF000694), + ), + "Exclamation Box Outline": ActorPresetInfo( + decomp_path="actors/exclamation_box_outline", + group="common0", + models={ + "Exclamation Box Outline": ModelInfo( + model_id=ModelIDInfo(0x83, "MODEL_EXCLAMATION_BOX_OUTLINE"), geolayout=0xF000A5A + ), + "Exclamation Point": ModelInfo( + model_id=ModelIDInfo(0x84, "MODEL_EXCLAMATION_POINT"), + displaylist=DisplaylistInfo(0x8025F08, "exclamation_box_outline_seg8_dl_08025F08"), + ), + }, + collision=CollisionInfo(address=0x8025F78, c_name="exclamation_box_outline_seg8_collision_08025F78"), + ), + "Explosion": ActorPresetInfo( + decomp_path="actors/explosion", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xCD, "MODEL_EXPLOSION"), geolayout=0x16000040), + ), + "Eyerok": ActorPresetInfo( + decomp_path="actors/eyerok", + group="group5", + animation=AnimInfo( + address=0x50116E4, + behaviours=0x130052B4, + names=["Recovering", "Death", "Idle", "Attacked", "Open", "Show Eye", "Sleep", "Close"], + ), + models={ + "Eyerok Left Hand": ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_EYEROK_LEFT_HAND"), geolayout=0xC0005A8), + "Eyerok Right Hand": ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_EYEROK_RIGHT_HAND"), geolayout=0xC0005E4), + }, + ), + "Flame": ActorPresetInfo( + decomp_path="actors/flame", + group="common1", + models={ + "Red Flame (With Shadow)": ModelInfo( + model_id=ModelIDInfo(0xCB, "MODEL_RED_FLAME_SHADOW"), geolayout=0x16000B10 + ), + "Red Flame": ModelInfo(model_id=ModelIDInfo(0x90, "MODEL_RED_FLAME"), geolayout=0x16000B2C), + "Blue Flame": ModelInfo(model_id=ModelIDInfo(0x91, "MODEL_BLUE_FLAME"), geolayout=0x16000B8C), + }, + ), + "Fly Guy": ActorPresetInfo( + decomp_path="actors/flyguy", + group="common0", + animation=AnimInfo(address=0x8011A64, behaviours=0x130046DC, names=["Flying"]), + models=ModelInfo(model_id=ModelIDInfo(0xDC, "MODEL_FLYGUY"), geolayout=0xF000518), + ), + "Fwoosh": ActorPresetInfo( + decomp_path="actors/fwoosh", + group="group6", + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_FWOOSH"), geolayout=0xC00036C), + ), + "Goomba": ActorPresetInfo( + decomp_path="actors/goomba", + group="common0", + animation=AnimInfo(address=0x801DA4C, behaviours=0x1300472C, names=["Walking"]), + models=ModelInfo(model_id=ModelIDInfo(0xC0, "MODEL_GOOMBA"), geolayout=0xF0006E4), + ), + "Haunted Cage": ActorPresetInfo( + decomp_path="actors/haunted_cage", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x5A, "MODEL_HAUNTED_CAGE"), geolayout=0xC000274), + ), + "Heart": ActorPresetInfo( + decomp_path="actors/heart", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x78, "MODEL_HEART"), geolayout=0xF0004FC), + ), + "Heave-Ho": ActorPresetInfo( + decomp_path="actors/heave_ho", + group="group1", + animation=AnimInfo(address=0x501534C, behaviours=0x13001548, names=["Moving", "Throwing", "Stop"]), + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_HEAVE_HO"), geolayout=0xC00028C), + ), + "Hoot": ActorPresetInfo( + decomp_path="actors/hoot", + group="group1", + animation=AnimInfo(address=0x5005768, behaviours=0x130033EC, names=["Flying", "Flying Fast"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_HOOT"), geolayout=0xC000018), + ), + "Bowser Impact Ring": ActorPresetInfo( + decomp_path="actors/impact_ring", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_BOWSER_WAVE"), geolayout=0xD000090), + ), + "Bowser Impact Smoke": ActorPresetInfo( + decomp_path="actors/impact_smoke", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_BOWSER_SMOKE"), geolayout=0xD000BFC), + ), + "King Bobomb": ActorPresetInfo( + decomp_path="actors/bobomb", + group="group3", + animation=AnimInfo( + address=0x500FE30, + behaviours=0x130001F4, + size=12, + names=[ + "Grab Mario", + "Holding Mario", + "Hit Ground", + "Unkwnown (Unused)", + "Stomp", + "Idle", + "Being Held", + "Landing", + "Jump", + "Throw Mario", + "Stand Up", + "Walking", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_KING_BOBOMB"), geolayout=0xC000000), + ), + "Klepto": ActorPresetInfo( + decomp_path="actors/klepto", + group="group5", + animation=AnimInfo( + address=0x5008CFC, + behaviours=0x13005310, + names=[ + "Dive", + "Struck By Mario", + "Dive at Mario", + "Dive at Mario 2", + "Dive at Mario 3", + "Dive at Mario 4", + "Dive Flap", + "Dive Flap 2", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_KLEPTO"), geolayout=0xC000000), + ), + "Koopa": ActorPresetInfo( + decomp_path="actors/koopa", + group="group14", + animation=AnimInfo( + address=0x6011364, + behaviours=0x13004580, + names=[ + "Falling Over (Unused Shelled Act 3)", + "Run Away", + "Laying (Unshelled)", + "Running", + "Run (Unused)", + "Laying (Shelled)", + "Stand Up", + "Stopped", + "Wake Up (Unused)", + "Walk", + "Walk Stop", + "Walk Start", + "Jump", + "Land", + ], + ), + models={ + "Koopa (Without Shell)": ModelInfo( + model_id=ModelIDInfo(0xBF, "MODEL_KOOPA_WITHOUT_SHELL"), geolayout=0xD0000D0 + ), + "Koopa (With Shell)": ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_KOOPA_WITH_SHELL"), geolayout=0xD000214), + }, + ), + "Koopa Flag": ActorPresetInfo( + decomp_path="actors/koopa_flag", + group="group14", + animation=AnimInfo(address=0x6001028, behaviours=0x130045F8, names=["Waving"]), + models=ModelInfo(model_id=ModelIDInfo(0x6A, "MODEL_KOOPA_FLAG"), geolayout=0xD000000), + ), + "Koopa Shell": ActorPresetInfo( + decomp_path="actors/koopa_shell", + group="common0", + models={ + "Koopa Shell": ModelInfo(model_id=ModelIDInfo(0xBE, "MODEL_KOOPA_SHELL"), geolayout=0xF000AB0), + "(Unused) Koopa Shell 1": ModelInfo(geolayout=0xF000ADC), + "(Unused) Koopa Shell 2": ModelInfo(geolayout=0xF000B08), + }, + ), + "Lakitu (Cameraman)": ActorPresetInfo( + decomp_path="actors/lakitu_cameraman", + group="group15", + animation=AnimInfo( + address=0x60058F8, + behaviours={"Lakitu (Beginning)": 0x13005610, "Lakitu (Cameraman)": 0x13004954}, + names=["Flying"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_LAKITU"), geolayout=0xD000000), + ), + "Lakitu (Enemy)": ActorPresetInfo( + decomp_path="actors/lakitu_enemy", + group="group11", + animation=AnimInfo( + address=0x50144D4, behaviours=0x13004918, names=["Flying", "No Spiny", "Throw Spiny", "Hold Spiny"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_ENEMY_LAKITU"), geolayout=0xC0001BC), + ), + "Leaves": ActorPresetInfo( + decomp_path="actors/leaves", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xA2, "MODEL_LEAVES"), geolayout=0x16000C8C), + ), + "Mad Piano": ActorPresetInfo( + decomp_path="actors/mad_piano", + group="group9", + animation=AnimInfo(address=0x5009B14, behaviours=0x13005024, names=["Sleeping", "Chomping"]), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_MAD_PIANO"), geolayout=0xC0001B4), + ), + "Manta Ray": ActorPresetInfo( + decomp_path="actors/manta", + group="group4", + animation=AnimInfo(address=0x5008EB4, behaviours=0x13004370, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_MANTA_RAY"), geolayout=0x5008D14), + ), + "Mario": ActorPresetInfo( + decomp_path="actors/mario", + group="group0", + animation=AnimInfo( + address=0x4EC000, + dma=True, + directory="assets/anims", + names=[ + "Slow ledge climb up", + "Fall over backwards", + "Backward air kb", + "Dying on back", + "Backflip", + "Climbing up pole", + "Grab pole short", + "Grab pole swing part 1", + "Grab pole swing part 2", + "Handstand idle", + "Handstand jump", + "Start handstand", + "Return from handstand", + "Idle on pole", + "A pose", + "Skid on ground", + "Stop skid", + "Crouch from fast longjump", + "Crouch from a slow longjump", + "Fast longjump", + "Slow longjump", + "Airborne on stomach", + "Walk with light object", + "Run with light object", + "Slow walk with light object", + "Shivering and warming hands", + "Shivering return to idle ", + "Shivering", + "Climb down on ledge", + "Waving (Credits)", + "Look up (Credits)", + "Return from look up (Credits)", + "Raising hand (Credits)", + "Lowering hand (Credits)", + "Taking off cap (Credits)", + "Start walking and look up (Credits)", + "Look back then run (Credits)", + "Final Bowser - Raise hand and spin", + "Final Bowser - Wing cap take off", + "Peach sign (Credits)", + "Stand up from lava boost", + "Fire/Lava burn", + "Wing cap flying", + "Hang on owl", + "Land on stomach", + "Air forward kb", + "Dying on stomach", + "Suffocating", + "Coughing", + "Throw catch key", + "Dying fall over", + "Idle on ledge", + "Fast ledge grab", + "Hang on ceiling", + "Put cap on", + "Take cap off then on", + "Quickly put cap on", + "Head stuck in ground", + "Ground pound landing", + "Triple jump ground-pound", + "Start ground-pound", + "Ground-pound", + "Bottom stuck in ground", + "Idle with light object", + "Jump land with light object", + "Jump with light object", + "Fall land with light object", + "Fall with light object", + "Fall from sliding with light object", + "Sliding on bottom with light object", + "Stand up from sliding with light object", + "Riding shell", + "Walking", + "Forward flip", + "Jump riding shell", + "Land from double jump", + "Double jump fall", + "Single jump", + "Land from single jump", + "Air kick", + "Double jump rise", + "Start forward spinning", + "Throw light object", + "Fall from slide kick", + "Bend kness riding shell", + "Legs stuck in ground", + "General fall", + "General land", + "Being grabbed", + "Grab heavy object", + "Slow land from dive", + "Fly from cannon", + "Moving right while hanging", + "Moving left while hanging", + "Missing cap", + "Pull door walk in", + "Push door walk in", + "Unlock door", + "Start reach pocket", + "Reach pocket", + "Stop reach pocket", + "Ground throw", + "Ground kick", + "First punch", + "Second punch", + "First punch fast", + "Second punch fast", + "Pick up light object", + "Pushing", + "Start riding shell", + "Place light object", + "Forward spinning", + "Backward spinning", + "Breakdance", + "Running", + "Running (unused)", + "Soft back kb", + "Soft front kb", + "Dying in quicksand", + "Idle in quicksand", + "Move in quicksand", + "Electrocution", + "Shocked", + "Backward kb", + "Forward kb", + "Idle heavy object", + "Stand against wall", + "Side step left", + "Side step right", + "Start sleep idle", + "Start sleep scratch", + "Start sleep yawn", + "Start sleep sitting", + "Sleep idle", + "Sleep start laying", + "Sleep laying", + "Dive", + "Slide dive", + "Ground bonk", + "Stop slide light object", + "Slide kick", + "Crouch from slide kick", + "Slide motionless", + "Stop slide", + "Fall from slide", + "Slide", + "Tiptoe", + "Twirl land", + "Twirl", + "Start twirl", + "Stop crouching", + "Start crouching", + "Crouching", + "Crawling", + "Stop crawling", + "Start crawling", + "Summon star", + "Return star approach door", + "Backwards water kb", + "Swim with object part 1", + "Swim with object part 2", + "Flutter kick with object", + "Action end with object in water", + "Stop holding object in water", + "Holding object in water", + "Drowning part 1", + "Drowning part 2", + "Dying in water", + "Forward kb in water", + "Falling from water", + "Swimming part 1", + "Swimming part 2", + "Flutter kick", + "Action end in water", + "Pick up object in water", + "Grab object in water part 2", + "Grab object in water part 1", + "Throw object in water", + "Idle in water", + "Star dance in water", + "Return from in water star dance", + "Grab bowser", + "Swing bowser", + "Release bowser", + "Holding bowser", + "Heavy throw", + "Walk panting", + "Walk with heavy object", + "Turning part 1", + "Turning part 2", + "Side flip land", + "Side flip", + "Triple jump land", + "Triple jump", + "First person", + "Idle head left", + "Idle head right", + "Idle head center", + "Handstand left", + "Handstand right", + "Wake up from sleeping", + "Wake up from laying", + "Start tiptoeing", + "Slide jump", + "Start wallkick", + "Star dance", + "Return from star dance", + "Forwards spinning flip", + "Triple jump fly", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x1, "MODEL_MARIO"), geolayout=0x17002DD4), + ), + "Mario's Cap": ActorPresetInfo( + decomp_path="actors/mario_cap", + group="common1", + models={ + "Mario's Cap": ModelInfo(model_id=ModelIDInfo(0x88, "MODEL_MARIOS_CAP"), geolayout=0x16000CA4), + "Mario's Metal Cap": ModelInfo(model_id=ModelIDInfo(0x86, "MODEL_MARIOS_METAL_CAP"), geolayout=0x16000CF0), + "Mario's Wing Cap": ModelInfo(model_id=ModelIDInfo(0x87, "MODEL_MARIOS_WING_CAP"), geolayout=0x16000D3C), + "Mario's Winged Metal Cap": ModelInfo( + model_id=ModelIDInfo(0x85, "MODEL_MARIOS_WINGED_METAL_CAP"), geolayout=0x16000DA8 + ), + }, + ), + "Metal Box": ActorPresetInfo( + decomp_path="actors/metal_box", + group="common0", + models={ + "Metal Box": ModelInfo(model_id=ModelIDInfo(0xD9, "MODEL_METAL_BOX"), geolayout=0xF000A30), + "Metal Box (DL)": ModelInfo( + model_id=ModelIDInfo(0xDA, "MODEL_METAL_BOX_DL"), displaylist=DisplaylistInfo(0x8024BB8, "metal_box_dl") + ), + }, + collision=CollisionInfo(address=0x8024C28, c_name="metal_box_seg8_collision_08024C28"), + ), + "Mips": ActorPresetInfo( + decomp_path="actors/mips", + group="group15", + animation=AnimInfo( + address=0x6015724, behaviours=0x130044FC, names=["Idle", "Hopping", "Thrown", "Thrown (Unused)", "Held"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_MIPS"), geolayout=0xD000448), + ), + "Mist": ActorPresetInfo( + decomp_path="actors/mist", + group="common1", + models={ + "Mist": ModelInfo(model_id=ModelIDInfo(0x8E, "MODEL_MIST"), geolayout=0x16000000), + "White Puff": ModelInfo(model_id=ModelIDInfo(0xE0, "MODEL_WHITE_PUFF"), geolayout=0x16000020), + }, + ), + "Moneybag": ActorPresetInfo( + decomp_path="actors/moneybag", + group="group16", + animation=AnimInfo(address=0x6005E5C, behaviours=0x130039A0, names=["Idle", "Prepare", "Jump", "Land", "Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_MONEYBAG"), geolayout=0xD0000F0), + ), + "Monty Mole": ActorPresetInfo( + decomp_path="actors/monty_mole", + group="group6", + animation=AnimInfo( + address=0x5007248, + behaviours=0x13004A00, + names=[ + "Jump Into Hole", + "Rise", + "Get Rock", + "Begin Jump Into Hole", + "Jump Out Of Hole Down", + "Unused 5", # TODO: Figure out + "Unused 6", + "Unused 7", + "Throw Rock", + "Jump Out Of Hole Up", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_MONTY_MOLE"), geolayout=0xC000000), + ), + "Montey Mole Hole": ActorPresetInfo( + decomp_path="actors/monty_mole_hole", + group="group6", + models=ModelInfo( + model_id=ModelIDInfo(0x54, "MODEL_DL_MONTY_MOLE_HOLE"), + displaylist=DisplaylistInfo(0x5000840, "monty_mole_hole_seg5_dl_05000840"), + ), + ), + "Mr. I Eyeball": ActorPresetInfo( + decomp_path="actors/mr_i_eyeball", + group="group16", + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_MR_I"), geolayout=0xD000000), + ), + "Mr. I Iris": ActorPresetInfo( + decomp_path="actors/mr_i_iris", + group="group16", + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_MR_I_IRIS"), geolayout=0xD00001C), + ), + "Mushroom 1up": ActorPresetInfo( + decomp_path="actors/mushroom_1up", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xD4, "MODEL_1UP"), geolayout=0x16000E84), + ), + "Orange Numbers": ActorPresetInfo( + decomp_path="actors/number", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xDB, "MODEL_NUMBER"), geolayout=0x16000E14), + ), + "Peach": ActorPresetInfo( + decomp_path="actors/peach", + group="group10", + animation=AnimInfo( + address=0x501C50C, + behaviours={"Peach (Beginning)": 0x13005638, "Peach (End)": 0x13000EAC}, + names=[ + "Walking away", + "Walking away 2", + "Descend", + "Descend And Look Down", + "Look Up And Open Eyes", + "Mario", + "Power Of The Stars", + "Thanks To You", + "Kiss", + "Waving", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDE, "MODEL_PEACH"), geolayout=0xC000410), + ), + "Pebble": ActorPresetInfo( + decomp_path="actors/pebble", + group="common1", + models=ModelInfo( + model_id=ModelIDInfo(0xA1, "MODEL_PEBBLE"), + displaylist=DisplaylistInfo(0x301CB00, "pebble_seg3_dl_0301CB00"), + ), + ), + "Penguin": ActorPresetInfo( + decomp_path="actors/penguin", + group="group7", + animation=AnimInfo( + address=0x5008B74, + behaviours={ + "Penguin (Tuxies Mother)": 0x13002088, + "Penguin (Small)": 0x130020E8, + "Penguin (SML)": 0x13002E58, + "Racing Penguin": 0x13005380, + }, + size=5, + names=["Walk", "Dive Slide", "Stand Up", "Idle", "Walk"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_PENGUIN"), geolayout=0xC000104), + collision=CollisionInfo(address=0x5008B88, c_name="penguin_seg5_collision_05008B88"), + ), + "Piranha Plant": ActorPresetInfo( + decomp_path="actors/piranha_plant", + group="group14", + animation=AnimInfo( + address=0x601C31C, + behaviours={"Fire Piranha Plant": 0x13005120, "Piranha Plant": 0x13001FBC}, + names=[ + "Bite", + "Sleeping? (Unused)", + "Falling over", + "Bite (Unused)", + "Grow", + "Attacked", + "Stop Bitting", + "Sleeping (Unused)", + "Sleeping", + "Bite (Duplicate)", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_PIRANHA_PLANT"), geolayout=0xD000358), + ), + "Pokey": ActorPresetInfo( + decomp_path="actors/pokey", + group="group5", + models={ + "Pokey Head": ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_POKEY_HEAD"), geolayout=0xC000610), + "Pokey Body Part": ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_POKEY_BODY_PART"), geolayout=0xC000644), + }, + ), + "Wooden Post": ActorPresetInfo( + decomp_path="actors/poundable_pole", + group="group14", + models=ModelInfo(model_id=ModelIDInfo(0x6B, "MODEL_WOODEN_POST"), geolayout=0xD0000B8), + collision=CollisionInfo(address=0x6002490, c_name="poundable_pole_collision_06002490"), + ), + # Should the power meter be included? + "Power Meter": ActorPresetInfo( + decomp_path="actors/power_meter", + group="common1", + models={ + "Power Meter (Base)": ModelInfo(displaylist=DisplaylistInfo(0x3029480, "dl_power_meter_base")), + "Power Meter (Health)": ModelInfo( + displaylist=DisplaylistInfo(0x3029570, "dl_power_meter_health_segments_begin") + ), + }, + ), + "Purple Switch": ActorPresetInfo( + decomp_path="actors/purple_switch", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0xCF, "MODEL_PURPLE_SWITCH"), geolayout=0xF0004CC), + collision=CollisionInfo(address=0x800C7A8, c_name="purple_switch_seg8_collision_0800C7A8"), + ), + "Sand": ActorPresetInfo( + decomp_path="actors/sand", + group="common1", + models=ModelInfo( + model_id=ModelIDInfo(0x9F, "MODEL_SAND_DUST"), + displaylist=DisplaylistInfo(0x302BCD0, "sand_seg3_dl_0302BCD0"), + ), + ), + "Scuttlebug": ActorPresetInfo( + decomp_path="actors/scuttlebug", + group="group17", + animation=AnimInfo(address=0x6015064, behaviours=0x13002B5C, names=["Walking"]), + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_SCUTTLEBUG"), geolayout=0xD000394), + ), + "Seaweed": ActorPresetInfo( + decomp_path="actors/seaweed", + group="group13", + animation=AnimInfo(address=0x0600A4D4, behaviours=0x13003134, size=1, names=["Wave"]), + models=ModelInfo(model_id=ModelIDInfo(0xC1, "MODEL_SEAWEED"), geolayout=0xD000284), + ), + "Skeeter": ActorPresetInfo( + decomp_path="actors/skeeter", + group="group13", + animation=AnimInfo( + address=0x6007DE0, behaviours=0x13005468, size=4, names=["Water Lunge", "Water Idle", "Walk", "Idle"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x69, "MODEL_SKEETER"), geolayout=0xD000000), + ), + "(Beta) Boo Key": ActorPresetInfo( + decomp_path="actors/small_key", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_BETA_BOO_KEY"), geolayout=0xC000188), + ), + "(Unused) Smoke": ActorPresetInfo( # TODO: double check + decomp_path="actors/smoke", + group="group6", + models=ModelInfo(displaylist=DisplaylistInfo(0x5007AF8, "smoke_seg5_dl_05007AF8")), + ), + "Mr. Blizzard": ActorPresetInfo( + decomp_path="actors/snowman", + group="group7", + animation=AnimInfo( + address=0x500D118, behaviours={"Mr. Blizzard": 0x13004DBC}, names=["Spawn Snowball", "Throw Snowball"] + ), + models={ + "Mr. Blizzard": ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_MR_BLIZZARD"), geolayout=0xC000348), + "Mr. Blizzard (Hidden)": ModelInfo( + model_id=ModelIDInfo(0x55, "MODEL_MR_BLIZZARD_HIDDEN"), geolayout=0xC00021C + ), + }, + ), + "Snufit": ActorPresetInfo( + decomp_path="actors/snufit", + group="group17", + models=ModelInfo(model_id=ModelIDInfo(0xCE, "MODEL_SNUFIT"), geolayout=0xD0001A0), + ), + "Sparkle": ActorPresetInfo( + decomp_path="actors/sparkle", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x95, "MODEL_SPARKLES"), geolayout=0x170001BC), + ), + "Sparkle Animation": ActorPresetInfo( + decomp_path="actors/sparkle_animation", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x8F, "MODEL_SPARKLES_ANIMATION"), geolayout=0x17000284), + ), + "Spindrift": ActorPresetInfo( + decomp_path="actors/spindrift", + group="group7", + animation=AnimInfo(address=0x5002D68, behaviours=0x130012B4, names=["Flying"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_SPINDRIFT"), geolayout=0xC000000), + ), + "Spiny": ActorPresetInfo( + decomp_path="actors/spiny", + group="group11", + animation=AnimInfo(address=0x5016EAC, behaviours={"Spiny": 0x130049C8}, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_SPINY"), geolayout=0xC000328), + ), + "Spiny Egg": ActorPresetInfo( + decomp_path="actors/spiny_egg", + group="group11", + animation=AnimInfo(address=0x50157E4, names=["Default"]), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_SPINY_BALL"), geolayout=0xC000290), + ), + "Springboard": ActorPresetInfo( + decomp_path="actors/springboard", + group="group8", + models={ + "Springboard Top": ModelInfo(model_id=ModelIDInfo(0xB5, "MODEL_TRAMPOLINE"), geolayout=0xC000000), + "Springboard Middle": ModelInfo(model_id=ModelIDInfo(0xB6, "MODEL_TRAMPOLINE_CENTER"), geolayout=0xC000018), + "Springboard Bottom": ModelInfo(model_id=ModelIDInfo(0xB7, "MODEL_TRAMPOLINE_BASE"), geolayout=0xC000030), + }, + ), + "Star": ActorPresetInfo( + decomp_path="actors/star", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x7A, "MODEL_STAR"), geolayout=0x16000EA0), + ), + "Small Water Splash": ActorPresetInfo( + decomp_path="actors/stomp_smoke", + group="group0", + models={ + "Small Water Splash": ModelInfo( + model_id=ModelIDInfo(0xA5, "MODEL_SMALL_WATER_SPLASH"), geolayout=0x1700009C + ), + "(Unused) Small Water Splash": ModelInfo(geolayout=0x170000E0), + }, + ), + "Sushi Shark": ActorPresetInfo( + decomp_path="actors/sushi", + group="group4", + animation=AnimInfo(address=0x500AE54, behaviours=0x13002338, size=1, names=["Swimming", "Diving"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_SUSHI"), geolayout=0xC000068), + ), + "Swoop": ActorPresetInfo( + decomp_path="actors/swoop", + group="group17", + animation=AnimInfo(address=0x60070D0, behaviours=0x13004698, size=2, names=["Idle", "Move"]), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_SWOOP"), geolayout=0xD0000DC), + ), + "Test Plataform": ActorPresetInfo( + decomp_path="actors/test_plataform", + group="common0", + collision=CollisionInfo(address=0x80262F8, c_name="unknown_seg8_collision_080262F8"), + ), + "Thwomp": ActorPresetInfo( + decomp_path="actors/thwomp", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_THWOMP"), geolayout=0xC000248), + collision={ + "Thwomp": CollisionInfo(address=0x500B7D0, c_name="thwomp_seg5_collision_0500B7D0"), + "Thwomp 2": CollisionInfo(address=0x500B92C, c_name="thwomp_seg5_collision_0500B92C"), + }, + ), + "Toad": ActorPresetInfo( + decomp_path="actors/toad", + group="group15", + animation=AnimInfo( + address=0x600FC48, + behaviours={"End Toad": 0x13000E88, "Toad Message": 0x13002EF8}, + size=8, + names=[ + "Wave Then Run (West)", + "Walking (West)", + "Node Then Turn (East)", + "Walking (East)", + "Standing (West)", + "Standing (East)", + "Waving Both Arms (West)", + "Waving One Arm (East)", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDD, "MODEL_TOAD"), geolayout=0xD0003E4), + ), + "Tweester/Tornado": ActorPresetInfo( + decomp_path="actors/tornado", + group="group5", + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_TWEESTER"), geolayout=0x5014630), + ), + "Transparent Star": ActorPresetInfo( + decomp_path="actors/transperant_star", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x79, "MODEL_TRANSPARENT_STAR"), geolayout=0x16000F6C), + ), + "Treasure Chest": ActorPresetInfo( + decomp_path="actors/treasure_chest", + group="group13", + models={ + "Treasure Chest Base": ModelInfo( + model_id=ModelIDInfo(0x65, "MODEL_TREASURE_CHEST_BASE"), geolayout=0xD000450 + ), + "Treasure Chest Lid": ModelInfo( + model_id=ModelIDInfo(0x66, "MODEL_TREASURE_CHEST_LID"), geolayout=0xD000468 + ), + }, + ), + "Tree": ActorPresetInfo( + decomp_path="actors/tree", + group="common1", + models={ + "Bubbly Tree": ModelInfo( + model_id=[ + ModelIDInfo(0x17, "MODEL_BOB_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_WDW_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_CASTLE_GROUNDS_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_WF_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_THI_BUBBLY_TREE"), + ], + geolayout=0x16000FE8, + ), + "Pine Tree": ModelInfo(model_id=ModelIDInfo(0x18, "MODEL_COURTYARD_SPIKY_TREE"), geolayout=0x16001000), + "(Unused) Pine Tree": ModelInfo(geolayout=0x16001030), + "Snow Tree": ModelInfo( + model_id=[ModelIDInfo(0x19, "MODEL_CCM_SNOW_TREE"), ModelIDInfo(0x19, "MODEL_SL_SNOW_TREE")], + geolayout=0x16001018, + ), + "Palm Tree": ModelInfo(model_id=ModelIDInfo(0x1B, "MODEL_SSL_PALM_TREE"), geolayout=0x16001048), + }, + ), + "Ukiki": ActorPresetInfo( + decomp_path="actors/ukiki", + group="group6", + animation=AnimInfo( + address=0x5015784, + behaviours={"Ukiki": 0x13001CB0}, + names=[ + "Run", + "Walk (Unused)", + "Apose (Unused)", + "Death (Unused)", + "Screech", + "Jump Clap", + "Hop (Unused)", + "Land", + "Jump", + "Itch", + "Handstand", + "Turn", + "Held", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_UKIKI"), geolayout=0xC000110), + ), + "Unagi": ActorPresetInfo( + decomp_path="actors/unagi", + group="group4", + animation=AnimInfo( + address=0x5012824, + behaviours=0x13004F40, + size=7, + names=["Yawn", "Bite", "Swimming", "Static Straight", "Idle", "Open Mouth", "Idle 2"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_UNAGI"), geolayout=0xC00010C), + ), + "Smoke": ActorPresetInfo( + decomp_path="actors/walk_smoke", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x96, "MODEL_SMOKE"), geolayout=0x17000038), + ), + "Warp Collision": ActorPresetInfo( + decomp_path="actors/warp_collision", + group="common1", + collision={ + "Door": CollisionInfo(address=0x301CE78, c_name="door_seg3_collision_0301CE78"), + "LLL Hexagonal Mesh": CollisionInfo(address=0x301CECC, c_name="lll_hexagonal_mesh_seg3_collision_0301CECC"), + }, + ), + "Warp Pipe": ActorPresetInfo( + decomp_path="actors/warp_pipe", + group="common1", + models=ModelInfo( + model_id=[ + ModelIDInfo(0x49, "MODEL_BITS_WARP_PIPE"), + ModelIDInfo(0x12, "MODEL_BITDW_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_THI_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_VCUTM_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_CASTLE_GROUNDS_WARP_PIPE"), + ], + geolayout=0x16000388, + ), + ), + "Water Bomb": ActorPresetInfo( + decomp_path="actors/water_bubble", + group="group3", + models={ + "Water Bomb": ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_WATER_BOMB"), geolayout=0xC000308), + "Water Bomb's Shadow": ModelInfo( + model_id=ModelIDInfo(0x55, "MODEL_WATER_BOMB_SHADOW"), geolayout=0xC000328 + ), + }, + ), + "Water Mine": ActorPresetInfo( + decomp_path="actors/water_mine", + group="group13", + models=ModelInfo(model_id=ModelIDInfo(0xB3, "MODEL_WATER_MINE"), geolayout=0xD0002F4), + ), + "Water Ring": ActorPresetInfo( + decomp_path="actors/water_ring", + group="group13", + animation=AnimInfo( + address=0x6013F7C, + behaviours={"Water Ring (Jet Stream)": 0x13003750, "Water Ring (Manta Ray)": 0xC66C16}, + names=["Wobble"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_WATER_RING"), geolayout=0xD000414), + ), + "Water Splash": ActorPresetInfo( + decomp_path="actors/water_splash", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0xA7, "MODEL_WATER_SPLASH"), geolayout=0x17000230), + ), + "Water Wave": ActorPresetInfo( + decomp_path="actors/water_wave", + group="group0", + models={ + "Idle Water Wave": ModelInfo(model_id=ModelIDInfo(0xA6, "MODEL_IDLE_WATER_WAVE"), geolayout=0x17000124), + "Water Wave Trail": ModelInfo(model_id=ModelIDInfo(0xA3, "MODEL_WAVE_TRAIL"), geolayout=0x17000168), + }, + ), + "Whirlpool": ActorPresetInfo( + decomp_path="actors/whirlpool", + group="group4", + models=ModelInfo( + model_id=ModelIDInfo(0x57, "MODEL_DL_WHIRLPOOL"), + displaylist=DisplaylistInfo(0x5013CB8, "whirlpool_seg5_dl_05013CB8"), + ), + ), + "White Particle": ActorPresetInfo( + decomp_path="actors/white_particle", + group="common1", + models={ + "White Particle": ModelInfo(model_id=ModelIDInfo(0xA0, "MODEL_WHITE_PARTICLE"), geolayout=0x16000F98), + "White Particle (DL)": ModelInfo( + model_id=ModelIDInfo(0x9E, "MODEL_WHITE_PARTICLE_DL"), + displaylist=DisplaylistInfo(0x302C8A0, "white_particle_dl"), + ), + }, + ), + "White Particle Small": ActorPresetInfo( + decomp_path="actors/white_particle_small", + group="group0", + models={ + "White Particle Small": ModelInfo( + model_id=ModelIDInfo(0xA4, "MODEL_WHITE_PARTICLE_SMALL"), + displaylist=DisplaylistInfo(0x4032A18, "white_particle_small_dl"), + ), + "(Unused) White Particle Small": ModelInfo( + displaylist=DisplaylistInfo(0x4032A30, "white_particle_small_unused_dl") + ), + }, + ), + "Whomp": ActorPresetInfo( + decomp_path="actors/whomp", + group="group14", + animation=AnimInfo(address=0x6020A04, behaviours=0x13002BCC, size=2, names=["Walk", "Jump"]), + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_WHOMP"), geolayout=0xD000480), + collision=CollisionInfo(address=0x6020A0C, c_name="whomp_seg6_collision_06020A0C"), + ), + "Wiggler Body": ActorPresetInfo( + decomp_path="actors/wiggler_body", + group="group11", + animation=AnimInfo(address=0x500C874, behaviours=0x130048E0, size=1, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_WIGGLER_BODY"), geolayout=0x500C778), + ), + "Wiggler Head": ActorPresetInfo( + decomp_path="actors/wiggler_head", + group="group11", + animation=AnimInfo(address=0x500EC8C, behaviours=0x13004898, size=1, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_WIGGLER_HEAD"), geolayout=0xC000030), + ), + "Wooden Signpost": ActorPresetInfo( + decomp_path="actors/wooden_signpost", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x7C, "MODEL_WOODEN_SIGNPOST"), geolayout=0x16000FB4), + collision=CollisionInfo(address=0x302DD80, c_name="wooden_signpost_seg3_collision_0302DD80"), + ), + "Yellow Sphere (Bowser 1)": ActorPresetInfo( + decomp_path="actors/yellow_sphere", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x3, "MODEL_LEVEL_GEOMETRY_03"), geolayout=0xD0000B0), + ), + "Yellow Sphere": ActorPresetInfo( + decomp_path="actors/yellow_sphere_small", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_YELLOW_SPHERE"), geolayout=0xC000000), + ), + "Yoshi": ActorPresetInfo( + decomp_path="actors/yoshi", + group="group10", + animation=AnimInfo(address=0x50241E8, behaviours=0x13004538, names=["Idle", "Walk", "Jump"]), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_YOSHI"), geolayout=0xC000468), + ), + "(Unused) Yoshi Egg": ActorPresetInfo( + decomp_path="actors/yoshi_egg", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_YOSHI_EGG"), geolayout=0xC0001E4), + ), + "Castle Flag": ActorPresetInfo( + decomp_path="levels/castle_grounds/areas/1/11", + level="CG", + animation=AnimInfo(address=0x700C95C, behaviours=0x13003C58, size=1, names=["Wave"]), + models=ModelInfo(model_id=ModelIDInfo(0x37, "MODEL_CASTLE_GROUNDS_FLAG"), geolayout=0xE000660), + ), +} + sm64_world_defaults = { "geometryMode": { "zBuffer": True, diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index ab69b095f..e93b2231f 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -1,3 +1,4 @@ +from pathlib import Path import shutil, copy, bpy, re, os from io import BytesIO from math import ceil, log, radians @@ -14,7 +15,15 @@ update_world_default_rendermode, ) from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import export_rom_checks, starSelectWarning +from .sm64_utility import ( + END_IF_FOOTER, + ModifyFoundDescriptor, + export_rom_checks, + starSelectWarning, + update_actor_includes, + write_or_delete_if_found, + write_material_headers, +) from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 from typing import Tuple, Union, Iterable @@ -61,11 +70,9 @@ applyRotation, toAlnum, checkIfPathExists, - writeIfNotFound, overwriteData, getExportDir, writeMaterialFiles, - writeMaterialHeaders, get64bitAlignedAddr, writeInsertableFile, getPathAndLevel, @@ -196,7 +203,9 @@ def exportTexRectToC(dirPath, texProp, texDir, savePNG, name, exportToProject, p overwriteData("const\s*u8\s*", textures[0].name, data, seg2CPath, None, False) # Append texture declaration to segment2.h - writeIfNotFound(seg2HPath, declaration, "#endif") + write_or_delete_if_found( + Path(seg2HPath), ModifyFoundDescriptor(declaration), path_must_exist=True, footer=END_IF_FOOTER + ) # Write/Overwrite function to hud.c overwriteData("void\s*", fTexRect.name, code, hudPath, projectExportData[1], True) @@ -425,24 +434,15 @@ def sm64ExportF3DtoC( cDefFile.write(staticData.header) cDefFile.close() + update_actor_includes(headerType, groupName, Path(dirPath), name, levelName, ["model.inc.c"], ["header.h"]) fileStatus = None if not customExport: if headerType == "Actor": - # Write to group files - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + toAlnum(name) + '/model.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + toAlnum(name) + '/header.h"', "\n#endif") - if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders( - basePath, - '#include "actors/' + toAlnum(name) + '/material.inc.c"', - '#include "actors/' + toAlnum(name) + '/material.inc.h"', + write_material_headers( + Path(basePath), + Path("actors") / toAlnum(name) / "material.inc.c", + Path("actors") / toAlnum(name) / "material.inc.h", ) texscrollIncludeC = '#include "actors/' + name + '/texscroll.inc.c"' @@ -451,19 +451,11 @@ def sm64ExportF3DtoC( texscrollGroupInclude = '#include "actors/' + groupName + '.h"' elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + toAlnum(name) + '/model.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + toAlnum(name) + '/header.h"', "\n#endif" - ) - if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders( + write_material_headers( basePath, - '#include "levels/' + levelName + "/" + toAlnum(name) + '/material.inc.c"', - '#include "levels/' + levelName + "/" + toAlnum(name) + '/material.inc.h"', + Path("levels") / levelName / toAlnum(name) / "material.inc.c", + Path("levels") / levelName / toAlnum(name) / "material.inc.h", ) texscrollIncludeC = '#include "levels/' + levelName + "/" + name + '/texscroll.inc.c"' diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index 7a3fb7abd..3c8038612 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -1,4 +1,5 @@ from __future__ import annotations +from pathlib import Path import bpy, mathutils, math, copy, os, shutil, re from bpy.utils import register_class, unregister_class @@ -13,7 +14,7 @@ from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 -from .sm64_utility import export_rom_checks, starSelectWarning +from .sm64_utility import export_rom_checks, starSelectWarning, update_actor_includes, write_material_headers from ..utility import ( PluginError, @@ -26,10 +27,8 @@ getExportDir, toAlnum, writeMaterialFiles, - writeIfNotFound, get64bitAlignedAddr, encodeSegmentedAddr, - writeMaterialHeaders, writeInsertableFile, bytesToHex, checkSM64EmptyUsesGeoLayout, @@ -659,38 +658,12 @@ def saveGeolayoutC( geoData = geolayoutGraph.to_c() if headerType == "Actor": - matCInclude = '#include "actors/' + dirName + '/material.inc.c"' - matHInclude = '#include "actors/' + dirName + '/material.inc.h"' + matCInclude = Path("actors") / dirName / "material.inc.c" + matHInclude = Path("actors") / dirName / "material.inc.h" headerInclude = '#include "actors/' + dirName + '/geo_header.h"' - - if not customExport: - # Group name checking, before anything is exported to prevent invalid state on error. - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathGeoC = os.path.join(dirPath, groupName + "_geo.c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - if not os.path.exists(groupPathC): - raise PluginError( - groupPathC + ' not found.\n Most likely issue is that "' + groupName + '" is an invalid group name.' - ) - elif not os.path.exists(groupPathGeoC): - raise PluginError( - groupPathGeoC - + ' not found.\n Most likely issue is that "' - + groupName - + '" is an invalid group name.' - ) - elif not os.path.exists(groupPathH): - raise PluginError( - groupPathH + ' not found.\n Most likely issue is that "' + groupName + '" is an invalid group name.' - ) - else: - matCInclude = '#include "levels/' + levelName + "/" + dirName + '/material.inc.c"' - matHInclude = '#include "levels/' + levelName + "/" + dirName + '/material.inc.h"' + matCInclude = Path("levels") / levelName / dirName / "material.inc.c" + matHInclude = Path("levels") / levelName / dirName / "material.inc.h" headerInclude = '#include "levels/' + levelName + "/" + dirName + '/geo_header.h"' modifyTexScrollFiles(exportDir, geoDirPath, scrollData) @@ -736,6 +709,16 @@ def saveGeolayoutC( cDefFile.close() fileStatus = None + update_actor_includes( + headerType, + groupName, + Path(dirPath), + dirName, + levelName, + [Path("model.inc.c")], + [Path("geo_header.h")], + [Path("geo.inc.c")], + ) if not customExport: if headerType == "Actor": if dirName == "star" and bpy.context.scene.replaceStarRefs: @@ -787,31 +770,12 @@ def saveGeolayoutC( appendSecondaryGeolayout(geoDirPath, 'bully', 'bully_boss', 'GEO_SCALE(0x00, 0x2000), GEO_NODE_OPEN(),') """ - # Write to group files - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathGeoC = os.path.join(dirPath, groupName + "_geo.c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/model.inc.c"', "") - writeIfNotFound(groupPathGeoC, '\n#include "' + dirName + '/geo.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + dirName + '/geo_header.h"', "\n#endif") - texscrollIncludeC = '#include "actors/' + dirName + '/texscroll.inc.c"' texscrollIncludeH = '#include "actors/' + dirName + '/texscroll.inc.h"' texscrollGroup = groupName texscrollGroupInclude = '#include "actors/' + groupName + '.h"' elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathGeoC = os.path.join(dirPath, "geo.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/model.inc.c"', "") - writeIfNotFound(groupPathGeoC, '\n#include "levels/' + levelName + "/" + dirName + '/geo.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + dirName + '/geo_header.h"', "\n#endif" - ) - texscrollIncludeC = '#include "levels/' + levelName + "/" + dirName + '/texscroll.inc.c"' texscrollIncludeH = '#include "levels/' + levelName + "/" + dirName + '/texscroll.inc.h"' texscrollGroup = levelName @@ -828,7 +792,7 @@ def saveGeolayoutC( ) if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders(exportDir, matCInclude, matHInclude) + write_material_headers(Path(exportDir), matCInclude, matHInclude) return staticData.header, fileStatus diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index 1587b43fe..b671896fb 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -1,3 +1,4 @@ +from pathlib import Path import bpy, os, math, re, shutil, mathutils from collections import defaultdict from typing import NamedTuple @@ -11,29 +12,27 @@ from .sm64_f3d_writer import SM64Model, SM64GfxFormatter from .sm64_geolayout_writer import setRooms, convertObjectToGeolayout from .sm64_f3d_writer import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import cameraWarning, starSelectWarning +from .sm64_utility import ( + cameraWarning, + starSelectWarning, + to_include_descriptor, + write_includes, + write_or_delete_if_found, + write_material_headers, +) from ..utility import ( PluginError, - writeIfNotFound, getDataFromFile, saveDataToFile, unhideAllAndGetHiddenState, restoreHiddenState, overwriteData, selectSingleObject, - deleteIfFound, applyBasicTweaks, applyRotation, - prop_split, - toAlnum, - writeMaterialHeaders, raisePluginError, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, writeMaterialFiles, - getPathAndLevel, ) from ..f3d.f3d_gbi import ( @@ -71,9 +70,7 @@ def createGeoFile(levelName, filepath): + '#include "game/screen_transition.h"\n' + '#include "game/paintings.h"\n\n' + '#include "make_const_nonconst.h"\n\n' - + '#include "levels/' - + levelName - + '/header.h"\n\n' + + '#include "header.h"\n\n' ) geoFile = open(filepath, "w", newline="\n") @@ -1008,10 +1005,10 @@ def include_proto(file_name, new_line_first=False): if not customExport: if DLFormat != DLFormat.Static: # Write material headers - writeMaterialHeaders( - exportDir, - include_proto("material.inc.c"), - include_proto("material.inc.h"), + write_material_headers( + Path(exportDir), + Path("levels") / level_name / "material.inc.c", + Path("levels") / level_name / "material.inc.c", ) # Export camera triggers @@ -1082,19 +1079,26 @@ def include_proto(file_name, new_line_first=False): createHeaderFile(level_name, headerPath) # Write level data - writeIfNotFound(geoPath, include_proto("geo.inc.c", new_line_first=True), "") - writeIfNotFound(levelDataPath, include_proto("leveldata.inc.c", new_line_first=True), "") - writeIfNotFound(headerPath, include_proto("header.inc.h", new_line_first=True), "#endif") + write_includes(Path(geoPath), [Path("geo.inc.c")]) + write_includes(Path(levelDataPath), [Path("leveldata.inc.c")]) + write_includes(Path(headerPath), [Path("header.inc.h")], before_endif=True) + old_include = to_include_descriptor(Path("levels") / level_name / "texture_include.inc.c") if fModel.texturesSavedLastExport == 0: textureIncludePath = os.path.join(level_dir, "texture_include.inc.c") if os.path.exists(textureIncludePath): os.remove(textureIncludePath) # This one is for backwards compatibility purposes - deleteIfFound(os.path.join(level_dir, "texture.inc.c"), include_proto("texture_include.inc.c")) + write_or_delete_if_found( + Path(level_dir) / "texture.inc.c", + to_remove=[old_include], + ) # This one is for backwards compatibility purposes - deleteIfFound(levelDataPath, include_proto("texture_include.inc.c")) + write_or_delete_if_found( + Path(levelDataPath), + to_remove=[old_include], + ) texscrollIncludeC = include_proto("texscroll.inc.c") texscrollIncludeH = include_proto("texscroll.inc.h") diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index 951e8bcc5..0a1120447 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -1,6 +1,7 @@ import math, bpy, mathutils import os from bpy.utils import register_class, unregister_class +from bpy.types import UILayout from re import findall, sub from pathlib import Path from ..panels import SM64_Panel @@ -31,6 +32,7 @@ from .sm64_constants import ( levelIDNames, + level_enums, enumLevelNames, enumModelIDs, enumMacrosNames, @@ -65,6 +67,13 @@ ScaleNode, ) +from .animation import ( + export_animation, + export_animation_table, + get_anim_obj, + is_obj_animatable, + SM64_ArmatureAnimProperties, +) enumTerrain = [ ("Custom", "Custom", "Custom"), @@ -1443,6 +1452,8 @@ class BehaviorScriptProperty(bpy.types.PropertyGroup): _inheritable_macros = { "LOAD_COLLISION_DATA", "SET_MODEL", + "LOAD_ANIMATIONS", + "ANIMATE" # add support later maybe # "SET_HITBOX_WITH_OFFSET", # "SET_HITBOX", @@ -1496,6 +1507,18 @@ def get_inherit_args(self, context, props): if not props.export_col: raise PluginError("Can't inherit collision without exporting collision data") return props.collision_name + if self.macro == "LOAD_ANIMATIONS": + if not props.export_anim: + raise PluginError("Can't inherit animation table without exporting animation data") + if not props.anims_name: + raise PluginError("No animation name to inherit in behavior script") + return f"oAnimations, {props.anims_name}" + if self.macro == "ANIMATE": + if not props.export_anim: + raise PluginError("Can't inherit animation table without exporting animation data") + if not props.anim_object: + raise PluginError("No animation properties to inherit in behavior script") + return f"oAnimations, {props.anim_object.fast64.sm64.animation.beginning_animation}" return self.macro_args def get_args(self, context, props): @@ -1548,7 +1571,7 @@ def write_file_lines(self, path, file_lines): # exports the model ID load into the appropriate script.c location def export_script_load(self, context, props): - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path if props.export_header_type == "Level": # for some reason full_level_path doesn't work here if props.non_decomp_level: @@ -1594,7 +1617,7 @@ def export_model_id(self, context, props, offset): if props.non_decomp_level: return # check if model_ids.h exists - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path model_ids = decomp_path / "include" / "model_ids.h" if not model_ids.exists(): PluginError("Could not find model_ids.h") @@ -1711,7 +1734,7 @@ def export_level_specific_load(self, script_path, props): def export_behavior_header(self, context, props): # check if behavior_header.h exists - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path behavior_header = decomp_path / "include" / "behavior_data.h" if not behavior_header.exists(): PluginError("Could not find behavior_data.h") @@ -1745,7 +1768,7 @@ def export_behavior_script(self, context, props): raise PluginError("Behavior must have more than 0 cmds to export") # export the behavior script itself - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path behavior_data = decomp_path / "data" / "behavior_data.c" if not behavior_data.exists(): PluginError("Could not find behavior_data.c") @@ -1812,7 +1835,7 @@ def verify_context(self, context, props): raise PluginError("Operator can only be used in object mode.") if context.scene.fast64.sm64.export_type != "C": raise PluginError("Combined Object Export only supports C exporting") - if not props.col_object and not props.gfx_object and not props.bhv_object: + if not props.col_object and not props.gfx_object and not props.anim_object and not props.bhv_object: raise PluginError("No export object selected") if ( context.active_object @@ -1823,7 +1846,7 @@ def verify_context(self, context, props): def get_export_objects(self, context, props): if not props.export_all_selected: - return {props.col_object, props.gfx_object, props.bhv_object}.difference({None}) + return {props.col_object, props.gfx_object, props.anim_object, props.bhv_object}.difference({None}) def obj_root(object, context): while object.parent and object.parent in context.selected_objects: @@ -1877,6 +1900,22 @@ def execute_gfx(self, props, context, obj, index): if not props.export_all_selected: raise Exception(e) + # writes table.inc.c file, anim_header.h + # writes include into aggregate file in export location (leveldata.c/.c) + # writes name to header in aggregate file location (actor/level) + # var name is: static const struct Animation *const _anims[] (or custom name) + def execute_anim(self, props, context, obj): + try: + if props.export_anim and obj is props.anim_object: + if props.export_single_action: + export_animation(context, obj) + else: + export_animation_table(context, obj) + except Exception as exc: + # pass on multiple export, throw on singular + if not props.export_all_selected: + raise Exception(exc) from exc + def execute(self, context): props = context.scene.fast64.sm64.combined_export try: @@ -1887,6 +1926,7 @@ def execute(self, context): props.context_obj = obj self.execute_col(props, obj) self.execute_gfx(props, context, obj, index) + self.execute_anim(props, context, obj) # do not export behaviors with multiple selection if props.export_bhv and props.obj_name_bhv and not props.export_all_selected: self.export_behavior_script(context, props) @@ -1959,6 +1999,17 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): name="Export Rooms", description="Collision export will generate rooms.inc.c file" ) + # anim export options + quick_anim_read: bpy.props.BoolProperty( + name="Quick Data Read", description="Read fcurves directly, should work with the majority of rigs", default=True + ) + export_single_action: bpy.props.BoolProperty( + name="Selected Action", + description="Animation export will only export the armature's current action like in older versions of fast64", + ) + binary_level: bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") + insertable_directory: bpy.props.StringProperty(name="Directory Path", subtype="FILE_PATH") + # export options export_bhv: bpy.props.BoolProperty( name="Export Behavior", default=False, description="Export behavior with given object name" @@ -1969,6 +2020,7 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): export_gfx: bpy.props.BoolProperty( name="Export Graphics", description="Export geo layouts for linked or selected mesh that have collision data" ) + export_anim: bpy.props.BoolProperty(name="Export Animations", description="Export animation table of an armature") export_script_loads: bpy.props.BoolProperty( name="Export Script Loads", description="Exports the Model ID and adds a level script load in the appropriate place", @@ -1991,6 +2043,7 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): collision_object: bpy.props.PointerProperty(type=bpy.types.Object) graphics_object: bpy.props.PointerProperty(type=bpy.types.Object) + animation_object: bpy.props.PointerProperty(type=bpy.types.Object, poll=lambda self, obj: is_obj_animatable(obj)) # is this abuse of properties? @property @@ -2011,6 +2064,18 @@ def gfx_object(self): else: return self.graphics_object or self.context_obj or bpy.context.active_object + @property + def anim_object(self): + if not self.export_anim: + return None + obj = get_anim_obj(bpy.context) + context_obj = self.context_obj if self.context_obj and is_obj_animatable(self.context_obj) else None + if self.export_all_selected: + return context_obj or obj + else: + assert not self.animation_object or is_obj_animatable(self.animation_object) + return self.animation_object or context_obj or obj + @property def bhv_object(self): if not self.export_bhv or self.export_all_selected: @@ -2054,6 +2119,15 @@ def obj_name_bhv(self): else: return self.filter_name(self.object_name or self.bhv_object.name) + @property + def obj_name_anim(self): + if self.export_all_selected and self.anim_object: + return self.filter_name(self.anim_object.name) + if not self.object_name and not self.anim_object: + return "" + else: + return self.filter_name(self.object_name or self.anim_object.name) + @property def bhv_name(self): return "bhv" + "".join([word.title() for word in toAlnum(self.obj_name_bhv).split("_")]) @@ -2070,6 +2144,12 @@ def collision_name(self): def model_id_define(self): return f"MODEL_{toAlnum(self.obj_name_gfx)}".upper() + @property + def anims_name(self): + if not self.anim_object: + return "" + return self.anim_object.fast64.sm64.animation.get_table_name(self.obj_name_anim) + @property def export_level_name(self): if self.level_name == "Custom" or self.non_decomp_level: @@ -2104,29 +2184,42 @@ def actor_custom_path(self): return self.custom_export_path @property - def level_directory(self): + def level_directory(self) -> Path: if self.non_decomp_level: - return self.custom_level_name + return Path(self.custom_level_name) level_name = self.custom_level_name if self.level_name == "Custom" else self.level_name - return os.path.join("/levels/", level_name) + return Path("levels") / level_name @property def base_level_path(self): if self.non_decomp_level: - return bpy.path.abspath(self.custom_level_path) - return bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + return Path(bpy.path.abspath(self.custom_level_path)) + return bpy.context.scene.fast64.sm64.abs_decomp_path @property def full_level_path(self): - return os.path.join(self.base_level_path, self.level_directory) + return self.base_level_path / self.level_directory # remove user prefixes/naming that I will be adding, such as _col, _geo etc. - def filter_name(self, name): - if self.use_name_filtering: + def filter_name(self, name, force_filtering=False): + if self.use_name_filtering or force_filtering: return sub("(_col)?(_geo)?(_bhv)?(lision)?", "", name) else: return name + def draw_anim_props(self, layout: UILayout, export_type="C", is_dma=False): + col = layout.column() + col.prop(self, "quick_anim_read") + if self.quick_anim_read: + col.label(text="May Break!", icon="INFO") + if not is_dma and export_type == "C": + col.prop(self, "export_single_action") + if export_type == "Binary": + if not is_dma: + prop_split(col, self, "binary_level", "Level") + elif export_type == "Insertable Binary": + prop_split(col, self, "insertable_directory", "Directory") + def draw_export_options(self, layout): split = layout.row(align=True) @@ -2149,6 +2242,14 @@ def draw_export_options(self, layout): box.prop(self, "graphics_object", icon_only=True) if self.export_script_loads: box.prop(self, "model_id", text="Model ID") + + box = split.box().column() + box.prop(self, "export_anim", toggle=1) + if self.export_anim: + self.draw_anim_props(box) + if not self.export_all_selected: + box.prop(self, "animation_object", icon_only=True) + col = layout.column() col.prop(self, "export_all_selected") col.prop(self, "use_name_filtering") @@ -2156,8 +2257,21 @@ def draw_export_options(self, layout): col.prop(self, "export_bhv") self.draw_obj_name(layout) + @property + def actor_names(self) -> list: + return list(dict.fromkeys(filter(None, [self.obj_name_col, self.obj_name_gfx, self.obj_name_anim])).keys()) + + @property + def export_locations(self) -> str | None: + names = self.actor_names + if len(names) > 1: + return f"({'/'.join(names)})" + elif len(names) == 1: + return names[0] + return None + def draw_level_path(self, layout): - if not directory_ui_warnings(layout, bpy.path.abspath(self.base_level_path)): + if not directory_ui_warnings(layout, self.base_level_path): return if self.non_decomp_level: layout.label(text=f"Level export path: {self.full_level_path}") @@ -2166,12 +2280,24 @@ def draw_level_path(self, layout): return True def draw_actor_path(self, layout): - actor_path = Path(bpy.context.scene.fast64.sm64.decomp_path) / "actors" - if not filepath_ui_warnings(layout, (actor_path / self.actor_group_name).with_suffix(".c")): + if self.export_locations is None: return - export_locations = ",".join({self.obj_name_col, self.obj_name_gfx}) - # can this be more clear? - layout.label(text=f"Actor export path: actors/{export_locations}") + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path + if self.export_header_type == "Actor": + actor_path = decomp_path / "actors" + if not filepath_ui_warnings(layout, (actor_path / self.actor_group_name).with_suffix(".c")): + return + layout.label(text=f"Actor export path: actors/{self.export_locations}/") + elif self.export_header_type == "Level": + if not directory_ui_warnings(layout, self.full_level_path): + return + level_path = self.full_level_path if self.non_decomp_level else self.level_directory + layout.label(text=f"Actor export path: {level_path / self.export_locations}/") + elif self.export_header_type == "Custom": + custom_path = Path(bpy.path.abspath(self.custom_export_path)) + if not directory_ui_warnings(layout, custom_path): + return + layout.label(text=f"Actor export path: {custom_path / self.export_locations}/") return True def draw_col_names(self, layout): @@ -2184,6 +2310,12 @@ def draw_gfx_names(self, layout): if self.export_script_loads: layout.label(text=f"Model ID: {self.model_id_define}") + def draw_anim_names(self, layout): + anim_props = self.anim_object.fast64.sm64.animation + if anim_props.is_dma: + layout.label(text=f"Animation path: {anim_props.dma_folder}(.c)") + layout.label(text=f"Animation table name: {self.anims_name}") + def draw_obj_name(self, layout): split_1 = layout.split(factor=0.45) split_2 = split_1.split(factor=0.45) @@ -2219,7 +2351,7 @@ def draw_props(self, layout): col.separator() # object exports box = col.box() - if not self.export_col and not self.export_bhv and not self.export_gfx: + if not self.export_col and not self.export_bhv and not self.export_gfx and not self.export_anim: col = box.column() col.operator("object.sm64_export_combined_object", text="Export Object") col.enabled = False @@ -2231,7 +2363,7 @@ def draw_props(self, layout): self.draw_export_options(box) # bhv export only, so enable bhv draw only - if not self.export_col and not self.export_gfx: + if not self.export_col and not self.export_gfx and not self.export_anim: return self.draw_bhv_options(col) # pathing for gfx/col exports @@ -2266,17 +2398,13 @@ def draw_props(self, layout): "Duplicates objects will be exported! Use with Caution.", icon="ERROR", ) + return info_box = box.box() info_box.scale_y = 0.5 - if self.export_header_type == "Level": - if not self.draw_level_path(info_box): - return - - elif self.export_header_type == "Actor": - if not self.draw_actor_path(info_box): - return + if not self.draw_actor_path(info_box): + return if self.obj_name_gfx and self.export_gfx: self.draw_gfx_names(info_box) @@ -2284,6 +2412,9 @@ def draw_props(self, layout): if self.obj_name_col and self.export_col: self.draw_col_names(info_box) + if self.obj_name_anim and self.export_anim: + self.draw_anim_names(info_box) + if self.obj_name_bhv: info_box.label(text=f"Behavior name: {self.bhv_name}") @@ -2785,6 +2916,8 @@ class SM64_ObjectProperties(bpy.types.PropertyGroup): game_object: bpy.props.PointerProperty(type=SM64_GameObjectProperties) segment_loads: bpy.props.PointerProperty(type=SM64_SegmentProperties) + animation: bpy.props.PointerProperty(type=SM64_ArmatureAnimProperties) + @staticmethod def upgrade_changed_props(): for obj in bpy.data.objects: diff --git a/fast64_internal/sm64/sm64_texscroll.py b/fast64_internal/sm64/sm64_texscroll.py index f68fe995f..01d4d83b8 100644 --- a/fast64_internal/sm64/sm64_texscroll.py +++ b/fast64_internal/sm64/sm64_texscroll.py @@ -1,7 +1,8 @@ +from pathlib import Path import os, re, bpy -from ..utility import PluginError, writeIfNotFound, getDataFromFile, saveDataToFile, CScrollData, CData +from ..utility import PluginError, getDataFromFile, saveDataToFile, CScrollData, CData from .c_templates.tile_scroll import tile_scroll_c, tile_scroll_h -from .sm64_utility import getMemoryCFilePath +from .sm64_utility import END_IF_FOOTER, ModifyFoundDescriptor, getMemoryCFilePath, write_or_delete_if_found # This is for writing framework for scroll code. # Actual scroll code found in f3d_gbi.py (FVertexScrollData) @@ -78,7 +79,16 @@ def writeSegmentROMTable(baseDir): memFile.close() # Add extern definition of segment table - writeIfNotFound(os.path.join(baseDir, "src/game/memory.h"), "\nextern uintptr_t sSegmentROMTable[32];", "#endif") + write_or_delete_if_found( + Path(baseDir) / "src/game/memory.h", + [ + ModifyFoundDescriptor( + "extern uintptr_t sSegmentROMTable[32];", r"extern\h*uintptr_t\h*sSegmentROMTable\[.*?\]\h?;" + ) + ], + path_must_exist=True, + footer=END_IF_FOOTER, + ) def writeScrollTextureCall(path, include, callString): diff --git a/fast64_internal/sm64/sm64_utility.py b/fast64_internal/sm64/sm64_utility.py index c7c55de1f..97ab4e563 100644 --- a/fast64_internal/sm64/sm64_utility.py +++ b/fast64_internal/sm64/sm64_utility.py @@ -1,8 +1,15 @@ +from typing import NamedTuple, Optional +from pathlib import Path +from io import StringIO +import random +import string import os +import re + import bpy from bpy.types import UILayout -from ..utility import PluginError, filepath_checks, run_and_draw_errors, multilineLabel, prop_split +from ..utility import PluginError, filepath_checks, run_and_draw_errors, multilineLabel, prop_split, COMMENT_PATTERN from .sm64_function_map import func_map @@ -122,3 +129,264 @@ def convert_addr_to_func(addr: str): return refresh_func_map[addr.lower()] else: return addr + + +def temp_file_path(path: Path): + """Generates a temporary file path that does not exist from the given path.""" + result, size = path.with_suffix(".tmp"), 0 + for size in range(5, 15): + if not result.exists(): + return result + random_suffix = "".join(random.choice(string.ascii_letters) for _ in range(size)) + result = path.with_suffix(f".{random_suffix}.tmp") + size += 1 + raise PluginError("Cannot create unique temporary file. 10 tries exceeded.") + + +class ModifyFoundDescriptor: + string: str + regex: str + + def __init__(self, string: str, regex: str = ""): + self.string = string + if regex: + self.regex = regex.replace(r"\h", r"[^\v\S]") # /h is invalid... for some reason + else: + self.regex = re.escape(string) + r"\n?" + + +class DescriptorMatch(NamedTuple): + string: str + start: int + end: int + + +class CommentMatch(NamedTuple): + commentless_pos: int + size: int + + +def adjust_start_end(start: int, end: int, comment_map: list[CommentMatch]): + for commentless_pos, comment_size in comment_map: + if start >= commentless_pos: + start += comment_size + if end >= commentless_pos: + end += comment_size + return start, end + + +def find_descriptor_in_text( + value: ModifyFoundDescriptor, commentless: str, comment_map: list[CommentMatch], start=0, end=-1 +): + matches: list[DescriptorMatch] = [] + for match in re.finditer(value.regex, commentless[start:end]): + matches.append( + DescriptorMatch(match.group(0), *adjust_start_end(start + match.start(), start + match.end(), comment_map)) + ) + return matches + + +def get_comment_map(text: str): + comment_map: list[CommentMatch] = [] + commentless, last_pos, pos = StringIO(), 0, 0 + for match in re.finditer(COMMENT_PATTERN, text): + pos += commentless.write(text[last_pos : match.start()]) # add text before comment + match_string = match.group(0) + if match_string.startswith("/"): # actual comment + comment_map.append(CommentMatch(pos, len(match_string) - 1)) + pos += commentless.write(" ") + else: # stuff like strings + pos += commentless.write(match_string) + last_pos = match.end() + + commentless.write(text[last_pos:]) # add any remaining text after the last match + return commentless.getvalue(), comment_map + + +def find_descriptors( + text: str, + descriptors: list[ModifyFoundDescriptor], + error_if_no_header=False, + header: Optional[ModifyFoundDescriptor] = None, + error_if_no_footer=False, + footer: Optional[ModifyFoundDescriptor] = None, + ignore_comments=True, +): + """Returns: The found matches from descriptors, the footer pos (the end of the text if none)""" + if ignore_comments: + commentless, comment_map = get_comment_map(text) + else: + commentless, comment_map = text, [] + + header_matches = find_descriptor_in_text(header, commentless, comment_map) if header is not None else [] + footer_matches = find_descriptor_in_text(footer, commentless, comment_map) if footer is not None else [] + + header_pos = 0 + if len(header_matches) > 0: + _, header_pos, _ = header_matches[0] + elif header is not None and error_if_no_header: + raise PluginError(f"Header {header.string} does not exist.") + + # find first footer after the header + if footer_matches: + if header_matches: + footer_pos = next((pos for _, pos, _ in footer_matches if pos >= header_pos), footer_matches[-1].start) + else: + _, footer_pos, _ = footer_matches[-1] + else: + if footer is not None and error_if_no_footer: + raise PluginError(f"Footer {footer.string} does not exist.") + footer_pos = len(text) + + found_matches: dict[ModifyFoundDescriptor, list[DescriptorMatch]] = {} + for descriptor in descriptors: + matches = find_descriptor_in_text(descriptor, commentless, comment_map, header_pos, footer_pos) + if matches: + found_matches.setdefault(descriptor, []).extend(matches) + return found_matches, footer_pos + + +def write_or_delete_if_found( + path: Path, + to_add: Optional[list[ModifyFoundDescriptor]] = None, + to_remove: Optional[list[ModifyFoundDescriptor]] = None, + path_must_exist=False, + create_new=False, + error_if_no_header=False, + header: Optional[ModifyFoundDescriptor] = None, + error_if_no_footer=False, + footer: Optional[ModifyFoundDescriptor] = None, + ignore_comments=True, +): + changed = False + to_add, to_remove = to_add or [], to_remove or [] + + assert not (path_must_exist and create_new), "path_must_exist and create_new" + if path_must_exist: + filepath_checks(path) + if not create_new and not to_add and not to_remove: + return False + + if os.path.exists(path) and not create_new: + text = path.read_text() + if text and text[-1] not in {"\n", "\r"}: # add end new line if not there + text += "\n" + found_matches, footer_pos = find_descriptors( + text, to_add + to_remove, error_if_no_header, header, error_if_no_footer, footer, ignore_comments + ) + else: + text, found_matches, footer_pos = "", {}, 0 + + for descriptor in to_remove: + matches = found_matches.get(descriptor) + if matches is None: + continue + print(f"Removing {descriptor.string} in {str(path)}") + for match in matches: + changed = True + text = text[: match.start] + text[match.end :] # Remove match + diff = match.end - match.start + for other_match in (other_match for matches in found_matches.values() for other_match in matches): + if other_match.start > match.start: + other_match.start -= diff + other_match.end -= diff + if footer_pos > match.start: + footer_pos -= diff + + additions = "" + for descriptor in to_add: + if descriptor in found_matches: + continue + print(f"Adding {descriptor.string} in {str(path)}") + additions += f"{descriptor.string}\n" + changed = True + text = text[:footer_pos] + additions + text[footer_pos:] + + if changed or create_new: + path.write_text(text) + return True + return False + + +def to_include_descriptor(include: Path, *alternatives: Path): + base_regex = r'\n?#\h*?include\h*?"{0}"' + regex = base_regex.format(include.as_posix()) + for alternative in alternatives: + regex += f"|{base_regex.format(alternative.as_posix())}" + return ModifyFoundDescriptor(f'#include "{include.as_posix()}"', regex) + + +END_IF_FOOTER = ModifyFoundDescriptor("#endif", r"#\h*?endif") + + +def write_includes( + path: Path, includes: Optional[list[Path]] = None, path_must_exist=False, create_new=False, before_endif=False +): + to_add = [] + for include in includes or []: + to_add.append(to_include_descriptor(include)) + return write_or_delete_if_found( + path, + to_add, + path_must_exist=path_must_exist, + create_new=create_new, + footer=END_IF_FOOTER if before_endif else None, + ) + + +def update_actor_includes( + header_type: str, + group_name: str, + header_dir: Path, + dir_name: str, + level_name: str | None = None, # for backwards compatibility + data_includes: Optional[list[Path]] = None, + header_includes: Optional[list[Path]] = None, + geo_includes: Optional[list[Path]] = None, +): + if header_type == "Actor": + if not group_name: + raise PluginError("Empty group name") + data_path = header_dir / f"{group_name}.c" + header_path = header_dir / f"{group_name}.h" + geo_path = header_dir / f"{group_name}_geo.c" + elif header_type == "Level": + data_path = header_dir / "leveldata.c" + header_path = header_dir / "header.h" + geo_path = header_dir / "geo.c" + elif header_type == "Custom": + return # Custom doesn't update includes + else: + raise PluginError(f'Unknown header type "{header_type}"') + + def write_includes_with_alternate(path: Path, includes: Optional[list[Path]], before_endif=False): + if includes is None: + return False + if header_type == "Level": + path_and_alternates = [ + [ + Path(dir_name) / include, + Path("levels") / level_name / (dir_name) / include, # backwards compatability + ] + for include in includes + ] + else: + path_and_alternates = [[Path(dir_name) / include] for include in includes] + return write_or_delete_if_found( + path, + [to_include_descriptor(*paths) for paths in path_and_alternates], + path_must_exist=True, + footer=END_IF_FOOTER if before_endif else None, + ) + + if write_includes_with_alternate(data_path, data_includes): + print(f"Updated data includes at {header_path}.") + if write_includes_with_alternate(header_path, header_includes, before_endif=True): + print(f"Updated header includes at {header_path}.") + if write_includes_with_alternate(geo_path, geo_includes): + print(f"Updated geo data at {geo_path}.") + + +def write_material_headers(decomp: Path, c_include: Path, h_include: Path): + write_includes(decomp / "src/game/materials.c", [c_include]) + write_includes(decomp / "src/game/materials.h", [h_include], before_endif=True) diff --git a/fast64_internal/sm64/tools/panels.py b/fast64_internal/sm64/tools/panels.py index 5a41accbe..c22aae05c 100644 --- a/fast64_internal/sm64/tools/panels.py +++ b/fast64_internal/sm64/tools/panels.py @@ -1,11 +1,11 @@ from bpy.utils import register_class, unregister_class +from typing import TYPE_CHECKING + from ...panels import SM64_Panel from .operators import SM64_CreateSimpleLevel, SM64_AddWaterBox, SM64_AddBoneGroups, SM64_CreateMetarig -from typing import TYPE_CHECKING - if TYPE_CHECKING: from ..settings.properties import SM64_Properties diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 14f6aea59..0cda4e229 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -1,7 +1,7 @@ import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator from math import pi, ceil, degrees, radians, copysign from mathutils import * -from .utility_anim import * + from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union from bpy.types import UILayout, Scene, World @@ -423,7 +423,7 @@ def getPathAndLevel(is_custom_export, custom_export_path, custom_level_name, lev export_path = bpy.path.abspath(custom_export_path) level_name = custom_level_name else: - export_path = bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + export_path = str(bpy.context.scene.fast64.sm64.abs_decomp_path) if level_enum == "Custom": level_name = custom_level_name else: @@ -482,6 +482,7 @@ def saveDataToFile(filepath, data): def applyBasicTweaks(baseDir): + directory_path_checks(baseDir, "Empty directory path.") if bpy.context.scene.fast64.sm64.force_extended_ram: enableExtendedRAM(baseDir) @@ -510,11 +511,6 @@ def enableExtendedRAM(baseDir): segmentFile.close() -def writeMaterialHeaders(exportDir, matCInclude, matHInclude): - writeIfNotFound(os.path.join(exportDir, "src/game/materials.c"), "\n" + matCInclude, "") - writeIfNotFound(os.path.join(exportDir, "src/game/materials.h"), "\n" + matHInclude, "#endif") - - def writeMaterialFiles( exportDir, assetDir, headerInclude, matHInclude, headerDynamic, dynamic_data, geoString, customExport ): @@ -691,11 +687,17 @@ def makeWriteInfoBox(layout): def writeBoxExportType(writeBox, headerType, name, levelName, levelOption): + if not name: + writeBox.label(text="Empty actor name", icon="ERROR") + return if headerType == "Actor": writeBox.label(text="actors/" + toAlnum(name)) elif headerType == "Level": if levelOption != "Custom": levelName = levelOption + if not name: + writeBox.label(text="Empty level name", icon="ERROR") + return writeBox.label(text="levels/" + toAlnum(levelName) + "/" + toAlnum(name)) @@ -742,40 +744,6 @@ def overwriteData(headerRegex, name, value, filePath, writeNewBeforeString, isFu raise PluginError(filePath + " does not exist.") -def writeIfNotFound(filePath, stringValue, footer): - if os.path.exists(filePath): - fileData = open(filePath, "r") - fileData.seek(0) - stringData = fileData.read() - fileData.close() - if stringValue not in stringData: - if len(footer) > 0: - footerIndex = stringData.rfind(footer) - if footerIndex == -1: - raise PluginError("Footer " + footer + " does not exist.") - stringData = stringData[:footerIndex] + stringValue + "\n" + stringData[footerIndex:] - else: - stringData += stringValue - fileData = open(filePath, "w", newline="\n") - fileData.write(stringData) - fileData.close() - else: - raise PluginError(filePath + " does not exist.") - - -def deleteIfFound(filePath, stringValue): - if os.path.exists(filePath): - fileData = open(filePath, "r") - fileData.seek(0) - stringData = fileData.read() - fileData.close() - if stringValue in stringData: - stringData = stringData.replace(stringValue, "") - fileData = open(filePath, "w", newline="\n") - fileData.write(stringData) - fileData.close() - - def yield_children(obj: bpy.types.Object): yield obj if obj.children: @@ -811,6 +779,13 @@ def scale_mtx_from_vector(scale: mathutils.Vector): return mathutils.Matrix.Diagonal(scale[0:3]).to_4x4() +def attemptModifierApply(modifier): + try: + bpy.ops.object.modifier_apply(modifier=modifier.name) + except Exception as e: + print("Skipping modifier " + str(modifier.name)) + + def copy_object_and_apply(obj: bpy.types.Object, apply_scale=False, apply_modifiers=False): if apply_scale or apply_modifiers: # it's a unique mesh, use object name @@ -1302,6 +1277,11 @@ def toAlnum(name, exceptions=[]): return name +def to_valid_file_name(name: str): + """Replace any invalid characters with an underscore""" + return re.sub(r'[/\\?%*:|"<>]', " ", name) + + def get64bitAlignedAddr(address): endNibble = hex(address)[-1] if endNibble != "0" and endNibble != "8": @@ -1362,15 +1342,15 @@ def bytesToInt(value): def bytesToHex(value, byteSize=4): - return format(bytesToInt(value), "#0" + str(byteSize * 2 + 2) + "x") + return format(bytesToInt(value), f"#0{(byteSize * 2 + 2)}x") def bytesToHexClean(value, byteSize=4): - return format(bytesToInt(value), "0" + str(byteSize * 2) + "x") + return format(bytesToInt(value), f"#0{(byteSize * 2)}x") -def intToHex(value, byteSize=4): - return format(value, "#0" + str(byteSize * 2 + 2) + "x") +def intToHex(value, byte_size=4, signed=True): + return format(value if signed else cast_integer(value, byte_size * 8, False), f"#0{(byte_size * 2 + 2)}x") def intToBytes(value, byteSize): @@ -1605,6 +1585,10 @@ def bitMask(data, offset, amount): return (~(-1 << amount) << offset & data) >> offset +def is_bit_active(x: int, index: int): + return ((x >> index) & 1) == 1 + + def read16bitRGBA(data): r = bitMask(data, 11, 5) / ((2**5) - 1) g = bitMask(data, 6, 5) / ((2**5) - 1) @@ -1716,9 +1700,11 @@ def getTextureSuffixFromFormat(texFmt): return texFmt.lower() -def removeComments(text: str): - # https://stackoverflow.com/a/241506 +# https://stackoverflow.com/a/241506 +COMMENT_PATTERN = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE) + +def removeComments(text: str): def replacer(match: re.Match[str]): s = match.group(0) if s.startswith("/"): @@ -1726,9 +1712,7 @@ def replacer(match: re.Match[str]): else: return s - pattern = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE) - - return re.sub(pattern, replacer, text) + return re.sub(COMMENT_PATTERN, replacer, text) binOps = { diff --git a/fast64_internal/utility_anim.py b/fast64_internal/utility_anim.py index 982706a16..0f5218c05 100644 --- a/fast64_internal/utility_anim.py +++ b/fast64_internal/utility_anim.py @@ -1,5 +1,10 @@ import bpy, math, mathutils +from bpy.types import Object, Action, AnimData from bpy.utils import register_class, unregister_class +from bpy.props import StringProperty + +from .operators import OperatorBase +from .utility import attemptModifierApply, raisePluginError, PluginError from typing import TYPE_CHECKING @@ -23,8 +28,6 @@ class ArmatureApplyWithMeshOperator(bpy.types.Operator): # Called on demand (i.e. button press, menu item) # Can also be called from operator search menu (Spacebar) def execute(self, context): - from .utility import PluginError, raisePluginError - try: if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") @@ -46,6 +49,51 @@ def execute(self, context): return {"FINISHED"} # must return a set +class CreateAnimData(OperatorBase): + bl_idname = "scene.fast64_create_anim_data" + bl_label = "Create Animation Data" + bl_description = "Create animation data" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ANIM" + + def execute_operator(self, context): + obj = context.object + if obj is None: + raise PluginError("No selected object") + if obj.animation_data is None: + obj.animation_data_create() + + +class AddBasicAction(OperatorBase): + bl_idname = "scene.fast64_add_basic_action" + bl_label = "Add Basic Action" + bl_description = "Create animation data and add basic action" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION" + + def execute_operator(self, context): + if context.object is None: + raise PluginError("No selected object") + create_basic_action(context.object) + + +class StashAction(OperatorBase): + bl_idname = "scene.fast64_stash_action" + bl_label = "Stash Action" + bl_description = "Stash an action in an object's nla tracks if not already stashed" + context_mode = "OBJECT" + icon = "NLA" + + action: StringProperty() + + def execute_operator(self, context): + if context.object is None: + raise PluginError("No selected object") + stashActionInArmature(context.object, get_action(self.action)) + + # This code only handles root bone with no parent, which is the only bone that translates. def getTranslationRelativeToRest(bone: bpy.types.Bone, inputVector: mathutils.Vector) -> mathutils.Vector: zUpToYUp = mathutils.Quaternion((1, 0, 0), math.radians(-90.0)).to_matrix().to_4x4() @@ -63,13 +111,6 @@ def getRotationRelativeToRest(bone: bpy.types.Bone, inputEuler: mathutils.Euler) return (restRotation.inverted() @ inputEuler.to_matrix().to_4x4()).to_euler("XYZ", inputEuler) -def attemptModifierApply(modifier): - try: - bpy.ops.object.modifier_apply(modifier=modifier.name) - except Exception as e: - print("Skipping modifier " + str(modifier.name)) - - def armatureApplyWithMesh(armatureObj: bpy.types.Object, context: bpy.types.Context): for child in armatureObj.children: if child.type != "MESH": @@ -179,28 +220,62 @@ def getIntersectionInterval(): return range_get_by_choice[anim_range_choice]() -def stashActionInArmature(armatureObj: bpy.types.Object, action: bpy.types.Action): +def is_action_stashed(obj: Object, action: Action): + animation_data: AnimData | None = obj.animation_data + if animation_data is None: + return False + for track in animation_data.nla_tracks: + for strip in track.strips: + if strip.action is None: + continue + if strip.action.name == action.name: + return True + return False + + +def stashActionInArmature(obj: Object, action: Action): """ Stashes an animation (action) into an armature´s nla tracks. This prevents animations from being deleted by blender or purged by the user on accident. """ - for track in armatureObj.animation_data.nla_tracks: - for strip in track.strips: - if strip.action is None: - continue + if is_action_stashed(obj, action): + return - if strip.action.name == action.name: - return + print(f'Stashing "{action.name}" in the object "{obj.name}".') + if obj.animation_data is None: + obj.animation_data_create() + track = obj.animation_data.nla_tracks.new() + track.name = action.name + track.strips.new(action.name, int(action.frame_range[0]), action) - print(f'Stashing "{action.name}" in the object "{armatureObj.name}".') - track = armatureObj.animation_data.nla_tracks.new() - track.strips.new(action.name, int(action.frame_range[0]), action) +def create_basic_action(obj: Object, name=""): + if obj.animation_data is None: + obj.animation_data_create() + if name == "": + name = f"{obj.name} Action" + action = bpy.data.actions.new(name) + stashActionInArmature(obj, action) + obj.animation_data.action = action + return action + + +def get_action(name: str): + if name == "": + raise ValueError("Empty action name.") + if not name in bpy.data.actions: + raise IndexError(f"Action ({name}) is not in this file´s action data.") + return bpy.data.actions[name] -classes = (ArmatureApplyWithMeshOperator,) +classes = ( + ArmatureApplyWithMeshOperator, + CreateAnimData, + AddBasicAction, + StashAction, +) def utility_anim_register(): diff --git a/piranha plant/toad.insertable b/piranha plant/toad.insertable new file mode 100644 index 0000000000000000000000000000000000000000..18090db2d246a18a674e46f29f92855007931309 GIT binary patch literal 38872 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

6cg0uKd$3AJhU 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 Date: Mon, 30 Sep 2024 22:03:26 +0100 Subject: [PATCH 02/14] [SM64] Animations Rewrite/Rework --- __init__.py | 14 +- fast64_internal/f3d_material_converter.py | 12 +- fast64_internal/oot/oot_utility.py | 10 + fast64_internal/operators.py | 64 +- fast64_internal/sm64/__init__.py | 33 +- fast64_internal/sm64/animation/__init__.py | 15 + fast64_internal/sm64/animation/classes.py | 1006 +++++++++ fast64_internal/sm64/animation/constants.py | 88 + fast64_internal/sm64/animation/exporting.py | 1025 +++++++++ fast64_internal/sm64/animation/importing.py | 808 +++++++ fast64_internal/sm64/animation/operators.py | 346 +++ fast64_internal/sm64/animation/panels.py | 198 ++ fast64_internal/sm64/animation/properties.py | 1204 +++++++++++ fast64_internal/sm64/animation/utility.py | 165 ++ fast64_internal/sm64/settings/properties.py | 43 +- .../sm64/settings/repo_settings.py | 5 + fast64_internal/sm64/sm64_anim.py | 1119 ---------- fast64_internal/sm64/sm64_classes.py | 290 +++ fast64_internal/sm64/sm64_collision.py | 50 +- fast64_internal/sm64/sm64_constants.py | 1878 +++++++++++++++-- fast64_internal/sm64/sm64_f3d_writer.py | 50 +- fast64_internal/sm64/sm64_geolayout_writer.py | 70 +- fast64_internal/sm64/sm64_level_writer.py | 48 +- fast64_internal/sm64/sm64_objects.py | 191 +- fast64_internal/sm64/sm64_texscroll.py | 16 +- fast64_internal/sm64/sm64_utility.py | 270 ++- fast64_internal/sm64/tools/panels.py | 4 +- fast64_internal/utility.py | 84 +- fast64_internal/utility_anim.py | 115 +- piranha plant/toad.insertable | Bin 0 -> 38872 bytes pyproject.toml | 3 + toad.insertable | Bin 0 -> 25796 bytes 32 files changed, 7627 insertions(+), 1597 deletions(-) create mode 100644 fast64_internal/sm64/animation/__init__.py create mode 100644 fast64_internal/sm64/animation/classes.py create mode 100644 fast64_internal/sm64/animation/constants.py create mode 100644 fast64_internal/sm64/animation/exporting.py create mode 100644 fast64_internal/sm64/animation/importing.py create mode 100644 fast64_internal/sm64/animation/operators.py create mode 100644 fast64_internal/sm64/animation/panels.py create mode 100644 fast64_internal/sm64/animation/properties.py create mode 100644 fast64_internal/sm64/animation/utility.py delete mode 100644 fast64_internal/sm64/sm64_anim.py create mode 100644 fast64_internal/sm64/sm64_classes.py create mode 100644 piranha plant/toad.insertable create mode 100644 toad.insertable diff --git a/__init__.py b/__init__.py index d3e5c548c..280b88408 100644 --- a/__init__.py +++ b/__init__.py @@ -13,7 +13,7 @@ repo_settings_operators_unregister, ) -from .fast64_internal.sm64 import sm64_register, sm64_unregister +from .fast64_internal.sm64 import sm64_register, sm64_unregister, SM64_ActionProperty from .fast64_internal.sm64.sm64_constants import sm64_world_defaults from .fast64_internal.sm64.settings.properties import SM64_Properties from .fast64_internal.sm64.sm64_geolayout_bone import SM64_BoneProperties @@ -227,6 +227,14 @@ class Fast64_Properties(bpy.types.PropertyGroup): renderSettings: bpy.props.PointerProperty(type=Fast64RenderSettings_Properties, name="Fast64 Render Settings") +class Fast64_ActionProperties(bpy.types.PropertyGroup): + """ + Properties in Action.fast64. + """ + + sm64: bpy.props.PointerProperty(type=SM64_ActionProperty, name="SM64 Properties") + + class Fast64_BoneProperties(bpy.types.PropertyGroup): """ Properties in bone.fast64 (bpy.types.Bone) @@ -314,6 +322,7 @@ def draw(self, context): Fast64RenderSettings_Properties, ManualUpdatePreviewOperator, Fast64_Properties, + Fast64_ActionProperties, Fast64_BoneProperties, Fast64_ObjectProperties, F3D_GlobalSettingsPanel, @@ -457,7 +466,7 @@ def register(): bpy.types.Scene.fast64 = bpy.props.PointerProperty(type=Fast64_Properties, name="Fast64 Properties") bpy.types.Bone.fast64 = bpy.props.PointerProperty(type=Fast64_BoneProperties, name="Fast64 Bone Properties") bpy.types.Object.fast64 = bpy.props.PointerProperty(type=Fast64_ObjectProperties, name="Fast64 Object Properties") - + bpy.types.Action.fast64 = bpy.props.PointerProperty(type=Fast64_ActionProperties, name="Fast64 Action Properties") bpy.app.handlers.load_post.append(after_load) @@ -486,6 +495,7 @@ def unregister(): del bpy.types.Scene.fast64 del bpy.types.Bone.fast64 del bpy.types.Object.fast64 + del bpy.types.Action.fast64 repo_settings_operators_unregister() diff --git a/fast64_internal/f3d_material_converter.py b/fast64_internal/f3d_material_converter.py index fffb903da..992c2c221 100644 --- a/fast64_internal/f3d_material_converter.py +++ b/fast64_internal/f3d_material_converter.py @@ -186,16 +186,16 @@ def convertAllBSDFtoF3D(objs, renameUV): def convertBSDFtoF3D(obj, index, material, materialDict): if not material.use_nodes: newMaterial = createF3DMat(obj, preset="Shaded Solid", index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = material.diffuse_color + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.default_light_color = material.diffuse_color updateMatWithName(newMaterial, material, materialDict) elif "Principled BSDF" in material.node_tree.nodes: tex0Node = material.node_tree.nodes["Principled BSDF"].inputs["Base Color"] if len(tex0Node.links) == 0: newMaterial = createF3DMat(obj, preset=getDefaultMaterialPreset("Shaded Solid"), index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = tex0Node.default_value + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.default_light_color = tex0Node.default_value updateMatWithName(newMaterial, material, materialDict) else: if isinstance(tex0Node.links[0].from_node, bpy.types.ShaderNodeTexImage): @@ -213,8 +213,8 @@ def convertBSDFtoF3D(obj, index, material, materialDict): else: presetName = getDefaultMaterialPreset("Shaded Texture") newMaterial = createF3DMat(obj, preset=presetName, index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.tex0.tex = tex0Node.links[0].from_node.image + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.tex0.tex = tex0Node.links[0].from_node.image updateMatWithName(newMaterial, material, materialDict) else: print("Principled BSDF material does not have an Image Node attached to its Base Color.") diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/oot/oot_utility.py index 3173cc299..464430478 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/oot/oot_utility.py @@ -496,6 +496,16 @@ def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: st return filepath +def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: str) -> str: + if isCustomExport: + filepath = exportPath + else: + filepath = os.path.join( + ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, False), folderName + ".h" + ) + return filepath + + def ootGetPath(exportPath, isCustomExport, subPath, folderName, makeIfNotExists, useFolderForCustom): if isCustomExport: path = bpy.path.abspath(os.path.join(exportPath, (folderName if useFolderForCustom else ""))) diff --git a/fast64_internal/operators.py b/fast64_internal/operators.py index e4a2041cd..57d099397 100644 --- a/fast64_internal/operators.py +++ b/fast64_internal/operators.py @@ -1,8 +1,20 @@ -import bpy, mathutils, math -from bpy.types import Operator, Context, UILayout +from cProfile import Profile +from pstats import SortKey, Stats +from typing import Optional + +import bpy, mathutils +from bpy.types import Operator, Context, UILayout, EnumProperty from bpy.utils import register_class, unregister_class -from .utility import * -from .f3d.f3d_material import * + +from .utility import ( + cleanupTempMeshes, + get_mode_set_from_context_mode, + raisePluginError, + parentObject, + store_original_meshes, + store_original_mtx, +) +from .f3d.f3d_material import createF3DMat def addMaterialByName(obj, matName, preset): @@ -14,6 +26,9 @@ def addMaterialByName(obj, matName, preset): material.name = matName +PROFILE_ENABLED = False + + class OperatorBase(Operator): """Base class for operators, keeps track of context mode and sets it back after running execute_operator() and catches exceptions for raisePluginError()""" @@ -21,13 +36,19 @@ class OperatorBase(Operator): context_mode: str = "" icon = "NONE" + @classmethod + def is_enabled(cls, context: Context, **op_values): + return True + @classmethod def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, **op_values): """Op args are passed to the operator via setattr()""" icon = icon if icon else cls.icon + layout = layout.column() op = layout.operator(cls.bl_idname, icon=icon, text=text) for key, value in op_values.items(): setattr(op, key, value) + layout.enabled = cls.is_enabled(bpy.context, **op_values) return op def execute_operator(self, context: Context): @@ -40,7 +61,12 @@ def execute(self, context: Context): try: if self.context_mode and self.context_mode != starting_mode_set: bpy.ops.object.mode_set(mode=self.context_mode) - self.execute_operator(context) + if PROFILE_ENABLED: + with Profile() as profile: + self.execute_operator(context) + print(Stats(profile).strip_dirs().sort_stats(SortKey.CUMULATIVE).print_stats()) + else: + self.execute_operator(context) return {"FINISHED"} except Exception as exc: raisePluginError(self, exc) @@ -53,6 +79,34 @@ def execute(self, context: Context): bpy.ops.object.mode_set(mode=starting_mode_set) +class SearchEnumOperatorBase(OperatorBase): + bl_description = "Search Enum" + bl_label = "Search" + bl_property = None + bl_options = {"UNDO"} + + @classmethod + def draw_props(cls, layout: UILayout, data, prop: str, name: str): + row = layout.row() + if name: + row.label(text=name) + row.prop(data, prop, text="") + row.operator(cls.bl_idname, icon="VIEWZOOM", text="") + + def update_enum(self, context: Context): + raise NotImplementedError() + + def execute_operator(self, context: Context): + assert self.bl_property + self.report({"INFO"}, f"Selected: {getattr(self, self.bl_property)}") + self.update_enum(context) + context.region.tag_redraw() + + def invoke(self, context: Context, _): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + class AddWaterBox(OperatorBase): bl_idname = "object.add_water_box" bl_label = "Add Water Box" diff --git a/fast64_internal/sm64/__init__.py b/fast64_internal/sm64/__init__.py index 81fdbf596..a9b429989 100644 --- a/fast64_internal/sm64/__init__.py +++ b/fast64_internal/sm64/__init__.py @@ -1,3 +1,7 @@ +from bpy.types import PropertyGroup +from bpy.props import PointerProperty +from bpy.utils import register_class, unregister_class + from .settings import ( settings_props_register, settings_props_unregister, @@ -83,14 +87,23 @@ sm64_dl_writer_unregister, ) -from .sm64_anim import ( - sm64_anim_panel_register, - sm64_anim_panel_unregister, - sm64_anim_register, - sm64_anim_unregister, +from .animation import ( + anim_panel_register, + anim_panel_unregister, + anim_register, + anim_unregister, + SM64_ActionAnimProperty, ) +class SM64_ActionProperty(PropertyGroup): + """ + Properties in Action.fast64.sm64. + """ + + animation: PointerProperty(type=SM64_ActionAnimProperty, name="SM64 Properties") + + def sm64_panel_register(): settings_panels_register() tools_panels_register() @@ -103,7 +116,7 @@ def sm64_panel_register(): sm64_spline_panel_register() sm64_dl_writer_panel_register() sm64_dl_parser_panel_register() - sm64_anim_panel_register() + anim_panel_register() def sm64_panel_unregister(): @@ -118,12 +131,13 @@ def sm64_panel_unregister(): sm64_spline_panel_unregister() sm64_dl_writer_panel_unregister() sm64_dl_parser_panel_unregister() - sm64_anim_panel_unregister() + anim_panel_unregister() def sm64_register(register_panels: bool): tools_operators_register() tools_props_register() + anim_register() sm64_col_register() sm64_bone_register() sm64_cam_register() @@ -134,8 +148,8 @@ def sm64_register(register_panels: bool): sm64_spline_register() sm64_dl_writer_register() sm64_dl_parser_register() - sm64_anim_register() settings_props_register() + register_class(SM64_ActionProperty) if register_panels: sm64_panel_register() @@ -144,6 +158,7 @@ def sm64_register(register_panels: bool): def sm64_unregister(unregister_panels: bool): tools_operators_unregister() tools_props_unregister() + anim_unregister() sm64_col_unregister() sm64_bone_unregister() sm64_cam_unregister() @@ -154,8 +169,8 @@ def sm64_unregister(unregister_panels: bool): sm64_spline_unregister() sm64_dl_writer_unregister() sm64_dl_parser_unregister() - sm64_anim_unregister() settings_props_unregister() + unregister_class(SM64_ActionProperty) if unregister_panels: sm64_panel_unregister() diff --git a/fast64_internal/sm64/animation/__init__.py b/fast64_internal/sm64/animation/__init__.py new file mode 100644 index 000000000..0aca0cc02 --- /dev/null +++ b/fast64_internal/sm64/animation/__init__.py @@ -0,0 +1,15 @@ +from .operators import anim_ops_register, anim_ops_unregister +from .properties import anim_props_register, anim_props_unregister, SM64_ArmatureAnimProperties, SM64_ActionAnimProperty +from .panels import anim_panel_register, anim_panel_unregister +from .exporting import export_animation, export_animation_table +from .utility import get_anim_obj, is_obj_animatable + + +def anim_register(): + anim_ops_register() + anim_props_register() + + +def anim_unregister(): + anim_ops_unregister() + anim_props_unregister() diff --git a/fast64_internal/sm64/animation/classes.py b/fast64_internal/sm64/animation/classes.py new file mode 100644 index 000000000..52ca93bb7 --- /dev/null +++ b/fast64_internal/sm64/animation/classes.py @@ -0,0 +1,1006 @@ +from typing import Optional +from pathlib import Path +from enum import IntFlag +from io import StringIO +from copy import copy +import dataclasses +import numpy as np +import functools +import typing +import re + +from bpy.types import Action + +from ...f3d.f3d_parser import math_eval + +from ...utility import PluginError, cast_integer, encodeSegmentedAddr, intToHex +from ..sm64_constants import MAX_U16, SegmentData +from ..sm64_utility import CommentMatch, adjust_start_end +from ..sm64_classes import RomReader, DMATable, DMATableElement, IntArray + +from .constants import HEADER_STRUCT, HEADER_SIZE, TABLE_ELEMENT_PATTERN +from .utility import get_dma_header_name, get_dma_anim_name + + +@dataclasses.dataclass +class CArrayDeclaration: + name: str = "" + path: Path = Path("") + file_name: str = "" + values: list[str] | dict[str, str] = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class SM64_AnimPair: + values: np.ndarray[typing.Any, np.dtype[np.int16]] = dataclasses.field(compare=False) + + # Importing + address: int = 0 + end_address: int = 0 + + offset: int = 0 # For compressing + + def __post_init__(self): + assert self.values.size > 0, "values cannot be empty" + + def clean_frames(self): + mask = self.values != self.values[-1] + # Reverse the order, find the last element with the same value + index = np.argmax(mask[::-1]) + if index != 1: + self.values = self.values[: 1 if index == 0 else (-index + 1)] + return self + + def get_frame(self, frame: int): + return self.values[min(frame, len(self.values) - 1)] + + +@dataclasses.dataclass +class SM64_AnimData: + pairs: list[SM64_AnimPair] = dataclasses.field(default_factory=list) + indice_reference: str | int = "" + values_reference: str | int = "" + + # Importing + indices_file_name: str = "" + values_file_name: str = "" + value_end_address: int = 0 + indice_end_address: int = 0 + start_address: int = 0 + end_address: int = 0 + + @property + def key(self): + return (self.indice_reference, self.values_reference) + + def create_tables(self, start_address=-1): + indice_tables, value_tables = create_tables([self], start_address=start_address) + assert ( + len(value_tables) == 1 and len(indice_tables) == 1 + ), "Single animation data export should only return 1 of each table." + return indice_tables[0], value_tables[0] + + def to_c(self, dma: bool = False): + text_data = StringIO() + + indice_table, value_table = self.create_tables() + if dma: + indice_table.to_c(text_data, new_lines=2) + value_table.to_c(text_data) + else: + value_table.to_c(text_data, new_lines=2) + indice_table.to_c(text_data) + + return text_data.getvalue() + + def to_binary(self, start_address=-1): + indice_table, value_table = self.create_tables(start_address) + values_offset = len(indice_table.data) * 2 + + data = bytearray() + data.extend(indice_table.to_binary()) + data.extend(value_table.to_binary()) + return data, values_offset + + def read_binary(self, indices_reader: RomReader, values_reader: RomReader, bone_count: int): + print( + f"Reading pairs from indices table at {intToHex(indices_reader.address)}", + f"and values table at {intToHex(values_reader.address)}.", + ) + self.indice_reference = indices_reader.start_address + self.values_reference = values_reader.start_address + + # 3 pairs per bone + 3 for root translation of 2, each 2 bytes + indices_size = (((bone_count + 1) * 3) * 2) * 2 + indices_values = np.frombuffer(indices_reader.read_data(indices_size), dtype=">u2") + for i in range(0, len(indices_values), 2): + max_frame, offset = indices_values[i], indices_values[i + 1] + address, size = values_reader.start_address + (offset * 2), max_frame * 2 + + values = np.frombuffer(values_reader.read_data(size, address), dtype=">i2", count=max_frame) + self.pairs.append(SM64_AnimPair(values, address, address + size, offset).clean_frames()) + self.indice_end_address = indices_reader.address + self.value_end_address = max(pair.end_address for pair in self.pairs) + + self.start_address = min(self.indice_reference, self.values_reference) + self.end_address = max(self.indice_end_address, self.value_end_address) + return self + + def read_c(self, indice_decl: CArrayDeclaration, value_decl: CArrayDeclaration): + print(f'Reading data from "{indice_decl.name}" and "{value_decl.name}" c declarations.') + self.indices_file_name, self.values_file_name = indice_decl.file_name, value_decl.file_name + self.indice_reference, self.values_reference = indice_decl.name, value_decl.name + + indices_values = np.vectorize(lambda x: int(x, 0), otypes=[np.uint16])(indice_decl.values) + values_array = np.vectorize(lambda x: int(x, 0), otypes=[np.int16])(value_decl.values) + + for i in range(0, len(indices_values), 2): + max_frame, offset = indices_values[i], indices_values[i + 1] + self.pairs.append(SM64_AnimPair(values_array[offset : offset + max_frame], -1, -1, offset).clean_frames()) + return self + + +class SM64_AnimFlags(IntFlag): + prop: Optional[str] + + def __new__(cls, value, blender_prop: str | None = None): + obj = int.__new__(cls, value) + obj._value_, obj.prop = 1 << value, blender_prop + return obj + + ANIM_FLAG_NOLOOP = (0, "no_loop") + ANIM_FLAG_FORWARD = (1, "backwards") + ANIM_FLAG_2 = (2, "no_acceleration") + ANIM_FLAG_HOR_TRANS = (3, "only_vertical") + ANIM_FLAG_VERT_TRANS = (4, "only_horizontal") + ANIM_FLAG_5 = (5, "disabled") + ANIM_FLAG_6 = (6, "no_trans") + ANIM_FLAG_7 = 7 + + ANIM_FLAG_BACKWARD = (1, "backwards") # refresh 16 + + # hackersm64 + ANIM_FLAG_NO_ACCEL = (2, "no_acceleration") + ANIM_FLAG_DISABLED = (5, "disabled") + ANIM_FLAG_NO_TRANS = (6, "no_trans") + ANIM_FLAG_UNUSED = 7 + + @classmethod + @functools.cache + def all_flags(cls): + flags = SM64_AnimFlags(0) + for flag in cls.__members__.values(): + flags |= flag + return flags + + @classmethod + @functools.cache + def all_flags_with_prop(cls): + flags = SM64_AnimFlags(0) + for flag in cls.__members__.values(): + if flag.prop is not None: + flags |= flag + return flags + + @classmethod + @functools.cache + def props_to_flags(cls): + return {flag.prop: flag for flag in cls.__members__.values() if flag.prop is not None} + + @classmethod + @functools.cache + def flags_to_names(cls): + names: dict[SM64_AnimFlags, list[str]] = {} + for name, flag in cls.__members__.items(): + if flag in names: + names[flag].append(name) + else: + names[flag] = [name] + return names + + @property + @functools.cache + def names(self): + names: list[str] = [] + for flag, flag_names in SM64_AnimFlags.flags_to_names().items(): + if flag in self: + names.append("/".join(flag_names)) + if self & ~self.__class__.all_flags(): # flag value outside known flags + names.append("unknown bits") + return names + + @classmethod + @functools.cache + def evaluate(cls, value: str | int): + if isinstance(value, cls): # the value was already evaluated + return value + elif isinstance(value, str): + try: + value = cls(math_eval(value, cls)) + except Exception as exc: # pylint: disable=broad-except + print(f"Failed to evaluate flags {value}: {exc}") + if isinstance(value, int): # the value was fully evaluated + if isinstance(value, cls): + value = value.value + # cast to u16 for simplicity + return cls(cast_integer(value, 16, signed=False)) + else: # the value was not evaluated + return value + + +@dataclasses.dataclass +class SM64_AnimHeader: + reference: str | int = "" + flags: SM64_AnimFlags | str = SM64_AnimFlags(0) + trans_divisor: int = 0 + start_frame: int = 0 + loop_start: int = 0 + loop_end: int = 1 + bone_count: int = 0 + length: int = 0 + indice_reference: Optional[str | int] = None + values_reference: Optional[str | int] = None + data: Optional[SM64_AnimData] = None + + enum_name: str = "" + # Imports + file_name: str = "" + end_address: int = 0 + header_variant: int = 0 + table_index: int = 0 + action: Action | None = None + + @property + def data_key(self): + return (self.indice_reference, self.values_reference) + + @property + def flags_comment(self): + if isinstance(self.flags, SM64_AnimFlags): + return ", ".join(self.flags.names) + return "" + + @property + def c_flags(self): + return self.flags if isinstance(self.flags, str) else intToHex(self.flags.value, 2) + + def get_reference(self, override: Optional[str | int], expected_type: type, reference_name: str): + name = reference_name.replace("_", " ") + if override: + reference = override + elif self.data and getattr(self.data, reference_name): + reference = getattr(self.data, reference_name) + elif getattr(self, reference_name): + reference = getattr(self, reference_name) + else: + assert False, f"Unknown {name}" + + assert isinstance( + reference, expected_type + ), f"{name.capitalize()} must be a {expected_type},is instead {type(reference)}." + return reference + + def get_values_reference(self, override: Optional[str | int] = None, expected_type: type = str): + return self.get_reference(override, expected_type, "values_reference") + + def get_indice_reference(self, override: Optional[str | int] = None, expected_type: type = str): + return self.get_reference(override, expected_type, "indice_reference") + + def to_c(self, values_override: Optional[str] = None, indice_override: Optional[str] = None, dma=False): + assert not dma or isinstance( # assert if dma and flags are not SM64_AnimFlags + self.flags, SM64_AnimFlags + ), f"Flags must be SM64_AnimFlags for C DMA, is instead {type(self.flags)}" + return ( + f"static const struct Animation {self.reference}{'[]' if dma else ''} = {{\n" + + f"\t{self.c_flags}, // flags {self.flags_comment}\n" + f"\t{self.trans_divisor}, // animYTransDivisor\n" + f"\t{self.start_frame}, // startFrame\n" + f"\t{self.loop_start}, // loopStart\n" + f"\t{self.loop_end}, // loopEnd\n" + f"\tANIMINDEX_NUMPARTS({self.get_indice_reference(indice_override, str)}), // unusedBoneCount\n" + f"\t{self.get_values_reference(values_override, str)}, // values\n" + f"\t{self.get_indice_reference(indice_override, str)}, // index\n" + "\t0 // length\n" + "};\n" + ) + + def to_binary( + self, + values_override: Optional[int] = None, + indice_override: Optional[int] = None, + segment_data: SegmentData | None = None, + length=0, + ): + assert isinstance( + self.flags, SM64_AnimFlags + ), f"Flags must be SM64_AnimFlags for binary, is instead {type(self.flags)}" + values_address = self.get_values_reference(values_override, int) + indice_address = self.get_indice_reference(indice_override, int) + if segment_data: + values_address = int.from_bytes(encodeSegmentedAddr(values_address, segment_data), "big") + indice_address = int.from_bytes(encodeSegmentedAddr(indice_address, segment_data), "big") + + return HEADER_STRUCT.pack( + self.flags.value, + self.trans_divisor, + self.start_frame, + self.loop_start, + self.loop_end, + self.bone_count, + values_address, + indice_address, + length, + ) + + @staticmethod + def read_binary( + reader: RomReader, + read_headers: dict[str, "SM64_AnimHeader"], + dma: bool = False, + bone_count: Optional[int] = None, + table_index: Optional[int] = None, + ): + if str(reader.start_address) in read_headers: + return read_headers[str(reader.start_address)] + print(f"Reading animation header at {intToHex(reader.start_address)}.") + + header = SM64_AnimHeader() + read_headers[str(reader.start_address)] = header + header.reference = reader.start_address + + header.flags = SM64_AnimFlags.evaluate(reader.read_int(2, True)) # /*0x00*/ s16 flags; + header.trans_divisor = reader.read_int(2, True) # /*0x02*/ s16 animYTransDivisor; + header.start_frame = reader.read_int(2, True) # /*0x04*/ s16 startFrame; + header.loop_start = reader.read_int(2, True) # /*0x06*/ s16 loopStart; + header.loop_end = reader.read_int(2, True) # /*0x08*/ s16 loopEnd; + + # /*0x0A*/ s16 unusedBoneCount; (Unused in engine) + header.bone_count = reader.read_int(2, True) + if header.bone_count <= 0: + if bone_count is None: + raise PluginError( + "No bone count in header and no bone count passed in from target armature, cannot figure out" + ) + header.bone_count = bone_count + print("Old exports lack a defined bone count, invalid armatures won't be detected") + elif bone_count is not None and header.bone_count != bone_count: + raise PluginError( + f"Imported header's bone count is {header.bone_count} but object's is {bone_count}", + ) + + # /*0x0C*/ const s16 *values; + # /*0x10*/ const u16 *index; + if dma: + header.values_reference = reader.start_address + reader.read_int(4) + header.indice_reference = reader.start_address + reader.read_int(4) + else: + header.values_reference, header.indice_reference = reader.read_ptr(), reader.read_ptr() + header.length = reader.read_int(4) + + header.end_address = reader.address + 1 + header.table_index = len(read_headers) if table_index is None else table_index + + data = next( + (other_header.data for other_header in read_headers.values() if header.data_key == other_header.data_key), + None, + ) + if not data: + indices_reader = reader.branch(header.indice_reference) + values_reader = reader.branch(header.values_reference) + if indices_reader and values_reader: + data = SM64_AnimData().read_binary( + indices_reader, + values_reader, + header.bone_count, + ) + header.data = data + + return header + + @staticmethod + def read_c( + header_decl: CArrayDeclaration, + value_decls, + indices_decls, + read_headers: dict[str, "SM64_AnimHeader"], + table_index: Optional[int] = None, + ): + if header_decl.name in read_headers: + return read_headers[header_decl.name] + if len(header_decl.values) != 9: + raise ValueError(f"Header declarion has {len(header_decl.values)} values instead of 9.\n {header_decl}") + print(f'Reading header "{header_decl.name}" c declaration.') + header = SM64_AnimHeader() + read_headers[header_decl.name] = header + header.reference = header_decl.name + header.file_name = header_decl.file_name + + # Place the values into a dictionary, handles designated initialization + if isinstance(header_decl.values, list): + designated = {} + for value, var in zip( + header_decl.values, + [ + "flags", + "animYTransDivisor", + "startFrame", + "loopStart", + "loopEnd", + "unusedBoneCount", + "values", + "index", + "length", + ], + ): + designated[var] = value + else: + designated = header_decl.values + + # Read from the dict + header.flags = SM64_AnimFlags.evaluate(designated["flags"]) + header.trans_divisor = int(designated["animYTransDivisor"], 0) + header.start_frame = int(designated["startFrame"], 0) + header.loop_start = int(designated["loopStart"], 0) + header.loop_end = int(designated["loopEnd"], 0) + # bone_count = designated["unusedBoneCount"] + header.values_reference = designated["values"] + header.indice_reference = designated["index"] + + header.table_index = len(read_headers) if table_index is None else table_index + + data = next( + (other_header.data for other_header in read_headers.values() if header.data_key == other_header.data_key), + None, + ) + if not data: + indices_decl = next((indice for indice in indices_decls if indice.name == header.indice_reference), None) + value_decl = next((value for value in value_decls if value.name == header.values_reference), None) + if indices_decl and value_decl: + data = SM64_AnimData().read_c(indices_decl, value_decl) + header.data = data + + return header + + +@dataclasses.dataclass +class SM64_Anim: + data: SM64_AnimData | None = None + headers: list[SM64_AnimHeader] = dataclasses.field(default_factory=list) + file_name: str = "" + + # Imports + action_name: str = "" # Used for the blender action's name + action: Action | None = None # Used in the table class to prop function + + @property + def names(self) -> tuple[list[str], list[str]]: + names, enums = [], [] + for header in self.headers: + names.append(header.reference) + enums.append(header.enum_name) + return names, enums + + @property + def header_names(self) -> list[str]: + return self.names[0] + + @property + def enum_names(self) -> list[str]: + return self.names[1] + + def to_binary_dma(self): + assert self.data + headers: list[bytes] = [] + + indice_offset = HEADER_SIZE * len(self.headers) + anim_data, values_offset = self.data.to_binary() + for header in self.headers: + header_data = header.to_binary( + indice_offset + values_offset, indice_offset, length=indice_offset + len(anim_data) + ) + headers.append(header_data) + indice_offset -= HEADER_SIZE + return headers, anim_data + + def to_binary(self, start_address: int = 0, segment_data: SegmentData | None = None): + data: bytearray = bytearray() + ptrs: list[int] = [] + if self.data: + anim_data, values_offset = self.data.to_binary() + indice_offset = start_address + (HEADER_SIZE * len(self.headers)) + values_offset = indice_offset + values_offset + else: + anim_data = bytearray() + indice_offset = values_offset = None + for header in self.headers: + if self.data: + ptrs.extend([start_address + len(data) + 12, start_address + len(data) + 16]) + header_data = header.to_binary( + values_offset, + indice_offset, + segment_data, + ) + data.extend(header_data) + + data.extend(anim_data) + return data, ptrs + + def headers_to_c(self, dma: bool) -> str: + text_data = StringIO() + for header in self.headers: + text_data.write(header.to_c(dma=dma)) + text_data.write("\n") + return text_data.getvalue() + + def to_c(self, dma: bool): + text_data = StringIO() + c_headers = self.headers_to_c(dma) + if dma: + text_data.write(c_headers) + text_data.write("\n") + if self.data: + text_data.write(self.data.to_c(dma)) + text_data.write("\n") + if not dma: + text_data.write(c_headers) + return text_data.getvalue() + + +@dataclasses.dataclass +class SM64_AnimTableElement: + reference: str | int | None = None + header: SM64_AnimHeader | None = None + + # C exporting + enum_name: str = "" + reference_start: int = -1 + reference_end: int = -1 + enum_start: int = -1 + enum_end: int = -1 + enum_val: str = "" + + @property + def c_name(self): + if self.reference: + return self.reference + return "" + + @property + def c_reference(self): + if self.reference: + return f"&{self.reference}" + return "NULL" + + @property + def enum_c(self): + if self.enum_val: + return f"{self.enum_name} = {self.enum_val}" + return self.enum_name + + @property + def data(self): + return self.header.data if self.header else None + + def to_c(self, designated: bool): + if designated and self.enum_name: + return f"[{self.enum_name}] = {self.c_reference}," + else: + return f"{self.c_reference}," + + +@dataclasses.dataclass +class SM64_AnimTable: + reference: str | int = "" + enum_list_reference: str = "" + enum_list_delimiter: str = "" + file_name: str = "" + elements: list[SM64_AnimTableElement] = dataclasses.field(default_factory=list) + # Importing + end_address: int = 0 + # C exporting + values_reference: str = "" + start: int = -1 + end: int = -1 + enum_list_start: int = -1 + enum_list_end: int = -1 + + @property + def names(self) -> tuple[list[str], list[str]]: + names, enums = [], [] + for element in self.elements: + names.append(element.c_name) + enums.append(element.enum_name) + return names, enums + + @property + def header_names(self) -> list[str]: + return self.names[0] + + @property + def enum_names(self) -> list[str]: + return self.names[1] + + @property + def header_data_sets(self) -> tuple[list[SM64_AnimHeader], list[SM64_AnimData]]: + # Remove duplicates of data and headers, keep order by using a list + data_set = [] + headers_set = [] + for element in self.elements: + if element.data and not element.data in data_set: + data_set.append(element.data) + if element.header and not element.header in headers_set: + headers_set.append(element.header) + return headers_set, data_set + + @property + def header_set(self) -> list[SM64_AnimHeader]: + return self.header_data_sets[0] + + @property + def has_null_delimiter(self): + return bool(self.elements and self.elements[-1].reference is None) + + def get_seperate_anims(self): + print("Getting seperate animations from table.") + anims: list[SM64_Anim] = [] + headers_set, headers_added = self.header_set, [] + for header in headers_set: + if header in headers_added: + continue + ordered_headers: list[SM64_AnimHeader] = [] + variant = 0 + for other_header in headers_set: + if other_header.data == header.data: + other_header.header_variant = variant + ordered_headers.append(other_header) + headers_added.append(other_header) + variant += 1 + + anims.append(SM64_Anim(header.data, ordered_headers, header.file_name)) + return anims + + def get_seperate_anims_dma(self) -> list[SM64_Anim]: + print("Getting seperate DMA animations from table.") + + anims = [] + header_nums = [] + included_headers: list[SM64_AnimHeader] = [] + data = None + # For creating duplicates + data_already_added = [] + headers_already_added = [] + + for i, element in enumerate(self.elements): + assert element.header, f"Header in table element {i} is not set." + assert element.data, f"Data in table element {i} is not set." + header_nums.append(i) + + header, data = element.header, element.data + if header in headers_already_added: + print(f"Made duplicate of header {i}.") + header = copy(header) + header.reference = get_dma_header_name(i) + headers_already_added.append(header) + + included_headers.append(header) + + # If not at the end of the list and the next element doesn´t have different data + if (i < len(self.elements) - 1) and self.elements[i + 1].data is data: + continue + + name = get_dma_anim_name(header_nums) + file_name = f"{name}.inc.c" + if data in data_already_added: + print(f"Made duplicate of header {i}'s data.") + data = copy(data) + data_already_added.append(data) + + data.indice_reference, data.values_reference = f"{name}_indices", f"{name}_values" + # Normal names are possible (order goes by line and file) but would break convention + for i, included_header in enumerate(included_headers): + included_header.file_name = file_name + included_header.indice_reference = data.indice_reference + included_header.values_reference = data.values_reference + included_header.data = data + included_header.header_variant = i + anims.append(SM64_Anim(data, included_headers, file_name)) + + header_nums.clear() + included_headers = [] + + return anims + + def to_binary_dma(self): + dma_table = DMATable() + for animation in self.get_seperate_anims_dma(): + headers, data = animation.to_binary_dma() + end_offset = len(dma_table.data) + (HEADER_SIZE * len(headers)) + len(data) + for header in headers: + offset = len(dma_table.data) + size = end_offset - offset + dma_table.entries.append(DMATableElement(offset, size)) + dma_table.data.extend(header) + dma_table.data.extend(data) + return dma_table.to_binary() + + def to_combined_binary(self, table_address=0, data_address=-1, segment_data: SegmentData | None = None): + table_data: bytearray = bytearray() + data: bytearray = bytearray() + ptrs: list[int] = [] + headers_set, data_set = self.header_data_sets + + # Pre calculate offsets + table_length = len(self.elements) * 4 + if data_address == -1: + data_address = table_address + table_length + + headers_length = len(headers_set) * HEADER_SIZE + indice_tables, value_tables = create_tables(data_set, self.values_reference, data_address + headers_length) + + # Add the animation table + for i, element in enumerate(self.elements): + if element.header: + ptrs.append(table_address + len(table_data)) + header_offset = data_address + (headers_set.index(element.header) * HEADER_SIZE) + if segment_data: + table_data.extend(encodeSegmentedAddr(header_offset, segment_data)) + else: + table_data.extend(header_offset.to_bytes(4, byteorder="big")) + continue + if element.reference is None: + table_data.extend(0x0.to_bytes(4, byteorder="big")) + continue + assert isinstance(element.reference, int), f"Reference at element {i} is not an int." + table_data.extend(element.reference.to_bytes(4, byteorder="big")) + + for anim_header in headers_set: # Add the headers + if not anim_header.data: + data.extend(anim_header.to_binary()) + continue + ptrs.extend([data_address + len(data) + 12, data_address + len(data) + 16]) + data.extend(anim_header.to_binary(segment_data=segment_data)) + + for table in indice_tables + value_tables: + data.extend(table.to_binary()) + + return table_data, data, ptrs + + def data_and_headers_to_c(self, dma: bool): + files_data: dict[str, str] = {} + animation: SM64_Anim + for animation in self.get_seperate_anims_dma() if dma else self.get_seperate_anims(): + files_data[animation.file_name] = animation.to_c(dma=dma) + return files_data + + def data_and_headers_to_c_combined(self): + text_data = StringIO() + headers_set, data_set = self.header_data_sets + if data_set: + indice_tables, value_tables = create_tables(data_set, self.values_reference) + for table in value_tables + indice_tables: + table.to_c(text_data, new_lines=2) + for anim_header in headers_set: + text_data.write(anim_header.to_c()) + text_data.write("\n") + + return text_data.getvalue() + + def read_binary( + self, + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + size: Optional[int] = None, + ): + print(f"Reading table at address {reader.start_address}.") + self.elements.clear() + self.reference = reader.start_address + + range_size = size or 300 + if table_index is not None: + range_size = min(range_size, table_index + 1) + for i in range(range_size): + ptr = reader.read_ptr() + if size is None and ptr == 0: # If no specified size and ptr is NULL, break + self.elements.append(SM64_AnimTableElement()) + break + elif table_index is not None and i != table_index: + continue # Skip entries until table_index if specified + + header_reader = reader.branch(ptr) + if header_reader is None: + self.elements.append(SM64_AnimTableElement(ptr)) + else: + try: + header = SM64_AnimHeader.read_binary( + header_reader, + read_headers, + False, + bone_count, + i, + ) + except Exception as exc: + raise PluginError(f"Failed to read header in table element {i}: {str(exc)}") from exc + self.elements.append(SM64_AnimTableElement(ptr, header)) + + if table_index is not None: # Break if table_index is specified + break + else: + if table_index is not None: + raise PluginError(f"Table index {table_index} not found in table.") + if size is None: + raise PluginError(f"Iterated through {range_size} elements and no NULL was found.") + self.end_address = reader.address + return self + + def read_dma_binary( + self, + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + ): + dma_table = DMATable() + dma_table.read_binary(reader) + self.reference = reader.start_address + if table_index is not None: + assert table_index >= 0 and table_index < len( + dma_table.entries + ), f"Index {table_index} outside of defined table ({len(dma_table.entries)} entries)." + entrie = dma_table.entries[table_index] + header_reader = reader.branch(entrie.address) + if header_reader is None: + raise PluginError("Failed to branch into DMA entrie's address") + return SM64_AnimHeader.read_binary( + header_reader, + read_headers, + True, + bone_count, + table_index, + ) + + for i, entrie in enumerate(dma_table.entries): + header_reader = reader.branch(entrie.address) + try: + if not header_reader: + raise PluginError("Failed to branch to header's address") + header = SM64_AnimHeader.read_binary(header_reader, read_headers, True, bone_count, i) + except Exception as exc: + raise PluginError(f"Failed to read header in table element {i}: {str(exc)}") from exc + self.elements.append(SM64_AnimTableElement(reader.start_address, header)) + self.end_address = dma_table.end_address + return self + + def read_c( + self, + c_data: str, + start: int, + end: int, + comment_map: list[CommentMatch], + read_headers: dict[str, SM64_AnimHeader], + header_decls: list[CArrayDeclaration], + values_decls: list[CArrayDeclaration], + indices_decls: list[CArrayDeclaration], + ): + table_start, table_end = adjust_start_end(start, end, comment_map) + self.start, self.end = table_start, table_end + + for i, element_match in enumerate(re.finditer(TABLE_ELEMENT_PATTERN, c_data[start:end])): + enum, element, null = ( + element_match.group("enum"), + element_match.group("element"), + element_match.group("null"), + ) + if enum is None and element is None and null is None: # comment + continue + header = None + if element is not None: + header_decl = next((header for header in header_decls if header.name == element), None) + if header_decl: + header = SM64_AnimHeader.read_c( + header_decl, + values_decls, + indices_decls, + read_headers, + i, + ) + element_start, element_end = adjust_start_end( + table_start + element_match.start(), table_start + element_match.end(), comment_map + ) + self.elements.append( + SM64_AnimTableElement( + element, + enum_name=enum, + reference_start=element_start - table_start, + reference_end=element_end - table_start, + header=header, + ) + ) + + +def create_tables(anims_data: list[SM64_AnimData], values_name="", start_address=-1): + """ + Can generate multiple indices table with only one value table (or multiple if needed), + which improves compression (this feature is used in table exports). + Update the animation data with the correct references. + Returns: indice_tables, value_tables (in that order) + """ + + def add_data(values_table: IntArray, size: int, anim_data: SM64_AnimData, values_address: int): + data = values_table.data + for pair in anim_data.pairs: + pair_values = pair.values + if len(pair_values) >= MAX_U16: + raise PluginError( + f"Pair frame count ({len(pair_values)}) is higher than the 16 bit max ({MAX_U16}). Too many frames." + ) + + # It's never worth it to find an existing offset for values bigger than 1 frame. + # From my (@Lilaa3) testing, the only improvement in Mario resulted in just 286 bytes saved. + offset = None + if len(pair_values) == 1: + indices = np.isin(data[:size], pair_values[0]).nonzero()[0] + offset = indices[0] if indices.size > 0 else None + + if offset is None: # no existing offset found + offset = size + size = offset + len(pair_values) + if size > MAX_U16: # exceeded limit, but we may be able to recover with a new table + return -1, None + data[offset:size] = pair_values + pair.offset = offset + + # build indice table + indice_values = np.empty((len(anim_data.pairs), 2), np.uint16) + for i, pair in enumerate(anim_data.pairs): + indice_values[i] = [len(pair.values), pair.offset] # Use calculated offsets + indice_values = indice_values.reshape(-1) + indice_table = IntArray(indice_values, str(anim_data.indice_reference), 6, -6) + + if values_address == -1: + anim_data.values_reference = value_table.name + else: + anim_data.values_reference = values_address + return size, indice_table + + indice_tables: list[IntArray] = [] + value_tables: list[IntArray] = [] + + values_name = values_name or str(anims_data[0].values_reference) + indices_address = start_address + if start_address != -1: + for anim_data in anims_data: + anim_data.indice_reference = indices_address + indices_address += len(anim_data.pairs) * 2 * 2 + values_address = indices_address + + print("Generating compressed value table and offsets.") + # opt: this is the max size possible, prevents tons of allocations and only about 65 kb + value_table = IntArray(np.empty(MAX_U16, np.int16), values_name, 8) + size = 0 + value_tables.append(value_table) + i = 0 # we can´t use enumarate, as we may repeat + while i < len(anims_data): + anim_data = anims_data[i] + + size_before_add = size + size, indice_table = add_data(value_table, size, anim_data, values_address) + if size != -1: # sucefully added the data to the value table + assert indice_table is not None + indice_tables.append(indice_table) + i += 1 # do the next animation + else: # Could not add to the value table + if size_before_add == 0: # If the table was empty, it is simply invalid + raise PluginError(f"Index table cannot fit into value table of 16 bit max size ({MAX_U16}).") + else: # try again with a fresh value table + value_table.data.resize(size_before_add, refcheck=False) + if start_address != -1: + values_address += size_before_add * 2 + value_table = IntArray(np.empty(MAX_U16, np.int16), f"{values_name}_{len(value_tables)}", 9) + value_tables.append(value_table) + size = 0 # reset size + # don't increment i, redo + value_table.data.resize(size, refcheck=False) + + return indice_tables, value_tables diff --git a/fast64_internal/sm64/animation/constants.py b/fast64_internal/sm64/animation/constants.py new file mode 100644 index 000000000..7ea4bbdc4 --- /dev/null +++ b/fast64_internal/sm64/animation/constants.py @@ -0,0 +1,88 @@ +import struct +import re + +from ...utility import intToHex +from ..sm64_constants import ACTOR_PRESET_INFO, ActorPresetInfo + +HEADER_STRUCT = struct.Struct(">h h h h h h I I I") +HEADER_SIZE = HEADER_STRUCT.size + +TABLE_ELEMENT_PATTERN = re.compile( # strict but only in the sense that it requires valid c code + r""" + (?:\[\s*(?P\w+)\s*\]\s*=\s*)? # Don´t capture brackets or equal, works with nums + (?:(?:&\s*(?P\w+))|(?PNULL)) # Capture element or null, element requires & + (?:\s*,|) # allow no comma, techinically not correct but no other method works + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_PATTERN = re.compile( + r""" + const\s+struct\s*Animation\s*\*const\s*(?P\w+)\s* + (?:\[.*?\])? # Optional size, don´t capture + \s*=\s*\{ + (?P[\s\S]*) # Capture any character including new lines + (?=\}\s*;) # Look ahead for the end + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_ENUM_PATTERN = re.compile( # strict but only in the sense that it requires valid c code + r""" + (?P\w+)\s* + (?:\s*=\s*(?P\w+)\s*)? + (?=,|) # lookahead, allow no comma, techinically not correct but no other method works + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_ENUM_LIST_PATTERN = re.compile( + r""" + enum\s*(?P\w+)\s*\{ + (?P[\s\S]*) # Capture any character including new lines, lazy + (?=\}\s*;) + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +enumAnimExportTypes = [ + ("Actor", "Actor Data", "Includes are added to a group in actors/"), + ("Level", "Level Data", "Includes are added to a specific level in levels/"), + ( + "DMA", + "DMA (Mario)", + "No headers or includes are genarated. Mario animation converter order is used (headers, indicies, values)", + ), + ("Custom", "Custom Path", "Exports to a specific path"), +] + +enum_anim_import_types = [ + ("C", "C", "Import a decomp folder or a specific animation"), + ("Binary", "Binary", "Import from ROM"), + ("Insertable Binary", "Insertable Binary", "Import from an insertable binary file"), +] + +enum_anim_binary_import_types = [ + ("DMA", "DMA (Mario)", "Import a DMA animation from a DMA table from a ROM"), + ("Table", "Table", "Import animations from an animation table from a ROM"), + ("Animation", "Animation", "Import one animation from a ROM"), +] + + +enum_animated_behaviours = [("Custom", "Custom Behavior", "Custom"), ("", "Presets", "")] +enum_anim_tables = [("Custom", "Custom", "Custom"), ("", "Presets", "")] +for actor_name, preset_info in ACTOR_PRESET_INFO.items(): + if not preset_info.animation: + continue + behaviours = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.behaviours) + enum_animated_behaviours.extend( + [(intToHex(address), name, intToHex(address)) for name, address in behaviours.items()] + ) + tables = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.address) + enum_anim_tables.extend( + [(name, name, f"{intToHex(address)}, {preset_info.level}") for name, address in tables.items()] + ) diff --git a/fast64_internal/sm64/animation/exporting.py b/fast64_internal/sm64/animation/exporting.py new file mode 100644 index 000000000..dadae5413 --- /dev/null +++ b/fast64_internal/sm64/animation/exporting.py @@ -0,0 +1,1025 @@ +from typing import TYPE_CHECKING, Optional +from pathlib import Path +import os +import typing +import numpy as np + +import bpy +from bpy.types import Object, Action, PoseBone, Context +from bpy.path import abspath +from mathutils import Euler, Quaternion + +from ...utility import ( + PluginError, + bytesToHex, + encodeSegmentedAddr, + decodeSegmentedAddr, + get64bitAlignedAddr, + getPathAndLevel, + getExportDir, + intToHex, + applyBasicTweaks, + toAlnum, + directory_path_checks, +) +from ...utility_anim import stashActionInArmature + +from ..sm64_constants import BEHAVIOR_COMMANDS, BEHAVIOR_EXITS, defaultExtendSegment4, level_pointers +from ..sm64_utility import ( + ModifyFoundDescriptor, + find_descriptor_in_text, + get_comment_map, + to_include_descriptor, + write_includes, + update_actor_includes, + int_from_str, + write_or_delete_if_found, +) +from ..sm64_classes import BinaryExporter, RomReader, InsertableBinaryData +from ..sm64_level_parser import parseLevelAtPointer +from ..sm64_rom_tweaks import ExtendBank0x04 + +from .classes import ( + SM64_Anim, + SM64_AnimHeader, + SM64_AnimData, + SM64_AnimPair, + SM64_AnimTable, + SM64_AnimTableElement, +) +from .importing import import_enums, import_tables, update_table_with_table_enum +from .utility import ( + get_anim_owners, + get_anim_actor_name, + anim_name_to_enum_name, + get_selected_action, + get_action_props, + duplicate_name, +) +from .constants import HEADER_SIZE + +if TYPE_CHECKING: + from .properties import ( + SM64_ActionAnimProperty, + SM64_AnimHeaderProperties, + SM64_ArmatureAnimProperties, + SM64_AnimTableElementProperties, + ) + from ..settings.properties import SM64_Properties + from ..sm64_objects import SM64_CombinedObjectProperties + + +def trim_duplicates_vectorized(arr2d: np.ndarray) -> list: + """ + Similar to the old removeTrailingFrames(), but using numpy vectorization. + Remove trailing duplicate elements along the last axis of a 2D array. + One dimensional example of this in SM64_AnimPair.clean_frames + """ + # Get the last element of each sub-array along the last axis + last_elements = arr2d[:, -1] + mask = arr2d != last_elements[:, None] + # Reverse the order, find the last element with the same value + trim_indices = np.argmax(mask[:, ::-1], axis=1) + # return list(arr2d) # uncomment to test large sizes + return [ + sub_array if index == 1 else sub_array[: 1 if index == 0 else (-index + 1)] + for sub_array, index in zip(arr2d, trim_indices) + ] + + +def get_entire_fcurve_data( + action: Action, + anim_owner: PoseBone | Object, + prop: str, + max_frame: int, + values: np.ndarray[tuple[typing.Any, typing.Any], np.dtype[np.float32]], +): + data_path = anim_owner.path_from_id(prop) + + default_values = list(getattr(anim_owner, prop)) + populated = [False] * len(default_values) + + for fcurve in action.fcurves: + if fcurve.data_path == data_path: + array_index = fcurve.array_index + for frame in range(max_frame): + values[array_index, frame] = fcurve.evaluate(frame) + populated[array_index] = True + + for i, is_populated in enumerate(populated): + if not is_populated: + values[i] = np.full(values[i].size, default_values[i]) + + return values + + +def read_quick(actions, max_frames, anim_owners, trans_values, rot_values): + def to_xyz(row): + euler = Euler(row, mode) + return [euler.x, euler.y, euler.z] + + for action, max_frame, action_trans, action_rot in zip(actions, max_frames, trans_values, rot_values): + quats = np.empty((4, max_frame), dtype=np.float32) + + get_entire_fcurve_data(action, anim_owners[0], "location", max_frame, action_trans) + + for bone_index, anim_owner in enumerate(anim_owners): + mode = anim_owner.rotation_mode + prop = {"QUATERNION": "rotation_quaternion", "AXIS_ANGLE": "rotation_axis_angle"}.get( + mode, "rotation_euler" + ) + + index = bone_index * 3 + if mode == "QUATERNION": + get_entire_fcurve_data(action, anim_owner, prop, max_frame, quats) + action_rot[index : index + 3] = np.apply_along_axis( + lambda row: Quaternion(row).to_euler(), 1, quats.T + ).T + elif mode == "AXIS_ANGLE": + get_entire_fcurve_data(action, anim_owner, prop, max_frame, quats) + action_rot[index : index + 3] = np.apply_along_axis( + lambda row: list(Quaternion(row[1:], row[0]).to_euler()), 1, quats.T + ).T + else: + get_entire_fcurve_data(action, anim_owner, prop, max_frame, action_rot[index : index + 3]) + if mode != "XYZ": + action_rot[index : index + 3] = np.apply_along_axis(to_xyz, -1, action_rot[index : index + 3].T).T + + +def read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, is_owner_obj): + pre_export_frame = bpy.context.scene.frame_current + pre_export_action = obj.animation_data.action + was_playing = bpy.context.screen.is_animation_playing + + try: + if bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() # if an animation is being played, stop it + for action, action_trans, action_rot, max_frame in zip(actions, trans_values, rot_values, max_frames): + print(f'Reading animation data from action "{action.name}".') + obj.animation_data.action = action + for frame in range(max_frame): + bpy.context.scene.frame_set(frame) + + for bone_index, anim_owner in enumerate(anim_owners): + if is_owner_obj: + local_matrix = anim_owner.matrix_local + else: + local_matrix = obj.convert_space( + pose_bone=anim_owner, matrix=anim_owner.matrix, from_space="POSE", to_space="LOCAL" + ) + if bone_index == 0: + action_trans[0:3, frame] = list(local_matrix.to_translation()) + index = bone_index * 3 + action_rot[index : index + 3, frame] = list(local_matrix.to_euler()) + finally: + obj.animation_data.action = pre_export_action + bpy.context.scene.frame_set(pre_export_frame) + if was_playing != bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() + + +def get_animation_pairs( + sm64_scale: float, actions: list[Action], obj: Object, quick_read=False +) -> dict[Action, list[SM64_AnimPair]]: + anim_owners = get_anim_owners(obj) + is_owner_obj = isinstance(obj.type == "MESH", Object) + + if len(anim_owners) == 0: + raise PluginError(f'No animation bones in armature "{obj.name}"') + + if len(actions) < 1: + return {} + + max_frames = [get_action_props(action).get_max_frame(action) for action in actions] + trans_values = [np.zeros((3, max_frame), dtype=np.float32) for max_frame in max_frames] + rot_values = [np.zeros((len(anim_owners) * 3, max_frame), dtype=np.float32) for max_frame in max_frames] + + if quick_read: + read_quick(actions, max_frames, anim_owners, trans_values, rot_values) + else: + read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, is_owner_obj) + + action_pairs = {} + for action, action_trans, action_rot in zip(actions, trans_values, rot_values): + action_trans = trim_duplicates_vectorized(np.round(action_trans * sm64_scale).astype(np.int16)) + action_rot = trim_duplicates_vectorized(np.round(np.degrees(action_rot) * (2**16 / 360.0)).astype(np.int16)) + + pairs = [SM64_AnimPair(values) for values in action_trans] + pairs.extend([SM64_AnimPair(values) for values in action_rot]) + action_pairs[action] = pairs + + return action_pairs + + +def to_header_class( + header_props: "SM64_AnimHeaderProperties", + bone_count: int, + data: SM64_AnimData | None, + action: Action, + values_reference: int | str, + indice_reference: int | str, + dma: bool, + export_type: str, + table_index: Optional[int] = None, + actor_name="mario", + gen_enums=False, + file_name="anim_00.inc.c", +): + header = SM64_AnimHeader() + header.reference = header_props.get_name(actor_name, action, dma) + if gen_enums: + header.enum_name = header_props.get_enum(actor_name, action) + + header.flags = header_props.get_flags(not (export_type.endswith("Binary") or dma)) + header.trans_divisor = header_props.trans_divisor + header.start_frame, header.loop_start, header.loop_end = header_props.get_loop_points(action) + header.values_reference = values_reference + header.indice_reference = indice_reference + header.bone_count = bone_count + header.table_index = header_props.table_index if table_index is None else table_index + header.file_name = file_name + header.data = data + return header + + +def to_data_class(pairs: list[SM64_AnimPair], data_name="anim_00", file_name: str = "anim_00.inc.c"): + return SM64_AnimData(pairs, f"{data_name}_indices", f"{data_name}_values", file_name, file_name) + + +def to_animation_class( + action_props: "SM64_ActionAnimProperty", + action: Action, + obj: Object, + blender_to_sm64_scale: float, + quick_read: bool, + export_type: str, + dma: bool, + actor_name="mario", + gen_enums=False, +) -> SM64_Anim: + can_reference = not dma + animation = SM64_Anim() + animation.file_name = action_props.get_file_name(action, export_type, dma) + + if can_reference and action_props.reference_tables: + if export_type.endswith("Binary"): + values_reference, indice_reference = int_from_str(action_props.values_address), int( + action_props.indices_address, 0 + ) + else: + values_reference, indice_reference = action_props.values_table, action_props.indices_table + else: + pairs = get_animation_pairs(blender_to_sm64_scale, [action], obj, quick_read)[action] + animation.data = to_data_class(pairs, action_props.get_name(action, dma), animation.file_name) + values_reference = animation.data.values_reference + indice_reference = animation.data.indice_reference + bone_count = len(get_anim_owners(obj)) + for header_props in action_props.headers: + animation.headers.append( + to_header_class( + header_props=header_props, + bone_count=bone_count, + data=animation.data, + action=action, + values_reference=values_reference, + indice_reference=indice_reference, + dma=dma, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + file_name=animation.file_name, + table_index=None, + ) + ) + + return animation + + +def to_table_element_class( + element_props: "SM64_AnimTableElementProperties", + header_dict: dict["SM64_AnimHeaderProperties", SM64_AnimHeader], + data_dict: dict[Action, SM64_AnimData], + action_pairs: dict[Action, list[SM64_AnimPair]], + bone_count: int, + table_index: int, + dma: bool, + export_type: str, + actor_name="mario", + gen_enums=False, + prev_enums: dict[str, int] | None = None, +): + prev_enums = prev_enums or {} + use_addresses, can_reference = export_type.endswith("Binary"), not dma + element = SM64_AnimTableElement() + + enum = None + if gen_enums: + enum = element_props.get_enum(can_reference, actor_name, prev_enums) + element.enum_name = enum + + if can_reference and element_props.reference: + reference = int_from_str(element_props.header_address) if use_addresses else element_props.header_name + element.reference = reference + if reference == "": + raise PluginError("Header is not set.") + if enum == "": + raise PluginError("Enum name is not set.") + return element + + # Not reference + header_props, action = element_props.get_header(can_reference), element_props.get_action(can_reference) + if not action: + raise PluginError("Action is not set.") + if not header_props: + raise PluginError("Header is not set.") + if enum == "": + raise PluginError("Enum name is not set.") + + action_props = get_action_props(action) + if can_reference and action_props.reference_tables: + data = None + if use_addresses: + values_reference, indice_reference = ( + int_from_str(action_props.values_address), + int_from_str(action_props.indices_address), + ) + else: + values_reference, indice_reference = action_props.values_table, action_props.indices_table + else: + if action in action_pairs and action not in data_dict: + data_dict[action] = to_data_class( + action_pairs[action], + action_props.get_name(action, dma), + action_props.get_file_name(action, export_type, dma), + ) + data = data_dict[action] + values_reference, indice_reference = data.values_reference, data.indice_reference + + if header_props not in header_dict: + header_dict[header_props] = to_header_class( + header_props=header_props, + bone_count=bone_count, + data=data, + action=action, + values_reference=values_reference, + indice_reference=indice_reference, + dma=dma, + export_type=export_type, + table_index=table_index, + actor_name=actor_name, + gen_enums=gen_enums, + file_name=action_props.get_file_name(action, export_type), + ) + + element.header = header_dict[header_props] + element.reference = element.header.reference + return element + + +def to_table_class( + anim_props: "SM64_ArmatureAnimProperties", + obj: Object, + blender_to_sm64_scale: float, + quick_read: bool, + dma: bool, + export_type: str, + actor_name="mario", + gen_enums=False, +) -> SM64_AnimTable: + can_reference = not dma + table = SM64_AnimTable( + anim_props.get_table_name(actor_name), + anim_props.get_enum_name(actor_name), + anim_props.get_enum_end(actor_name), + anim_props.get_table_file_name(actor_name, export_type), + values_reference=toAlnum(f"anim_{actor_name}_values"), + ) + + header_dict: dict[SM64_AnimHeaderProperties, SM64_AnimHeader] = {} + + bone_count = len(get_anim_owners(obj)) + action_pairs = get_animation_pairs( + blender_to_sm64_scale, + [action for action in anim_props.actions if not (can_reference and get_action_props(action).reference_tables)], + obj, + quick_read, + ) + data_dict = {} + + prev_enums = {} + element_props: SM64_AnimTableElementProperties + for i, element_props in enumerate(anim_props.elements): + try: + table.elements.append( + to_table_element_class( + element_props=element_props, + header_dict=header_dict, + data_dict=data_dict, + action_pairs=action_pairs, + bone_count=bone_count, + table_index=i, + dma=dma, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + prev_enums=prev_enums, + ) + ) + except Exception as exc: + raise PluginError(f"Table element {i}: {exc}") from exc + if not dma and anim_props.null_delimiter: + table.elements.append(SM64_AnimTableElement(enum_name=table.enum_list_delimiter)) + return table + + +def update_includes( + combined_props: "SM64_CombinedObjectProperties", + header_dir: Path, + actor_name, + update_table: bool, +): + data_includes = [Path("anims/data.inc.c")] + header_includes = [] + if update_table: + data_includes.append(Path("anims/table.inc.c")) + header_includes.append(Path("anim_header.h")) + update_actor_includes( + combined_props.export_header_type, + combined_props.actor_group_name, + header_dir, + actor_name, + combined_props.export_level_name, + data_includes, + header_includes, + ) + + +def update_anim_header(path: Path, table_name: str, gen_enums: bool, override_files: bool): + to_add = [ + ModifyFoundDescriptor( + f"extern const struct Animation *const {table_name}[];", + rf"extern\h*const\h*struct\h*Animation\h?\*const\h*{table_name}\[.*?\]\h*?;", + ) + ] + if gen_enums: + to_add.append(to_include_descriptor(Path("anims/table_enum.h"))) + if write_or_delete_if_found(path, to_add, create_new=override_files): + print(f"Updated animation header {path}") + + +def update_enum_file(path: Path, override_files: bool, table: SM64_AnimTable): + text, comment_map = "", [] + existing_file = path.exists() and not override_files + if existing_file: + text, comment_map = get_comment_map(path.read_text()) + + if table.enum_list_start == -1 and table.enum_list_end == -1: # create new enum list + if text and text[-1] not in {"\n", "\r"}: + text += "\n" + table.enum_list_start = len(text) + text += f"enum {table.enum_list_reference} {{\n" + table.enum_list_end = len(text) + text += "};\n" + + content = text[table.enum_list_start : table.enum_list_end] + for i, element in enumerate(table.elements): + if element.enum_start == -1 or element.enum_end == -1: + content += f"\t{element.enum_c},\n" + if existing_file: + print(f"Added enum list entrie {element.enum_c}.") + continue + + old_text = content[element.enum_start : element.enum_end] + if old_text != element.enum_c: + content = content[: element.enum_start] + element.enum_c + content[element.enum_end :] + if existing_file: + print(f'Replaced "{old_text}" with "{element.enum_c}".') + # acccount for changed size + size_increase = len(element.enum_c) - len(old_text) + for next_element in table.elements[i + 1 :]: + if next_element.enum_start != -1 and next_element.enum_end != -1: + next_element.enum_start += size_increase + next_element.enum_end += size_increase + if not existing_file: + print(f"Creating enum list file at {path}.") + text = text[: table.enum_list_start] + content + text[table.enum_list_end :] + path.write_text(text) + + +def update_table_file( + table: SM64_AnimTable, + table_path: Path, + add_null_delimiter: bool, + override_files: bool, + gen_enums: bool, + designated: bool, + enum_list_path: Path, +): + assert isinstance(table.reference, str) and table.reference, "Invalid table reference" + + text, comment_less, enum_text, comment_map = "", "", "", [] + existing_file = table_path.exists() and not override_files + if existing_file: + text = table_path.read_text() + comment_less, comment_map = get_comment_map(text) + + # add include if not already there + descriptor = to_include_descriptor(Path("table_enum.h")) + if gen_enums and len(find_descriptor_in_text(descriptor, comment_less, comment_map)) == 0: + text = '#include "table_enum.h"\n' + text + + # First, find existing tables + tables = import_tables(comment_less, table_path, comment_map, table.reference) + enum_tables = [] + if gen_enums: + assert isinstance(table.enum_list_reference, str) and table.enum_list_reference + enum_text, enum_comment_less, enum_comment_map = "", "", [] + if enum_list_path.exists() and not override_files: + enum_text = enum_list_path.read_text() + enum_comment_less, enum_comment_map = get_comment_map(enum_text) + enum_tables = import_enums(enum_comment_less, enum_list_path, enum_comment_map, table.enum_list_reference) + if len(enum_tables) > 1: + raise PluginError(f'Duplicate enum list "{table.enum_list_reference}"') + + if len(tables) > 1: + raise PluginError(f'Duplicate animation table "{table.reference}"') + elif len(tables) == 1: + existing_table = tables[0] + if gen_enums: + if enum_tables: # apply enum table names to existing unset enums + update_table_with_table_enum(existing_table, enum_tables[0]) + table.enum_list_reference, table.enum_list_start, table.enum_list_end = ( + existing_table.enum_list_reference, + existing_table.enum_list_start, + existing_table.enum_list_end, + ) + + # Figure out enums on existing enum-less elements + prev_enums = {name: 0 for name in existing_table.enum_names} + for i, element in enumerate(existing_table.elements): + if element.enum_name: + continue + if not element.reference: + if i == len(existing_table.elements) - 1: + element.enum_name = duplicate_name(table.enum_list_delimiter, prev_enums) + else: + element.enum_name = duplicate_name( + anim_name_to_enum_name(f"{existing_table.reference}_NULL"), prev_enums + ) + continue + element.enum_name = duplicate_name( + next( + (enum for name, enum in zip(*table.names) if enum and name == element.reference), + anim_name_to_enum_name(element.reference), + ), + prev_enums, + ) + + new_elements = existing_table.elements.copy() + has_null_delimiter = existing_table.has_null_delimiter + for element in table.elements: + if element.c_name in existing_table.header_names and ( + not gen_enums or element.enum_name in existing_table.enum_names + ): + continue + if has_null_delimiter: + new_elements[-1].reference = element.reference + new_elements[-1].enum_name = element.enum_name + has_null_delimiter = False + else: + new_elements.append(element) + table.elements = new_elements + table.start, table.end = (existing_table.start, existing_table.end) + else: # create new table + if text and text[-1] not in {"\n", "\r"}: + text += "\n" + table.start = len(text) + text += f"const struct Animation *const {table.reference}[] = {{\n" + table.end = len(text) + text += "};\n" + + if add_null_delimiter and not table.has_null_delimiter: # add null delimiter if not present or replaced + table.elements.append(SM64_AnimTableElement(enum_name=table.enum_list_delimiter)) + + if gen_enums: + update_enum_file(enum_list_path, override_files, table) + + content = text[table.start : table.end] + for i, element in enumerate(table.elements): + element_text = element.to_c(designated and gen_enums) + if element.reference_start == -1 or element.reference_end == -1: + content += f"\t{element_text}\n" + if existing_file: + print(f"Added table entrie {element_text}.") + continue + + # update existing region instead + old_text = content[element.reference_start : element.reference_end] + if old_text != element_text: + content = content[: element.reference_start] + element_text + content[element.reference_end :] + if existing_file: + print(f'Replaced "{old_text}" with "{element_text}".') + + size_increase = len(element_text) - len(old_text) + if size_increase == 0: + continue + for next_element in table.elements[i + 1 :]: # acccount for changed size + if next_element.reference_start != -1 and next_element.reference_end != -1: + next_element.reference_start += size_increase + next_element.reference_end += size_increase + + if not existing_file: + print(f"Creating table file at {table_path}.") + text = text[: table.start] + content + text[table.end :] + table_path.write_text(text) + + +def update_data_file(path: Path, anim_file_names: list[str], override_files: bool = False): + includes = [Path(file_name) for file_name in anim_file_names] + if write_includes(path, includes, create_new=override_files): + print(f"Updating animation data file includes at {path}") + + +def update_behaviour_binary( + binary_exporter: BinaryExporter, address: int, table_address: bytes, beginning_animation: int +): + load_set = False + animate_set = False + exited = False + while not exited and not (load_set and animate_set): + command_index = int.from_bytes(binary_exporter.read(1, address), "big") + name, size = BEHAVIOR_COMMANDS[command_index] + print(name, intToHex(address)) + if name in BEHAVIOR_EXITS: + exited = True + if name == "LOAD_ANIMATIONS": + ptr_address = address + 4 + print( + f"Found LOAD_ANIMATIONS at {intToHex(address)}, " + f"replacing ptr {bytesToHex(binary_exporter.read(4, ptr_address))} " + f"at {intToHex(ptr_address)} with {bytesToHex(table_address)}" + ) + binary_exporter.write(table_address, ptr_address) + load_set = True + elif name == "ANIMATE": + value_address = address + 1 + print( + f"Found ANIMATE at {intToHex(address)}, " + f"replacing value {int.from_bytes(binary_exporter.read(1, value_address), 'big')} " + f"at {intToHex(value_address)} with {beginning_animation}" + ) + binary_exporter.write(beginning_animation.to_bytes(1, "big"), value_address) + animate_set = True + address += 4 * size + if exited: + if not load_set: + raise IndexError("Could not find LOAD_ANIMATIONS command") + if not animate_set: + print("Could not find ANIMATE command") + + +def export_animation_table_binary( + binary_exporter: BinaryExporter, + anim_props: "SM64_ArmatureAnimProperties", + table: SM64_AnimTable, + is_dma: bool, + level_option: str, + extend_bank_4: bool, +): + if is_dma: + data = table.to_binary_dma() + binary_exporter.write_to_range( + get64bitAlignedAddr(int_from_str(anim_props.dma_address)), int_from_str(anim_props.dma_end_address), data + ) + return + + level_parsed = parseLevelAtPointer(binary_exporter.rom_file_output, level_pointers[level_option]) + segment_data = level_parsed.segmentData + if extend_bank_4: + ExtendBank0x04(binary_exporter.rom_file_output, segment_data, defaultExtendSegment4) + + address = get64bitAlignedAddr(int_from_str(anim_props.address)) + end_address = int_from_str(anim_props.end_address) + + if anim_props.write_data_seperately: # Write the data and the table into seperate address range + data_address = get64bitAlignedAddr(int_from_str(anim_props.data_address)) + data_end_address = int_from_str(anim_props.data_end_address) + table_data, data = table.to_combined_binary(address, data_address, segment_data)[:2] + binary_exporter.write_to_range(address, end_address, table_data) + binary_exporter.write_to_range(data_address, data_end_address, data) + else: # Write table then the data in one address range + table_data, data = table.to_combined_binary(address, -1, segment_data)[:2] + binary_exporter.write_to_range(address, end_address, table_data + data) + if anim_props.update_behavior: + update_behaviour_binary( + binary_exporter, + decodeSegmentedAddr(anim_props.behavior_address.to_bytes(4, "big"), segment_data), + encodeSegmentedAddr(address, segment_data), + int_from_str(anim_props.beginning_animation), + ) + + +def export_animation_table_insertable(table: SM64_AnimTable, is_dma: bool, directory: Path): + directory_path_checks(directory, "Empty directory path.") + path = directory / table.file_name + if is_dma: + data = table.to_binary_dma() + InsertableBinaryData("Animation DMA Table", data).write(path) + else: + table_data, data, ptrs = table.to_combined_binary() + InsertableBinaryData("Animation Table", table_data + data, 0, ptrs).write(path) + + +def create_and_get_paths( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + decomp: Path, +): + anim_directory = geo_directory = header_directory = None + if anim_props.is_dma: + if combined_props.export_header_type == "Custom": + geo_directory = Path(abspath(combined_props.custom_export_path)) + anim_directory = Path(abspath(combined_props.custom_export_path), anim_props.dma_folder) + else: + anim_directory = Path(decomp, anim_props.dma_folder) + else: + export_path, level_name = getPathAndLevel( + combined_props.is_actor_custom_export, + combined_props.actor_custom_path, + combined_props.export_level_name, + combined_props.level_name, + ) + header_directory, _tex_dir = getExportDir( + combined_props.is_actor_custom_export, + export_path, + combined_props.export_header_type, + level_name, + texDir="", + dirName=actor_name, + ) + header_directory = Path(bpy.path.abspath(header_directory)) + geo_directory = header_directory / actor_name + anim_directory = geo_directory / "anims" + + for path in (anim_directory, geo_directory, header_directory): + if path is not None and not os.path.exists(path): + os.makedirs(path, exist_ok=True) + return (anim_directory, geo_directory, header_directory) + + +def export_animation_table_c( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + table: SM64_AnimTable, + decomp: Path, + actor_name: str, + designated: bool, +): + if not combined_props.is_actor_custom_export: + applyBasicTweaks(decomp) + anim_directory, geo_directory, header_directory = create_and_get_paths( + anim_props, combined_props, actor_name, decomp + ) + + print("Creating all animation C data") + if anim_props.export_seperately or anim_props.is_dma: + files_data = table.data_and_headers_to_c(anim_props.is_dma) + print("Saving all generated data files") + for file_name, file_data in files_data.items(): + (anim_directory / file_name).write_text(file_data) + print(file_name) + if not anim_props.is_dma: + update_data_file( + anim_directory / "data.inc.c", + list(files_data.keys()), + anim_props.override_files, + ) + else: + result = table.data_and_headers_to_c_combined() + print("Saving generated data file") + (anim_directory / "data.inc.c").write_text(result) + print("All animation data files exported.") + if anim_props.is_dma: # Don´t create an actual table and or update includes for dma exports + return + assert geo_directory and header_directory and isinstance(table.reference, str) + + header_path = geo_directory / "anim_header.h" + update_anim_header(header_path, table.reference, anim_props.gen_enums, anim_props.override_files) + update_table_file( + table=table, + table_path=anim_directory / "table.inc.c", + add_null_delimiter=anim_props.null_delimiter, + gen_enums=anim_props.gen_enums, + designated=designated, + enum_list_path=anim_directory / "table_enum.h", + override_files=anim_props.override_files, + ) + update_includes(combined_props, header_directory, actor_name, True) + + +def export_animation_binary( + binary_exporter: BinaryExporter, + animation: SM64_Anim, + action_props: "SM64_ActionAnimProperty", + anim_props: "SM64_ArmatureAnimProperties", + bone_count: int, + level_option: str, + extend_bank_4: bool, +): + if anim_props.is_dma: + dma_address = int_from_str(anim_props.dma_address) + print("Reading DMA table from ROM") + table = SM64_AnimTable().read_dma_binary( + reader=RomReader(rom_file=binary_exporter.rom_file_output, start_address=dma_address), + read_headers={}, + table_index=None, + bone_count=bone_count, + ) + empty_data = SM64_AnimData() + for header in animation.headers: + while header.table_index >= len(table.elements): + table.elements.append(SM64_AnimTableElement(header=SM64_AnimHeader(data=empty_data))) + table.elements[header.table_index] = SM64_AnimTableElement(header=header) + print("Converting to binary data") + data = table.to_binary_dma() + binary_exporter.write_to_range(dma_address, int_from_str(anim_props.dma_end_address), data) + return + level_parsed = parseLevelAtPointer(binary_exporter.rom_file_output, level_pointers[level_option]) + segment_data = level_parsed.segmentData + if extend_bank_4: + ExtendBank0x04(binary_exporter.rom_file_output, segment_data, defaultExtendSegment4) + + animation_address = get64bitAlignedAddr(int_from_str(action_props.start_address)) + animation_end_address = int_from_str(action_props.end_address) + + data = animation.to_binary(animation_address, segment_data)[0] + binary_exporter.write_to_range( + animation_address, + animation_end_address, + data, + ) + table_address = get64bitAlignedAddr(int_from_str(anim_props.address)) + if anim_props.update_table: + for i, header in enumerate(animation.headers): + element_address = table_address + (4 * header.table_index) + binary_exporter.seek(element_address) + binary_exporter.write(encodeSegmentedAddr(animation_address + (i * HEADER_SIZE), segment_data)) + if anim_props.update_behavior: + update_behaviour_binary( + binary_exporter, + decodeSegmentedAddr(anim_props.behavior_address.to_bytes(4, "big"), segment_data), + encodeSegmentedAddr(table_address, segment_data), + int_from_str(anim_props.beginning_animation), + ) + + +def export_animation_insertable(animation: SM64_Anim, is_dma: bool, directory: Path): + data, ptrs = animation.to_binary(is_dma) + InsertableBinaryData("Animation", data, 0, ptrs).write(directory / animation.file_name) + + +def export_animation_c( + animation: SM64_Anim, + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + decomp: Path, + actor_name: str, + designated: bool, +): + if not combined_props.is_actor_custom_export: + applyBasicTweaks(decomp) + anim_directory, geo_directory, header_directory = create_and_get_paths( + anim_props, combined_props, actor_name, decomp + ) + + (anim_directory / animation.file_name).write_text(animation.to_c(anim_props.is_dma)) + + if anim_props.is_dma: # Don´t create an actual table and don´t update includes for dma exports + return + + table_name = anim_props.get_table_name(actor_name) + + if anim_props.update_table: + update_anim_header(geo_directory / "anim_header.h", table_name, anim_props.gen_enums, False) + update_table_file( + table=SM64_AnimTable( + table_name, + enum_list_reference=anim_props.get_enum_name(actor_name), + enum_list_delimiter=anim_props.get_enum_end(actor_name), + elements=[ + SM64_AnimTableElement(header.reference, header, header.enum_name) for header in animation.headers + ], + ), + table_path=anim_directory / "table.inc.c", + add_null_delimiter=anim_props.null_delimiter, + gen_enums=anim_props.gen_enums, + designated=designated, + enum_list_path=anim_directory / "table_enum.h", + override_files=False, + ) + update_data_file(anim_directory / "data.inc.c", [animation.file_name]) + update_includes(combined_props, header_directory, actor_name, anim_props.update_table) + + +def export_animation(context: Context, obj: Object): + scene = context.scene + sm64_props: SM64_Properties = scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + actor_name: str = get_anim_actor_name(context) + + action = get_selected_action(obj) + action_props = get_action_props(action) + stashActionInArmature(obj, action) + bone_count = len(get_anim_owners(obj)) + + try: + animation = to_animation_class( + action_props=action_props, + action=action, + obj=obj, + blender_to_sm64_scale=sm64_props.blender_to_sm64_scale, + quick_read=combined_props.quick_anim_read, + export_type=sm64_props.export_type, + dma=anim_props.is_dma, + actor_name=actor_name, + gen_enums=not sm64_props.binary_export and anim_props.gen_enums, + ) + except Exception as exc: + raise PluginError(f"Failed to generate animation class. {exc}") from exc + if sm64_props.export_type == "C": + export_animation_c( + animation, anim_props, combined_props, sm64_props.abs_decomp_path, actor_name, sm64_props.designated + ) + elif sm64_props.export_type == "Insertable Binary": + export_animation_insertable(animation, anim_props.is_dma, Path(abspath(combined_props.insertable_directory))) + elif sm64_props.export_type == "Binary": + with BinaryExporter( + Path(abspath(sm64_props.export_rom)), Path(abspath(sm64_props.output_rom)) + ) as binary_exporter: + export_animation_binary( + binary_exporter, + animation, + action_props, + anim_props, + bone_count, + combined_props.binary_level, + sm64_props.extend_bank_4, + ) + else: + raise NotImplementedError(f"Export type {sm64_props.export_type} is not implemented") + + +def export_animation_table(context: Context, obj: Object): + bpy.ops.object.mode_set(mode="OBJECT") + + scene = context.scene + sm64_props: SM64_Properties = scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + actor_name: str = get_anim_actor_name(context) + + print("Stashing all actions in table") + for action in anim_props.actions: + stashActionInArmature(obj, action) + + if len(anim_props.elements) == 0: + raise PluginError("Empty animation table") + + try: + print("Reading table data from fast64") + table = to_table_class( + anim_props=anim_props, + obj=obj, + blender_to_sm64_scale=sm64_props.blender_to_sm64_scale, + quick_read=combined_props.quick_anim_read, + dma=anim_props.is_dma, + export_type=sm64_props.export_type, + actor_name=actor_name, + gen_enums=not anim_props.is_dma and not sm64_props.binary_export and anim_props.gen_enums, + ) + except Exception as exc: + raise PluginError(f"Failed to generate table class. {exc}") from exc + + print("Exporting table data") + if sm64_props.export_type == "C": + export_animation_table_c( + anim_props, combined_props, table, sm64_props.abs_decomp_path, actor_name, sm64_props.designated + ) + elif sm64_props.export_type == "Insertable Binary": + export_animation_table_insertable(table, anim_props.is_dma, Path(abspath(combined_props.insertable_directory))) + elif sm64_props.export_type == "Binary": + with BinaryExporter( + Path(abspath(sm64_props.export_rom)), Path(abspath(sm64_props.output_rom)) + ) as binary_exporter: + export_animation_table_binary( + binary_exporter, + anim_props, + table, + anim_props.is_dma, + combined_props.binary_level, + sm64_props.extend_bank_4, + ) + else: + raise NotImplementedError(f"Export type {sm64_props.export_type} is not implemented") diff --git a/fast64_internal/sm64/animation/importing.py b/fast64_internal/sm64/animation/importing.py new file mode 100644 index 000000000..21237a57d --- /dev/null +++ b/fast64_internal/sm64/animation/importing.py @@ -0,0 +1,808 @@ +from typing import TYPE_CHECKING, Optional +from pathlib import Path +import dataclasses +import functools +import os +import re +import numpy as np + +import bpy +from bpy.path import abspath +from bpy.types import Object, Action, Context, PoseBone +from mathutils import Quaternion + +from ...f3d.f3d_parser import math_eval +from ...utility import PluginError, decodeSegmentedAddr, filepath_checks, path_checks, intToHex +from ...utility_anim import create_basic_action + +from ..sm64_constants import AnimInfo, level_pointers +from ..sm64_level_parser import parseLevelAtPointer +from ..sm64_utility import CommentMatch, get_comment_map, adjust_start_end, import_rom_checks +from ..sm64_classes import RomReader + +from .utility import ( + animation_operator_checks, + get_action_props, + get_anim_owners, + get_scene_anim_props, + get_anim_actor_name, + anim_name_to_enum_name, + table_name_to_enum, +) +from .classes import ( + SM64_Anim, + CArrayDeclaration, + SM64_AnimHeader, + SM64_AnimTable, + SM64_AnimTableElement, +) +from .constants import ACTOR_PRESET_INFO, TABLE_ENUM_LIST_PATTERN, TABLE_ENUM_PATTERN, TABLE_PATTERN + +if TYPE_CHECKING: + from .properties import ( + SM64_AnimImportProperties, + SM64_ArmatureAnimProperties, + SM64_AnimHeaderProperties, + SM64_ActionAnimProperty, + SM64_AnimTableElementProperties, + ) + from ..settings.properties import SM64_Properties + + +def get_preset_anim_name_list(preset_name: str): + assert preset_name in ACTOR_PRESET_INFO, "Selected preset not in actor presets" + preset = ACTOR_PRESET_INFO[preset_name] + assert preset.animation is not None and isinstance( + preset.animation, AnimInfo + ), "Selected preset's actor has not animation information" + return preset.animation.names + + +def flip_euler(euler: np.ndarray) -> np.ndarray: + euler = euler.copy() + euler[1] = -euler[1] + euler += np.pi + return euler + + +def naive_flip_diff(a1: np.ndarray, a2: np.ndarray) -> np.ndarray: + diff = a1 - a2 + mask = np.abs(diff) > np.pi + return a2 + mask * np.sign(diff) * 2 * np.pi + + +@dataclasses.dataclass +class FramesHolder: + frames: np.ndarray = dataclasses.field(default_factory=list) + + def populate_action(self, action: Action, pose_bone: PoseBone, path: str): + for property_index in range(3): + f_curve = action.fcurves.new( + data_path=pose_bone.path_from_id(path), + index=property_index, + action_group=pose_bone.name, + ) + for time, frame in enumerate(self.frames): + f_curve.keyframe_points.insert(time, frame[property_index], options={"FAST"}) + + +def euler_to_quaternion(euler_angles: np.ndarray): + """ + Fast vectorized euler to quaternion function, euler_angles is an array of shape (-1, 3) + """ + phi = euler_angles[:, 0] + theta = euler_angles[:, 1] + psi = euler_angles[:, 2] + + half_phi = phi / 2.0 + half_theta = theta / 2.0 + half_psi = psi / 2.0 + + cos_half_phi = np.cos(half_phi) + sin_half_phi = np.sin(half_phi) + cos_half_theta = np.cos(half_theta) + sin_half_theta = np.sin(half_theta) + cos_half_psi = np.cos(half_psi) + sin_half_psi = np.sin(half_psi) + + q_w = cos_half_phi * cos_half_theta * cos_half_psi + sin_half_phi * sin_half_theta * sin_half_psi + q_x = sin_half_phi * cos_half_theta * cos_half_psi - cos_half_phi * sin_half_theta * sin_half_psi + q_y = cos_half_phi * sin_half_theta * cos_half_psi + sin_half_phi * cos_half_theta * sin_half_psi + q_z = cos_half_phi * cos_half_theta * sin_half_psi - sin_half_phi * sin_half_theta * cos_half_psi + + quaternions = np.vstack((q_w, q_x, q_y, q_z)).T # shape (-1, 4) + return quaternions + + +@dataclasses.dataclass +class RotationFramesHolder(FramesHolder): + @property + def quaternion(self): + return euler_to_quaternion(self.frames) # We make this code path as optiomal as it can be + + def get_euler(self, order: str): + if order == "XYZ": + return self.frames + return [Quaternion(x).to_euler(order) for x in self.quaternion] + + @property + def axis_angle(self): + result = [] + for x in self.quaternion: + x = Quaternion(x).to_axis_angle() + result.append([x[1]] + list(x[0])) + return result + + def populate_action(self, action: Action, pose_bone: PoseBone): + rotation_mode = pose_bone.rotation_mode + rotation_mode_name = { + "QUATERNION": "rotation_quaternion", + "AXIS_ANGLE": "rotation_axis_angle", + }.get(rotation_mode, "rotation_euler") + data_path = pose_bone.path_from_id(rotation_mode_name) + + size = 4 + if rotation_mode == "QUATERNION": + rotations = self.quaternion + elif rotation_mode == "AXIS_ANGLE": + rotations = self.axis_angle + else: + rotations = self.get_euler(rotation_mode) + size = 3 + for property_index in range(size): + f_curve = action.fcurves.new( + data_path=data_path, + index=property_index, + action_group=pose_bone.name, + ) + for frame, rotation in enumerate(rotations): + f_curve.keyframe_points.insert(frame, rotation[property_index], options={"FAST"}) + + +@dataclasses.dataclass +class IntermidiateAnimationBone: + translation: FramesHolder = dataclasses.field(default_factory=FramesHolder) + rotation: RotationFramesHolder = dataclasses.field(default_factory=RotationFramesHolder) + + def read_pairs(self, pairs: list["SM64_AnimPair"]): + pair_count = len(pairs) + max_length = max(len(pair.values) for pair in pairs) + result = np.empty((max_length, pair_count), dtype=np.int16) + + for i, pair in enumerate(pairs): + current_length = len(pair.values) + result[:current_length, i] = pair.values + result[current_length:, i] = pair.values[-1] + return result + + def read_translation(self, pairs: list["SM64_AnimPair"], scale: float): + self.translation.frames = self.read_pairs(pairs) / scale + + def continuity_filter(self, frames: np.ndarray) -> np.ndarray: + if len(frames) <= 1: + return frames + + # There is no way to fully vectorize this function + prev = frames[0] + for frame, euler in enumerate(frames): + euler = naive_flip_diff(prev, euler) + flipped_euler = naive_flip_diff(prev, flip_euler(euler)) + if np.all((prev - flipped_euler) ** 2 < (prev - euler) ** 2): + euler = flipped_euler + frames[frame] = prev = euler + + return frames + + def read_rotation(self, pairs: list["SM64_AnimPair"], continuity_filter: bool): + frames = self.read_pairs(pairs).astype(np.uint16).astype(np.float32) + frames *= 360.0 / (2**16) + frames = np.radians(frames) + if continuity_filter: + frames = self.continuity_filter(frames) + self.rotation.frames = frames + + def populate_action(self, action: Action, pose_bone: PoseBone): + self.translation.populate_action(action, pose_bone, "location") + self.rotation.populate_action(action, pose_bone) + + +def from_header_class( + header_props: "SM64_AnimHeaderProperties", + header: SM64_AnimHeader, + action: Action, + actor_name: str, + use_custom_name: bool, +): + if isinstance(header.reference, str) and header.reference != header_props.get_name(actor_name, action): + header_props.custom_name = header.reference + if use_custom_name: + header_props.use_custom_name = True + if header.enum_name and header.enum_name != header_props.get_enum(actor_name, action): + header_props.custom_enum = header.enum_name + header_props.use_custom_enum = True + + correct_loop_points = header.start_frame, header.loop_start, header.loop_end + header_props.start_frame, header_props.loop_start, header_props.loop_end = correct_loop_points + if correct_loop_points != header_props.get_loop_points(action): # check if auto loop points don´t match + header_props.use_manual_loop = True + + header_props.trans_divisor = header.trans_divisor + header_props.set_flags(header.flags) + + header_props.table_index = header.table_index + + +def from_anim_class( + action_props: "SM64_ActionAnimProperty", + action: Action, + animation: SM64_Anim, + actor_name: str, + use_custom_name: bool, + import_type: str, +): + main_header = animation.headers[0] + is_from_binary = import_type.endswith("Binary") + + if animation.action_name: + action_name = animation.action_name + elif main_header.file_name: + action_name = main_header.file_name.removesuffix(".c").removesuffix(".inc") + elif is_from_binary: + action_name = intToHex(main_header.reference) + + action.name = action_name.removeprefix("anim_") + print(f'Populating action "{action.name}" properties.') + + indice_reference, values_reference = main_header.indice_reference, main_header.values_reference + if is_from_binary: + action_props.indices_address, action_props.values_address = intToHex(indice_reference), intToHex( + values_reference + ) + else: + action_props.indices_table, action_props.values_table = indice_reference, values_reference + + if animation.data: + file_name = animation.data.indices_file_name + action_props.custom_max_frame = max([1] + [len(x.values) for x in animation.data.pairs]) + if action_props.get_max_frame(action) != action_props.custom_max_frame: + action_props.use_custom_max_frame = True + else: + file_name = main_header.file_name + action_props.reference_tables = True + if file_name: + action_props.custom_file_name = file_name + if use_custom_name and action_props.get_file_name(action, import_type) != action_props.custom_file_name: + action_props.use_custom_file_name = True + if is_from_binary: + start_addresses = [x.reference for x in animation.headers] + end_addresses = [x.end_address for x in animation.headers] + if animation.data: + start_addresses.append(animation.data.start_address) + end_addresses.append(animation.data.end_address) + + action_props.start_address = intToHex(min(start_addresses)) + action_props.end_address = intToHex(max(end_addresses)) + + print("Populating header properties.") + for i, header in enumerate(animation.headers): + if i: + action_props.header_variants.add() + header_props = action_props.headers[-1] + header.action = action # Used in table class to prop + from_header_class(header_props, header, action, actor_name, use_custom_name) + + action_props.update_variant_numbers() + + +def from_table_element_class( + element_props: "SM64_AnimTableElementProperties", + element: SM64_AnimTableElement, + use_custom_name: bool, + actor_name: str, + prev_enums: dict[str, int], +): + if element.header: + assert element.header.action + element_props.set_variant(element.header.action, element.header.header_variant) + else: + element_props.reference = True + + if isinstance(element.reference, int): + element_props.header_address = intToHex(element.reference) + else: + element_props.header_name = element.c_name + element_props.header_address = intToHex(0) + + if element.enum_name: + element_props.custom_enum = element.enum_name + if use_custom_name and element.enum_name != element_props.get_enum(True, actor_name, prev_enums): + element_props.use_custom_enum = True + + +def from_anim_table_class( + anim_props: "SM64_ArmatureAnimProperties", + table: SM64_AnimTable, + clear_table: bool, + use_custom_name: bool, + actor_name: str, +): + if clear_table: + anim_props.elements.clear() + anim_props.null_delimiter = table.has_null_delimiter + + prev_enums: dict[str, int] = {} + for i, element in enumerate(table.elements): + if anim_props.null_delimiter and i == len(table.elements) - 1: + break + anim_props.elements.add() + from_table_element_class(anim_props.elements[-1], element, use_custom_name, actor_name, prev_enums) + + if isinstance(table.reference, int): # Binary + anim_props.dma_address = intToHex(table.reference) + anim_props.dma_end_address = intToHex(table.end_address) + anim_props.address = intToHex(table.reference) + anim_props.end_address = intToHex(table.end_address) + + # Data + start_addresses = [] + end_addresses = [] + for element in table.elements: + if element.header and element.header.data: + start_addresses.append(element.header.data.start_address) + end_addresses.append(element.header.data.end_address) + if start_addresses and end_addresses: + anim_props.write_data_seperately = True + anim_props.data_address = intToHex(min(start_addresses)) + anim_props.data_end_address = intToHex(max(end_addresses)) + elif isinstance(table.reference, str) and table.reference: # C + if use_custom_name: + anim_props.custom_table_name = table.reference + if anim_props.get_table_name(actor_name) != anim_props.custom_table_name: + anim_props.use_custom_table_name = True + + +def animation_import_to_blender( + obj: Object, + blender_to_sm64_scale: float, + anim_import: SM64_Anim, + actor_name: str, + use_custom_name: bool, + import_type: str, + force_quaternion: bool, + continuity_filter: bool, +): + action = create_basic_action(obj, "") + try: + if anim_import.data: + print("Converting pairs to intermidiate data.") + bones = get_anim_owners(obj) + bones_data: list[IntermidiateAnimationBone] = [] + pairs = anim_import.data.pairs + for pair_num in range(3, len(pairs), 3): + bone = IntermidiateAnimationBone() + if pair_num == 3: + bone.read_translation(pairs[0:3], blender_to_sm64_scale) + bone.read_rotation(pairs[pair_num : pair_num + 3], continuity_filter) + bones_data.append(bone) + print("Populating action keyframes.") + for pose_bone, bone_data in zip(bones, bones_data): + if force_quaternion: + pose_bone.rotation_mode = "QUATERNION" + bone_data.populate_action(action, pose_bone) + + from_anim_class(get_action_props(action), action, anim_import, actor_name, use_custom_name, import_type) + return action + except PluginError as exc: + bpy.data.actions.remove(action) + raise exc + + +def update_table_with_table_enum(table: SM64_AnimTable, enum_table: SM64_AnimTable): + for element, enum_element in zip(table.elements, enum_table.elements): + if element.enum_name: + enum_element = next( + ( + other_enum_element + for other_enum_element in enum_table.elements + if element.enum_name == other_enum_element.enum_name + ), + enum_element, + ) + element.enum_name = enum_element.enum_name + element.enum_val = enum_element.enum_val + element.enum_start = enum_element.enum_start + element.enum_end = enum_element.enum_end + table.enum_list_reference = enum_table.enum_list_reference + table.enum_list_start = enum_table.enum_list_start + table.enum_list_end = enum_table.enum_list_end + + +def import_enums(c_data: str, path: Path, comment_map: list[CommentMatch], specific_name=""): + tables = [] + for list_match in re.finditer(TABLE_ENUM_LIST_PATTERN, c_data): + name, content = list_match.group("name"), list_match.group("content") + if name is None and content is None: # comment + continue + if specific_name and name != specific_name: + continue + list_start, list_end = adjust_start_end(c_data.find(content, list_match.start()), list_match.end(), comment_map) + content = c_data[list_start:list_end] + table = SM64_AnimTable( + file_name=path.name, + enum_list_reference=name, + enum_list_start=list_start, + enum_list_end=list_end, + ) + for element_match in re.finditer(TABLE_ENUM_PATTERN, content): + name, num = (element_match.group("name"), element_match.group("num")) + if name is None and num is None: # comment + continue + enum_start, enum_end = adjust_start_end( + list_start + element_match.start(), list_start + element_match.end(), comment_map + ) + table.elements.append( + SM64_AnimTableElement( + enum_name=name, enum_val=num, enum_start=enum_start - list_start, enum_end=enum_end - list_start + ) + ) + tables.append(table) + return tables + + +def import_tables( + c_data: str, + path: Path, + comment_map: list[CommentMatch], + specific_name="", + header_decls: Optional[list[CArrayDeclaration]] = None, + values_decls: Optional[list[CArrayDeclaration]] = None, + indices_decls: Optional[list[CArrayDeclaration]] = None, +): + read_headers = {} + header_decls, values_decls, indices_decls = ( + header_decls or [], + values_decls or [], + indices_decls or [], + ) + tables: list[SM64_AnimTable] = [] + for table_match in re.finditer(TABLE_PATTERN, c_data): + table_elements = [] + name, content = table_match.group("name"), table_match.group("content") + if name is None and content is None: # comment + continue + if specific_name and name != specific_name: + continue + + table = SM64_AnimTable(name, file_name=path.name, elements=table_elements) + table.read_c( + c_data, + c_data.find(content, table_match.start()), + table_match.end(), + comment_map, + read_headers, + header_decls, + values_decls, + indices_decls, + ) + tables.append(table) + return tables + + +DECL_PATTERN = re.compile( + r"(static\s+const\s+struct\s+Animation|static\s+const\s+u16|static\s+const\s+s16)\s+" + r"(\w+)\s*?(?:\[.*?\])?\s*?=\s*?\{(.*?)\s*?\};", + re.DOTALL, +) +VALUE_SPLIT_PATTERN = re.compile(r"\s*(?:(?:\.(?P\w+)|\[\s*(?P.*?)\s*\])\s*=\s*)?(?P.+?)(?:,|\Z)") + + +def find_decls(c_data: str, path: Path, decl_list: dict[str, list[CArrayDeclaration]]): + """At this point a generilized c parser would be better""" + matches = DECL_PATTERN.findall(c_data) + for decl_type, name, value_text in matches: + values = [] + for match in VALUE_SPLIT_PATTERN.finditer(value_text): + var, designator, val = match.group("var"), match.group("designator"), match.group("val") + assert val is not None + if designator is not None: + designator = math_eval(designator, object()) + if isinstance(designator, int): + if isinstance(values, dict): + raise PluginError("Invalid mix of designated initializers") + first_val = values[0] if values else "0" + values.extend([first_val] * (designator + 1 - len(values))) + else: + if not values: + values = {} + elif isinstance(values, list): + raise PluginError("Invalid mix of designated initializers") + values[designator] = val + elif var is not None: + if not values: + values = {} + elif isinstance(values, list): + raise PluginError("Mix of designated and positional variable assignment") + values[var] = val + else: + if isinstance(values, dict): + raise PluginError("Mix of designated and positional variable assignment") + values.append(val) + decl_list[decl_type].append(CArrayDeclaration(name, path, path.name, values)) + + +def import_c_animations(path: Path) -> tuple[SM64_AnimTable | None, dict[str, SM64_AnimHeader]]: + path_checks(path) + if path.is_file(): + file_paths = [path] + elif path.is_dir(): + file_paths = sorted([f for f in path.rglob("*") if f.suffix in {".c", ".h"}]) + else: + raise PluginError("Path is neither a file or a folder but it exists, somehow.") + + print("Reading from:\n" + "\n".join([f.name for f in file_paths])) + c_files = {file_path: get_comment_map(file_path.read_text()) for file_path in file_paths} + + decl_lists = {"static const struct Animation": [], "static const u16": [], "static const s16": []} + header_decls, indices_decls, value_decls = ( + decl_lists["static const struct Animation"], + decl_lists["static const u16"], + decl_lists["static const s16"], + ) + tables: list[SM64_AnimTable] = [] + enum_lists: list[SM64_AnimTable] = [] + for file_path, (comment_less, _comment_map) in c_files.items(): + find_decls(comment_less, file_path, decl_lists) + for file_path, (comment_less, comment_map) in c_files.items(): + tables.extend(import_tables(comment_less, file_path, comment_map, "", header_decls, value_decls, indices_decls)) + enum_lists.extend(import_enums(comment_less, file_path, comment_map)) + + if len(tables) > 1: + raise ValueError("More than 1 table declaration") + elif len(tables) == 1: + table: SM64_AnimTable = tables[0] + if enum_lists: + enum_table = next( # find enum with the same name or use the first + ( + enum_table + for enum_table in enum_lists + if enum_table.reference == table_name_to_enum(table.reference) + ), + enum_lists[0], + ) + update_table_with_table_enum(table, enum_table) + read_headers = {header.reference: header for header in table.header_set} + return table, read_headers + else: + read_headers: dict[str, SM64_AnimHeader] = {} + for table_index, header_decl in enumerate(sorted(header_decls, key=lambda h: h.name)): + SM64_AnimHeader().read_c(header_decl, value_decls, indices_decls, read_headers, table_index) + return None, read_headers + + +def import_binary_animations( + data_reader: RomReader, + import_type: str, + read_headers: dict[str, SM64_AnimHeader], + table: SM64_AnimTable, + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + table_size: Optional[int] = None, +): + if import_type == "Table": + table.read_binary(data_reader, read_headers, table_index, bone_count, table_size) + elif import_type == "DMA": + table.read_dma_binary(data_reader, read_headers, table_index, bone_count) + elif import_type == "Animation": + SM64_AnimHeader.read_binary( + data_reader, + read_headers, + False, + bone_count, + table_size, + ) + else: + raise PluginError("Unimplemented binary import type.") + + +def import_insertable_binary_animations( + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table: SM64_AnimTable, + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + table_size: Optional[int] = None, +): + if reader.insertable.data_type == "Animation": + SM64_AnimHeader.read_binary( + reader, + read_headers, + False, + bone_count, + ) + elif reader.insertable.data_type == "Animation Table": + table.read_binary(reader, read_headers, table_index, bone_count, table_size) + elif reader.insertable.data_type == "Animation DMA Table": + table.read_dma_binary(reader, read_headers, table_index, bone_count) + + +def import_animations(context: Context): + animation_operator_checks(context, False) + + scene = context.scene + obj: Object = context.object + sm64_props: SM64_Properties = scene.fast64.sm64 + import_props: SM64_AnimImportProperties = sm64_props.animation.importing + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + + update_table_preset(import_props, context) + + read_headers: dict[str, SM64_AnimHeader] = {} + table = SM64_AnimTable() + + print("Reading animation data.") + + if import_props.binary: + rom_path = Path(abspath(import_props.rom if import_props.rom else sm64_props.import_rom)) + binary_args = ( + read_headers, + table, + import_props.table_index, + None if import_props.ignore_bone_count else len(get_anim_owners(obj)), + import_props.table_size, + ) + if import_props.import_type == "Binary": + import_rom_checks(rom_path) + address = import_props.address + with rom_path.open("rb") as rom_file: + if import_props.binary_import_type == "DMA": + segment_data = None + else: + segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData + if import_props.is_segmented_address: + address = decodeSegmentedAddr(address.to_bytes(4, "big"), segment_data) + import_binary_animations( + RomReader(rom_file, start_address=address, segment_data=segment_data), + import_props.binary_import_type, + *binary_args, + ) + elif import_props.import_type == "Insertable Binary": + insertable_path = Path(abspath(import_props.path)) + filepath_checks(insertable_path) + with insertable_path.open("rb") as insertable_file: + if import_props.read_from_rom: + import_rom_checks(rom_path) + with rom_path.open("rb") as rom_file: + segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData + import_insertable_binary_animations( + RomReader(rom_file, insertable_file=insertable_file, segment_data=segment_data), + *binary_args, + ) + else: + import_insertable_binary_animations(RomReader(insertable_file=insertable_file), *binary_args) + elif import_props.import_type == "C": + table, read_headers = import_c_animations(Path(abspath(import_props.path))) + table = table or SM64_AnimTable() + else: + raise NotImplementedError(f"Unimplemented animation import type {import_props.import_type}") + + if not table.elements: + print("No table was read. Automatically creating table.") + table.elements = [SM64_AnimTableElement(header=header) for header in read_headers.values()] + seperate_anims = table.get_seperate_anims() + + actor_name: str = get_anim_actor_name(context) + if import_props.use_preset and import_props.preset in ACTOR_PRESET_INFO: + preset_animation_names = get_preset_anim_name_list(import_props.preset) + for animation in seperate_anims: + if len(animation.headers) == 0: + continue + names, indexes = [], [] + for header in animation.headers: + if header.table_index >= len(preset_animation_names): + continue + name = preset_animation_names[header.table_index] + header.enum_name = header.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}") + names.append(name) + indexes.append(str(header.table_index)) + animation.action_name = f"{'/'.join(indexes)} - {'/'.join(names)}" + for i, element in enumerate(table.elements[: len(preset_animation_names)]): + name = preset_animation_names[i] + element.enum_name = element.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}") + + print("Importing animations into blender.") + actions = [] + for animation in seperate_anims: + actions.append( + animation_import_to_blender( + obj, + sm64_props.blender_to_sm64_scale, + animation, + actor_name, + import_props.use_custom_name, + import_props.import_type, + import_props.force_quaternion, + import_props.continuity_filter if not import_props.force_quaternion else True, + ) + ) + + if import_props.run_decimate: + print("Decimating imported actions's fcurves") + old_area = bpy.context.area.type + old_action = obj.animation_data.action + try: + if obj.type == "ARMATURE": + bpy.ops.object.posemode_toggle() # Select all bones + bpy.ops.pose.select_all(action="SELECT") + + bpy.context.area.type = "GRAPH_EDITOR" + for action in actions: + print(f"Decimating {action.name}.") + obj.animation_data.action = action + bpy.ops.graph.select_all(action="SELECT") + bpy.ops.graph.decimate(mode="ERROR", factor=1, remove_error_margin=import_props.decimate_margin) + finally: + bpy.context.area.type = old_area + obj.animation_data.action = old_action + + if import_props.binary: + anim_props.is_dma = import_props.binary_import_type == "DMA" + if table: + print("Importing animation table into properties.") + from_anim_table_class(anim_props, table, import_props.clear_table, import_props.use_custom_name, actor_name) + + +@functools.cache +def cached_enum_from_import_preset(preset: str): + animation_names = get_preset_anim_name_list(preset) + enum_items: list[tuple[str, str, str, int]] = [] + enum_items.append(("Custom", "Custom", "Pick your own animation index", 0)) + if animation_names: + enum_items.append(("", "Presets", "", 1)) + for i, name in enumerate(animation_names): + enum_items.append((str(i), f"{i} - {name}", f'"{preset}" Animation {i}', i + 2)) + return enum_items + + +def get_enum_from_import_preset(_import_props: "SM64_AnimImportProperties", context): + try: + return cached_enum_from_import_preset(get_scene_anim_props(context).importing.preset) + except Exception as exc: # pylint: disable=broad-except + print(str(exc)) + return [("Custom", "Custom", "Pick your own animation index", 0)] + + +def update_table_preset(import_props: "SM64_AnimImportProperties", context): + if not import_props.use_preset: + return + + preset = ACTOR_PRESET_INFO[import_props.preset] + assert preset.animation is not None and isinstance( + preset.animation, AnimInfo + ), "Selected preset's actor has not animation information" + + if import_props.preset_animation == "": + # If the previously selected animation isn't in this preset, select animation 0 + import_props.preset_animation = "0" + + # C + decomp_path = import_props.decomp_path if import_props.decomp_path else context.scene.fast64.sm64.decomp_path + directory = preset.animation.directory if preset.animation.directory else f"{preset.decomp_path}/anims" + import_props.path = os.path.join(decomp_path, directory) + + # Binary + import_props.ignore_bone_count = preset.animation.ignore_bone_count + import_props.level = preset.level + if preset.animation.dma: + import_props.dma_table_address = intToHex(preset.animation.address) + import_props.binary_import_type = "DMA" + import_props.is_segmented_address_prop = False + else: + import_props.table_address = intToHex(preset.animation.address) + import_props.binary_import_type = "Table" + import_props.is_segmented_address_prop = True + + if preset.animation.size is None: + import_props.check_null = True + else: + import_props.check_null = False + import_props.table_size_prop = preset.animation.size diff --git a/fast64_internal/sm64/animation/operators.py b/fast64_internal/sm64/animation/operators.py new file mode 100644 index 000000000..8f0e6426e --- /dev/null +++ b/fast64_internal/sm64/animation/operators.py @@ -0,0 +1,346 @@ +from typing import TYPE_CHECKING + +import bpy +from bpy.utils import register_class, unregister_class +from bpy.types import Context, Scene, Action +from bpy.props import EnumProperty, StringProperty, IntProperty +from bpy.app.handlers import persistent + +from ...operators import OperatorBase, SearchEnumOperatorBase +from ...utility import copyPropertyGroup +from ...utility_anim import get_action + +from .importing import import_animations, get_enum_from_import_preset +from .exporting import export_animation, export_animation_table +from .utility import ( + animation_operator_checks, + get_action_props, + get_anim_obj, + get_scene_anim_props, + get_anim_props, + get_anim_actor_name, +) +from .constants import enum_anim_tables, enum_animated_behaviours + +if TYPE_CHECKING: + from .properties import SM64_AnimProperties, SM64_AnimHeaderProperties + + +@persistent +def emulate_no_loop(scene: Scene): + if scene.gameEditorMode != "SM64": + return + anim_props: SM64_AnimProperties = scene.fast64.sm64.animation + played_action: Action = anim_props.played_action + if not played_action: + return + if not bpy.context.screen.is_animation_playing or anim_props.played_header >= len( + get_action_props(played_action).headers + ): + anim_props.played_action = None + return + + frame = scene.frame_current + header_props = get_action_props(played_action).headers[anim_props.played_header] + _start, loop_start, end = header_props.get_loop_points(played_action) + if header_props.backwards: + if frame < loop_start: + if header_props.no_loop: + scene.frame_set(loop_start) + else: + scene.frame_set(end - 1) + elif frame >= end: + if header_props.no_loop: + scene.frame_set(end - 1) + else: + scene.frame_set(loop_start) + + +class SM64_PreviewAnim(OperatorBase): + bl_idname = "scene.sm64_preview_animation" + bl_label = "Preview Animation" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "PLAY" + + played_header: IntProperty(name="Header", min=0, default=0) + played_action: StringProperty(name="Action") + + def execute_operator(self, context): + animation_operator_checks(context) + played_action = get_action(self.played_action) + scene = context.scene + anim_props = scene.fast64.sm64.animation + + context.object.animation_data.action = played_action + action_props = get_action_props(played_action) + + if self.played_header >= len(action_props.headers): + raise ValueError("Invalid Header Index") + header_props: SM64_AnimHeaderProperties = action_props.headers[self.played_header] + start_frame = header_props.get_loop_points(played_action)[0] + scene.frame_set(start_frame) + scene.render.fps = 30 + + if bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() # in case it was already playing, stop it + bpy.ops.screen.animation_play() + + anim_props.played_header = self.played_header + anim_props.played_action = played_action + + +class SM64_AnimTableOps(OperatorBase): + bl_idname = "scene.sm64_table_operations" + bl_label = "Table Operations" + bl_description = "Move, remove, clear or add table elements" + bl_options = {"UNDO"} + + index: IntProperty() + op_name: StringProperty() + action_name: StringProperty() + header_variant: IntProperty() + + @classmethod + def is_enabled(cls, context: Context, op_name: str, index: int, **_kwargs): + table_elements = get_anim_props(context).elements + if op_name == "MOVE_UP" and index == 0: + return False + elif op_name == "MOVE_DOWN" and index >= len(table_elements) - 1: + return False + elif op_name == "CLEAR" and len(table_elements) == 0: + return False + return True + + def execute_operator(self, context): + table_elements = get_anim_props(context).elements + if self.op_name == "MOVE_UP": + table_elements.move(self.index, self.index - 1) + elif self.op_name == "MOVE_DOWN": + table_elements.move(self.index, self.index + 1) + elif self.op_name == "ADD": + if self.index != -1: + table_element = table_elements[self.index] + table_elements.add() + if self.action_name: # set based on action variant + table_elements[-1].set_variant(bpy.data.actions[self.action_name], self.header_variant) + elif self.index != -1: # copy from table + copyPropertyGroup(table_element, table_elements[-1]) + if self.index != -1: + table_elements.move(len(table_elements) - 1, self.index + 1) + elif self.op_name == "ADD_ALL": + action = bpy.data.actions[self.action_name] + for header_variant in range(len(get_action_props(action).headers)): + table_elements.add() + table_elements[-1].set_variant(action, header_variant) + elif self.op_name == "REMOVE": + table_elements.remove(self.index) + elif self.op_name == "CLEAR": + table_elements.clear() + else: + raise NotImplementedError(f"Unimplemented table op {self.op_name}") + + +class SM64_AnimVariantOps(OperatorBase): + bl_idname = "scene.sm64_header_variant_operations" + bl_label = "Header Variant Operations" + bl_description = "Move, remove, clear or add variants" + bl_options = {"UNDO"} + + index: IntProperty() + op_name: StringProperty() + action_name: StringProperty() + + @classmethod + def is_enabled(cls, context: Context, action_name: str, op_name: str, index: int, **_kwargs): + action_props = get_action_props(get_action(action_name)) + headers = action_props.headers + if op_name == "REMOVE" and index == 0: + return False + elif op_name == "MOVE_UP" and index <= 0: + return False + elif op_name == "MOVE_DOWN" and index >= len(headers) - 1: + return False + elif op_name == "CLEAR" and len(headers) <= 1: + return False + return True + + def execute_operator(self, context): + action = get_action(self.action_name) + action_props = get_action_props(action) + headers = action_props.headers + variants = action_props.header_variants + variant_position = self.index - 1 + if self.op_name == "MOVE_UP": + if self.index - 1 == 0: + variants.add() + copyPropertyGroup(headers[0], variants[-1]) + copyPropertyGroup(headers[self.index], headers[0]) + copyPropertyGroup(variants[-1], headers[self.index]) + variants.remove(len(variants) - 1) + else: + variants.move(variant_position, variant_position - 1) + elif self.op_name == "MOVE_DOWN": + if self.index == 0: + variants.add() + copyPropertyGroup(headers[0], variants[-1]) + copyPropertyGroup(headers[1], headers[0]) + copyPropertyGroup(variants[-1], headers[1]) + variants.remove(len(variants) - 1) + else: + variants.move(variant_position, variant_position + 1) + elif self.op_name == "ADD": + variants.add() + added_variant = variants[-1] + + copyPropertyGroup(action_props.headers[self.index], added_variant) + variants.move(len(variants) - 1, variant_position + 1) + action_props.update_variant_numbers() + added_variant.action = action + added_variant.expand_tab = True + added_variant.use_custom_name = False + added_variant.use_custom_enum = False + added_variant.custom_name = added_variant.get_name(get_anim_actor_name(context), action) + elif self.op_name == "REMOVE": + variants.remove(variant_position) + elif self.op_name == "CLEAR": + variants.clear() + else: + raise NotImplementedError(f"Unimplemented table op {self.op_name}") + action_props.update_variant_numbers() + + +class SM64_AddNLATracksToTable(OperatorBase): + bl_idname = "scene.sm64_add_nla_tracks_to_table" + bl_label = "Add Existing NLA Tracks To Animation Table" + bl_description = "Adds all NLA tracks in the selected armature to the animation table" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "NLA" + + @classmethod + def poll(cls, context): + if get_anim_obj(context) is None or get_anim_obj(context).animation_data is None: + return False + actions = get_anim_props(context).actions + for track in context.object.animation_data.nla_tracks: + for strip in track.strips: + if strip.action is not None and strip.action not in actions: + return True + return False + + def execute_operator(self, context): + assert self.__class__.poll(context) + anim_props = get_anim_props(context) + for track in context.object.animation_data.nla_tracks: + for strip in track.strips: + action = strip.action + if action is None or action in anim_props.actions: + continue + for header_variant in range(len(get_action_props(action).headers)): + anim_props.elements.add() + anim_props.elements[-1].set_variant(action, header_variant) + + +class SM64_ExportAnimTable(OperatorBase): + bl_idname = "scene.sm64_export_anim_table" + bl_label = "Export Animation Table" + bl_description = "Exports the animation table of the selected armature" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "EXPORT" + + @classmethod + def poll(cls, context): + return get_anim_obj(context) is not None + + def execute_operator(self, context): + animation_operator_checks(context) + export_animation_table(context, context.object) + self.report({"INFO"}, "Exported animation table successfully!") + + +class SM64_ExportAnim(OperatorBase): + bl_idname = "scene.sm64_export_anim" + bl_label = "Export Individual Animation" + bl_description = "Exports the select action of the selected armature" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION" + + @classmethod + def poll(cls, context): + return get_anim_obj(context) is not None + + def execute_operator(self, context): + animation_operator_checks(context) + export_animation(context, context.object) + self.report({"INFO"}, "Exported animation successfully!") + + +class SM64_ImportAnim(OperatorBase): + bl_idname = "scene.sm64_import_anim" + bl_label = "Import Animation(s)" + bl_description = "Imports animations into the call context's animation propreties, scene or object" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "IMPORT" + + def execute_operator(self, context): + import_animations(context) + + +class SM64_SearchAnimPresets(SearchEnumOperatorBase): + bl_idname = "scene.search_mario_anim_enum_operator" + bl_property = "preset_animation" + + preset_animation: EnumProperty(items=get_enum_from_import_preset) + + def update_enum(self, context: Context): + get_scene_anim_props(context).importing.preset_animation = self.preset_animation + + +class SM64_SearchAnimTablePresets(SearchEnumOperatorBase): + bl_idname = "scene.search_anim_table_enum_operator" + bl_property = "preset" + + preset: EnumProperty(items=enum_anim_tables) + + def update_enum(self, context: Context): + get_scene_anim_props(context).importing.preset = self.preset + + +class SM64_SearchAnimatedBhvs(SearchEnumOperatorBase): + bl_idname = "scene.search_animated_behavior_enum_operator" + bl_property = "behaviour" + + behaviour: EnumProperty(items=enum_animated_behaviours) + + def update_enum(self, context: Context): + get_anim_props(context).behaviour = self.behaviour + + +classes = ( + SM64_ExportAnimTable, + SM64_ExportAnim, + SM64_PreviewAnim, + SM64_AnimTableOps, + SM64_AnimVariantOps, + SM64_AddNLATracksToTable, + SM64_ImportAnim, + SM64_SearchAnimPresets, + SM64_SearchAnimatedBhvs, + SM64_SearchAnimTablePresets, +) + + +def anim_ops_register(): + for cls in classes: + register_class(cls) + + bpy.app.handlers.frame_change_pre.append(emulate_no_loop) + + +def anim_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/panels.py b/fast64_internal/sm64/animation/panels.py new file mode 100644 index 000000000..f7a48ba02 --- /dev/null +++ b/fast64_internal/sm64/animation/panels.py @@ -0,0 +1,198 @@ +from typing import TYPE_CHECKING + +from bpy.utils import register_class, unregister_class +from bpy.types import Context + +from ...utility_anim import is_action_stashed, CreateAnimData, AddBasicAction, StashAction +from ...panels import SM64_Panel + +from .utility import ( + get_action_props, + get_anim_actor_name, + get_anim_props, + get_selected_action, + dma_structure_context, + get_anim_obj, +) +from .operators import SM64_ExportAnim, SM64_ExportAnimTable, SM64_AddNLATracksToTable + +if TYPE_CHECKING: + from ..settings.properties import SM64_Properties + from ..sm64_objects import SM64_CombinedObjectProperties + from .properties import SM64_AnimImportProperties + + +# Base +class AnimationPanel(SM64_Panel): + bl_label = "SM64 Animation Inspector" + goal = "Object/Actor/Anim" + + +# Base panels +class SceneAnimPanel(AnimationPanel): + bl_idname = "SM64_PT_anim" + bl_parent_id = bl_idname + + +class ObjAnimPanel(AnimationPanel): + bl_idname = "OBJECT_PT_SM64_anim" + bl_context = "object" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_parent_id = bl_idname + + +# Main tab +class SceneAnimPanelMain(SceneAnimPanel): + bl_parent_id = "" + + def draw(self, context): + col = self.layout.column() + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + + if sm64_props.export_type == "C": + if not sm64_props.hackersm64: + col.prop(sm64_props, "designated_prop", text="Designated Initialization for Tables") + else: + combined_props.draw_anim_props(col, sm64_props.export_type, dma_structure_context(context)) + SM64_ExportAnimTable.draw_props(col) + anim_obj = get_anim_obj(context) + if anim_obj is None: + col.box().label(text="No selected armature/animated object") + else: + col.box().label(text=f'Armature "{anim_obj.name}"') + + +class ObjAnimPanelMain(ObjAnimPanel): + bl_parent_id = "OBJECT_PT_context_object" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None + + def draw(self, context): + sm64_props: SM64_Properties = context.scene.fast64.sm64 + get_anim_props(context).draw_props( + self.layout, sm64_props.export_type, sm64_props.combined_export.export_header_type + ) + + +# Action tab + + +class AnimationPanelAction(AnimationPanel): + bl_label = "Action Inspector" + + def draw(self, context): + col = self.layout.column() + + if context.object.animation_data is None: + col.box().label(text="Select object has no animation data") + CreateAnimData.draw_props(col) + action = None + else: + col.prop(context.object.animation_data, "action", text="Selected Action") + action = get_selected_action(context.object, False) + if action is None: + AddBasicAction.draw_props(col) + return + + if not is_action_stashed(context.object, action): + warn_col = col.column() + StashAction.draw_props(warn_col, action=action.name) + warn_col.alert = True + + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + if sm64_props.export_type != "C": + SM64_ExportAnim.draw_props(col) + + export_seperately = get_anim_props(context).export_seperately + if sm64_props.export_type == "C": + export_seperately = export_seperately or combined_props.export_single_action + elif sm64_props.export_type == "Insertable Binary": + export_seperately = True + get_action_props(action).draw_props( + layout=col, + action=action, + specific_variant=None, + in_table=False, + updates_table=get_anim_props(context).update_table, + export_seperately=export_seperately, + export_type=sm64_props.export_type, + actor_name=get_anim_actor_name(context), + gen_enums=get_anim_props(context).gen_enums, + dma=dma_structure_context(context), + ) + + +class SceneAnimPanelAction(AnimationPanelAction, SceneAnimPanel): + bl_idname = "SM64_PT_anim_panel_action" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None and SceneAnimPanel.poll(context) + + +class ObjAnimPanelAction(AnimationPanelAction, ObjAnimPanel): + bl_idname = "OBJECT_PT_SM64_anim_action" + + +class ObjAnimPanelTable(ObjAnimPanel): + bl_label = "Table" + bl_idname = "OBJECT_PT_SM64_anim_table" + + def draw(self, context): + if SM64_AddNLATracksToTable.poll(context): + SM64_AddNLATracksToTable.draw_props(self.layout) + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + get_anim_props(context).draw_table( + self.layout, sm64_props.export_type, get_anim_actor_name(context), combined_props.export_bhv + ) + + +# Importing tab + + +class AnimationPanelImport(AnimationPanel): + bl_label = "Importing" + import_panel = True + + def draw(self, context): + sm64_props: SM64_Properties = context.scene.fast64.sm64 + importing: SM64_AnimImportProperties = sm64_props.animation.importing + importing.draw_props(self.layout, sm64_props.import_rom, sm64_props.decomp_path) + + +class SceneAnimPanelImport(SceneAnimPanel, AnimationPanelImport): + bl_idname = "SM64_PT_anim_panel_import" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None and AnimationPanelImport.poll(context) + + +class ObjAnimPanelImport(ObjAnimPanel, AnimationPanelImport): + bl_idname = "OBJECT_PT_SM64_anim_panel_import" + + +classes = ( + ObjAnimPanelMain, + ObjAnimPanelTable, + ObjAnimPanelAction, + SceneAnimPanelMain, + SceneAnimPanelAction, + SceneAnimPanelImport, +) + + +def anim_panel_register(): + for cls in classes: + register_class(cls) + + +def anim_panel_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py new file mode 100644 index 000000000..4dd9f5bb1 --- /dev/null +++ b/fast64_internal/sm64/animation/properties.py @@ -0,0 +1,1204 @@ +import os + +import bpy +from bpy.types import PropertyGroup, Action, UILayout, Scene, Context +from bpy.utils import register_class, unregister_class +from bpy.props import ( + BoolProperty, + StringProperty, + EnumProperty, + IntProperty, + FloatProperty, + CollectionProperty, + PointerProperty, +) +from bpy.path import abspath, clean_name + +from ...utility import ( + decompFolderMessage, + directory_ui_warnings, + run_and_draw_errors, + path_ui_warnings, + draw_and_check_tab, + multilineLabel, + prop_split, + intToHex, + upgrade_old_prop, + toAlnum, +) +from ...utility_anim import getFrameInterval + +from ..sm64_utility import import_rom_ui_warnings, int_from_str, string_int_prop, string_int_warning +from ..sm64_constants import MAX_U16, MIN_S16, MAX_S16, level_enums + +from .operators import ( + OperatorBase, + SM64_PreviewAnim, + SM64_AnimTableOps, + SM64_AnimVariantOps, + SM64_ImportAnim, + SM64_SearchAnimPresets, + SM64_SearchAnimatedBhvs, + SM64_SearchAnimTablePresets, +) +from .constants import enum_anim_import_types, enum_anim_binary_import_types, enum_animated_behaviours, enum_anim_tables +from .classes import SM64_AnimFlags +from .utility import ( + dma_structure_context, + get_action_props, + get_dma_anim_name, + get_dma_header_name, + is_obj_animatable, + anim_name_to_enum_name, + action_name_to_enum_name, + duplicate_name, + table_name_to_enum, +) +from .importing import get_enum_from_import_preset, update_table_preset + + +def draw_custom_or_auto(holder, layout: UILayout, prop: str, default: str, factor=0.5, **kwargs): + use_custom_prop = "use_custom_" + prop + name_split = layout.split(factor=factor) + name_split.prop(holder, use_custom_prop, **kwargs) + if getattr(holder, use_custom_prop): + name_split.prop(holder, "custom_" + prop, text="") + else: + prop_size_label(name_split, text=default, icon="LOCKED") + + +def draw_forced(layout: UILayout, holder, prop: str, forced: bool): + row = layout.row(align=True) if forced else layout.column() + if forced: + prop_size_label(row, text="", icon="LOCKED") + row.alignment = "LEFT" + row.enabled = not forced + row.prop(holder, prop, invert_checkbox=not getattr(holder, prop) if forced else False) + + +def prop_size_label(layout: UILayout, **label_args): + box = layout.box() + box.scale_y = 0.5 + box.label(**label_args) + return box + + +def draw_list_op(layout: UILayout, op_cls: OperatorBase, op_name: str, index=-1, text="", icon="", **op_args): + col = layout.column() + icon = icon or {"MOVE_UP": "TRIA_UP", "MOVE_DOWN": "TRIA_DOWN", "CLEAR": "TRASH"}.get(op_name) or op_name + return op_cls.draw_props(col, icon, text, index=index, op_name=op_name, **op_args) + + +def draw_list_ops(layout: UILayout, op_cls: OperatorBase, index: int, **op_args): + layout.label(text=str(index)) + ops = ("MOVE_UP", "MOVE_DOWN", "ADD", "REMOVE") + for op_name in ops: + draw_list_op(layout, op_cls, op_name, index, **op_args) + + +def set_if_different(owner, prop: str, value): + if getattr(owner, prop) != value: + setattr(owner, prop, value) + + +def on_flag_update(self: "SM64_AnimHeaderProperties", context: Context): + use_int = context.scene.fast64.sm64.binary_export or dma_structure_context(context) + self.set_flags(self.get_flags(not use_int), set_custom=not self.use_custom_flags) + + +class SM64_AnimHeaderProperties(PropertyGroup): + expand_tab_in_action: BoolProperty(name="Header Properties", default=True) + header_variant: IntProperty(name="Header Variant Number", min=0) + + use_custom_name: BoolProperty(name="Name") + custom_name: StringProperty(name="Name", default="anim_00") + use_custom_enum: BoolProperty(name="Enum") + custom_enum: StringProperty(name="Enum", default="ANIM_00") + use_manual_loop: BoolProperty(name="Manual Loop Points") + start_frame: IntProperty(name="Start", min=0, max=MAX_S16) + loop_start: IntProperty(name="Loop Start", min=0, max=MAX_S16) + loop_end: IntProperty(name="End", min=0, max=MAX_S16) + trans_divisor: IntProperty( + name="Translation Divisor", + description="(animYTransDivisor)\n" + "If set to 0, the translation multiplier will be 1. " + "Otherwise, the translation multiplier is determined by " + "dividing the object's translation dividend (animYTrans) by this divisor", + min=MIN_S16, + max=MAX_S16, + ) + use_custom_flags: BoolProperty(name="Set Custom Flags") + custom_flags: StringProperty(name="Flags", default="ANIM_NO_LOOP", update=on_flag_update) + # Some flags are inverted in the ui for readability, descriptions match ui behavior + no_loop: BoolProperty( + name="No Loop", + description="(ANIM_FLAG_NOLOOP)\n" + "When disabled, the animation will not repeat from the loop start after reaching the loop " + "end frame", + update=on_flag_update, + ) + backwards: BoolProperty( + name="Loop Backwards", + description="(ANIM_FLAG_FORWARD/ANIM_FLAG_BACKWARD)\n" + "When enabled, the animation will loop (or stop if looping is disabled) after reaching " + "the loop start frame.\n" + "Tipically used with animations which use acceleration to play an animation backwards", + update=on_flag_update, + ) + no_acceleration: BoolProperty( + name="No Acceleration", + description="(ANIM_FLAG_NO_ACCEL/ANIM_FLAG_2)\n" + "When disabled, acceleration will not be used when calculating which animation frame is " + "next", + update=on_flag_update, + ) + disabled: BoolProperty( + name="No Shadow Translation", + description="(ANIM_FLAG_DISABLED/ANIM_FLAG_5)\n" + "When disabled, the animation translation will not be applied to shadows", + update=on_flag_update, + ) + only_vertical: BoolProperty( + name="Only Vertical Translation", + description="(ANIM_FLAG_HOR_TRANS)\n" + "When enabled, only the animation vertical translation will be applied during rendering (takes priority over no translation and only horizontal)\n" + "(shadows included), the horizontal translation will still be exported and included", + update=on_flag_update, + ) + only_horizontal: BoolProperty( + name="Only Horizontal Translation", + description="(ANIM_FLAG_VERT_TRANS)\n" + "When enabled, only the animation horizontal translation will be applied during rendering (takes priority over no translation)\n" + "(shadows included) the vertical translation will still be exported and included", + update=on_flag_update, + ) + no_trans: BoolProperty( + name="No Translation", + description="(ANIM_FLAG_NO_TRANS/ANIM_FLAG_6)\n" + "When disabled, the animation translation will not be used during rendering\n" + "(shadows included), the translation will still be exported and included", + update=on_flag_update, + ) + # Binary + table_index: IntProperty(name="Table Index", min=0) + + def get_flags(self, allow_str: bool) -> SM64_AnimFlags | str: + if self.use_custom_flags: + result = SM64_AnimFlags.evaluate(self.custom_flags) + if not allow_str and isinstance(result, str): + raise ValueError("Failed to evaluate custom flags") + return result + value = SM64_AnimFlags(0) + for prop, flag in SM64_AnimFlags.props_to_flags().items(): + if getattr(self, prop, False): + value |= flag + return value + + @property + def int_flags(self): + return self.get_flags(allow_str=False) + + def set_flags(self, value: SM64_AnimFlags | str, set_custom=True): + if isinstance(value, SM64_AnimFlags): # the value was fully evaluated + for prop, flag in SM64_AnimFlags.props_to_flags().items(): # set prop flags + set_if_different(self, prop, flag in value) + if set_custom: + if value not in SM64_AnimFlags.all_flags_with_prop(): # if a flag does not have a prop + set_if_different(self, "use_custom_flags", True) + set_if_different(self, "custom_flags", intToHex(value, 2)) + elif isinstance(value, str): + if set_custom: + set_if_different(self, "custom_flags", value) + set_if_different(self, "use_custom_flags", True) + else: # invalid + raise ValueError(f"Invalid type: {value}") + + @property + def manual_loop_range(self) -> tuple[int, int, int]: + if self.use_manual_loop: + return (self.start_frame, self.loop_start, self.loop_end) + + def get_loop_points(self, action: Action): + if self.use_manual_loop: + return self.manual_loop_range + loop_start, loop_end = getFrameInterval(action) + return (0, loop_start, loop_end + 1) + + def get_name(self, actor_name: str, action: Action, dma=False) -> str: + if dma: + return get_dma_header_name(self.table_index) + elif self.use_custom_name: + return self.custom_name + elif self.header_variant == 0: + return toAlnum(f"{actor_name}_anim_{action.name}") + else: + main_header_name = get_action_props(action).headers[0].get_name(actor_name, action, dma) + return toAlnum(f"{main_header_name}_{self.header_variant}") + + def get_enum(self, actor_name: str, action: Action) -> str: + if self.use_custom_enum: + return self.custom_enum + elif self.use_custom_name: + return anim_name_to_enum_name(self.get_name(actor_name, action)) + elif self.header_variant == 0: + clean_name = action_name_to_enum_name(action.name) + return anim_name_to_enum_name(f"{actor_name}_anim_{clean_name}") + else: + main_enum = get_action_props(action).headers[0].get_enum(actor_name, action) + return f"{main_enum}_{self.header_variant}" + + def draw_flag_props(self, layout: UILayout, use_int_flags: bool = False): + col = layout.column() + custom_split = col.split() + custom_split.prop(self, "use_custom_flags") + if self.use_custom_flags: + custom_split.prop(self, "custom_flags", text="") + if use_int_flags: + run_and_draw_errors(col, self.get_flags, False) + return + else: + prop_size_label(custom_split, text=intToHex(self.int_flags, 2), icon="LOCKED") + # Draw flag toggles + row = col.row(align=True) + row.prop(self, "no_loop", invert_checkbox=True, text="Loop", toggle=1) + row.prop(self, "backwards", toggle=1) + row.prop(self, "no_acceleration", invert_checkbox=True, text="Acceleration", toggle=1) + if self.no_acceleration and self.backwards: + col.label(text="Backwards has no porpuse without acceleration.", icon="INFO") + + trans_row = col.row(align=True) + no_row = trans_row.row() + no_row.enabled = not self.only_vertical and not self.only_horizontal + no_row.prop(self, "no_trans", invert_checkbox=True, text="Translate", toggle=1) + + vert_row = trans_row.row() + vert_row.prop(self, "only_vertical", text="Only Vertical", toggle=1) + + hor_row = trans_row.row() + hor_row.enabled = not self.only_vertical + hor_row.prop(self, "only_horizontal", text="Only Horizontal", toggle=1) + if self.only_vertical and self.only_horizontal: + multilineLabel( + layout=col, + text='"Only Vertical" takes priority, only vertical\n translation will be used.', + icon="INFO", + ) + if (self.only_vertical or self.only_horizontal) and self.no_trans: + multilineLabel( + layout=col, + text='"Only Horizontal" and "Only Vertical" take\n priority over no translation.', + icon="INFO", + ) + + disabled_row = trans_row.row() + disabled_row.enabled = not self.no_trans and not self.only_vertical + disabled_row.prop(self, "disabled", invert_checkbox=True, text="Shadow", toggle=1) + + def draw_frame_range(self, layout: UILayout, action: Action): + split = layout.split() + split.prop(self, "use_manual_loop") + if self.use_manual_loop: + split = layout.split() + split.prop(self, "start_frame") + split.prop(self, "loop_start") + split.prop(self, "loop_end") + else: + start, loop_start, end = self.get_loop_points(action) + prop_size_label(split, text=f"Start {start}, Loop Start {loop_start}, End {end}", icon="LOCKED") + + def draw_names(self, layout: UILayout, action: Action, actor_name: str, gen_enums: bool, dma: bool): + col = layout.column() + if gen_enums: + draw_custom_or_auto(self, col, "enum", self.get_enum(actor_name, action)) + draw_custom_or_auto(self, col, "name", self.get_name(actor_name, action, dma)) + + def draw_props( + self, + layout: UILayout, + action: Action, + in_table: bool, + updates_table: bool, + dma: bool, + export_type: str, + actor_name: str, + gen_enums: bool, + ): + col = layout.column() + split = col.split() + preview_op = SM64_PreviewAnim.draw_props(split) + preview_op.played_header = self.header_variant + preview_op.played_action = action.name + if not in_table: # Don´t show index or name in table props + draw_list_op( + split, + SM64_AnimTableOps, + "ADD", + text="Add To Table", + icon="LINKED", + action_name=action.name, + header_variant=self.header_variant, + ) + if (export_type == "C" and dma) or (export_type == "Binary" and updates_table): + prop_split(col, self, "table_index", "Table Index") + if not dma and export_type == "C": + self.draw_names(col, action, actor_name, gen_enums, dma) + col.separator() + + prop_split(col, self, "trans_divisor", "Translation Divisor") + self.draw_frame_range(col, action) + self.draw_flag_props(col, use_int_flags=dma or export_type.endswith("Binary")) + + +class SM64_ActionAnimProperty(PropertyGroup): + """Properties in Action.fast64.sm64.animation""" + + header: PointerProperty(type=SM64_AnimHeaderProperties) + variants_tab: BoolProperty(name="Header Variants") + header_variants: CollectionProperty(type=SM64_AnimHeaderProperties) + use_custom_file_name: BoolProperty(name="File Name") + custom_file_name: StringProperty(name="File Name", default="anim_00.inc.c") + use_custom_max_frame: BoolProperty(name="Max Frame") + custom_max_frame: IntProperty(name="Max Frame", min=1, max=MAX_U16, default=1) + reference_tables: BoolProperty(name="Reference Tables") + indices_table: StringProperty(name="Indices Table", default="anim_00_indices") + values_table: StringProperty(name="Value Table", default="anim_00_values") + # Binary, toad anim 0 for defaults + indices_address: StringProperty(name="Indices Table", default=intToHex(0x00A42150)) + values_address: StringProperty(name="Value Table", default=intToHex(0x00A40CC8)) + start_address: StringProperty(name="Start Address", default=intToHex(0x00A40CC8)) + end_address: StringProperty(name="End Address", default=intToHex(0x00A42265)) + + @property + def headers(self) -> list[SM64_AnimHeaderProperties]: + return [self.header] + list(self.header_variants) + + @property + def dma_name(self): + return get_dma_anim_name([header.table_index for header in self.headers]) + + def get_name(self, action: Action, dma=False) -> str: + if dma: + return self.dma_name + return toAlnum(f"anim_{action.name}") + + def get_file_name(self, action: Action, export_type: str, dma=False) -> str: + if not export_type in {"C", "Insertable Binary"}: + return "" + if export_type == "C" and dma: + return f"{self.dma_name}.inc.c" + elif self.use_custom_file_name: + return self.custom_file_name + else: + name = clean_name(f"anim_{action.name}", replace=" ") + return name + (".inc.c" if export_type == "C" else ".insertable") + + def get_max_frame(self, action: Action) -> int: + if self.use_custom_max_frame: + return self.custom_max_frame + loop_ends: list[int] = [getFrameInterval(action)[1]] + header_props: SM64_AnimHeaderProperties + for header_props in self.headers: + loop_ends.append(header_props.get_loop_points(action)[2]) + + return max(loop_ends) + + def update_variant_numbers(self): + for i, variant in enumerate(self.headers): + variant.header_variant = i + + def draw_variants( + self, + layout: UILayout, + action: Action, + dma: bool, + actor_name: str, + header_args: list, + ): + col = layout.column() + op_row = col.row() + op_row.label(text=f"Header Variants ({len(self.headers)})", icon="NLA") + draw_list_op(op_row, SM64_AnimVariantOps, "CLEAR", action_name=action.name) + + for i, header_props in enumerate(self.headers): + if i != 0: + col.separator() + + row = col.row() + if draw_and_check_tab( + row, + header_props, + "expand_tab_in_action", + header_props.get_name(actor_name, action, dma), + ): + header_props.draw_props(col, *header_args) + op_row = row.row() + op_row.alignment = "RIGHT" + draw_list_ops(op_row, SM64_AnimVariantOps, i, action_name=action.name) + + def draw_references(self, layout: UILayout, is_binary: bool = False): + col = layout.column() + col.prop(self, "reference_tables") + if not self.reference_tables: + return + if is_binary: + string_int_prop(col, self, "indices_address", "Indices Table") + string_int_prop(col, self, "values_address", "Value Table") + else: + prop_split(col, self, "indices_table", "Indices Table") + prop_split(col, self, "values_table", "Value Table") + + def draw_props( + self, + layout: UILayout, + action: Action, + specific_variant: int | None, + in_table: bool, + updates_table: bool, + export_seperately: bool, + export_type: str, + actor_name: str, + gen_enums: bool, + dma: bool, + ): + # Args to pass to the headers + header_args = (action, in_table, updates_table, dma, export_type, actor_name, gen_enums) + + col = layout.column() + if specific_variant is not None: + col.label(text="Action Properties", icon="ACTION") + if not in_table: + draw_list_op( + col, + SM64_AnimTableOps, + "ADD_ALL", + text="Add All Variants To Table", + icon="LINKED", + action_name=action.name, + ) + col.separator() + + if export_type == "Binary" and not dma: + string_int_prop(col, self, "start_address", "Start Address") + string_int_prop(col, self, "end_address", "End Address") + if export_type != "Binary" and (export_seperately or not in_table): + if not dma or export_type == "Insertable Binary": # not c dma or insertable + text = "File Name" + if not in_table and not export_seperately: + text = "File Name (individual action export)" + draw_custom_or_auto(self, col, "file_name", self.get_file_name(action, export_type), text=text) + elif not in_table: # C DMA forced auto name + split = col.split(factor=0.5) + split.label(text="File Name") + file_name = self.get_file_name(action, export_type, dma) + prop_size_label(split, text=file_name, icon="LOCKED") + if dma or not self.reference_tables: # DMA tables don´t allow references + draw_custom_or_auto(self, col, "max_frame", str(self.get_max_frame(action))) + if not dma: + self.draw_references(col, is_binary=export_type.endswith("Binary")) + col.separator() + + if specific_variant is not None: + if specific_variant < 0 or specific_variant >= len(self.headers): + col.box().label(text="Header variant does not exist.", icon="ERROR") + else: + col.label(text="Variant Properties", icon="NLA") + self.headers[specific_variant].draw_props(col, *header_args) + else: + self.draw_variants(col, action, dma, actor_name, header_args) + + +class SM64_AnimTableElementProperties(PropertyGroup): + expand_tab: BoolProperty() + action_prop: PointerProperty(name="Action", type=Action) + variant: IntProperty(name="Variant", min=0) + reference: BoolProperty(name="Reference") + # Toad example + header_name: StringProperty(name="Header Reference", default="toad_seg6_anim_0600B66C") + header_address: StringProperty(name="Header Reference", default=intToHex(0x0600B75C)) + use_custom_enum: BoolProperty(name="Enum") + custom_enum: StringProperty(name="Enum Name") + + def get_enum(self, can_reference: bool, actor_name: str, prev_enums: dict[str, int]): + """Updates prev_enums""" + enum = "" + if self.use_custom_enum: + self.custom_enum: str + enum = self.custom_enum + elif can_reference and self.reference: + enum = duplicate_name(anim_name_to_enum_name(self.header_name), prev_enums) + else: + action, header = self.get_action_header(can_reference) + if header and action: + enum = duplicate_name(header.get_enum(actor_name, action), prev_enums) + return enum + + def get_action_header(self, can_reference: bool): + self.variant: int + self.action_prop: Action + if (not can_reference or not self.reference) and self.action_prop: + headers = get_action_props(self.action_prop).headers + if self.variant < len(headers): + return (self.action_prop, headers[self.variant]) + return (None, None) + + def get_action(self, can_reference: bool) -> Action | None: + return self.get_action_header(can_reference)[0] + + def get_header(self, can_reference: bool) -> SM64_AnimHeaderProperties | None: + return self.get_action_header(can_reference)[1] + + def set_variant(self, action: Action, variant: int): + self.action_prop = action + self.variant = variant + + def draw_reference( + self, layout: UILayout, export_type: str = "C", gen_enums: bool = False, prev_enums: dict[str, int] = None + ): + if export_type.endswith("Binary"): + string_int_prop(layout, self, "header_address", "Header Address") + return + split = layout.split() + if gen_enums: + draw_custom_or_auto(self, split, "enum", self.get_enum(True, "", prev_enums), factor=0.3) + split.prop(self, "header_name", text="") + + def draw_props( + self, + row: UILayout, # left side of the row for table ops + prop_layout: UILayout, + index: int, + dma: bool, + updates_table: bool, + export_seperately: bool, + export_type: str, + gen_enums: bool, + actor_name: str, + prev_enums: dict[str, int], + ): + can_reference = not dma + col = prop_layout.column() + if can_reference: + reference_row = row.row() + reference_row.alignment = "LEFT" + reference_row.prop(self, "reference") + if self.reference: + self.draw_reference(col, export_type, gen_enums, prev_enums) + return + action_row = row.row() + action_row.alignment = "EXPAND" + action_row.prop(self, "action_prop", text="") + + if not self.action_prop: + col.box().label(text="Header´s action does not exist.", icon="ERROR") + return + action = self.action_prop + action_props = get_action_props(action) + + variant_split = col.split(factor=0.3) + variant_split.prop(self, "variant") + + if 0 <= self.variant < len(action_props.headers): + header_props = self.get_header(can_reference) + if dma: + name = get_dma_header_name(index) + else: + name = header_props.get_name(actor_name, action, dma) + if gen_enums: + draw_custom_or_auto( + self, + variant_split, + "enum", + self.get_enum(can_reference, actor_name, prev_enums), + factor=0.3, + ) + tab_name = name + (f" (Variant {self.variant})" if self.variant > 0 else "") + if not draw_and_check_tab(col, self, "expand_tab", tab_name): + return + + action_props.draw_props( + layout=col, + action=action, + specific_variant=self.variant, + in_table=True, + updates_table=updates_table, + export_seperately=export_seperately, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + dma=dma, + ) + + +class SM64_AnimImportProperties(PropertyGroup): + run_decimate: BoolProperty(name="Run Decimate (Allowed Change)", default=True) + decimate_margin: FloatProperty( + name="Error Margin", + default=0.025, + min=0.0, + max=0.025, + description="Use blender's builtin decimate (allowed change) operator to clean up all the " + "keyframes, generally the better option compared to clean keyframes but can be slow", + ) + + continuity_filter: BoolProperty(name="Continuity Filter", default=True) + force_quaternion: BoolProperty( + name="Force Quaternions", + description="Changes bones to quaternion rotation mode, can break existing actions", + ) + + clear_table: BoolProperty(name="Clear Table On Import", default=True) + import_type: EnumProperty(items=enum_anim_import_types, name="Import Type", default="C") + preset: bpy.props.EnumProperty( + items=enum_anim_tables, + name="Preset", + update=update_table_preset, + default="Mario", + ) + decomp_path: StringProperty(name="Decomp Path", subtype="FILE_PATH", default="") + binary_import_type: EnumProperty( + items=enum_anim_binary_import_types, + name="Binary Import Type", + default="Table", + ) + read_entire_table: BoolProperty(name="Read Entire Table", default=True) + check_null: BoolProperty(name="Check NULL Delimiter", default=True) + table_size_prop: IntProperty(name="Size", min=1) + table_index_prop: IntProperty(name="Index", min=0) + ignore_bone_count: BoolProperty( + name="Ignore bone count", + description="The armature bone count won´t be used when importing, a safety check will be skipped and old " + "fast64 animations won´t import, needed to import bowser's broken animation", + ) + preset_animation: EnumProperty(name="Preset Animation", items=get_enum_from_import_preset) + + rom: StringProperty(name="Import ROM", subtype="FILE_PATH") + table_address: StringProperty(name="Address", default=intToHex(0x0600FC48)) # Toad + animation_address: StringProperty(name="Address", default=intToHex(0x0600B75C)) + is_segmented_address_prop: BoolProperty(name="Is Segmented Address", default=True) + level: EnumProperty(items=level_enums, name="Level", default="IC") + dma_table_address: StringProperty(name="DMA Table Address", default="0x4EC000") + + read_from_rom: BoolProperty( + name="Read From Import ROM", + description="When enabled, the importer will read from the import ROM given an " + "address not included in the insertable file's defined pointers", + ) + + path: StringProperty(name="Path", subtype="FILE_PATH", default="anims/") + use_custom_name: BoolProperty(name="Use Custom Name", default=True) + + @property + def binary(self) -> bool: + return self.import_type.endswith("Binary") + + @property + def table_index(self): + if self.read_entire_table: + return + elif self.preset_animation == "Custom" or not self.use_preset: + return self.table_index_prop + else: + return int_from_str(self.preset_animation) + + @property + def address(self): + if self.import_type != "Binary": + return + elif self.binary_import_type == "DMA": + return int_from_str(self.dma_table_address) + elif self.binary_import_type == "Table": + return int_from_str(self.table_address) + else: + return int_from_str(self.animation_address) + + @property + def is_segmented_address(self): + if self.import_type != "Binary": + return + return ( + self.is_segmented_address_prop + if self.import_type == "Binary" and self.binary_import_type in {"Table", "Animation"} + else False + ) + + @property + def table_size(self): + return None if self.check_null else self.table_size_prop + + @property + def use_preset(self): + return self.import_type != "Insertable Binary" and self.preset != "Custom" + + def upgrade_old_props(self, scene: Scene): + upgrade_old_prop( + self, + "animation_address", + scene, + "animStartImport", + fix_forced_base_16=True, + ) + upgrade_old_prop(self, "is_segmented_address_prop", scene, "animIsSegPtr") + upgrade_old_prop(self, "level", scene, "levelAnimImport") + upgrade_old_prop(self, "table_index_prop", scene, "animListIndexImport") + if scene.pop("isDMAImport", False): + self.binary_import_type = "DMA" + elif scene.pop("animIsAnimList", True): + self.binary_import_type = "Table" + + def draw_clean_up(self, layout: UILayout): + col = layout.column() + col.prop(self, "run_decimate") + if self.run_decimate: + prop_split(col, self, "decimate_margin", "Error Margin") + col.box().label(text="While very useful and stable, it can be very slow", icon="INFO") + col.separator() + + row = col.row() + row.prop(self, "force_quaternion") + continuity_row = row.row() + continuity_row.enabled = not self.force_quaternion + continuity_row.prop( + self, + "continuity_filter", + text="Continuity Filter" + (" (Always on)" if self.force_quaternion else ""), + invert_checkbox=not self.continuity_filter if self.force_quaternion else False, + ) + + def draw_path(self, layout: UILayout): + prop_split(layout, self, "path", "Directory or File Path") + path_ui_warnings(layout, abspath(self.path)) + + def draw_c(self, layout: UILayout, decomp: os.PathLike = ""): + col = layout.column() + if self.preset == "Custom": + self.draw_path(col) + else: + col.label(text="Uses scene decomp path by default", icon="INFO") + prop_split(col, self, "decomp_path", "Decomp Path") + directory_ui_warnings(col, abspath(self.decomp_path or decomp)) + col.prop(self, "use_custom_name") + + def draw_import_rom(self, layout: UILayout, import_rom: os.PathLike = ""): + col = layout.column() + col.label(text="Uses scene import ROM by default", icon="INFO") + prop_split(col, self, "rom", "Import ROM") + return import_rom_ui_warnings(col, abspath(self.rom or import_rom)) + + def draw_table_settings(self, layout: UILayout): + row = layout.row(align=True) + left_row = row.row(align=True) + left_row.alignment = "LEFT" + left_row.prop(self, "read_entire_table") + left_row.prop(self, "check_null") + right_row = row.row(align=True) + right_row.alignment = "EXPAND" + if not self.read_entire_table: + right_row.prop(self, "table_index_prop", text="Index") + elif not self.check_null: + right_row.prop(self, "table_size_prop") + + def draw_binary(self, layout: UILayout, import_rom: os.PathLike): + col = layout.column() + self.draw_import_rom(col, import_rom) + col.separator() + + if self.preset != "Custom": + split = col.split() + split.prop(self, "read_entire_table") + if not self.read_entire_table: + SM64_SearchAnimPresets.draw_props(split, self, "preset_animation", "") + if self.preset_animation == "Custom": + split.prop(self, "table_index_prop", text="Index") + return + col.prop(self, "ignore_bone_count") + prop_split(col, self, "binary_import_type", "Animation Type") + if self.binary_import_type == "DMA": + string_int_prop(col, self, "dma_table_address", "DMA Table Address") + split = col.split() + split.prop(self, "read_entire_table") + if not self.read_entire_table: + split.prop(self, "table_index_prop", text="Index") + return + + split = col.split() + split.prop(self, "is_segmented_address_prop") + if self.binary_import_type == "Table": + split.prop(self, "table_address", text="") + string_int_warning(col, self.table_address) + elif self.binary_import_type == "Animation": + split.prop(self, "animation_address", text="") + string_int_warning(col, self.animation_address) + prop_split(col, self, "level", "Level") + if self.binary_import_type == "Table": # Draw settings after level + self.draw_table_settings(col) + + def draw_insertable_binary(self, layout: UILayout, import_rom: os.PathLike): + col = layout.column() + self.draw_path(col) + col.separator() + + col.label(text="Animation type will be read from the files", icon="INFO") + + table_box = col.column() + table_box.label(text="Table Imports", icon="ANIM") + self.draw_table_settings(table_box) + col.separator() + + col.prop(self, "read_from_rom") + if self.read_from_rom: + self.draw_import_rom(col, import_rom) + prop_split(col, self, "level", "Level") + + col.prop(self, "ignore_bone_count") + + def draw_props(self, layout: UILayout, import_rom: os.PathLike = "", decomp: os.PathLike = ""): + col = layout.column() + + prop_split(col, self, "import_type", "Type") + + if self.import_type in {"C", "Binary"}: + SM64_SearchAnimTablePresets.draw_props(col, self, "preset", "Preset") + col.separator() + + if self.import_type == "C": + self.draw_c(col, decomp) + elif self.binary: + if self.import_type == "Binary": + self.draw_binary(col, import_rom) + elif self.import_type == "Insertable Binary": + self.draw_insertable_binary(col, import_rom) + col.separator() + + self.draw_clean_up(col) + col.prop(self, "clear_table") + SM64_ImportAnim.draw_props(col) + + +class SM64_AnimProperties(PropertyGroup): + version: IntProperty(name="SM64_AnimProperties Version", default=0) + cur_version = 1 # version after property migration + + played_header: IntProperty(min=0) + played_action: PointerProperty(name="Action", type=Action) + + importing: PointerProperty(type=SM64_AnimImportProperties) + + def upgrade_old_props(self, scene: Scene): + self.importing.upgrade_old_props(scene) + + # Export + loop = scene.pop("loopAnimation", None) + start_address = scene.pop("animExportStart", None) + end_address = scene.pop("animExportEnd", None) + + for action in bpy.data.actions: + action_props: SM64_ActionAnimProperty = get_action_props(action) + action_props.header: SM64_AnimHeaderProperties + if loop is not None: + action_props.header.set_flags(SM64_AnimFlags(0) if loop else SM64_AnimFlags.ANIM_FLAG_NOLOOP) + if start_address is not None: + action_props.start_address = intToHex(int(start_address, 16)) + if end_address is not None: + action_props.end_address = intToHex(int(end_address, 16)) + + insertable_path = scene.pop("animInsertableBinaryPath", "") + is_dma = scene.pop("loopAnimation", None) + update_table = scene.pop("animExportStart", None) + update_behavior = scene.pop("animExportEnd", None) + beginning_animation = scene.pop("animListIndexExport", None) + for obj in bpy.data.objects: + if not is_obj_animatable(obj): + continue + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + if is_dma is not None: + anim_props.is_dma = is_dma + if update_table is not None: + anim_props.update_table = update_table + if update_behavior is not None: + anim_props.update_behavior = update_behavior + if beginning_animation is not None: + anim_props.beginning_animation = beginning_animation + if insertable_path is not None: # Ignores directory + anim_props.use_custom_file_name = True + anim_props.custom_file_name = os.path.split(insertable_path)[0] + + # Deprecated: + # - addr 0x27 was a pointer to a load anim cmd that would be used to update table pointers + # the actual table pointer is used instead + # - addr 0x28 was a pointer to a animate cmd that would be updated to the beggining + # animation a behavior script pointer is used instead so both load an animate can be updated + # easily without much thought + + self.version = 1 + + def upgrade_changed_props(self, scene): + if self.version != self.cur_version: + self.upgrade_old_props(scene) + self.version = SM64_AnimProperties.cur_version + + +class SM64_ArmatureAnimProperties(PropertyGroup): + version: IntProperty(name="SM64_AnimProperties Version", default=0) + cur_version = 1 # version after property migration + + is_dma: BoolProperty(name="Is DMA Export") + dma_folder: StringProperty(name="DMA Folder", default="assets/anims/") + update_table: BoolProperty( + name="Update Table On Action Export", + description="Update table outside of table exports", + default=True, + ) + + # Table + elements: CollectionProperty(type=SM64_AnimTableElementProperties) + + export_seperately_prop: BoolProperty(name="Export All Seperately") + write_data_seperately: BoolProperty(name="Write Data Seperately") + null_delimiter: BoolProperty(name="Add Null Delimiter") + override_files_prop: BoolProperty(name="Override Table and Data Files", default=True) + gen_enums: BoolProperty(name="Generate Enums", default=True) + use_custom_table_name: BoolProperty(name="Table Name") + custom_table_name: StringProperty(name="Table Name", default="mario_anims") + # Binary, Toad animation table example + data_address: StringProperty( + name="Data Address", + default=intToHex(0x00A3F7E0), + ) + data_end_address: StringProperty( + name="Data End", + default=intToHex(0x00A466C0), + ) + address: StringProperty(name="Table Address", default=intToHex(0x00A46738)) + end_address: StringProperty(name="Table End", default=intToHex(0x00A4675C)) + update_behavior: BoolProperty(name="Update Behavior", default=True) + behaviour: bpy.props.EnumProperty(items=enum_animated_behaviours, default=intToHex(0x13002EF8)) + behavior_address_prop: StringProperty(name="Behavior Address", default=intToHex(0x13002EF8)) + beginning_animation: StringProperty(name="Begining Animation", default="0x00") + # Mario animation table + dma_address: StringProperty(name="DMA Table Address", default=intToHex(0x4EC000)) + dma_end_address: StringProperty(name="DMA Table End", default=intToHex(0x4EC000 + 0x8DC20)) + + use_custom_file_name: BoolProperty(name="File Name") + custom_file_name: StringProperty(name="File Name", default="toad.insertable") + + @property + def behavior_address(self) -> int: + if self.behaviour == "Custom": + return int_from_str(self.behavior_address_prop) + return int_from_str(self.behaviour) + + @property + def export_seperately(self): + return self.is_dma or self.export_seperately_prop + + @property + def override_files(self) -> bool: + return not self.export_seperately or self.override_files_prop + + @property + def actions(self) -> list[Action]: + actions = [] + for element_props in self.elements: + action = element_props.get_action(not self.is_dma) + if action and action not in actions: + actions.append(action) + return actions + + def get_table_name(self, actor_name: str) -> str: + if self.use_custom_table_name: + return self.custom_table_name + return f"{actor_name}_anims" + + def get_enum_name(self, actor_name: str): + return table_name_to_enum(self.get_table_name(actor_name)) + + def get_enum_end(self, actor_name: str): + table_name = self.get_table_name(actor_name) + return f"{table_name.upper()}_END" + + def get_table_file_name(self, actor_name: str, export_type: str) -> str: + if not export_type in {"C", "Insertable Binary"}: + return "" + elif export_type == "Insertable Binary": + if self.use_custom_file_name: + return self.custom_file_name + return clean_name(actor_name + ("_dma_table" if self.is_dma else "_table")) + ".insertable" + else: + return "table.inc.c" + + def draw_element( + self, + layout: UILayout, + index: int, + table_element: SM64_AnimTableElementProperties, + export_type: str, + actor_name: str, + prev_enums: dict[str, int], + ): + col = layout.column() + row = col.row() + left_row = row.row() + left_row.alignment = "EXPAND" + op_row = row.row() + op_row.alignment = "RIGHT" + draw_list_ops(op_row, SM64_AnimTableOps, index) + + table_element.draw_props( + left_row, + col, + index, + self.is_dma, + self.update_table, + self.export_seperately, + export_type, + export_type == "C" and self.gen_enums and not self.is_dma, + actor_name, + prev_enums, + ) + + def draw_table(self, layout: UILayout, export_type: str, actor_name: str, bhv_export: bool): + col = layout.column() + + if self.is_dma: + if export_type == "Binary": + string_int_prop(col, self, "dma_address", "DMA Table Address") + string_int_prop(col, self, "dma_end_address", "DMA Table End") + elif export_type == "C": + multilineLabel( + col, + "The export will follow the vanilla DMA naming\n" + "conventions (anim_xx.inc.c, anim_xx, anim_xx_values, etc).", + icon="INFO", + ) + else: + if export_type == "C": + draw_custom_or_auto(self, col, "table_name", self.get_table_name(actor_name)) + col.prop(self, "gen_enums") + if self.gen_enums: + multilineLabel( + col.box(), + f"Enum List Name: {self.get_enum_name(actor_name)}\n" + f"End Enum: {self.get_enum_end(actor_name)}", + ) + col.separator() + col.prop(self, "export_seperately_prop") + draw_forced(col, self, "override_files_prop", not self.export_seperately) + if bhv_export: + prop_split(col, self, "beginning_animation", "Beginning Animation") + elif export_type == "Binary": + string_int_prop(col, self, "address", "Table Address") + string_int_prop(col, self, "end_address", "Table End") + + box = col.box().column() + box.prop(self, "update_behavior") + if self.update_behavior: + multilineLabel( + box, + "Will update the LOAD_ANIMATIONS and ANIMATE commands.\n" + "Does not raise an error if there is no ANIMATE command", + "INFO", + ) + SM64_SearchAnimatedBhvs.draw_props(box, self, "behaviour", "Behaviour") + if self.behaviour == "Custom": + prop_split(box, self, "behavior_address_prop", "Behavior Address") + prop_split(box, self, "beginning_animation", "Beginning Animation") + + col.prop(self, "write_data_seperately") + if self.write_data_seperately: + string_int_prop(col, self, "data_address", "Data Address") + string_int_prop(col, self, "data_end_address", "Data End") + col.prop(self, "null_delimiter") + if export_type == "Insertable Binary": + draw_custom_or_auto(self, col, "file_name", self.get_table_file_name(actor_name, export_type)) + + col.separator() + + op_row = col.row() + op_row.label( + text="Headers " + (f"({len(self.elements)})" if self.elements else "(Empty)"), + icon="NLA", + ) + draw_list_op(op_row, SM64_AnimTableOps, "ADD") + draw_list_op(op_row, SM64_AnimTableOps, "CLEAR") + + if not self.elements: + return + + box = col.box().column() + actions_dups: dict[Action, list[int]] = {} + if self.is_dma: + actions_repeats: dict[Action, list[int]] = {} # possible dups + last_action = None + for i, element_props in enumerate(self.elements): + action: Action = element_props.get_action(can_reference=False) + if action != last_action: + if action in actions_repeats: + actions_repeats[action].append(i) + if action not in actions_dups: + actions_dups[action] = actions_repeats[action] + else: + actions_repeats[action] = [i] + last_action = action + + if actions_dups: + lines = [f'Action "{a.name}", Headers: {i}' for a, i in actions_dups.items()] + warn_box = box.box() + warn_box.alert = True + multilineLabel( + warn_box, + "In DMA tables, headers for each action must be \n" + "in one sequence or the data will be duplicated.\n" + "This will be handeled automatically but is undesirable.\n" + f'Data duplicate{"s" if len(actions_dups) > 1 else ""} in:\n' + "\n".join(lines), + "INFO", + ) + + prev_enums = {} + element_props: SM64_AnimTableElementProperties + for i, element_props in enumerate(self.elements): + if i != 0: + box.separator() + element_box = box.column() + action = element_props.get_action(not self.is_dma) + if action in actions_dups: + other_actions = [j for j in actions_dups[action] if j != i] + element_box.box().label(text=f"Action duplicates at {other_actions}") + self.draw_element(element_box, i, element_props, export_type, actor_name, prev_enums) + + def draw_c_settings(self, layout: UILayout, header_type: str): + col = layout.column() + if self.is_dma: + prop_split(col, self, "dma_folder", "Folder", icon="FILE_FOLDER") + if header_type == "Custom": + col.label(text="This folder will be relative to your custom path") + else: + decompFolderMessage(col) + return + + def draw_props(self, layout: UILayout, export_type: str, header_type: str): + col = layout.column() + col.prop(self, "is_dma") + if export_type == "C": + self.draw_c_settings(col, header_type) + elif export_type == "Binary" and not self.is_dma: + col.prop(self, "update_table") + + +classes = ( + SM64_AnimHeaderProperties, + SM64_AnimTableElementProperties, + SM64_ActionAnimProperty, + SM64_AnimImportProperties, + SM64_AnimProperties, + SM64_ArmatureAnimProperties, +) + + +def anim_props_register(): + for cls in classes: + register_class(cls) + + +def anim_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/utility.py b/fast64_internal/sm64/animation/utility.py new file mode 100644 index 000000000..b8f18efb9 --- /dev/null +++ b/fast64_internal/sm64/animation/utility.py @@ -0,0 +1,165 @@ +from typing import TYPE_CHECKING +import functools +import re + +from bpy.types import Context, Object, Action, PoseBone + +from ...utility import findStartBones, PluginError, toAlnum +from ..sm64_geolayout_bone import animatableBoneTypes + +if TYPE_CHECKING: + from .properties import SM64_ActionAnimProperty, SM64_AnimProperties, SM64_ArmatureAnimProperties + + +def is_obj_animatable(obj: Object) -> bool: + if obj.type == "ARMATURE" or (obj.type == "MESH" and obj.geo_cmd_static in animatableBoneTypes): + return True + return False + + +def get_anim_obj(context: Context) -> Object | None: + obj = context.object + if obj is None and len(context.selected_objects) > 0: + obj = context.selected_objects[0] + if obj is not None and is_obj_animatable(obj): + return obj + + +def animation_operator_checks(context: Context, requires_animation=True, specific_obj: Object | None = None): + if specific_obj is None: + if len(context.selected_objects) > 1: + raise PluginError("Multiple objects selected at once.") + obj = get_anim_obj(context) + else: + obj = specific_obj + if is_obj_animatable(obj): + raise PluginError(f'Selected object "{obj.name}" is not an armature.') + if requires_animation and obj.animation_data is None: + raise PluginError(f'Armature "{obj.name}" has no animation data.') + + +def get_selected_action(obj: Object, raise_exc=True) -> Action: + assert obj is not None + if not is_obj_animatable(obj): + if raise_exc: + raise ValueError(f'Object "{obj.name}" is not animatable in SM64.') + elif obj.animation_data is not None and obj.animation_data.action is not None: + return obj.animation_data.action + if raise_exc: + raise ValueError(f'No action selected in object "{obj.name}".') + + +def get_anim_owners(obj: Object): + """Get SM64 animation bones from an armature or return the obj if it's an animated cmd mesh""" + + def check_children(children: list[Object] | None): + if children is None: + return + for child in children: + if child.geo_cmd_static in animatableBoneTypes: + raise PluginError("Cannot have child mesh with animation, use an armature") + check_children(child.children) + + if obj.type == "MESH": # Object will be treated as a bone + if obj.geo_cmd_static in animatableBoneTypes: + check_children(obj.children) + return [obj] + else: + raise PluginError("Mesh is not animatable") + + assert obj.type == "ARMATURE", "Obj is neither mesh or armature" + + bones_to_process: list[str] = findStartBones(obj) + current_bone = obj.data.bones[bones_to_process[0]] + anim_bones: list[PoseBone] = [] + + # Get animation bones in order + while len(bones_to_process) > 0: + bone_name = bones_to_process[0] + current_bone = obj.data.bones[bone_name] + current_pose_bone = obj.pose.bones[bone_name] + bones_to_process = bones_to_process[1:] + + # Only handle 0x13 bones for animation + if current_bone.geo_cmd in animatableBoneTypes: + anim_bones.append(current_pose_bone) + + # Traverse children in alphabetical order. + children_names = sorted([bone.name for bone in current_bone.children]) + bones_to_process = children_names + bones_to_process + + return anim_bones + + +def num_to_padded_hex(num: int): + hex_str = hex(num)[2:].upper() # remove the '0x' prefix + return hex_str.zfill(2) + + +@functools.cache +def get_dma_header_name(index: int): + return f"anim_{num_to_padded_hex(index)}" + + +def get_dma_anim_name(header_indices: list[int]): + return f'anim_{"_".join([f"{num_to_padded_hex(num)}" for num in header_indices])}' + + +@functools.cache +def action_name_to_enum_name(action_name: str) -> str: + return re.sub(r"^_(\d+_)+(?=\w)", "", toAlnum(action_name), flags=re.MULTILINE) + + +@functools.cache +def anim_name_to_enum_name(anim_name: str) -> str: + enum_name = anim_name.upper() + enum_name: str = re.sub(r"(?<=_)_|_$", "", toAlnum(enum_name), flags=re.MULTILINE) + if anim_name == enum_name: + enum_name = f"{enum_name}_ENUM" + return enum_name + + +def duplicate_name(name: str, existing_names: dict[str, int]) -> str: + """Updates existing_names""" + current_num = existing_names.get(name) + if current_num is None: + existing_names[name] = 0 + elif name != "": + current_num += 1 + existing_names[name] = current_num + return f"{name}_{current_num}" + return name + + +def table_name_to_enum(name: str): + return name.title().replace("_", "") + + +def get_action_props(action: Action) -> "SM64_ActionAnimProperty": + return action.fast64.sm64.animation + + +def get_scene_anim_props(context: Context) -> "SM64_AnimProperties": + return context.scene.fast64.sm64.animation + + +def get_anim_props(context: Context) -> "SM64_ArmatureAnimProperties": + obj = get_anim_obj(context) + assert obj is not None + return obj.fast64.sm64.animation + + +def get_anim_actor_name(context: Context) -> str | None: + sm64_props = context.scene.fast64.sm64 + if sm64_props.export_type == "C" and sm64_props.combined_export.export_anim: + return toAlnum(sm64_props.combined_export.obj_name_anim) + elif context.object: + return sm64_props.combined_export.filter_name(toAlnum(context.object.name), True) + else: + return None + + +def dma_structure_context(context: Context) -> bool: + if get_anim_obj(context) is None: + return False + return get_anim_props(context).is_dma diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 471ad617d..1cd39eea0 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import bpy from bpy.types import PropertyGroup, UILayout, Context from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty, PointerProperty @@ -6,11 +7,12 @@ from bpy.utils import register_class, unregister_class from ...render_settings import on_update_render_settings -from ...utility import directory_path_checks, directory_ui_warnings, prop_split, upgrade_old_prop +from ...utility import directory_path_checks, directory_ui_warnings, prop_split, upgrade_old_prop, get_first_set_prop from ..sm64_constants import defaultExtendSegment4 from ..sm64_objects import SM64_CombinedObjectProperties from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings from ..tools import SM64_AddrConvProperties +from ..animation.properties import SM64_AnimProperties from .constants import ( enum_refresh_versions, @@ -22,17 +24,17 @@ def decomp_path_update(self, context: Context): fast64_settings = context.scene.fast64.settings - if fast64_settings.repo_settings_path: + if fast64_settings.repo_settings_path and Path(abspath(fast64_settings.repo_settings_path)).exists(): return - directory_path_checks(abspath(self.decomp_path)) - fast64_settings.repo_settings_path = os.path.join(abspath(self.decomp_path), "fast64.json") + directory_path_checks(self.abs_decomp_path) + fast64_settings.repo_settings_path = str(self.abs_decomp_path / "fast64.json") class SM64_Properties(PropertyGroup): """Global SM64 Scene Properties found under scene.fast64.sm64""" version: IntProperty(name="SM64_Properties Version", default=0) - cur_version = 3 # version after property migration + cur_version = 4 # version after property migration # UI Selection show_importing_menus: BoolProperty(name="Show Importing Menus", default=False) @@ -80,10 +82,29 @@ class SM64_Properties(PropertyGroup): name="Matstack Fix", description="Exports account for matstack fix requirements", ) + # could be used for other properties outside animation + designated_prop: BoolProperty( + name="Designated Initialization for Animation Tables", + description="Extremely recommended but must be off when compiling with IDO", + ) + + animation: PointerProperty(type=SM64_AnimProperties) @property def binary_export(self): - return self.export_type in ["Binary", "Insertable Binary"] + return self.export_type in {"Binary", "Insertable Binary"} + + @property + def abs_decomp_path(self) -> Path: + return Path(abspath(self.decomp_path)) + + @property + def hackersm64(self) -> bool: + return self.refresh_version.startswith("HackerSM64") + + @property + def designated(self) -> bool: + return self.designated_prop or self.hackersm64 @staticmethod def upgrade_changed_props(): @@ -107,10 +128,13 @@ def upgrade_changed_props(): "custom_level_name": {"levelName", "geoLevelName", "colLevelName", "animLevelName"}, "non_decomp_level": {"levelCustomExport"}, "export_header_type": {"geoExportHeaderType", "colExportHeaderType", "animExportHeaderType"}, + "binary_level": {"levelAnimExport"}, + # as the others binary props get carried over to here we need to update the cur_version again } for scene in bpy.data.scenes: sm64_props: SM64_Properties = scene.fast64.sm64 sm64_props.address_converter.upgrade_changed_props(scene) + sm64_props.animation.upgrade_changed_props(scene) if sm64_props.version == SM64_Properties.cur_version: continue upgrade_old_prop( @@ -131,6 +155,11 @@ def upgrade_changed_props(): combined_props = scene.fast64.sm64.combined_export for new, old in old_export_props_to_new.items(): upgrade_old_prop(combined_props, new, scene, old) + + insertable_directory = get_first_set_prop(scene, "animInsertableBinaryPath") + if insertable_directory is not None: # Ignores file name + combined_props.insertable_directory = os.path.split(insertable_directory)[1] + sm64_props.version = SM64_Properties.cur_version def draw_props(self, layout: UILayout, show_repo_settings: bool = True): @@ -149,7 +178,7 @@ def draw_props(self, layout: UILayout, show_repo_settings: bool = True): col.prop(self, "extend_bank_4") elif not self.binary_export: prop_split(col, self, "decomp_path", "Decomp Path") - directory_ui_warnings(col, abspath(self.decomp_path)) + directory_ui_warnings(col, self.abs_decomp_path) col.separator() if not self.binary_export: diff --git a/fast64_internal/sm64/settings/repo_settings.py b/fast64_internal/sm64/settings/repo_settings.py index f74b7eb3d..5c651170f 100644 --- a/fast64_internal/sm64/settings/repo_settings.py +++ b/fast64_internal/sm64/settings/repo_settings.py @@ -22,6 +22,8 @@ def save_sm64_repo_settings(scene: Scene): data["compression_format"] = sm64_props.compression_format data["force_extended_ram"] = sm64_props.force_extended_ram data["matstack_fix"] = sm64_props.matstack_fix + if not sm64_props.hackersm64: + data["designated"] = sm64_props.designated_prop return data @@ -42,6 +44,7 @@ def load_sm64_repo_settings(scene: Scene, data: dict[str, Any]): sm64_props.compression_format = data.get("compression_format", sm64_props.compression_format) sm64_props.force_extended_ram = data.get("force_extended_ram", sm64_props.force_extended_ram) sm64_props.matstack_fix = data.get("matstack_fix", sm64_props.matstack_fix) + sm64_props.designated_prop = data.get("designated", sm64_props.designated_prop) def draw_repo_settings(scene: Scene, layout: UILayout): @@ -54,5 +57,7 @@ def draw_repo_settings(scene: Scene, layout: UILayout): prop_split(col, sm64_props, "refresh_version", "Refresh (Function Map)") col.prop(sm64_props, "force_extended_ram") col.prop(sm64_props, "matstack_fix") + if not sm64_props.hackersm64: + col.prop(sm64_props, "designated_prop") col.label(text="See Fast64 repo settings for general settings", icon="INFO") diff --git a/fast64_internal/sm64/sm64_anim.py b/fast64_internal/sm64/sm64_anim.py deleted file mode 100644 index 0aa570cb0..000000000 --- a/fast64_internal/sm64/sm64_anim.py +++ /dev/null @@ -1,1119 +0,0 @@ -import bpy, os, copy, shutil, mathutils, math -from bpy.utils import register_class, unregister_class -from ..panels import SM64_Panel -from .sm64_level_parser import parseLevelAtPointer -from .sm64_rom_tweaks import ExtendBank0x04 -from .sm64_geolayout_bone import animatableBoneTypes - -from ..utility import ( - CData, - PluginError, - ValueFrameData, - raisePluginError, - encodeSegmentedAddr, - decodeSegmentedAddr, - getExportDir, - toAlnum, - writeIfNotFound, - get64bitAlignedAddr, - writeInsertableFile, - getFrameInterval, - findStartBones, - saveTranslationFrame, - saveQuaternionFrame, - removeTrailingFrames, - applyRotation, - getPathAndLevel, - applyBasicTweaks, - tempName, - bytesToHex, - prop_split, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, - writeBoxExportType, - stashActionInArmature, - enumExportHeaderType, -) - -from .sm64_constants import ( - bank0Segment, - insertableBinaryTypes, - level_pointers, - defaultExtendSegment4, - level_enums, - enumLevelNames, - marioAnimations, -) - -from .sm64_utility import export_rom_checks, import_rom_checks - -sm64_anim_types = {"ROTATE", "TRANSLATE"} - - -class SM64_Animation: - def __init__(self, name): - self.name = name - self.header = None - self.indices = SM64_ShortArray(name + "_indices", False) - self.values = SM64_ShortArray(name + "_values", True) - - def get_ptr_offsets(self, isDMA): - return [12, 16] if not isDMA else [] - - def to_binary(self, segmentData, isDMA, startAddress): - return ( - self.header.to_binary(segmentData, isDMA, startAddress) + self.indices.to_binary() + self.values.to_binary() - ) - - def to_c(self): - data = CData() - data.header = "extern const struct Animation *const " + self.name + "[];\n" - data.source = self.values.to_c() + "\n" + self.indices.to_c() + "\n" + self.header.to_c() + "\n" - return data - - -class SM64_ShortArray: - def __init__(self, name, signed): - self.name = name - self.shortData = [] - self.signed = signed - - def to_binary(self): - data = bytearray(0) - for short in self.shortData: - # All euler values have been pre-converted to positive values, so don't care about signed. - data += short.to_bytes(2, "big", signed=False) - return data - - def to_c(self): - data = "static const " + ("s" if self.signed else "u") + "16 " + self.name + "[] = {\n\t" - wrapCounter = 0 - for short in self.shortData: - data += "0x" + format(short, "04X") + ", " - wrapCounter += 1 - if wrapCounter > 8: - data += "\n\t" - wrapCounter = 0 - data += "\n};\n" - return data - - -class SM64_AnimationHeader: - def __init__( - self, - name, - repetitions, - marioYOffset, - frameInterval, - nodeCount, - transformValuesStart, - transformIndicesStart, - animSize, - ): - self.name = name - self.repetitions = repetitions - self.marioYOffset = marioYOffset - self.frameInterval = frameInterval - self.nodeCount = nodeCount - self.transformValuesStart = transformValuesStart - self.transformIndicesStart = transformIndicesStart - self.animSize = animSize # DMA animations only - - self.transformIndices = [] - - # presence of segmentData indicates DMA. - def to_binary(self, segmentData, isDMA, startAddress): - if isDMA: - transformValuesStart = self.transformValuesStart - transformIndicesStart = self.transformIndicesStart - else: - transformValuesStart = self.transformValuesStart + startAddress - transformIndicesStart = self.transformIndicesStart + startAddress - - data = bytearray(0) - data.extend(self.repetitions.to_bytes(2, byteorder="big")) - data.extend(self.marioYOffset.to_bytes(2, byteorder="big")) # y offset, only used for mario - data.extend([0x00, 0x00]) # unknown, common with secondary anims, variable length animations? - data.extend(int(round(self.frameInterval[0])).to_bytes(2, byteorder="big")) - data.extend(int(round(self.frameInterval[1] - 1)).to_bytes(2, byteorder="big")) - data.extend(self.nodeCount.to_bytes(2, byteorder="big")) - if not isDMA: - data.extend(encodeSegmentedAddr(transformValuesStart, segmentData)) - data.extend(encodeSegmentedAddr(transformIndicesStart, segmentData)) - data.extend(bytearray([0x00] * 6)) - else: - data.extend(transformValuesStart.to_bytes(4, byteorder="big")) - data.extend(transformIndicesStart.to_bytes(4, byteorder="big")) - data.extend(self.animSize.to_bytes(4, byteorder="big")) - data.extend(bytearray([0x00] * 2)) - return data - - def to_c(self): - data = ( - "static const struct Animation " - + self.name - + " = {\n" - + "\t" - + str(self.repetitions) - + ",\n" - + "\t" - + str(self.marioYOffset) - + ",\n" - + "\t0,\n" - + "\t" - + str(int(round(self.frameInterval[0]))) - + ",\n" - + "\t" - + str(int(round(self.frameInterval[1] - 1))) - + ",\n" - + "\tANIMINDEX_NUMPARTS(" - + self.name - + "_indices),\n" - + "\t" - + self.name - + "_values,\n" - + "\t" - + self.name - + "_indices,\n" - + "\t0,\n" - + "};\n" - ) - return data - - -class SM64_AnimIndexNode: - def __init__(self, x, y, z): - self.x = x - self.y = y - self.z = z - - -class SM64_AnimIndex: - def __init__(self, numFrames, startOffset): - self.startOffset = startOffset - self.numFrames = numFrames - - -def getLastKeyframeTime(keyframes): - last = keyframes[0].co[0] - for keyframe in keyframes: - if keyframe.co[0] > last: - last = keyframe.co[0] - return last - - -# add definition to groupN.h -# add data/table includes to groupN.c (bin_id?) -# add data/table files -def exportAnimationC(armatureObj, loopAnim, dirPath, dirName, groupName, customExport, headerType, levelName): - dirPath, texDir = getExportDir(customExport, dirPath, headerType, levelName, "", dirName) - - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, dirName + "_anim") - animName = armatureObj.animation_data.action.name - - geoDirPath = os.path.join(dirPath, toAlnum(dirName)) - if not os.path.exists(geoDirPath): - os.mkdir(geoDirPath) - - animDirPath = os.path.join(geoDirPath, "anims") - if not os.path.exists(animDirPath): - os.mkdir(animDirPath) - - animsName = dirName + "_anims" - animFileName = "anim_" + toAlnum(animName) + ".inc.c" - animPath = os.path.join(animDirPath, animFileName) - - data = sm64_anim.to_c() - outFile = open(animPath, "w", newline="\n") - outFile.write(data.source) - outFile.close() - - headerPath = os.path.join(geoDirPath, "anim_header.h") - headerFile = open(headerPath, "w", newline="\n") - headerFile.write("extern const struct Animation *const " + animsName + "[];\n") - headerFile.close() - - # write to data.inc.c - dataFilePath = os.path.join(animDirPath, "data.inc.c") - if not os.path.exists(dataFilePath): - dataFile = open(dataFilePath, "w", newline="\n") - dataFile.close() - writeIfNotFound(dataFilePath, '#include "' + animFileName + '"\n', "") - - # write to table.inc.c - tableFilePath = os.path.join(animDirPath, "table.inc.c") - - # if table doesn´t exist, create one - if not os.path.exists(tableFilePath): - tableFile = open(tableFilePath, "w", newline="\n") - tableFile.write("const struct Animation *const " + animsName + "[] = {\n\tNULL,\n};\n") - tableFile.close() - - stringData = "" - with open(tableFilePath, "r") as f: - stringData = f.read() - - # if animation header isn´t already in the table then add it. - if sm64_anim.header.name not in stringData: - # search for the NULL value which represents the end of the table - # (this value is not present in vanilla animation tables) - footerIndex = stringData.rfind("\tNULL,\n") - - # if the null value cant be found, look for the end of the array - if footerIndex == -1: - footerIndex = stringData.rfind("};") - - # if that can´t be found then throw an error. - if footerIndex == -1: - raise PluginError("Animation table´s footer does not seem to exist.") - - stringData = stringData[:footerIndex] + "\tNULL,\n" + stringData[footerIndex:] - - stringData = stringData[:footerIndex] + f"\t&{sm64_anim.header.name},\n" + stringData[footerIndex:] - - with open(tableFilePath, "w") as f: - f.write(stringData) - - if not customExport: - if headerType == "Actor": - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/anims/data.inc.c"', "") - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/anims/table.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + dirName + '/anim_header.h"', "#endif") - elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/anims/data.inc.c"', "") - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/anims/table.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + dirName + '/anim_header.h"', "\n#endif" - ) - - -def exportAnimationBinary(romfile, exportRange, armatureObj, DMAAddresses, segmentData, isDMA, loopAnim): - startAddress = get64bitAlignedAddr(exportRange[0]) - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, armatureObj.name) - - animData = sm64_anim.to_binary(segmentData, isDMA, startAddress) - - if startAddress + len(animData) > exportRange[1]: - raise PluginError( - "Size too big: Data ends at " - + hex(startAddress + len(animData)) - + ", which is larger than the specified range." - ) - - romfile.seek(startAddress) - romfile.write(animData) - - addrRange = (startAddress, startAddress + len(animData)) - - if not isDMA: - animTablePointer = get64bitAlignedAddr(startAddress + len(animData)) - romfile.seek(animTablePointer) - romfile.write(encodeSegmentedAddr(startAddress, segmentData)) - return addrRange, animTablePointer - else: - if DMAAddresses is not None: - romfile.seek(DMAAddresses["entry"]) - romfile.write((startAddress - DMAAddresses["start"]).to_bytes(4, byteorder="big")) - romfile.seek(DMAAddresses["entry"] + 4) - romfile.write(len(animData).to_bytes(4, byteorder="big")) - return addrRange, None - - -def exportAnimationInsertableBinary(filepath, armatureObj, isDMA, loopAnim): - startAddress = get64bitAlignedAddr(0) - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, armatureObj.name) - segmentData = copy.copy(bank0Segment) - - animData = sm64_anim.to_binary(segmentData, isDMA, startAddress) - - if startAddress + len(animData) > 0xFFFFFF: - raise PluginError( - "Size too big: Data ends at " - + hex(startAddress + len(animData)) - + ", which is larger than the specified range." - ) - - writeInsertableFile( - filepath, insertableBinaryTypes["Animation"], sm64_anim.get_ptr_offsets(isDMA), startAddress, animData - ) - - -def exportAnimationCommon(armatureObj, loopAnim, name): - if armatureObj.animation_data is None or armatureObj.animation_data.action is None: - raise PluginError("No active animation selected.") - - anim = armatureObj.animation_data.action - stashActionInArmature(armatureObj, anim) - - sm64_anim = SM64_Animation(toAlnum(name + "_" + anim.name)) - - nodeCount = len(armatureObj.data.bones) - - frame_start, frame_last = getFrameInterval(anim) - - translationData, armatureFrameData = convertAnimationData( - anim, - armatureObj, - frame_start=frame_start, - frame_count=(frame_last - frame_start + 1), - ) - - repetitions = 0 if loopAnim else 1 - marioYOffset = 0x00 # ??? Seems to be this value for most animations - - transformValuesOffset = 0 - headerSize = 0x1A - transformIndicesStart = headerSize # 0x18 if including animSize? - - # all node rotations + root translation - # *3 for each property (xyz) and *4 for entry size - # each keyframe stored as 2 bytes - # transformValuesStart = transformIndicesStart + (nodeCount + 1) * 3 * 4 - transformValuesStart = transformIndicesStart - - for translationFrameProperty in translationData: - frameCount = len(translationFrameProperty.frames) - sm64_anim.indices.shortData.append(frameCount) - sm64_anim.indices.shortData.append(transformValuesOffset) - if (transformValuesOffset) > 2**16 - 1: - raise PluginError("Animation is too large.") - transformValuesOffset += frameCount - transformValuesStart += 4 - for value in translationFrameProperty.frames: - sm64_anim.values.shortData.append( - int.from_bytes(value.to_bytes(2, "big", signed=True), byteorder="big", signed=False) - ) - - for boneFrameData in armatureFrameData: - for boneFrameDataProperty in boneFrameData: - frameCount = len(boneFrameDataProperty.frames) - sm64_anim.indices.shortData.append(frameCount) - sm64_anim.indices.shortData.append(transformValuesOffset) - if (transformValuesOffset) > 2**16 - 1: - raise PluginError("Animation is too large.") - transformValuesOffset += frameCount - transformValuesStart += 4 - for value in boneFrameDataProperty.frames: - sm64_anim.values.shortData.append(value) - - animSize = headerSize + len(sm64_anim.indices.shortData) * 2 + len(sm64_anim.values.shortData) * 2 - - sm64_anim.header = SM64_AnimationHeader( - sm64_anim.name, - repetitions, - marioYOffset, - [frame_start, frame_last + 1], - nodeCount, - transformValuesStart, - transformIndicesStart, - animSize, - ) - - return sm64_anim - - -def convertAnimationData(anim, armatureObj, *, frame_start, frame_count): - bonesToProcess = findStartBones(armatureObj) - currentBone = armatureObj.data.bones[bonesToProcess[0]] - animBones = [] - - # Get animation bones in order - while len(bonesToProcess) > 0: - boneName = bonesToProcess[0] - currentBone = armatureObj.data.bones[boneName] - currentPoseBone = armatureObj.pose.bones[boneName] - bonesToProcess = bonesToProcess[1:] - - # Only handle 0x13 bones for animation - if currentBone.geo_cmd in animatableBoneTypes: - animBones.append(boneName) - - # Traverse children in alphabetical order. - childrenNames = sorted([bone.name for bone in currentBone.children]) - bonesToProcess = childrenNames + bonesToProcess - - # list of boneFrameData, which is [[x frames], [y frames], [z frames]] - translationData = [ValueFrameData(0, i, []) for i in range(3)] - armatureFrameData = [ - [ValueFrameData(i, 0, []), ValueFrameData(i, 1, []), ValueFrameData(i, 2, [])] for i in range(len(animBones)) - ] - - currentFrame = bpy.context.scene.frame_current - for frame in range(frame_start, frame_start + frame_count): - bpy.context.scene.frame_set(frame) - rootPoseBone = armatureObj.pose.bones[animBones[0]] - - translation = ( - mathutils.Matrix.Scale(bpy.context.scene.fast64.sm64.blender_to_sm64_scale, 4) @ rootPoseBone.matrix_basis - ).decompose()[0] - saveTranslationFrame(translationData, translation) - - for boneIndex in range(len(animBones)): - boneName = animBones[boneIndex] - currentBone = armatureObj.data.bones[boneName] - currentPoseBone = armatureObj.pose.bones[boneName] - - rotationValue = (currentBone.matrix.to_4x4().inverted() @ currentPoseBone.matrix).to_quaternion() - if currentBone.parent is not None: - rotationValue = ( - currentBone.matrix.to_4x4().inverted() - @ currentPoseBone.parent.matrix.inverted() - @ currentPoseBone.matrix - ).to_quaternion() - - # rest pose local, compared to current pose local - - saveQuaternionFrame(armatureFrameData[boneIndex], rotationValue) - - bpy.context.scene.frame_set(currentFrame) - removeTrailingFrames(translationData) - for frameData in armatureFrameData: - removeTrailingFrames(frameData) - - return translationData, armatureFrameData - - -def getNextBone(boneStack, armatureObj): - if len(boneStack) == 0: - raise PluginError("More bones in animation than on armature.") - bone = armatureObj.data.bones[boneStack[0]] - boneStack = boneStack[1:] - boneStack = sorted([child.name for child in bone.children]) + boneStack - - # Only return 0x13 bone - while armatureObj.data.bones[bone.name].geo_cmd not in animatableBoneTypes: - if len(boneStack) == 0: - raise PluginError("More bones in animation than on armature.") - bone = armatureObj.data.bones[boneStack[0]] - boneStack = boneStack[1:] - boneStack = sorted([child.name for child in bone.children]) + boneStack - - return bone, boneStack - - -def importAnimationToBlender(romfile, startAddress, armatureObj, segmentData, isDMA, animName): - boneStack = findStartBones(armatureObj) - startBoneName = boneStack[0] - if armatureObj.data.bones[startBoneName].geo_cmd not in animatableBoneTypes: - startBone, boneStack = getNextBone(boneStack, armatureObj) - startBoneName = startBone.name - boneStack = [startBoneName] + boneStack - - animationHeader, armatureFrameData = readAnimation(animName, romfile, startAddress, segmentData, isDMA) - - if len(armatureFrameData) > len(armatureObj.data.bones) + 1: - raise PluginError("More bones in animation than on armature.") - - # bpy.context.scene.render.fps = 30 - bpy.context.scene.frame_end = animationHeader.frameInterval[1] - anim = bpy.data.actions.new(animName) - - isRootTranslation = True - # boneFrameData = [[x keyframes], [y keyframes], [z keyframes]] - # len(armatureFrameData) should be = number of bones - # property index = 0,1,2 (aka x,y,z) - for boneFrameData in armatureFrameData: - if isRootTranslation: - for propertyIndex in range(3): - fcurve = anim.fcurves.new( - data_path='pose.bones["' + startBoneName + '"].location', - index=propertyIndex, - action_group=startBoneName, - ) - for frame in range(len(boneFrameData[propertyIndex])): - fcurve.keyframe_points.insert(frame, boneFrameData[propertyIndex][frame]) - isRootTranslation = False - else: - bone, boneStack = getNextBone(boneStack, armatureObj) - for propertyIndex in range(3): - fcurve = anim.fcurves.new( - data_path='pose.bones["' + bone.name + '"].rotation_euler', - index=propertyIndex, - action_group=bone.name, - ) - for frame in range(len(boneFrameData[propertyIndex])): - fcurve.keyframe_points.insert(frame, boneFrameData[propertyIndex][frame]) - - if armatureObj.animation_data is None: - armatureObj.animation_data_create() - - stashActionInArmature(armatureObj, anim) - armatureObj.animation_data.action = anim - - -def readAnimation(name, romfile, startAddress, segmentData, isDMA): - animationHeader = readAnimHeader(name, romfile, startAddress, segmentData, isDMA) - - print("Frames: " + str(animationHeader.frameInterval[1]) + " / Nodes: " + str(animationHeader.nodeCount)) - - animationHeader.transformIndices = readAnimIndices( - romfile, animationHeader.transformIndicesStart, animationHeader.nodeCount - ) - - armatureFrameData = [] # list of list of frames - - # sm64 space -> blender space -> pose space - # BlenderToSM64: YZX (set rotation mode of bones) - # SM64toBlender: ZXY (set anim keyframes and model armature) - # new bones should extrude in +Y direction - - # handle root translation - boneFrameData = [[], [], []] - rootIndexNode = animationHeader.transformIndices[0] - boneFrameData[0] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.x) - ] - boneFrameData[1] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.y) - ] - boneFrameData[2] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.z) - ] - armatureFrameData.append(boneFrameData) - - # handle rotations - for boneIndexNode in animationHeader.transformIndices[1:]: - boneFrameData = [[], [], []] - - # Transforming SM64 space to Blender space - boneFrameData[0] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.x) - ] - boneFrameData[1] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.y) - ] - boneFrameData[2] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.z) - ] - - armatureFrameData.append(boneFrameData) - - return (animationHeader, armatureFrameData) - - -def getKeyFramesRotation(romfile, transformValuesStart, boneIndex): - ptrToValue = transformValuesStart + boneIndex.startOffset - romfile.seek(ptrToValue) - - keyframes = [] - for frame in range(boneIndex.numFrames): - romfile.seek(ptrToValue + frame * 2) - value = int.from_bytes(romfile.read(2), "big") * 360 / (2**16) - keyframes.append(math.radians(value)) - - return keyframes - - -def getKeyFramesTranslation(romfile, transformValuesStart, boneIndex): - ptrToValue = transformValuesStart + boneIndex.startOffset - romfile.seek(ptrToValue) - - keyframes = [] - for frame in range(boneIndex.numFrames): - romfile.seek(ptrToValue + frame * 2) - keyframes.append( - int.from_bytes(romfile.read(2), "big", signed=True) / bpy.context.scene.fast64.sm64.blender_to_sm64_scale - ) - - return keyframes - - -def readAnimHeader(name, romfile, startAddress, segmentData, isDMA): - frameInterval = [0, 0] - - romfile.seek(startAddress + 0x00) - numRepeats = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x02) - marioYOffset = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x06) - frameInterval[0] = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x08) - frameInterval[1] = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x0A) - numNodes = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x0C) - transformValuesOffset = int.from_bytes(romfile.read(4), "big") - if isDMA: - transformValuesStart = startAddress + transformValuesOffset - else: - transformValuesStart = decodeSegmentedAddr(transformValuesOffset.to_bytes(4, byteorder="big"), segmentData) - - romfile.seek(startAddress + 0x10) - transformIndicesOffset = int.from_bytes(romfile.read(4), "big") - if isDMA: - transformIndicesStart = startAddress + transformIndicesOffset - else: - transformIndicesStart = decodeSegmentedAddr(transformIndicesOffset.to_bytes(4, byteorder="big"), segmentData) - - romfile.seek(startAddress + 0x14) - animSize = int.from_bytes(romfile.read(4), "big") - - return SM64_AnimationHeader( - name, numRepeats, marioYOffset, frameInterval, numNodes, transformValuesStart, transformIndicesStart, animSize - ) - - -def readAnimIndices(romfile, ptrAddress, nodeCount): - indices = [] - - # Handle root transform - rootPosIndex = readTransformIndex(romfile, ptrAddress) - indices.append(rootPosIndex) - - # Handle rotations - for i in range(nodeCount): - rotationIndex = readTransformIndex(romfile, ptrAddress + (i + 1) * 12) - indices.append(rotationIndex) - - return indices - - -def readTransformIndex(romfile, startAddress): - x = readValueIndex(romfile, startAddress + 0) - y = readValueIndex(romfile, startAddress + 4) - z = readValueIndex(romfile, startAddress + 8) - - return SM64_AnimIndexNode(x, y, z) - - -def readValueIndex(romfile, startAddress): - romfile.seek(startAddress) - numFrames = int.from_bytes(romfile.read(2), "big") - romfile.seek(startAddress + 2) - - # multiply 2 because value is the index in array of shorts (???) - startOffset = int.from_bytes(romfile.read(2), "big") * 2 - # print(str(hex(startAddress)) + ": " + str(numFrames) + " " + str(startOffset)) - return SM64_AnimIndex(numFrames, startOffset) - - -def writeAnimation(romfile, startAddress, segmentData): - pass - - -def writeAnimHeader(romfile, startAddress, segmentData): - pass - - -class SM64_ExportAnimMario(bpy.types.Operator): - bl_idname = "object.sm64_export_anim" - bl_label = "Export Animation" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileOutput = None - tempROM = None - try: - if len(context.selected_objects) == 0 or not isinstance( - context.selected_objects[0].data, bpy.types.Armature - ): - raise PluginError("Armature not selected.") - if len(context.selected_objects) > 1: - raise PluginError("Multiple objects selected, make sure to select only one.") - armatureObj = context.selected_objects[0] - if context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - try: - # Rotate all armatures 90 degrees - applyRotation([armatureObj], math.radians(90), "X") - - if context.scene.fast64.sm64.export_type == "C": - exportPath, levelName = getPathAndLevel( - context.scene.animCustomExport, - context.scene.animExportPath, - context.scene.animLevelName, - context.scene.animLevelOption, - ) - if not context.scene.animCustomExport: - applyBasicTweaks(exportPath) - exportAnimationC( - armatureObj, - context.scene.loopAnimation, - exportPath, - bpy.context.scene.animName, - bpy.context.scene.animGroupName, - context.scene.animCustomExport, - context.scene.animExportHeaderType, - levelName, - ) - self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.export_type == "Insertable Binary": - exportAnimationInsertableBinary( - bpy.path.abspath(context.scene.animInsertableBinaryPath), - armatureObj, - context.scene.isDMAExport, - context.scene.loopAnimation, - ) - self.report({"INFO"}, "Success! Animation at " + context.scene.animInsertableBinaryPath) - else: - export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) - tempROM = tempName(context.scene.fast64.sm64.output_rom) - romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") - shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) - romfileExport.close() - romfileOutput = open(bpy.path.abspath(tempROM), "rb+") - - # Note actual level doesn't matter for Mario, since he is in all of them - levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelAnimExport]) - segmentData = levelParsed.segmentData - if context.scene.fast64.sm64.extend_bank_4: - ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) - - DMAAddresses = None - if context.scene.animOverwriteDMAEntry: - DMAAddresses = {} - DMAAddresses["start"] = int(context.scene.DMAStartAddress, 16) - DMAAddresses["entry"] = int(context.scene.DMAEntryAddress, 16) - - addrRange, nonDMAListPtr = exportAnimationBinary( - romfileOutput, - [int(context.scene.animExportStart, 16), int(context.scene.animExportEnd, 16)], - bpy.context.active_object, - DMAAddresses, - segmentData, - context.scene.isDMAExport, - context.scene.loopAnimation, - ) - - if not context.scene.isDMAExport: - segmentedPtr = encodeSegmentedAddr(addrRange[0], segmentData) - if context.scene.setAnimListIndex: - romfileOutput.seek(int(context.scene.addr_0x27, 16) + 4) - segAnimPtr = romfileOutput.read(4) - virtAnimPtr = decodeSegmentedAddr(segAnimPtr, segmentData) - romfileOutput.seek(virtAnimPtr + 4 * context.scene.animListIndexExport) - romfileOutput.write(segmentedPtr) - if context.scene.overwrite_0x28: - romfileOutput.seek(int(context.scene.addr_0x28, 16) + 1) - romfileOutput.write(bytearray([context.scene.animListIndexExport])) - else: - segmentedPtr = None - - romfileOutput.close() - if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): - os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) - - if not context.scene.isDMAExport: - if context.scene.setAnimListIndex: - self.report( - {"INFO"}, - "Sucess! Animation table at " - + hex(virtAnimPtr) - + ", animation at (" - + hex(addrRange[0]) - + ", " - + hex(addrRange[1]) - + ") " - + "(Seg. " - + bytesToHex(segmentedPtr) - + ").", - ) - else: - self.report( - {"INFO"}, - "Sucess! Animation at (" - + hex(addrRange[0]) - + ", " - + hex(addrRange[1]) - + ") " - + "(Seg. " - + bytesToHex(segmentedPtr) - + ").", - ) - else: - self.report( - {"INFO"}, "Success! Animation at (" + hex(addrRange[0]) + ", " + hex(addrRange[1]) + ")." - ) - - applyRotation([armatureObj], math.radians(-90), "X") - except Exception as e: - applyRotation([armatureObj], math.radians(-90), "X") - - if romfileOutput is not None: - romfileOutput.close() - if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): - os.remove(bpy.path.abspath(tempROM)) - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ExportAnimPanel(SM64_Panel): - bl_idname = "SM64_PT_export_anim" - bl_label = "SM64 Animation Exporter" - goal = "Object/Actor/Anim" - - # called every frame - def draw(self, context): - col = self.layout.column() - propsAnimExport = col.operator(SM64_ExportAnimMario.bl_idname) - - col.prop(context.scene, "loopAnimation") - - if context.scene.fast64.sm64.export_type == "C": - col.prop(context.scene, "animCustomExport") - if context.scene.animCustomExport: - col.prop(context.scene, "animExportPath") - prop_split(col, context.scene, "animName", "Name") - customExportWarning(col) - else: - prop_split(col, context.scene, "animExportHeaderType", "Export Type") - prop_split(col, context.scene, "animName", "Name") - if context.scene.animExportHeaderType == "Actor": - prop_split(col, context.scene, "animGroupName", "Group Name") - elif context.scene.animExportHeaderType == "Level": - prop_split(col, context.scene, "animLevelOption", "Level") - if context.scene.animLevelOption == "Custom": - prop_split(col, context.scene, "animLevelName", "Level Name") - - decompFolderMessage(col) - writeBox = makeWriteInfoBox(col) - writeBoxExportType( - writeBox, - context.scene.animExportHeaderType, - context.scene.animName, - context.scene.animLevelName, - context.scene.animLevelOption, - ) - - elif context.scene.fast64.sm64.export_type == "Insertable Binary": - col.prop(context.scene, "isDMAExport") - col.prop(context.scene, "animInsertableBinaryPath") - else: - col.prop(context.scene, "isDMAExport") - if context.scene.isDMAExport: - col.prop(context.scene, "animOverwriteDMAEntry") - if context.scene.animOverwriteDMAEntry: - prop_split(col, context.scene, "DMAStartAddress", "DMA Start Address") - prop_split(col, context.scene, "DMAEntryAddress", "DMA Entry Address") - else: - col.prop(context.scene, "setAnimListIndex") - if context.scene.setAnimListIndex: - prop_split(col, context.scene, "addr_0x27", "27 Command Address") - prop_split(col, context.scene, "animListIndexExport", "Anim List Index") - col.prop(context.scene, "overwrite_0x28") - if context.scene.overwrite_0x28: - prop_split(col, context.scene, "addr_0x28", "28 Command Address") - col.prop(context.scene, "levelAnimExport") - col.separator() - prop_split(col, context.scene, "animExportStart", "Start Address") - prop_split(col, context.scene, "animExportEnd", "End Address") - - -class SM64_ImportAnimMario(bpy.types.Operator): - bl_idname = "object.sm64_import_anim" - bl_label = "Import Animation" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileSrc = None - try: - import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) - romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - try: - levelParsed = parseLevelAtPointer(romfileSrc, level_pointers[context.scene.levelAnimImport]) - segmentData = levelParsed.segmentData - - animStart = int(context.scene.animStartImport, 16) - if context.scene.animIsSegPtr: - animStart = decodeSegmentedAddr(animStart.to_bytes(4, "big"), segmentData) - - if not context.scene.isDMAImport and context.scene.animIsAnimList: - romfileSrc.seek(animStart + 4 * context.scene.animListIndexImport) - actualPtr = romfileSrc.read(4) - animStart = decodeSegmentedAddr(actualPtr, segmentData) - - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - armatureObj = context.active_object - if armatureObj.type != "ARMATURE": - raise PluginError("Armature not selected.") - - importAnimationToBlender( - romfileSrc, animStart, armatureObj, segmentData, context.scene.isDMAImport, "sm64_anim" - ) - romfileSrc.close() - self.report({"INFO"}, "Success!") - except Exception as e: - if romfileSrc is not None: - romfileSrc.close() - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ImportAllMarioAnims(bpy.types.Operator): - bl_idname = "object.sm64_import_mario_anims" - bl_label = "Import All Mario Animations" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileSrc = None - try: - import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) - romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - try: - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - armatureObj = context.active_object - if armatureObj.type != "ARMATURE": - raise PluginError("Armature not selected.") - - for adress, animName in marioAnimations: - importAnimationToBlender(romfileSrc, adress, armatureObj, {}, context.scene.isDMAImport, animName) - - romfileSrc.close() - self.report({"INFO"}, "Success!") - except Exception as e: - if romfileSrc is not None: - romfileSrc.close() - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ImportAnimPanel(SM64_Panel): - bl_idname = "SM64_PT_import_anim" - bl_label = "SM64 Animation Importer" - goal = "Object/Actor/Anim" - import_panel = True - - # called every frame - def draw(self, context): - col = self.layout.column() - propsAnimImport = col.operator(SM64_ImportAnimMario.bl_idname) - propsMarioAnimsImport = col.operator(SM64_ImportAllMarioAnims.bl_idname) - - col.prop(context.scene, "isDMAImport") - if not context.scene.isDMAImport: - col.prop(context.scene, "animIsAnimList") - if context.scene.animIsAnimList: - prop_split(col, context.scene, "animListIndexImport", "Anim List Index") - - prop_split(col, context.scene, "animStartImport", "Start Address") - col.prop(context.scene, "animIsSegPtr") - col.prop(context.scene, "levelAnimImport") - - -sm64_anim_classes = ( - SM64_ExportAnimMario, - SM64_ImportAnimMario, - SM64_ImportAllMarioAnims, -) - -sm64_anim_panels = ( - SM64_ImportAnimPanel, - SM64_ExportAnimPanel, -) - - -def sm64_anim_panel_register(): - for cls in sm64_anim_panels: - register_class(cls) - - -def sm64_anim_panel_unregister(): - for cls in sm64_anim_panels: - unregister_class(cls) - - -def sm64_anim_register(): - for cls in sm64_anim_classes: - register_class(cls) - - bpy.types.Scene.animStartImport = bpy.props.StringProperty(name="Import Start", default="4EC690") - bpy.types.Scene.animExportStart = bpy.props.StringProperty(name="Start", default="11D8930") - bpy.types.Scene.animExportEnd = bpy.props.StringProperty(name="End", default="11FFF00") - bpy.types.Scene.isDMAImport = bpy.props.BoolProperty(name="Is DMA Animation", default=True) - bpy.types.Scene.isDMAExport = bpy.props.BoolProperty(name="Is DMA Animation") - bpy.types.Scene.DMAEntryAddress = bpy.props.StringProperty(name="DMA Entry Address", default="4EC008") - bpy.types.Scene.DMAStartAddress = bpy.props.StringProperty(name="DMA Start Address", default="4EC000") - bpy.types.Scene.levelAnimImport = bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") - bpy.types.Scene.levelAnimExport = bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") - bpy.types.Scene.loopAnimation = bpy.props.BoolProperty(name="Loop Animation", default=True) - bpy.types.Scene.setAnimListIndex = bpy.props.BoolProperty(name="Set Anim List Entry", default=True) - bpy.types.Scene.overwrite_0x28 = bpy.props.BoolProperty(name="Overwrite 0x28 behaviour command", default=True) - bpy.types.Scene.addr_0x27 = bpy.props.StringProperty(name="0x27 Command Address", default="21CD00") - bpy.types.Scene.addr_0x28 = bpy.props.StringProperty(name="0x28 Command Address", default="21CD08") - bpy.types.Scene.animExportPath = bpy.props.StringProperty(name="Directory", subtype="FILE_PATH") - bpy.types.Scene.animOverwriteDMAEntry = bpy.props.BoolProperty(name="Overwrite DMA Entry") - bpy.types.Scene.animInsertableBinaryPath = bpy.props.StringProperty(name="Filepath", subtype="FILE_PATH") - bpy.types.Scene.animIsSegPtr = bpy.props.BoolProperty(name="Is Segmented Address", default=False) - bpy.types.Scene.animIsAnimList = bpy.props.BoolProperty(name="Is Anim List", default=True) - bpy.types.Scene.animListIndexImport = bpy.props.IntProperty(name="Anim List Index", min=0, max=255) - bpy.types.Scene.animListIndexExport = bpy.props.IntProperty(name="Anim List Index", min=0, max=255) - bpy.types.Scene.animName = bpy.props.StringProperty(name="Name", default="mario") - bpy.types.Scene.animGroupName = bpy.props.StringProperty(name="Group Name", default="group0") - bpy.types.Scene.animWriteHeaders = bpy.props.BoolProperty(name="Write Headers For Actor", default=True) - bpy.types.Scene.animCustomExport = bpy.props.BoolProperty(name="Custom Export Path") - bpy.types.Scene.animExportHeaderType = bpy.props.EnumProperty( - items=enumExportHeaderType, name="Header Export", default="Actor" - ) - bpy.types.Scene.animLevelName = bpy.props.StringProperty(name="Level", default="bob") - bpy.types.Scene.animLevelOption = bpy.props.EnumProperty(items=enumLevelNames, name="Level", default="bob") - - -def sm64_anim_unregister(): - for cls in reversed(sm64_anim_classes): - unregister_class(cls) - - del bpy.types.Scene.animStartImport - del bpy.types.Scene.animExportStart - del bpy.types.Scene.animExportEnd - del bpy.types.Scene.levelAnimImport - del bpy.types.Scene.levelAnimExport - del bpy.types.Scene.isDMAImport - del bpy.types.Scene.isDMAExport - del bpy.types.Scene.DMAStartAddress - del bpy.types.Scene.DMAEntryAddress - del bpy.types.Scene.loopAnimation - del bpy.types.Scene.setAnimListIndex - del bpy.types.Scene.overwrite_0x28 - del bpy.types.Scene.addr_0x27 - del bpy.types.Scene.addr_0x28 - del bpy.types.Scene.animExportPath - del bpy.types.Scene.animOverwriteDMAEntry - del bpy.types.Scene.animInsertableBinaryPath - del bpy.types.Scene.animIsSegPtr - del bpy.types.Scene.animIsAnimList - del bpy.types.Scene.animListIndexImport - del bpy.types.Scene.animListIndexExport - del bpy.types.Scene.animName - del bpy.types.Scene.animGroupName - del bpy.types.Scene.animWriteHeaders - del bpy.types.Scene.animCustomExport - del bpy.types.Scene.animExportHeaderType - del bpy.types.Scene.animLevelName - del bpy.types.Scene.animLevelOption diff --git a/fast64_internal/sm64/sm64_classes.py b/fast64_internal/sm64/sm64_classes.py new file mode 100644 index 000000000..48c165b50 --- /dev/null +++ b/fast64_internal/sm64/sm64_classes.py @@ -0,0 +1,290 @@ +from io import BufferedReader, StringIO +from typing import BinaryIO +from pathlib import Path +import dataclasses +import shutil +import struct +import os +import numpy as np + +from ..utility import intToHex, decodeSegmentedAddr, PluginError, toAlnum +from .sm64_constants import insertableBinaryTypes, SegmentData +from .sm64_utility import export_rom_checks, temp_file_path + + +@dataclasses.dataclass +class InsertableBinaryData: + data_type: str = "" + data: bytearray = dataclasses.field(default_factory=bytearray) + start_address: int = 0 + ptrs: list[int] = dataclasses.field(default_factory=list) + + def write(self, path: Path): + path.write_bytes(self.to_binary()) + + def to_binary(self): + data = bytearray() + data.extend(insertableBinaryTypes[self.data_type].to_bytes(4, "big")) # 0-4 + data.extend(len(self.data).to_bytes(4, "big")) # 4-8 + data.extend(self.start_address.to_bytes(4, "big")) # 8-12 + data.extend(len(self.ptrs).to_bytes(4, "big")) # 12-16 + for ptr in self.ptrs: # 16-(16 + len(ptr) * 4) + data.extend(ptr.to_bytes(4, "big")) + data.extend(self.data) + return data + + def read(self, file: BufferedReader, expected_type: list = None): + print(f"Reading insertable binary data from {file.name}") + reader = RomReader(file) + type_num = reader.read_int(4) + if type_num not in insertableBinaryTypes.values(): + raise ValueError(f"Unknown data type: {intToHex(type_num)}") + self.data_type = next(k for k, v in insertableBinaryTypes.items() if v == type_num) + if expected_type and self.data_type not in expected_type: + raise ValueError(f"Unexpected data type: {self.data_type}") + + data_size = reader.read_int(4) + self.start_address = reader.read_int(4) + pointer_count = reader.read_int(4) + self.ptrs = [] + for _ in range(pointer_count): + self.ptrs.append(reader.read_int(4)) + + actual_start = reader.address + self.start_address + self.data = reader.read_data(data_size, actual_start) + return self + + +@dataclasses.dataclass +class RomReader: + """ + Helper class that simplifies reading data continously from a starting address. + Can read insertable binary files, in which it can also read data from ROM if provided. + """ + + rom_file: BufferedReader = None + insertable_file: BufferedReader = None + start_address: int = 0 + segment_data: SegmentData = dataclasses.field(default_factory=dict) + insertable: InsertableBinaryData = None + address: int = dataclasses.field(init=False) + + def __post_init__(self): + self.address = self.start_address + if self.insertable_file and not self.insertable: + self.insertable = InsertableBinaryData().read(self.insertable_file) + assert self.insertable or self.rom_file + + def branch(self, start_address=-1): + start_address = self.address if start_address == -1 else start_address + if self.read_int(1, specific_address=start_address) is None: + if self.insertable and self.rom_file: + return RomReader(self.rom_file, start_address=start_address, segment_data=self.segment_data) + return None + return RomReader( + self.rom_file, + self.insertable_file, + start_address, + self.segment_data, + self.insertable, + ) + + def skip(self, size: int): + self.address += size + + def read_data(self, size=-1, specific_address=-1): + if specific_address == -1: + address = self.address + self.skip(size) + else: + address = specific_address + + if self.insertable: + data = self.insertable.data[address : address + size] + else: + self.rom_file.seek(address) + data = self.rom_file.read(size) + if size > 0 and not data: + raise IndexError(f"Value at {intToHex(address)} not present in data.") + return data + + def read_ptr(self, specific_address=-1): + address = self.address if specific_address == -1 else specific_address + ptr = self.read_int(4, specific_address=specific_address) + if self.insertable and address in self.insertable.ptrs: + return ptr + if ptr and self.segment_data: + return decodeSegmentedAddr(ptr.to_bytes(4, "big"), self.segment_data) + return ptr + + def read_int(self, size=4, signed=False, specific_address=-1): + return int.from_bytes(self.read_data(size, specific_address), "big", signed=signed) + + def read_float(self, size=4, specific_address=-1): + return struct.unpack(">f", self.read_data(size, specific_address))[0] + + def read_str(self, specific_address=-1): + ptr = self.read_ptr() if specific_address == -1 else specific_address + if not ptr: + return None + branch = self.branch(ptr) + text_data = bytearray() + while True: + byte = branch.read_data(1) + if byte == b"\x00" or not byte: + break + text_data.append(ord(byte)) + text = text_data.decode("utf-8") + return text + + +@dataclasses.dataclass +class BinaryExporter: + export_rom: Path + output_rom: Path + rom_file_output: BinaryIO = dataclasses.field(init=False) + temp_rom: Path = dataclasses.field(init=False) + + @property + def tell(self): + return self.rom_file_output.tell() + + def __enter__(self): + export_rom_checks(self.export_rom) + print(f"Binary export started, exporting to {self.output_rom}") + self.temp_rom = temp_file_path(self.output_rom) + print(f'Copying "{self.export_rom}" to temporary file "{self.temp_rom}".') + shutil.copy(self.export_rom, self.temp_rom) + self.rom_file_output = self.temp_rom.open("rb+") + return self + + def write_to_range(self, start_address: int, end_address: int, data: bytes | bytearray): + address_range_str = f"[{intToHex(start_address)}, {intToHex(end_address)}]" + if end_address < start_address: + raise PluginError(f"Start address is higher than the end address: {address_range_str}") + if start_address + len(data) > end_address: + raise PluginError( + f"Data ({len(data) / 1000.0} kb) does not fit in range {address_range_str} " + f"({(end_address - start_address) / 1000.0} kb).", + ) + print(f"Writing {len(data) / 1000.0} kb to {address_range_str} ({(end_address - start_address) / 1000.0} kb))") + self.write(data, start_address) + + def seek(self, offset: int, whence: int = 0): + self.rom_file_output.seek(offset, whence) + + def read(self, n=-1, offset=-1): + if offset != -1: + self.seek(offset) + return self.rom_file_output.read(n) + + def write(self, s: bytes, offset=-1): + if offset != -1: + self.seek(offset) + return self.rom_file_output.write(s) + + def __exit__(self, exc_type, exc_value, traceback): + if self.temp_rom.exists(): + print(f"Closing temporary file {self.temp_rom}.") + self.rom_file_output.close() + else: + raise FileNotFoundError(f"Temporary file {self.temp_rom} does not exist?") + if exc_value: + print("Deleting temporary file because of exception.") + os.remove(self.temp_rom) + print("Type:", exc_type, "\nValue:", exc_value, "\nTraceback:", traceback) + else: + print(f"Moving temporary file to {self.output_rom}.") + if os.path.exists(self.output_rom): + os.remove(self.output_rom) + self.temp_rom.rename(self.output_rom) + + +@dataclasses.dataclass +class DMATableElement: + offset: int = 0 + size: int = 0 + address: int = 0 + end_address: int = 0 + + +@dataclasses.dataclass +class DMATable: + address_place_holder: int = 0 + entries: list[DMATableElement] = dataclasses.field(default_factory=list) + data: bytearray = dataclasses.field(default_factory=bytearray) + address: int = 0 + end_address: int = 0 + + def to_binary(self): + print( + f"Generating DMA table with {len(self.entries)} entries", + f"and {len(self.data)} bytes of data", + ) + data = bytearray() + data.extend(len(self.entries).to_bytes(4, "big", signed=False)) + data.extend(self.address_place_holder.to_bytes(4, "big", signed=False)) + + entries_offset = 8 + entries_length = len(self.entries) * 8 + entrie_data_offset = entries_offset + entries_length + + for entrie in self.entries: + offset = entrie_data_offset + entrie.offset + data.extend(offset.to_bytes(4, "big", signed=False)) + data.extend(entrie.size.to_bytes(4, "big", signed=False)) + data.extend(self.data) + + return data + + def read_binary(self, reader: RomReader): + print("Reading DMA table at", intToHex(reader.start_address)) + self.address = reader.start_address + + num_entries = reader.read_int(4) # numEntries + self.address_place_holder = reader.read_int(4) # addrPlaceholder + + table_size = 0 + for _ in range(num_entries): + offset = reader.read_int(4) + size = reader.read_int(4) + address = self.address + offset + self.entries.append(DMATableElement(offset, size, address, address + size)) + end_of_entry = offset + size + if end_of_entry > table_size: + table_size = end_of_entry + self.end_address = self.address + table_size + print(f"Found {len(self.entries)} DMA entries") + return self + + +@dataclasses.dataclass +class IntArray: + data: np.ndarray + name: str = "" + wrap: int = 6 + wrap_start: int = 0 # -6 To replicate decomp animation index table formatting + + def to_binary(self): + return self.data.astype(">i2").tobytes() + + def to_c(self, c_data: StringIO | None = None, new_lines=1): + assert self.name, "Array must have a name" + data = self.data + byte_count = data.itemsize + data_type = f"{'s' if data.dtype == np.int16 else 'u'}{byte_count * 8}" + print(f'Generating {data_type} array "{self.name}" with {len(self.data)} elements') + + c_data = c_data or StringIO() + c_data.write(f"// {len(self.data)}\n") + c_data.write(f"static const {data_type} {toAlnum(self.name)}[] = {{\n\t") + i = self.wrap_start + for value in self.data: + c_data.write(f"{intToHex(value, byte_count, False)}, ") + i += 1 + if i >= self.wrap: + c_data.write("\n\t") + i = 0 + + c_data.write("\n};" + ("\n" * new_lines)) + return c_data diff --git a/fast64_internal/sm64/sm64_collision.py b/fast64_internal/sm64/sm64_collision.py index 6e2907f0c..5f342bd27 100644 --- a/fast64_internal/sm64/sm64_collision.py +++ b/fast64_internal/sm64/sm64_collision.py @@ -1,3 +1,4 @@ +from pathlib import Path import bpy, shutil, os, math, mathutils from bpy.utils import register_class, unregister_class from io import BytesIO @@ -8,7 +9,7 @@ insertableBinaryTypes, defaultExtendSegment4, ) -from .sm64_utility import export_rom_checks +from .sm64_utility import export_rom_checks, to_include_descriptor, update_actor_includes, write_or_delete_if_found from .sm64_objects import SM64_Area, start_process_sm64_objects from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 @@ -23,8 +24,6 @@ get64bitAlignedAddr, prop_split, getExportDir, - writeIfNotFound, - deleteIfFound, duplicateHierarchy, cleanupDuplicatedObjects, writeInsertableFile, @@ -331,31 +330,26 @@ def exportCollisionC( cDefFile.write(cDefine) cDefFile.close() - if headerType == "Actor": - # Write to group files - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"') - writeIfNotFound(groupPathH, '\n#include "' + name + '/collision_header.h"', "\n#endif") - - elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"') - writeIfNotFound(groupPathH, '\n#include "levels/' + levelName + "/" + name + '/collision_header.h"', "\n#endif") + data_includes = [Path("collision.inc.c")] + if writeRoomsFile: + data_includes.append(Path("rooms.inc.c")) + update_actor_includes( + headerType, groupName, Path(dirPath), name, levelName, data_includes, [Path("collision_header.h")] + ) + if not writeRoomsFile: # TODO: Could be done better + if headerType == "Actor": + group_path_c = Path(dirPath) / f"{groupName}.c" + write_or_delete_if_found(group_path_c, to_remove=[to_include_descriptor(Path(name) / "rooms.inc.c")]) + elif headerType == "Level": + group_path_c = Path(dirPath) / "leveldata.c" + write_or_delete_if_found( + group_path_c, + to_remove=[ + to_include_descriptor( + Path(name) / "rooms.inc.c", Path("levels") / levelName / name / "rooms.inc.c" + ), + ], + ) return cDefine diff --git a/fast64_internal/sm64/sm64_constants.py b/fast64_internal/sm64/sm64_constants.py index fd1893770..226b250dc 100644 --- a/fast64_internal/sm64/sm64_constants.py +++ b/fast64_internal/sm64/sm64_constants.py @@ -1,3 +1,6 @@ +import dataclasses +from typing import Any, Iterable, TypeVar + # RAM address used in evaluating switch for hatless Mario marioHatSwitch = 0x80277740 marioLowPolySwitch = 0x80277150 @@ -27,6 +30,28 @@ "metal": 0x9EC, } +NULL = 0x00000000 + +MIN_U8 = 0 +MAX_U8 = (2**8) - 1 + +MIN_S8 = -(2**7) +MAX_S8 = (2**7) - 1 + +MIN_S16 = -(2**15) +MAX_S16 = (2**15) - 1 + +MIN_U16 = 0 +MAX_U16 = 2**16 - 1 + +MIN_S32 = -(2**31) +MAX_S32 = 2**31 - 1 + +MIN_U32 = 0 +MAX_U32 = 2**32 - 1 + +SegmentData = dict[int, tuple[int, int]] + commonGeolayoutPointers = { "Dorrie": [2039136, "HMC"], "Bowser": [1809204, "BFB"], @@ -294,7 +319,14 @@ def __init__(self, geoAddr, level, switchDict): "TTM": 0x2AC2EC, } -insertableBinaryTypes = {"Display List": 0, "Geolayout": 1, "Animation": 2, "Collision": 3} +insertableBinaryTypes = { + "Display List": 0, + "Geolayout": 1, + "Animation": 2, + "Collision": 3, + "Animation Table": 4, + "Animation DMA Table": 5, +} enumBehaviourPresets = [ ("Custom", "Custom", "Custom"), ("1300407c", "1 Up", "1 Up"), @@ -2114,6 +2146,7 @@ def __init__(self, geoAddr, level, switchDict): ("Custom", "Custom", "Custom"), ] + # groups you can use for the combined object export groups_obj_export = [ ("common0", "common0", "chuckya, boxes, blue coin switch"), @@ -2139,219 +2172,1640 @@ def __init__(self, geoAddr, level, switchDict): ("Custom", "Custom", "Custom"), ] -marioAnimations = [ - # ( Adress, "Animation name" ), - (5162640, "0 - Slow ledge climb up"), - (5165520, "1 - Fall over backwards"), - (5165544, "2 - Backward air kb"), - (5172396, "3 - Dying on back"), - (5177044, "4 - Backflip"), - (5179584, "5 - Climbing up pole"), - (5185656, "6 - Grab pole short"), - (5186824, "7 - Grab pole swing part 1"), - (5186848, "8 - Grab pole swing part 2"), - (5191920, "9 - Handstand idle"), - (5194740, "10 - Handstand jump"), - (5194764, "11 - Start handstand"), - (5188592, "12 - Return from handstand"), - (5196388, "13 - Idle on pole"), - (5197436, "14 - A pose"), - (5197792, "15 - Skid on ground"), - (5197816, "16 - Stop skid"), - (5199596, "17 - Crouch from fast longjump"), - (5201048, "18 - Crouch from a slow longjump"), - (5202644, "19 - Fast longjump"), - (5204600, "20 - Slow longjump"), - (5205980, "21 - Airborne on stomach"), - (5207188, "22 - Walk with light object"), - (5211916, "23 - Run with light object"), - (5215136, "24 - Slow walk with light object"), - (5219864, "25 - Shivering and warming hands"), - (5225496, "26 - Shivering return to idle "), - (5226920, "27 - Shivering"), - (5230056, "28 - Climb down on ledge"), - (5231112, "29 - Credits - Waving"), - (5232768, "30 - Credits - Look up"), - (5234576, "31 - Credits - Return from look up"), - (5235700, "32 - Credits - Raising hand"), - (5243100, "33 - Credits - Lowering hand"), - (5245988, "34 - Credits - Taking off cap"), - (5248016, "35 - Credits - Start walking and look up"), - (5256508, "36 - Credits - Look back then run"), - (5266160, "37 - Final Bowser - Raise hand and spin"), - (5274456, "38 - Final Bowser - Wing cap take off"), - (5282084, "39 - Credits - Peach sign"), - (5291340, "40 - Stand up from lava boost"), - (5292628, "41 - Fire/Lava burn"), - (5293488, "42 - Wing cap flying"), - (5295016, "43 - Hang on owl"), - (5296876, "44 - Land on stomach"), - (5296900, "45 - Air forward kb"), - (5302796, "46 - Dying on stomach"), - (5306100, "47 - Suffocating"), - (5313796, "48 - Coughing"), - (5319500, "49 - Throw catch key"), - (5330436, "50 - Dying fall over"), - (5338604, "51 - Idle on ledge"), - (5341720, "52 - Fast ledge grab"), - (5343296, "53 - Hang on ceiling"), - (5347276, "54 - Put cap on"), - (5351252, "55 - Take cap off then on"), - (5358356, "56 - Quickly put cap on"), - (5359476, "57 - Head stuck in ground"), - (5372172, "58 - Ground pound landing"), - (5372824, "59 - Triple jump ground-pound"), - (5374304, "60 - Start ground-pound"), - (5374328, "61 - Ground-pound"), - (5375380, "62 - Bottom stuck in ground"), - (5387148, "63 - Idle with light object"), - (5390520, "64 - Jump land with light object"), - (5391892, "65 - Jump with light object"), - (5392704, "66 - Fall land with light object"), - (5393936, "67 - Fall with light object"), - (5394296, "68 - Fall from sliding with light object"), - (5395224, "69 - Sliding on bottom with light object"), - (5395248, "70 - Stand up from sliding with light object"), - (5396716, "71 - Riding shell"), - (5397832, "72 - Walking"), - (5403208, "73 - Forward flip"), - (5404784, "74 - Jump riding shell"), - (5405676, "75 - Land from double jump"), - (5407340, "76 - Double jump fall"), - (5408288, "77 - Single jump"), - (5408312, "78 - Land from single jump"), - (5411044, "79 - Air kick"), - (5412900, "80 - Double jump rise"), - (5413596, "81 - Start forward spinning"), - (5414876, "82 - Throw light object"), - (5416032, "83 - Fall from slide kick"), - (5418280, "84 - Bend kness riding shell"), - (5419872, "85 - Legs stuck in ground"), - (5431416, "86 - General fall"), - (5431440, "87 - General land"), - (5433276, "88 - Being grabbed"), - (5434636, "89 - Grab heavy object"), - (5437964, "90 - Slow land from dive"), - (5441520, "91 - Fly from cannon"), - (5442516, "92 - Moving right while hanging"), - (5444052, "93 - Moving left while hanging"), - (5445472, "94 - Missing cap"), - (5457860, "95 - Pull door walk in"), - (5463196, "96 - Push door walk in"), - (5467492, "97 - Unlock door"), - (5480428, "98 - Start reach pocket"), - (5481448, "99 - Reach pocket"), - (5483352, "100 - Stop reach pocket"), - (5484876, "101 - Ground throw"), - (5486852, "102 - Ground kick"), - (5489076, "103 - First punch"), - (5489740, "104 - Second punch"), - (5490356, "105 - First punch fast"), - (5491396, "106 - Second punch fast"), - (5492732, "107 - Pick up light object"), - (5493948, "108 - Pushing"), - (5495508, "109 - Start riding shell"), - (5497072, "110 - Place light object"), - (5498484, "111 - Forward spinning"), - (5498508, "112 - Backward spinning"), - (5498884, "113 - Breakdance"), - (5501240, "114 - Running"), - (5501264, "115 - Running (unused)"), - (5505884, "116 - Soft back kb"), - (5508004, "117 - Soft front kb"), - (5510172, "118 - Dying in quicksand"), - (5515096, "119 - Idle in quicksand"), - (5517836, "120 - Move in quicksand"), - (5528568, "121 - Electrocution"), - (5532480, "122 - Shocked"), - (5533160, "123 - Backward kb"), - (5535796, "124 - Forward kb"), - (5538372, "125 - Idle heavy object"), - (5539764, "126 - Stand against wall"), - (5544580, "127 - Side step left"), - (5548480, "128 - Side step right"), - (5553004, "129 - Start sleep idle"), - (5557588, "130 - Start sleep scratch"), - (5563636, "131 - Start sleep yawn"), - (5568648, "132 - Start sleep sitting"), - (5573680, "133 - Sleep idle"), - (5574280, "134 - Sleep start laying"), - (5577460, "135 - Sleep laying"), - (5579300, "136 - Dive"), - (5579324, "137 - Slide dive"), - (5580860, "138 - Ground bonk"), - (5584116, "139 - Stop slide light object"), - (5587364, "140 - Slide kick"), - (5588288, "141 - Crouch from slide kick"), - (5589652, "142 - Slide motionless"), - (5589676, "143 - Stop slide"), - (5591572, "144 - Fall from slide"), - (5592860, "145 - Slide"), - (5593404, "146 - Tiptoe"), - (5599280, "147 - Twirl land"), - (5600160, "148 - Twirl"), - (5600516, "149 - Start twirl"), - (5601072, "150 - Stop crouching"), - (5602028, "151 - Start crouching"), - (5602720, "152 - Crouching"), - (5605756, "153 - Crawling"), - (5613048, "154 - Stop crawling"), - (5613968, "155 - Start crawling"), - (5614876, "156 - Summon star"), - (5620036, "157 - Return star approach door"), - (5622256, "158 - Backwards water kb"), - (5626540, "159 - Swim with object part 1"), - (5627592, "160 - Swim with object part 2"), - (5628260, "161 - Flutter kick with object"), - (5629456, "162 - Action end with object in water"), - (5631180, "163 - Stop holding object in water"), - (5634048, "164 - Holding object in water"), - (5635976, "165 - Drowning part 1"), - (5641400, "166 - Drowning part 2"), - (5646324, "167 - Dying in water"), - (5649660, "168 - Forward kb in water"), - (5653848, "169 - Falling from water"), - (5655852, "170 - Swimming part 1"), - (5657100, "171 - Swimming part 2"), - (5658128, "172 - Flutter kick"), - (5660112, "173 - Action end in water"), - (5662248, "174 - Pick up object in water"), - (5663480, "175 - Grab object in water part 2"), - (5665916, "176 - Grab object in water part 1"), - (5666632, "177 - Throw object in water"), - (5669328, "178 - Idle in water"), - (5671428, "179 - Star dance in water"), - (5678200, "180 - Return from in water star dance"), - (5680324, "181 - Grab bowser"), - (5680348, "182 - Swing bowser"), - (5682008, "183 - Release bowser"), - (5685264, "184 - Holding bowser"), - (5686316, "185 - Heavy throw"), - (5688660, "186 - Walk panting"), - (5689924, "187 - Walk with heavy object"), - (5694332, "188 - Turning part 1"), - (5694356, "189 - Turning part 2"), - (5696160, "190 - Side flip land"), - (5697196, "191 - Side flip"), - (5699408, "192 - Triple jump land"), - (5702136, "193 - Triple jump"), - (5704880, "194 - First person"), - (5710580, "195 - Idle head left"), - (5712800, "196 - Idle head right"), - (5715020, "197 - Idle head center"), - (5717240, "198 - Handstand left"), - (5719184, "199 - Handstand right"), - (5722304, "200 - Wake up from sleeping"), - (5724228, "201 - Wake up from laying"), - (5726444, "202 - Start tiptoeing"), - (5728720, "203 - Slide jump"), - (5728744, "204 - Start wallkick"), - (5730404, "205 - Star dance"), - (5735864, "206 - Return from star dance"), - (5737600, "207 - Forwards spinning flip"), - (5740584, "208 - Triple jump fly"), +BEHAVIOR_EXITS = [ + "RETURN", + "GOTO", + "END_LOOP", + "BREAK", + "BREAK_UNUSED", + "DEACTIVATE", ] +BEHAVIOR_COMMANDS = [ + # Name, Size + ("BEGIN", 1), # bhv_cmd_begin + ("DELAY", 1), # bhv_cmd_delay + ("CALL", 1), # bhv_cmd_call + ("RETURN", 1), # bhv_cmd_return + ("GOTO", 1), # bhv_cmd_goto + ("BEGIN_REPEAT", 1), # bhv_cmd_begin_repeat + ("END_REPEAT", 1), # bhv_cmd_end_repeat + ("END_REPEAT_CONTINUE", 1), # bhv_cmd_end_repeat_continue + ("BEGIN_LOOP", 1), # bhv_cmd_begin_loop + ("END_LOOP", 1), # bhv_cmd_end_loop + ("BREAK", 1), # bhv_cmd_break + ("BREAK_UNUSED", 1), # bhv_cmd_break_unused + ("CALL_NATIVE", 2), # bhv_cmd_call_native + ("ADD_FLOAT", 1), # bhv_cmd_add_float + ("SET_FLOAT", 1), # bhv_cmd_set_float + ("ADD_INT", 1), # bhv_cmd_add_int + ("SET_INT", 1), # bhv_cmd_set_int + ("OR_INT", 1), # bhv_cmd_or_int + ("BIT_CLEAR", 1), # bhv_cmd_bit_clear + ("SET_INT_RAND_RSHIFT", 2), # bhv_cmd_set_int_rand_rshift + ("SET_RANDOM_FLOAT", 2), # bhv_cmd_set_random_float + ("SET_RANDOM_INT", 2), # bhv_cmd_set_random_int + ("ADD_RANDOM_FLOAT", 2), # bhv_cmd_add_random_float + ("ADD_INT_RAND_RSHIFT", 2), # bhv_cmd_add_int_rand_rshift + ("NOP_1", 1), # bhv_cmd_nop_1 + ("NOP_2", 1), # bhv_cmd_nop_2 + ("NOP_3", 1), # bhv_cmd_nop_3 + ("SET_MODEL", 1), # bhv_cmd_set_model + ("SPAWN_CHILD", 3), # bhv_cmd_spawn_child + ("DEACTIVATE", 1), # bhv_cmd_deactivate + ("DROP_TO_FLOOR", 1), # bhv_cmd_drop_to_floor + ("SUM_FLOAT", 1), # bhv_cmd_sum_float + ("SUM_INT", 1), # bhv_cmd_sum_int + ("BILLBOARD", 1), # bhv_cmd_billboard + ("HIDE", 1), # bhv_cmd_hide + ("SET_HITBOX", 2), # bhv_cmd_set_hitbox + ("NOP_4", 1), # bhv_cmd_nop_4 + ("DELAY_VAR", 1), # bhv_cmd_delay_var + ("BEGIN_REPEAT_UNUSED", 1), # bhv_cmd_begin_repeat_unused + ("LOAD_ANIMATIONS", 2), # bhv_cmd_load_animations + ("ANIMATE", 1), # bhv_cmd_animate + ("SPAWN_CHILD_WITH_PARAM", 3), # bhv_cmd_spawn_child_with_param + ("LOAD_COLLISION_DATA", 2), # bhv_cmd_load_collision_data + ("SET_HITBOX_WITH_OFFSET", 3), # bhv_cmd_set_hitbox_with_offset + ("SPAWN_OBJ", 3), # bhv_cmd_spawn_obj + ("SET_HOME", 1), # bhv_cmd_set_home + ("SET_HURTBOX", 2), # bhv_cmd_set_hurtbox + ("SET_INTERACT_TYPE", 2), # bhv_cmd_set_interact_type + ("SET_OBJ_PHYSICS", 5), # bhv_cmd_set_obj_physics + ("SET_INTERACT_SUBTYPE", 2), # bhv_cmd_set_interact_subtype + ("SCALE", 1), # bhv_cmd_scale + ("PARENT_BIT_CLEAR", 2), # bhv_cmd_parent_bit_clear + ("ANIMATE_TEXTURE", 1), # bhv_cmd_animate_texture + ("DISABLE_RENDERING", 1), # bhv_cmd_disable_rendering + ("SET_INT_UNUSED", 2), # bhv_cmd_set_int_unused + ("SPAWN_WATER_DROPLET", 2), # bhv_cmd_spawn_water_droplet +] + +T = TypeVar("T") +DictOrVal = T | dict[str, T] | None +ListOrVal = T | list[T] | None + + +def as_list(val: ListOrVal[Any]) -> list[T]: + if isinstance(val, Iterable): + return list(val) + if val is None: + return [] + return [val] + + +def as_dict(val: DictOrVal[T], name: str = "") -> dict[str, T]: + """If val is a dict, returns it, otherwise returns {name: member}""" + if isinstance(val, dict): + return val + elif val is not None: + return {name: val} + return {} + + +def validate_dict(val: DictOrVal, val_type: type): + return all(isinstance(k, str) and isinstance(v, val_type) for k, v in as_dict(val).items()) + + +def validate_list(val: ListOrVal, val_type: type): + return all(isinstance(v, val_type) for v in as_list(val)) + + +@dataclasses.dataclass +class AnimInfo: + address: int + behaviours: DictOrVal[int] = dataclasses.field(default_factory=dict) + size: int | None = None # None means the size can be determined from the NULL delimiter + ignore_bone_count: bool = False + dma: bool = False + directory: str | None = None + names: list[str] = dataclasses.field(default_factory=list) + + def __post_init__(self): + assert isinstance(self.address, int) + assert validate_dict(self.behaviours, int) + assert self.size is None or isinstance(self.size, int) + assert isinstance(self.ignore_bone_count, bool) + assert isinstance(self.dma, bool) + assert self.directory is None or isinstance(self.directory, str) + assert validate_list(self.names, str) + + +@dataclasses.dataclass +class ModelIDInfo: + number: int + enum: str + + def __post_init__(self): + assert isinstance(self.number, int) + assert isinstance(self.enum, str) + + +@dataclasses.dataclass +class DisplaylistInfo: + address: int + # Displaylists are compressed, so their c name can´t be fetched from func_map like geolayouts + c_name: str + + def __post_init__(self): + assert isinstance(self.address, int) + assert isinstance(self.c_name, str) + + +@dataclasses.dataclass +class ModelInfo: + model_id: ListOrVal[ModelIDInfo] = dataclasses.field(default_factory=list) + geolayout: int | None = None + displaylist: DisplaylistInfo | None = None + + def __post_init__(self): + self.model_id = as_list(self.model_id) + assert validate_list(self.model_id, ModelIDInfo) + assert validate_list(self.geolayout, int) + assert validate_list(self.displaylist, DisplaylistInfo) + + +@dataclasses.dataclass +class CollisionInfo: + address: int + c_name: str + + def __post_init__(self): + assert isinstance(self.address, int) + assert isinstance(self.c_name, str) + + +@dataclasses.dataclass +class ActorPresetInfo: + decomp_path: str = None + level: str | None = None + group: str | None = None + animation: DictOrVal[AnimInfo] = dataclasses.field(default_factory=dict) + models: DictOrVal[ModelInfo] = dataclasses.field(default_factory=dict) + collision: DictOrVal[CollisionInfo] = dataclasses.field(default_factory=dict) + + def __post_init__(self): + assert self.decomp_path is not None and isinstance(self.decomp_path, str) + assert self.group is None or isinstance(self.group, str) + assert validate_dict(self.animation, AnimInfo) + assert validate_dict(self.models, ModelInfo) + assert validate_dict(self.collision, CollisionInfo) + group_to_level = { + "common0": "HH", + "common1": "HH", + "group0": "HH", + "group1": "WF", + "group2": "LLL", + "group3": "BOB", + "group4": "JRB", + "group5": "SSL", + "group6": "TTM", + "group7": "CCM", + "group8": "VC", + "group9": "HH", + "group10": "CG", + "group11": "THI", + "group12": "BFB", + "group13": "WDW", + "group14": "BOB", + "group15": "IC", + "group16": "CCM", + "group17": "HMC", + } + if self.level is None and self.group is not None: + self.level = group_to_level[self.group] + assert isinstance(self.level, str) + + @staticmethod + def get_member_as_dict(name: str, member: DictOrVal[T]): + return as_dict(member, name) + + +ACTOR_PRESET_INFO = { + "Amp": ActorPresetInfo( + decomp_path="actors/amp", + group="common0", + animation=AnimInfo( + address=0x8004034, + behaviours={"Circling Amp": 0x13003388, "Homing Amp": 0x13003354}, + names=["Moving"], + ignore_bone_count=True, + ), + models=ModelInfo(model_id=ModelIDInfo(0xC2, "MODEL_AMP"), geolayout=0xF000028), + ), + "Bird": ActorPresetInfo( + decomp_path="actors/bird", + group="group10", + animation=AnimInfo( + address=0x50009E8, + behaviours={"Bird": 0x13005354, "End Birds 1": 0x1300565C, "End Birds 2": 0x13005680}, + names=["Flying", "Gliding"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BIRDS"), geolayout=0xC000000), + ), + "Blargg": ActorPresetInfo( + decomp_path="actors/blargg", + group="group2", + animation=AnimInfo(address=0x500616C, names=["Idle", "Bite"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BLARGG"), geolayout=0xC000240), + ), + "Blue Coin Switch": ActorPresetInfo( + decomp_path="actors/blue_coin_switch", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x8C, "MODEL_BLUE_COIN_SWITCH"), geolayout=0xF000000), + collision=CollisionInfo(address=0x8000E98, c_name="blue_coin_switch_seg8_collision_08000E98"), + ), + "Blue Fish": ActorPresetInfo( + decomp_path="actors/blue_fish", + group="common1", + animation=AnimInfo(address=0x301C2B0, behaviours=0x13001B2C, names=["Swimming", "Diving"]), + models={ + "Fish": ModelInfo(model_id=ModelIDInfo(0xB9, "MODEL_FISH"), geolayout=0x16000C44), + "Fish (With Shadow)": ModelInfo(model_id=ModelIDInfo(0xBA, "MODEL_FISH_SHADOW"), geolayout=0x16000BEC), + }, + ), + "Bobomb": ActorPresetInfo( + decomp_path="actors/bobomb", + group="common0", + animation=AnimInfo( + address=0x802396C, + behaviours={"Bobomb": 0x13003174, "Bobomb Buddy": 0x130031DC, "Bobomb Buddy (Opens Cannon)": 0x13003228}, + names=["Walking", "Strugling"], + ), + models={ + "Bobomb": ModelInfo(model_id=ModelIDInfo(0xBC, "MODEL_BLACK_BOBOMB"), geolayout=0xF0007B8), + "Bobomb Buddy": ModelInfo(model_id=ModelIDInfo(0xC3, "MODEL_BOBOMB_BUDDY"), geolayout=0xF0008F4), + }, + ), + "Bowser Bomb": ActorPresetInfo( + decomp_path="actors/bomb", + group="group12", + models=ModelInfo( + model_id=[ModelIDInfo(0x65, "MODEL_BOWSER_BOMB_CHILD_OBJ"), ModelIDInfo(0xB3, "MODEL_BOWSER_BOMB")], + geolayout=0xD000BBC, + ), + ), + "Boo": ActorPresetInfo( + decomp_path="actors/boo", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BOO"), geolayout=0xC000224), + ), + "Boo (Inside Castle)": ActorPresetInfo( + decomp_path="actors/boo_castle", + group="group15", + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_BOO_CASTLE"), geolayout=0xD0005B0), + ), + "Bookend": ActorPresetInfo( + decomp_path="actors/book", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_BOOKEND"), geolayout=0xC0000C0), + ), + "Bookend Part": ActorPresetInfo( + decomp_path="actors/bookend", + group="group9", + animation=AnimInfo(address=0x5002540, behaviours=0x1300506C, names=["Opening Mouth", "Bite", "Closed"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_BOOKEND_PART"), geolayout=0xC000000), + ), + "Metal Ball": ActorPresetInfo( + decomp_path="actors/bowling_ball", + group="common0", + models={ + "Bowling Ball": ModelInfo(model_id=ModelIDInfo(0xB4, "MODEL_BOWLING_BALL"), geolayout=0xF000640), + "Trajectory Marker Ball": ModelInfo( + model_id=ModelIDInfo(0xE1, "MODEL_TRAJECTORY_MARKER_BALL"), geolayout=0xF00066C + ), + }, + ), + "Bowser": ActorPresetInfo( + decomp_path="actors/bowser", + group="group12", + animation=AnimInfo( + address=0x60577E0, + behaviours=0x13001850, + size=27, + ignore_bone_count=True, + names=[ + "Stand Up", + "Stand Up (Unused)", + "Shaking", + "Grabbed", + "Broken Animation (Unused)", + "Fall Down", + "Fire Breath", + "Jump", + "Jump Stop", + "Jump Start", + "Dance", + "Fire Breath Up", + "Idle", + "Slow Gait", + "Look Down Stop Walk", + "Look Up Start Walk", + "Flip Down", + "Lay Down", + "Run Start", + "Run", + "Run Stop", + "Run Slip", + "Fire Breath Quick", + "Edge Move", + "Edge Stop", + "Flip", + "Stand Up From Flip", + ], + ), + models={ + "Bowser": ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_BOWSER"), geolayout=0xD000AC4), + "Bowser (No Shadow)": ModelInfo(model_id=ModelIDInfo(0x69, "MODEL_BOWSER_NO_SHADOW"), geolayout=0xD000B40), + }, + ), + "Bowser Flame": ActorPresetInfo( + decomp_path="actors/bowser_flame", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_BOWSER_FLAMES"), geolayout=0xD000000), + ), + "Bowser Key": ActorPresetInfo( + decomp_path="actors/bowser_key", + group="common1", + animation=AnimInfo( + address=0x30172D0, + behaviours={"Bowser Key": 0x13001BB4, "Bowser Key (Cutscene)": 0x13001BD4}, + size=2, + names=["Unlock Door", "Course Exit"], + ), + models={ + "Bowser Key (Cutscene)": ModelInfo( + model_id=ModelIDInfo(0xC8, "MODEL_BOWSER_KEY_CUTSCENE"), geolayout=0x16000AB0 + ), + "Bowser Key": ModelInfo(model_id=ModelIDInfo(0xCC, "MODEL_BOWSER_KEY"), geolayout=0x16000A84), + }, + ), + "Breakable Box": ActorPresetInfo( + decomp_path="actors/breakable_box", + group="common0", + models={ + "Breakable Box": ModelInfo(model_id=ModelIDInfo(0x81, "MODEL_BREAKABLE_BOX"), geolayout=0xF0005D0), + "Breakable Box (Small)": ModelInfo( + model_id=ModelIDInfo(0x82, "MODEL_BREAKABLE_BOX_SMALL"), geolayout=0xF000610 + ), + }, + collision=CollisionInfo(address=0x8012D70, c_name="breakable_box_seg8_collision_08012D70"), + ), + "Bub": ActorPresetInfo( + decomp_path="actors/bub", + group="group13", + animation=AnimInfo(address=0x6012354, behaviours=0x1300220C, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_BUB"), geolayout=0xD00038C), + ), + "Bubba": ActorPresetInfo( + decomp_path="actors/bubba", + group="group11", + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_BUBBA"), geolayout=0xC000000), + ), + "Bubble": ActorPresetInfo( + decomp_path="actors/bubble", + group="group0", + models={ + "Bubble": ModelInfo(model_id=ModelIDInfo(0xA8, "MODEL_BUBBLE"), geolayout=0x17000000), + "Bubble Marble": ModelInfo(model_id=ModelIDInfo(0xAA, "MODEL_PURPLE_MARBLE"), geolayout=0x1700001C), + }, + ), + "Bullet Bill": ActorPresetInfo( + decomp_path="actors/bullet_bill", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BULLET_BILL"), geolayout=0xC000264), + ), + "Bully": ActorPresetInfo( + decomp_path="actors/bully", + group="group2", + animation=AnimInfo( + address=0x500470C, + behaviours={"Bully": 0x13003660, "Bully (With Minions)": 0x13003694, "Bully (Small)": 0x1300362C}, + names=["Patrol", "Chase", "Falling over (Unused)", "Knockback"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_BULLY"), geolayout=0xC000000), + ), + "Burn Smoke": ActorPresetInfo( + decomp_path="actors/burn_smoke", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x94, "MODEL_BURN_SMOKE"), geolayout=0x17000084), + ), + "Butterfly": ActorPresetInfo( + decomp_path="actors/butterfly", + group="common1", + animation=AnimInfo( + address=0x30056B0, + behaviours={"Butterfly": 0x130033BC, "Triplet Butterfly": 0x13005598}, + size=2, + names=["Flying", "Resting"], + ), + models=ModelInfo(model_id=ModelIDInfo(0xBB, "MODEL_BUTTERFLY"), geolayout=0x160000A8), + ), + "Cannon Barrel": ActorPresetInfo( + decomp_path="actors/cannon_barrel", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x7F, "MODEL_CANNON_BARREL"), geolayout=0xF0001C0), + ), + "Cannon Base": ActorPresetInfo( + decomp_path="actors/cannon_base", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x80, "MODEL_CANNON_BASE"), geolayout=0xF0001A8), + ), + "Cannon Lid": ActorPresetInfo( + decomp_path="actors/cannon_lid", + group="common0", + collision=CollisionInfo(address=0x8004950, c_name="cannon_lid_seg8_collision_08004950"), + models=ModelInfo( + model_id=ModelIDInfo(0xC9, "MODEL_DL_CANNON_LID"), + displaylist=DisplaylistInfo(0x80048E0, "cannon_lid_seg8_dl_080048E0"), + ), + ), + "Cap Switch": ActorPresetInfo( + decomp_path="actors/capswitch", + group="group8", + models={ + "Cap Switch": ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_CAP_SWITCH"), geolayout=0xC000048), + "Cap Switch (Exclamation)": ModelInfo( + model_id=ModelIDInfo(0x54, "MODEL_CAP_SWITCH_EXCLAMATION"), + displaylist=DisplaylistInfo(0x5002E00, "cap_switch_exclamation_seg5_dl_05002E00"), + ), + "Cap Switch (Base)": ModelInfo( + model_id=ModelIDInfo(0x56, "MODEL_CAP_SWITCH_BASE"), + displaylist=DisplaylistInfo(0x5003120, "cap_switch_base_seg5_dl_05003120"), + ), + }, + collision={ + "Cap Switch (Base)": CollisionInfo(address=0x50033D0, c_name="capswitch_collision_050033D0"), + "Cap Switch (Top)": CollisionInfo(address=0x5003448, c_name="capswitch_collision_05003448"), + }, + ), + "Chain Ball": ActorPresetInfo( # also known as metallic ball + decomp_path="actors/chain_ball", + group="group14", + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_METALLIC_BALL"), geolayout=0xD0005D0), + ), + "Chain Chomp": ActorPresetInfo( + decomp_path="actors/chain_chomp", + group="group14", + animation=AnimInfo(address=0x6025178, behaviours=0x1300478C, names=["Chomping"]), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_CHAIN_CHOMP"), geolayout=0xD0005EC), + ), + "Haunted Chair": ActorPresetInfo( + decomp_path="actors/chair", + group="group9", + animation=AnimInfo(address=0x5005784, behaviours=0x13004FD4, names=["Default Pose"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_HAUNTED_CHAIR"), geolayout=0xC0000D8), + ), + "Checkerboard Platform": ActorPresetInfo( + decomp_path="actors/checkerboard_platform", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0xCA, "MODEL_CHECKERBOARD_PLATFORM"), geolayout=0xF0004E4), + collision=CollisionInfo(address=0x800D710, c_name="checkerboard_platform_seg8_collision_0800D710"), + ), + "Chilly Chief": ActorPresetInfo( + decomp_path="actors/chilly_chief", + group="group16", + animation=AnimInfo( + address=0x6003994, + behaviours={"Chilly Chief (Small)": 0x130036C8, "Chilly Chief (Big)": 0x13003700}, + names=["Patrol", "Chase", "Falling over (Unused)", "Knockback"], + ), + models={ + "Chilly Chief (Small)": ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_CHILL_BULLY"), geolayout=0x6003754), + "Chilly Chief (Big)": ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_BIG_CHILL_BULLY"), geolayout=0x6003874), + }, + ), + "Chuckya": ActorPresetInfo( + decomp_path="actors/chuckya", + group="common0", + animation=AnimInfo( + address=0x800C070, + behaviours=0x13000528, + names=["Grab Mario", "Holding Mario", "Being Held", "Throwing", "Moving", "Balancing/Idle (Unused)"], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDF, "MODEL_CHUCKYA"), geolayout=0xF0001D8), + ), + "Clam Shell": ActorPresetInfo( + decomp_path="actors/clam", + group="group4", + animation=AnimInfo(address=0x5001744, behaviours=0x13005440, names=["Close", "Open"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_CLAM_SHELL"), geolayout=0xC000000), + ), + "Coin": ActorPresetInfo( + decomp_path="actors/coin", + group="common1", + models={ + "Yellow Coin": ModelInfo(model_id=ModelIDInfo(0x74, "MODEL_YELLOW_COIN"), geolayout=0x1600013C), + "Yellow Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0x75, "MODEL_YELLOW_COIN_NO_SHADOW"), geolayout=0x160001A0 + ), + "Blue Coin": ModelInfo(model_id=ModelIDInfo(0x76, "MODEL_BLUE_COIN"), geolayout=0x16000200), + "Blue Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0x77, "MODEL_BLUE_COIN_NO_SHADOW"), geolayout=0x16000264 + ), + "Red Coin": ModelInfo(model_id=ModelIDInfo(0xD7, "MODEL_RED_COIN"), geolayout=0x160002C4), + "Red Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0xD8, "MODEL_RED_COIN_NO_SHADOW"), geolayout=0x16000328 + ), + }, + ), + "Cyan Fish": ActorPresetInfo( + decomp_path="actors/cyan_fish", + group="group13", + animation=AnimInfo(address=0x600E264, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_CYAN_FISH"), geolayout=0xD000324), + ), + "Dirt": ActorPresetInfo( + decomp_path="actors/dirt", + group="common1", + models={ + "Dirt": ModelInfo(model_id=ModelIDInfo(0x8A, "MODEL_DIRT_ANIMATION"), geolayout=0x16000ED4), + "(Unused) Cartoon Start": ModelInfo(model_id=ModelIDInfo(0x8B, "MODEL_CARTOON_STAR"), geolayout=0x16000F24), + }, + ), + "Door": ActorPresetInfo( + decomp_path="actors/door", + group="common1", + animation=AnimInfo( + address=0x30156C0, + behaviours=0x13000B0C, + names=[ + "Closed", + "Open and Close", + "Open and Close (Slower?)", + "Open and Close (Slower? Last 10 frames)", + "Open and Close (Last 10 frames)", + ], + ignore_bone_count=True, + ), + models={ + "Castle Door": ModelInfo( + model_id=[ + ModelIDInfo(0x26, "MODEL_CASTLE_GROUNDS_CASTLE_DOOR"), + ModelIDInfo(0x26, "MODEL_CASTLE_CASTLE_DOOR"), + ModelIDInfo(0x1C, "MODEL_CASTLE_CASTLE_DOOR_UNUSED"), + ], + geolayout=0x160003A8, + ), + "Cabin Door": ModelInfo(model_id=ModelIDInfo(0x27, "MODEL_CCM_CABIN_DOOR"), geolayout=0x1600043C), + "Wooden Door": ModelInfo( + model_id=[ + ModelIDInfo(0x1D, "MODEL_CASTLE_WOODEN_DOOR_UNUSED"), + ModelIDInfo(0x1D, "MODEL_HMC_WOODEN_DOOR"), + ModelIDInfo(0x27, "MODEL_CASTLE_WOODEN_DOOR"), + ModelIDInfo(0x27, "MODEL_COURTYARD_WOODEN_DOOR"), + ], + geolayout=0x160004D0, + ), + "Wooden Door 2": ModelInfo(geolayout=0x16000564), + "Metal Door": ModelInfo( + model_id=[ + ModelIDInfo(0x1F, "MODEL_HMC_METAL_DOOR"), + ModelIDInfo(0x29, "MODEL_CASTLE_METAL_DOOR"), + ModelIDInfo(0x29, "MODEL_CASTLE_GROUNDS_METAL_DOOR"), + ], + geolayout=0x16000618, + ), + "Hazy Maze Door": ModelInfo(model_id=ModelIDInfo(0x20, "MODEL_HMC_HAZY_MAZE_DOOR"), geolayout=0x1600068C), + "Haunted Door": ModelInfo(model_id=ModelIDInfo(0x1D, "MODEL_BBH_HAUNTED_DOOR"), geolayout=0x16000720), + "Castle Door (0 Star)": ModelInfo( + model_id=ModelIDInfo(0x22, "MODEL_CASTLE_DOOR_0_STARS"), geolayout=0x160007B4 + ), + "Castle Door (1 Star)": ModelInfo( + model_id=ModelIDInfo(0x23, "MODEL_CASTLE_DOOR_1_STAR"), geolayout=0x16000868 + ), + "Castle Door (3 Star)": ModelInfo( + model_id=ModelIDInfo(0x24, "MODEL_CASTLE_DOOR_3_STARS"), geolayout=0x1600091C + ), + "Key Door": ModelInfo(model_id=ModelIDInfo(0x25, "MODEL_CASTLE_KEY_DOOR"), geolayout=0x160009D0), + }, + ), + "Dorrie": ActorPresetInfo( + decomp_path="actors/dorrie", + group="group17", + animation=AnimInfo( + address=0x600F638, behaviours=0x13004F90, size=3, names=["Idle", "Moving", "Lower and Raise Head"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_DORRIE"), geolayout=0xD000230), + ), + "Exclamation Box": ActorPresetInfo( + decomp_path="actors/exclamation_box", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x89, "MODEL_EXCLAMATION_BOX"), geolayout=0xF000694), + ), + "Exclamation Box Outline": ActorPresetInfo( + decomp_path="actors/exclamation_box_outline", + group="common0", + models={ + "Exclamation Box Outline": ModelInfo( + model_id=ModelIDInfo(0x83, "MODEL_EXCLAMATION_BOX_OUTLINE"), geolayout=0xF000A5A + ), + "Exclamation Point": ModelInfo( + model_id=ModelIDInfo(0x84, "MODEL_EXCLAMATION_POINT"), + displaylist=DisplaylistInfo(0x8025F08, "exclamation_box_outline_seg8_dl_08025F08"), + ), + }, + collision=CollisionInfo(address=0x8025F78, c_name="exclamation_box_outline_seg8_collision_08025F78"), + ), + "Explosion": ActorPresetInfo( + decomp_path="actors/explosion", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xCD, "MODEL_EXPLOSION"), geolayout=0x16000040), + ), + "Eyerok": ActorPresetInfo( + decomp_path="actors/eyerok", + group="group5", + animation=AnimInfo( + address=0x50116E4, + behaviours=0x130052B4, + names=["Recovering", "Death", "Idle", "Attacked", "Open", "Show Eye", "Sleep", "Close"], + ), + models={ + "Eyerok Left Hand": ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_EYEROK_LEFT_HAND"), geolayout=0xC0005A8), + "Eyerok Right Hand": ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_EYEROK_RIGHT_HAND"), geolayout=0xC0005E4), + }, + ), + "Flame": ActorPresetInfo( + decomp_path="actors/flame", + group="common1", + models={ + "Red Flame (With Shadow)": ModelInfo( + model_id=ModelIDInfo(0xCB, "MODEL_RED_FLAME_SHADOW"), geolayout=0x16000B10 + ), + "Red Flame": ModelInfo(model_id=ModelIDInfo(0x90, "MODEL_RED_FLAME"), geolayout=0x16000B2C), + "Blue Flame": ModelInfo(model_id=ModelIDInfo(0x91, "MODEL_BLUE_FLAME"), geolayout=0x16000B8C), + }, + ), + "Fly Guy": ActorPresetInfo( + decomp_path="actors/flyguy", + group="common0", + animation=AnimInfo(address=0x8011A64, behaviours=0x130046DC, names=["Flying"]), + models=ModelInfo(model_id=ModelIDInfo(0xDC, "MODEL_FLYGUY"), geolayout=0xF000518), + ), + "Fwoosh": ActorPresetInfo( + decomp_path="actors/fwoosh", + group="group6", + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_FWOOSH"), geolayout=0xC00036C), + ), + "Goomba": ActorPresetInfo( + decomp_path="actors/goomba", + group="common0", + animation=AnimInfo(address=0x801DA4C, behaviours=0x1300472C, names=["Walking"]), + models=ModelInfo(model_id=ModelIDInfo(0xC0, "MODEL_GOOMBA"), geolayout=0xF0006E4), + ), + "Haunted Cage": ActorPresetInfo( + decomp_path="actors/haunted_cage", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x5A, "MODEL_HAUNTED_CAGE"), geolayout=0xC000274), + ), + "Heart": ActorPresetInfo( + decomp_path="actors/heart", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x78, "MODEL_HEART"), geolayout=0xF0004FC), + ), + "Heave-Ho": ActorPresetInfo( + decomp_path="actors/heave_ho", + group="group1", + animation=AnimInfo(address=0x501534C, behaviours=0x13001548, names=["Moving", "Throwing", "Stop"]), + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_HEAVE_HO"), geolayout=0xC00028C), + ), + "Hoot": ActorPresetInfo( + decomp_path="actors/hoot", + group="group1", + animation=AnimInfo(address=0x5005768, behaviours=0x130033EC, names=["Flying", "Flying Fast"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_HOOT"), geolayout=0xC000018), + ), + "Bowser Impact Ring": ActorPresetInfo( + decomp_path="actors/impact_ring", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_BOWSER_WAVE"), geolayout=0xD000090), + ), + "Bowser Impact Smoke": ActorPresetInfo( + decomp_path="actors/impact_smoke", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_BOWSER_SMOKE"), geolayout=0xD000BFC), + ), + "King Bobomb": ActorPresetInfo( + decomp_path="actors/bobomb", + group="group3", + animation=AnimInfo( + address=0x500FE30, + behaviours=0x130001F4, + size=12, + names=[ + "Grab Mario", + "Holding Mario", + "Hit Ground", + "Unkwnown (Unused)", + "Stomp", + "Idle", + "Being Held", + "Landing", + "Jump", + "Throw Mario", + "Stand Up", + "Walking", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_KING_BOBOMB"), geolayout=0xC000000), + ), + "Klepto": ActorPresetInfo( + decomp_path="actors/klepto", + group="group5", + animation=AnimInfo( + address=0x5008CFC, + behaviours=0x13005310, + names=[ + "Dive", + "Struck By Mario", + "Dive at Mario", + "Dive at Mario 2", + "Dive at Mario 3", + "Dive at Mario 4", + "Dive Flap", + "Dive Flap 2", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_KLEPTO"), geolayout=0xC000000), + ), + "Koopa": ActorPresetInfo( + decomp_path="actors/koopa", + group="group14", + animation=AnimInfo( + address=0x6011364, + behaviours=0x13004580, + names=[ + "Falling Over (Unused Shelled Act 3)", + "Run Away", + "Laying (Unshelled)", + "Running", + "Run (Unused)", + "Laying (Shelled)", + "Stand Up", + "Stopped", + "Wake Up (Unused)", + "Walk", + "Walk Stop", + "Walk Start", + "Jump", + "Land", + ], + ), + models={ + "Koopa (Without Shell)": ModelInfo( + model_id=ModelIDInfo(0xBF, "MODEL_KOOPA_WITHOUT_SHELL"), geolayout=0xD0000D0 + ), + "Koopa (With Shell)": ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_KOOPA_WITH_SHELL"), geolayout=0xD000214), + }, + ), + "Koopa Flag": ActorPresetInfo( + decomp_path="actors/koopa_flag", + group="group14", + animation=AnimInfo(address=0x6001028, behaviours=0x130045F8, names=["Waving"]), + models=ModelInfo(model_id=ModelIDInfo(0x6A, "MODEL_KOOPA_FLAG"), geolayout=0xD000000), + ), + "Koopa Shell": ActorPresetInfo( + decomp_path="actors/koopa_shell", + group="common0", + models={ + "Koopa Shell": ModelInfo(model_id=ModelIDInfo(0xBE, "MODEL_KOOPA_SHELL"), geolayout=0xF000AB0), + "(Unused) Koopa Shell 1": ModelInfo(geolayout=0xF000ADC), + "(Unused) Koopa Shell 2": ModelInfo(geolayout=0xF000B08), + }, + ), + "Lakitu (Cameraman)": ActorPresetInfo( + decomp_path="actors/lakitu_cameraman", + group="group15", + animation=AnimInfo( + address=0x60058F8, + behaviours={"Lakitu (Beginning)": 0x13005610, "Lakitu (Cameraman)": 0x13004954}, + names=["Flying"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_LAKITU"), geolayout=0xD000000), + ), + "Lakitu (Enemy)": ActorPresetInfo( + decomp_path="actors/lakitu_enemy", + group="group11", + animation=AnimInfo( + address=0x50144D4, behaviours=0x13004918, names=["Flying", "No Spiny", "Throw Spiny", "Hold Spiny"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_ENEMY_LAKITU"), geolayout=0xC0001BC), + ), + "Leaves": ActorPresetInfo( + decomp_path="actors/leaves", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xA2, "MODEL_LEAVES"), geolayout=0x16000C8C), + ), + "Mad Piano": ActorPresetInfo( + decomp_path="actors/mad_piano", + group="group9", + animation=AnimInfo(address=0x5009B14, behaviours=0x13005024, names=["Sleeping", "Chomping"]), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_MAD_PIANO"), geolayout=0xC0001B4), + ), + "Manta Ray": ActorPresetInfo( + decomp_path="actors/manta", + group="group4", + animation=AnimInfo(address=0x5008EB4, behaviours=0x13004370, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_MANTA_RAY"), geolayout=0x5008D14), + ), + "Mario": ActorPresetInfo( + decomp_path="actors/mario", + group="group0", + animation=AnimInfo( + address=0x4EC000, + dma=True, + directory="assets/anims", + names=[ + "Slow ledge climb up", + "Fall over backwards", + "Backward air kb", + "Dying on back", + "Backflip", + "Climbing up pole", + "Grab pole short", + "Grab pole swing part 1", + "Grab pole swing part 2", + "Handstand idle", + "Handstand jump", + "Start handstand", + "Return from handstand", + "Idle on pole", + "A pose", + "Skid on ground", + "Stop skid", + "Crouch from fast longjump", + "Crouch from a slow longjump", + "Fast longjump", + "Slow longjump", + "Airborne on stomach", + "Walk with light object", + "Run with light object", + "Slow walk with light object", + "Shivering and warming hands", + "Shivering return to idle ", + "Shivering", + "Climb down on ledge", + "Waving (Credits)", + "Look up (Credits)", + "Return from look up (Credits)", + "Raising hand (Credits)", + "Lowering hand (Credits)", + "Taking off cap (Credits)", + "Start walking and look up (Credits)", + "Look back then run (Credits)", + "Final Bowser - Raise hand and spin", + "Final Bowser - Wing cap take off", + "Peach sign (Credits)", + "Stand up from lava boost", + "Fire/Lava burn", + "Wing cap flying", + "Hang on owl", + "Land on stomach", + "Air forward kb", + "Dying on stomach", + "Suffocating", + "Coughing", + "Throw catch key", + "Dying fall over", + "Idle on ledge", + "Fast ledge grab", + "Hang on ceiling", + "Put cap on", + "Take cap off then on", + "Quickly put cap on", + "Head stuck in ground", + "Ground pound landing", + "Triple jump ground-pound", + "Start ground-pound", + "Ground-pound", + "Bottom stuck in ground", + "Idle with light object", + "Jump land with light object", + "Jump with light object", + "Fall land with light object", + "Fall with light object", + "Fall from sliding with light object", + "Sliding on bottom with light object", + "Stand up from sliding with light object", + "Riding shell", + "Walking", + "Forward flip", + "Jump riding shell", + "Land from double jump", + "Double jump fall", + "Single jump", + "Land from single jump", + "Air kick", + "Double jump rise", + "Start forward spinning", + "Throw light object", + "Fall from slide kick", + "Bend kness riding shell", + "Legs stuck in ground", + "General fall", + "General land", + "Being grabbed", + "Grab heavy object", + "Slow land from dive", + "Fly from cannon", + "Moving right while hanging", + "Moving left while hanging", + "Missing cap", + "Pull door walk in", + "Push door walk in", + "Unlock door", + "Start reach pocket", + "Reach pocket", + "Stop reach pocket", + "Ground throw", + "Ground kick", + "First punch", + "Second punch", + "First punch fast", + "Second punch fast", + "Pick up light object", + "Pushing", + "Start riding shell", + "Place light object", + "Forward spinning", + "Backward spinning", + "Breakdance", + "Running", + "Running (unused)", + "Soft back kb", + "Soft front kb", + "Dying in quicksand", + "Idle in quicksand", + "Move in quicksand", + "Electrocution", + "Shocked", + "Backward kb", + "Forward kb", + "Idle heavy object", + "Stand against wall", + "Side step left", + "Side step right", + "Start sleep idle", + "Start sleep scratch", + "Start sleep yawn", + "Start sleep sitting", + "Sleep idle", + "Sleep start laying", + "Sleep laying", + "Dive", + "Slide dive", + "Ground bonk", + "Stop slide light object", + "Slide kick", + "Crouch from slide kick", + "Slide motionless", + "Stop slide", + "Fall from slide", + "Slide", + "Tiptoe", + "Twirl land", + "Twirl", + "Start twirl", + "Stop crouching", + "Start crouching", + "Crouching", + "Crawling", + "Stop crawling", + "Start crawling", + "Summon star", + "Return star approach door", + "Backwards water kb", + "Swim with object part 1", + "Swim with object part 2", + "Flutter kick with object", + "Action end with object in water", + "Stop holding object in water", + "Holding object in water", + "Drowning part 1", + "Drowning part 2", + "Dying in water", + "Forward kb in water", + "Falling from water", + "Swimming part 1", + "Swimming part 2", + "Flutter kick", + "Action end in water", + "Pick up object in water", + "Grab object in water part 2", + "Grab object in water part 1", + "Throw object in water", + "Idle in water", + "Star dance in water", + "Return from in water star dance", + "Grab bowser", + "Swing bowser", + "Release bowser", + "Holding bowser", + "Heavy throw", + "Walk panting", + "Walk with heavy object", + "Turning part 1", + "Turning part 2", + "Side flip land", + "Side flip", + "Triple jump land", + "Triple jump", + "First person", + "Idle head left", + "Idle head right", + "Idle head center", + "Handstand left", + "Handstand right", + "Wake up from sleeping", + "Wake up from laying", + "Start tiptoeing", + "Slide jump", + "Start wallkick", + "Star dance", + "Return from star dance", + "Forwards spinning flip", + "Triple jump fly", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x1, "MODEL_MARIO"), geolayout=0x17002DD4), + ), + "Mario's Cap": ActorPresetInfo( + decomp_path="actors/mario_cap", + group="common1", + models={ + "Mario's Cap": ModelInfo(model_id=ModelIDInfo(0x88, "MODEL_MARIOS_CAP"), geolayout=0x16000CA4), + "Mario's Metal Cap": ModelInfo(model_id=ModelIDInfo(0x86, "MODEL_MARIOS_METAL_CAP"), geolayout=0x16000CF0), + "Mario's Wing Cap": ModelInfo(model_id=ModelIDInfo(0x87, "MODEL_MARIOS_WING_CAP"), geolayout=0x16000D3C), + "Mario's Winged Metal Cap": ModelInfo( + model_id=ModelIDInfo(0x85, "MODEL_MARIOS_WINGED_METAL_CAP"), geolayout=0x16000DA8 + ), + }, + ), + "Metal Box": ActorPresetInfo( + decomp_path="actors/metal_box", + group="common0", + models={ + "Metal Box": ModelInfo(model_id=ModelIDInfo(0xD9, "MODEL_METAL_BOX"), geolayout=0xF000A30), + "Metal Box (DL)": ModelInfo( + model_id=ModelIDInfo(0xDA, "MODEL_METAL_BOX_DL"), displaylist=DisplaylistInfo(0x8024BB8, "metal_box_dl") + ), + }, + collision=CollisionInfo(address=0x8024C28, c_name="metal_box_seg8_collision_08024C28"), + ), + "Mips": ActorPresetInfo( + decomp_path="actors/mips", + group="group15", + animation=AnimInfo( + address=0x6015724, behaviours=0x130044FC, names=["Idle", "Hopping", "Thrown", "Thrown (Unused)", "Held"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_MIPS"), geolayout=0xD000448), + ), + "Mist": ActorPresetInfo( + decomp_path="actors/mist", + group="common1", + models={ + "Mist": ModelInfo(model_id=ModelIDInfo(0x8E, "MODEL_MIST"), geolayout=0x16000000), + "White Puff": ModelInfo(model_id=ModelIDInfo(0xE0, "MODEL_WHITE_PUFF"), geolayout=0x16000020), + }, + ), + "Moneybag": ActorPresetInfo( + decomp_path="actors/moneybag", + group="group16", + animation=AnimInfo(address=0x6005E5C, behaviours=0x130039A0, names=["Idle", "Prepare", "Jump", "Land", "Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_MONEYBAG"), geolayout=0xD0000F0), + ), + "Monty Mole": ActorPresetInfo( + decomp_path="actors/monty_mole", + group="group6", + animation=AnimInfo( + address=0x5007248, + behaviours=0x13004A00, + names=[ + "Jump Into Hole", + "Rise", + "Get Rock", + "Begin Jump Into Hole", + "Jump Out Of Hole Down", + "Unused 5", # TODO: Figure out + "Unused 6", + "Unused 7", + "Throw Rock", + "Jump Out Of Hole Up", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_MONTY_MOLE"), geolayout=0xC000000), + ), + "Montey Mole Hole": ActorPresetInfo( + decomp_path="actors/monty_mole_hole", + group="group6", + models=ModelInfo( + model_id=ModelIDInfo(0x54, "MODEL_DL_MONTY_MOLE_HOLE"), + displaylist=DisplaylistInfo(0x5000840, "monty_mole_hole_seg5_dl_05000840"), + ), + ), + "Mr. I Eyeball": ActorPresetInfo( + decomp_path="actors/mr_i_eyeball", + group="group16", + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_MR_I"), geolayout=0xD000000), + ), + "Mr. I Iris": ActorPresetInfo( + decomp_path="actors/mr_i_iris", + group="group16", + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_MR_I_IRIS"), geolayout=0xD00001C), + ), + "Mushroom 1up": ActorPresetInfo( + decomp_path="actors/mushroom_1up", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xD4, "MODEL_1UP"), geolayout=0x16000E84), + ), + "Orange Numbers": ActorPresetInfo( + decomp_path="actors/number", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xDB, "MODEL_NUMBER"), geolayout=0x16000E14), + ), + "Peach": ActorPresetInfo( + decomp_path="actors/peach", + group="group10", + animation=AnimInfo( + address=0x501C50C, + behaviours={"Peach (Beginning)": 0x13005638, "Peach (End)": 0x13000EAC}, + names=[ + "Walking away", + "Walking away 2", + "Descend", + "Descend And Look Down", + "Look Up And Open Eyes", + "Mario", + "Power Of The Stars", + "Thanks To You", + "Kiss", + "Waving", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDE, "MODEL_PEACH"), geolayout=0xC000410), + ), + "Pebble": ActorPresetInfo( + decomp_path="actors/pebble", + group="common1", + models=ModelInfo( + model_id=ModelIDInfo(0xA1, "MODEL_PEBBLE"), + displaylist=DisplaylistInfo(0x301CB00, "pebble_seg3_dl_0301CB00"), + ), + ), + "Penguin": ActorPresetInfo( + decomp_path="actors/penguin", + group="group7", + animation=AnimInfo( + address=0x5008B74, + behaviours={ + "Penguin (Tuxies Mother)": 0x13002088, + "Penguin (Small)": 0x130020E8, + "Penguin (SML)": 0x13002E58, + "Racing Penguin": 0x13005380, + }, + size=5, + names=["Walk", "Dive Slide", "Stand Up", "Idle", "Walk"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_PENGUIN"), geolayout=0xC000104), + collision=CollisionInfo(address=0x5008B88, c_name="penguin_seg5_collision_05008B88"), + ), + "Piranha Plant": ActorPresetInfo( + decomp_path="actors/piranha_plant", + group="group14", + animation=AnimInfo( + address=0x601C31C, + behaviours={"Fire Piranha Plant": 0x13005120, "Piranha Plant": 0x13001FBC}, + names=[ + "Bite", + "Sleeping? (Unused)", + "Falling over", + "Bite (Unused)", + "Grow", + "Attacked", + "Stop Bitting", + "Sleeping (Unused)", + "Sleeping", + "Bite (Duplicate)", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_PIRANHA_PLANT"), geolayout=0xD000358), + ), + "Pokey": ActorPresetInfo( + decomp_path="actors/pokey", + group="group5", + models={ + "Pokey Head": ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_POKEY_HEAD"), geolayout=0xC000610), + "Pokey Body Part": ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_POKEY_BODY_PART"), geolayout=0xC000644), + }, + ), + "Wooden Post": ActorPresetInfo( + decomp_path="actors/poundable_pole", + group="group14", + models=ModelInfo(model_id=ModelIDInfo(0x6B, "MODEL_WOODEN_POST"), geolayout=0xD0000B8), + collision=CollisionInfo(address=0x6002490, c_name="poundable_pole_collision_06002490"), + ), + # Should the power meter be included? + "Power Meter": ActorPresetInfo( + decomp_path="actors/power_meter", + group="common1", + models={ + "Power Meter (Base)": ModelInfo(displaylist=DisplaylistInfo(0x3029480, "dl_power_meter_base")), + "Power Meter (Health)": ModelInfo( + displaylist=DisplaylistInfo(0x3029570, "dl_power_meter_health_segments_begin") + ), + }, + ), + "Purple Switch": ActorPresetInfo( + decomp_path="actors/purple_switch", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0xCF, "MODEL_PURPLE_SWITCH"), geolayout=0xF0004CC), + collision=CollisionInfo(address=0x800C7A8, c_name="purple_switch_seg8_collision_0800C7A8"), + ), + "Sand": ActorPresetInfo( + decomp_path="actors/sand", + group="common1", + models=ModelInfo( + model_id=ModelIDInfo(0x9F, "MODEL_SAND_DUST"), + displaylist=DisplaylistInfo(0x302BCD0, "sand_seg3_dl_0302BCD0"), + ), + ), + "Scuttlebug": ActorPresetInfo( + decomp_path="actors/scuttlebug", + group="group17", + animation=AnimInfo(address=0x6015064, behaviours=0x13002B5C, names=["Walking"]), + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_SCUTTLEBUG"), geolayout=0xD000394), + ), + "Seaweed": ActorPresetInfo( + decomp_path="actors/seaweed", + group="group13", + animation=AnimInfo(address=0x0600A4D4, behaviours=0x13003134, size=1, names=["Wave"]), + models=ModelInfo(model_id=ModelIDInfo(0xC1, "MODEL_SEAWEED"), geolayout=0xD000284), + ), + "Skeeter": ActorPresetInfo( + decomp_path="actors/skeeter", + group="group13", + animation=AnimInfo( + address=0x6007DE0, behaviours=0x13005468, size=4, names=["Water Lunge", "Water Idle", "Walk", "Idle"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x69, "MODEL_SKEETER"), geolayout=0xD000000), + ), + "(Beta) Boo Key": ActorPresetInfo( + decomp_path="actors/small_key", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_BETA_BOO_KEY"), geolayout=0xC000188), + ), + "(Unused) Smoke": ActorPresetInfo( # TODO: double check + decomp_path="actors/smoke", + group="group6", + models=ModelInfo(displaylist=DisplaylistInfo(0x5007AF8, "smoke_seg5_dl_05007AF8")), + ), + "Mr. Blizzard": ActorPresetInfo( + decomp_path="actors/snowman", + group="group7", + animation=AnimInfo( + address=0x500D118, behaviours={"Mr. Blizzard": 0x13004DBC}, names=["Spawn Snowball", "Throw Snowball"] + ), + models={ + "Mr. Blizzard": ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_MR_BLIZZARD"), geolayout=0xC000348), + "Mr. Blizzard (Hidden)": ModelInfo( + model_id=ModelIDInfo(0x55, "MODEL_MR_BLIZZARD_HIDDEN"), geolayout=0xC00021C + ), + }, + ), + "Snufit": ActorPresetInfo( + decomp_path="actors/snufit", + group="group17", + models=ModelInfo(model_id=ModelIDInfo(0xCE, "MODEL_SNUFIT"), geolayout=0xD0001A0), + ), + "Sparkle": ActorPresetInfo( + decomp_path="actors/sparkle", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x95, "MODEL_SPARKLES"), geolayout=0x170001BC), + ), + "Sparkle Animation": ActorPresetInfo( + decomp_path="actors/sparkle_animation", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x8F, "MODEL_SPARKLES_ANIMATION"), geolayout=0x17000284), + ), + "Spindrift": ActorPresetInfo( + decomp_path="actors/spindrift", + group="group7", + animation=AnimInfo(address=0x5002D68, behaviours=0x130012B4, names=["Flying"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_SPINDRIFT"), geolayout=0xC000000), + ), + "Spiny": ActorPresetInfo( + decomp_path="actors/spiny", + group="group11", + animation=AnimInfo(address=0x5016EAC, behaviours={"Spiny": 0x130049C8}, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_SPINY"), geolayout=0xC000328), + ), + "Spiny Egg": ActorPresetInfo( + decomp_path="actors/spiny_egg", + group="group11", + animation=AnimInfo(address=0x50157E4, names=["Default"]), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_SPINY_BALL"), geolayout=0xC000290), + ), + "Springboard": ActorPresetInfo( + decomp_path="actors/springboard", + group="group8", + models={ + "Springboard Top": ModelInfo(model_id=ModelIDInfo(0xB5, "MODEL_TRAMPOLINE"), geolayout=0xC000000), + "Springboard Middle": ModelInfo(model_id=ModelIDInfo(0xB6, "MODEL_TRAMPOLINE_CENTER"), geolayout=0xC000018), + "Springboard Bottom": ModelInfo(model_id=ModelIDInfo(0xB7, "MODEL_TRAMPOLINE_BASE"), geolayout=0xC000030), + }, + ), + "Star": ActorPresetInfo( + decomp_path="actors/star", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x7A, "MODEL_STAR"), geolayout=0x16000EA0), + ), + "Small Water Splash": ActorPresetInfo( + decomp_path="actors/stomp_smoke", + group="group0", + models={ + "Small Water Splash": ModelInfo( + model_id=ModelIDInfo(0xA5, "MODEL_SMALL_WATER_SPLASH"), geolayout=0x1700009C + ), + "(Unused) Small Water Splash": ModelInfo(geolayout=0x170000E0), + }, + ), + "Sushi Shark": ActorPresetInfo( + decomp_path="actors/sushi", + group="group4", + animation=AnimInfo(address=0x500AE54, behaviours=0x13002338, size=1, names=["Swimming", "Diving"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_SUSHI"), geolayout=0xC000068), + ), + "Swoop": ActorPresetInfo( + decomp_path="actors/swoop", + group="group17", + animation=AnimInfo(address=0x60070D0, behaviours=0x13004698, size=2, names=["Idle", "Move"]), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_SWOOP"), geolayout=0xD0000DC), + ), + "Test Plataform": ActorPresetInfo( + decomp_path="actors/test_plataform", + group="common0", + collision=CollisionInfo(address=0x80262F8, c_name="unknown_seg8_collision_080262F8"), + ), + "Thwomp": ActorPresetInfo( + decomp_path="actors/thwomp", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_THWOMP"), geolayout=0xC000248), + collision={ + "Thwomp": CollisionInfo(address=0x500B7D0, c_name="thwomp_seg5_collision_0500B7D0"), + "Thwomp 2": CollisionInfo(address=0x500B92C, c_name="thwomp_seg5_collision_0500B92C"), + }, + ), + "Toad": ActorPresetInfo( + decomp_path="actors/toad", + group="group15", + animation=AnimInfo( + address=0x600FC48, + behaviours={"End Toad": 0x13000E88, "Toad Message": 0x13002EF8}, + size=8, + names=[ + "Wave Then Run (West)", + "Walking (West)", + "Node Then Turn (East)", + "Walking (East)", + "Standing (West)", + "Standing (East)", + "Waving Both Arms (West)", + "Waving One Arm (East)", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDD, "MODEL_TOAD"), geolayout=0xD0003E4), + ), + "Tweester/Tornado": ActorPresetInfo( + decomp_path="actors/tornado", + group="group5", + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_TWEESTER"), geolayout=0x5014630), + ), + "Transparent Star": ActorPresetInfo( + decomp_path="actors/transperant_star", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x79, "MODEL_TRANSPARENT_STAR"), geolayout=0x16000F6C), + ), + "Treasure Chest": ActorPresetInfo( + decomp_path="actors/treasure_chest", + group="group13", + models={ + "Treasure Chest Base": ModelInfo( + model_id=ModelIDInfo(0x65, "MODEL_TREASURE_CHEST_BASE"), geolayout=0xD000450 + ), + "Treasure Chest Lid": ModelInfo( + model_id=ModelIDInfo(0x66, "MODEL_TREASURE_CHEST_LID"), geolayout=0xD000468 + ), + }, + ), + "Tree": ActorPresetInfo( + decomp_path="actors/tree", + group="common1", + models={ + "Bubbly Tree": ModelInfo( + model_id=[ + ModelIDInfo(0x17, "MODEL_BOB_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_WDW_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_CASTLE_GROUNDS_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_WF_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_THI_BUBBLY_TREE"), + ], + geolayout=0x16000FE8, + ), + "Pine Tree": ModelInfo(model_id=ModelIDInfo(0x18, "MODEL_COURTYARD_SPIKY_TREE"), geolayout=0x16001000), + "(Unused) Pine Tree": ModelInfo(geolayout=0x16001030), + "Snow Tree": ModelInfo( + model_id=[ModelIDInfo(0x19, "MODEL_CCM_SNOW_TREE"), ModelIDInfo(0x19, "MODEL_SL_SNOW_TREE")], + geolayout=0x16001018, + ), + "Palm Tree": ModelInfo(model_id=ModelIDInfo(0x1B, "MODEL_SSL_PALM_TREE"), geolayout=0x16001048), + }, + ), + "Ukiki": ActorPresetInfo( + decomp_path="actors/ukiki", + group="group6", + animation=AnimInfo( + address=0x5015784, + behaviours={"Ukiki": 0x13001CB0}, + names=[ + "Run", + "Walk (Unused)", + "Apose (Unused)", + "Death (Unused)", + "Screech", + "Jump Clap", + "Hop (Unused)", + "Land", + "Jump", + "Itch", + "Handstand", + "Turn", + "Held", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_UKIKI"), geolayout=0xC000110), + ), + "Unagi": ActorPresetInfo( + decomp_path="actors/unagi", + group="group4", + animation=AnimInfo( + address=0x5012824, + behaviours=0x13004F40, + size=7, + names=["Yawn", "Bite", "Swimming", "Static Straight", "Idle", "Open Mouth", "Idle 2"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_UNAGI"), geolayout=0xC00010C), + ), + "Smoke": ActorPresetInfo( + decomp_path="actors/walk_smoke", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x96, "MODEL_SMOKE"), geolayout=0x17000038), + ), + "Warp Collision": ActorPresetInfo( + decomp_path="actors/warp_collision", + group="common1", + collision={ + "Door": CollisionInfo(address=0x301CE78, c_name="door_seg3_collision_0301CE78"), + "LLL Hexagonal Mesh": CollisionInfo(address=0x301CECC, c_name="lll_hexagonal_mesh_seg3_collision_0301CECC"), + }, + ), + "Warp Pipe": ActorPresetInfo( + decomp_path="actors/warp_pipe", + group="common1", + models=ModelInfo( + model_id=[ + ModelIDInfo(0x49, "MODEL_BITS_WARP_PIPE"), + ModelIDInfo(0x12, "MODEL_BITDW_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_THI_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_VCUTM_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_CASTLE_GROUNDS_WARP_PIPE"), + ], + geolayout=0x16000388, + ), + ), + "Water Bomb": ActorPresetInfo( + decomp_path="actors/water_bubble", + group="group3", + models={ + "Water Bomb": ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_WATER_BOMB"), geolayout=0xC000308), + "Water Bomb's Shadow": ModelInfo( + model_id=ModelIDInfo(0x55, "MODEL_WATER_BOMB_SHADOW"), geolayout=0xC000328 + ), + }, + ), + "Water Mine": ActorPresetInfo( + decomp_path="actors/water_mine", + group="group13", + models=ModelInfo(model_id=ModelIDInfo(0xB3, "MODEL_WATER_MINE"), geolayout=0xD0002F4), + ), + "Water Ring": ActorPresetInfo( + decomp_path="actors/water_ring", + group="group13", + animation=AnimInfo( + address=0x6013F7C, + behaviours={"Water Ring (Jet Stream)": 0x13003750, "Water Ring (Manta Ray)": 0xC66C16}, + names=["Wobble"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_WATER_RING"), geolayout=0xD000414), + ), + "Water Splash": ActorPresetInfo( + decomp_path="actors/water_splash", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0xA7, "MODEL_WATER_SPLASH"), geolayout=0x17000230), + ), + "Water Wave": ActorPresetInfo( + decomp_path="actors/water_wave", + group="group0", + models={ + "Idle Water Wave": ModelInfo(model_id=ModelIDInfo(0xA6, "MODEL_IDLE_WATER_WAVE"), geolayout=0x17000124), + "Water Wave Trail": ModelInfo(model_id=ModelIDInfo(0xA3, "MODEL_WAVE_TRAIL"), geolayout=0x17000168), + }, + ), + "Whirlpool": ActorPresetInfo( + decomp_path="actors/whirlpool", + group="group4", + models=ModelInfo( + model_id=ModelIDInfo(0x57, "MODEL_DL_WHIRLPOOL"), + displaylist=DisplaylistInfo(0x5013CB8, "whirlpool_seg5_dl_05013CB8"), + ), + ), + "White Particle": ActorPresetInfo( + decomp_path="actors/white_particle", + group="common1", + models={ + "White Particle": ModelInfo(model_id=ModelIDInfo(0xA0, "MODEL_WHITE_PARTICLE"), geolayout=0x16000F98), + "White Particle (DL)": ModelInfo( + model_id=ModelIDInfo(0x9E, "MODEL_WHITE_PARTICLE_DL"), + displaylist=DisplaylistInfo(0x302C8A0, "white_particle_dl"), + ), + }, + ), + "White Particle Small": ActorPresetInfo( + decomp_path="actors/white_particle_small", + group="group0", + models={ + "White Particle Small": ModelInfo( + model_id=ModelIDInfo(0xA4, "MODEL_WHITE_PARTICLE_SMALL"), + displaylist=DisplaylistInfo(0x4032A18, "white_particle_small_dl"), + ), + "(Unused) White Particle Small": ModelInfo( + displaylist=DisplaylistInfo(0x4032A30, "white_particle_small_unused_dl") + ), + }, + ), + "Whomp": ActorPresetInfo( + decomp_path="actors/whomp", + group="group14", + animation=AnimInfo(address=0x6020A04, behaviours=0x13002BCC, size=2, names=["Walk", "Jump"]), + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_WHOMP"), geolayout=0xD000480), + collision=CollisionInfo(address=0x6020A0C, c_name="whomp_seg6_collision_06020A0C"), + ), + "Wiggler Body": ActorPresetInfo( + decomp_path="actors/wiggler_body", + group="group11", + animation=AnimInfo(address=0x500C874, behaviours=0x130048E0, size=1, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_WIGGLER_BODY"), geolayout=0x500C778), + ), + "Wiggler Head": ActorPresetInfo( + decomp_path="actors/wiggler_head", + group="group11", + animation=AnimInfo(address=0x500EC8C, behaviours=0x13004898, size=1, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_WIGGLER_HEAD"), geolayout=0xC000030), + ), + "Wooden Signpost": ActorPresetInfo( + decomp_path="actors/wooden_signpost", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x7C, "MODEL_WOODEN_SIGNPOST"), geolayout=0x16000FB4), + collision=CollisionInfo(address=0x302DD80, c_name="wooden_signpost_seg3_collision_0302DD80"), + ), + "Yellow Sphere (Bowser 1)": ActorPresetInfo( + decomp_path="actors/yellow_sphere", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x3, "MODEL_LEVEL_GEOMETRY_03"), geolayout=0xD0000B0), + ), + "Yellow Sphere": ActorPresetInfo( + decomp_path="actors/yellow_sphere_small", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_YELLOW_SPHERE"), geolayout=0xC000000), + ), + "Yoshi": ActorPresetInfo( + decomp_path="actors/yoshi", + group="group10", + animation=AnimInfo(address=0x50241E8, behaviours=0x13004538, names=["Idle", "Walk", "Jump"]), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_YOSHI"), geolayout=0xC000468), + ), + "(Unused) Yoshi Egg": ActorPresetInfo( + decomp_path="actors/yoshi_egg", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_YOSHI_EGG"), geolayout=0xC0001E4), + ), + "Castle Flag": ActorPresetInfo( + decomp_path="levels/castle_grounds/areas/1/11", + level="CG", + animation=AnimInfo(address=0x700C95C, behaviours=0x13003C58, size=1, names=["Wave"]), + models=ModelInfo(model_id=ModelIDInfo(0x37, "MODEL_CASTLE_GROUNDS_FLAG"), geolayout=0xE000660), + ), +} + sm64_world_defaults = { "geometryMode": { "zBuffer": True, diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index ab69b095f..e93b2231f 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -1,3 +1,4 @@ +from pathlib import Path import shutil, copy, bpy, re, os from io import BytesIO from math import ceil, log, radians @@ -14,7 +15,15 @@ update_world_default_rendermode, ) from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import export_rom_checks, starSelectWarning +from .sm64_utility import ( + END_IF_FOOTER, + ModifyFoundDescriptor, + export_rom_checks, + starSelectWarning, + update_actor_includes, + write_or_delete_if_found, + write_material_headers, +) from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 from typing import Tuple, Union, Iterable @@ -61,11 +70,9 @@ applyRotation, toAlnum, checkIfPathExists, - writeIfNotFound, overwriteData, getExportDir, writeMaterialFiles, - writeMaterialHeaders, get64bitAlignedAddr, writeInsertableFile, getPathAndLevel, @@ -196,7 +203,9 @@ def exportTexRectToC(dirPath, texProp, texDir, savePNG, name, exportToProject, p overwriteData("const\s*u8\s*", textures[0].name, data, seg2CPath, None, False) # Append texture declaration to segment2.h - writeIfNotFound(seg2HPath, declaration, "#endif") + write_or_delete_if_found( + Path(seg2HPath), ModifyFoundDescriptor(declaration), path_must_exist=True, footer=END_IF_FOOTER + ) # Write/Overwrite function to hud.c overwriteData("void\s*", fTexRect.name, code, hudPath, projectExportData[1], True) @@ -425,24 +434,15 @@ def sm64ExportF3DtoC( cDefFile.write(staticData.header) cDefFile.close() + update_actor_includes(headerType, groupName, Path(dirPath), name, levelName, ["model.inc.c"], ["header.h"]) fileStatus = None if not customExport: if headerType == "Actor": - # Write to group files - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + toAlnum(name) + '/model.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + toAlnum(name) + '/header.h"', "\n#endif") - if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders( - basePath, - '#include "actors/' + toAlnum(name) + '/material.inc.c"', - '#include "actors/' + toAlnum(name) + '/material.inc.h"', + write_material_headers( + Path(basePath), + Path("actors") / toAlnum(name) / "material.inc.c", + Path("actors") / toAlnum(name) / "material.inc.h", ) texscrollIncludeC = '#include "actors/' + name + '/texscroll.inc.c"' @@ -451,19 +451,11 @@ def sm64ExportF3DtoC( texscrollGroupInclude = '#include "actors/' + groupName + '.h"' elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + toAlnum(name) + '/model.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + toAlnum(name) + '/header.h"', "\n#endif" - ) - if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders( + write_material_headers( basePath, - '#include "levels/' + levelName + "/" + toAlnum(name) + '/material.inc.c"', - '#include "levels/' + levelName + "/" + toAlnum(name) + '/material.inc.h"', + Path("levels") / levelName / toAlnum(name) / "material.inc.c", + Path("levels") / levelName / toAlnum(name) / "material.inc.h", ) texscrollIncludeC = '#include "levels/' + levelName + "/" + name + '/texscroll.inc.c"' diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index 7a3fb7abd..3c8038612 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -1,4 +1,5 @@ from __future__ import annotations +from pathlib import Path import bpy, mathutils, math, copy, os, shutil, re from bpy.utils import register_class, unregister_class @@ -13,7 +14,7 @@ from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 -from .sm64_utility import export_rom_checks, starSelectWarning +from .sm64_utility import export_rom_checks, starSelectWarning, update_actor_includes, write_material_headers from ..utility import ( PluginError, @@ -26,10 +27,8 @@ getExportDir, toAlnum, writeMaterialFiles, - writeIfNotFound, get64bitAlignedAddr, encodeSegmentedAddr, - writeMaterialHeaders, writeInsertableFile, bytesToHex, checkSM64EmptyUsesGeoLayout, @@ -659,38 +658,12 @@ def saveGeolayoutC( geoData = geolayoutGraph.to_c() if headerType == "Actor": - matCInclude = '#include "actors/' + dirName + '/material.inc.c"' - matHInclude = '#include "actors/' + dirName + '/material.inc.h"' + matCInclude = Path("actors") / dirName / "material.inc.c" + matHInclude = Path("actors") / dirName / "material.inc.h" headerInclude = '#include "actors/' + dirName + '/geo_header.h"' - - if not customExport: - # Group name checking, before anything is exported to prevent invalid state on error. - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathGeoC = os.path.join(dirPath, groupName + "_geo.c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - if not os.path.exists(groupPathC): - raise PluginError( - groupPathC + ' not found.\n Most likely issue is that "' + groupName + '" is an invalid group name.' - ) - elif not os.path.exists(groupPathGeoC): - raise PluginError( - groupPathGeoC - + ' not found.\n Most likely issue is that "' - + groupName - + '" is an invalid group name.' - ) - elif not os.path.exists(groupPathH): - raise PluginError( - groupPathH + ' not found.\n Most likely issue is that "' + groupName + '" is an invalid group name.' - ) - else: - matCInclude = '#include "levels/' + levelName + "/" + dirName + '/material.inc.c"' - matHInclude = '#include "levels/' + levelName + "/" + dirName + '/material.inc.h"' + matCInclude = Path("levels") / levelName / dirName / "material.inc.c" + matHInclude = Path("levels") / levelName / dirName / "material.inc.h" headerInclude = '#include "levels/' + levelName + "/" + dirName + '/geo_header.h"' modifyTexScrollFiles(exportDir, geoDirPath, scrollData) @@ -736,6 +709,16 @@ def saveGeolayoutC( cDefFile.close() fileStatus = None + update_actor_includes( + headerType, + groupName, + Path(dirPath), + dirName, + levelName, + [Path("model.inc.c")], + [Path("geo_header.h")], + [Path("geo.inc.c")], + ) if not customExport: if headerType == "Actor": if dirName == "star" and bpy.context.scene.replaceStarRefs: @@ -787,31 +770,12 @@ def saveGeolayoutC( appendSecondaryGeolayout(geoDirPath, 'bully', 'bully_boss', 'GEO_SCALE(0x00, 0x2000), GEO_NODE_OPEN(),') """ - # Write to group files - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathGeoC = os.path.join(dirPath, groupName + "_geo.c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/model.inc.c"', "") - writeIfNotFound(groupPathGeoC, '\n#include "' + dirName + '/geo.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + dirName + '/geo_header.h"', "\n#endif") - texscrollIncludeC = '#include "actors/' + dirName + '/texscroll.inc.c"' texscrollIncludeH = '#include "actors/' + dirName + '/texscroll.inc.h"' texscrollGroup = groupName texscrollGroupInclude = '#include "actors/' + groupName + '.h"' elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathGeoC = os.path.join(dirPath, "geo.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/model.inc.c"', "") - writeIfNotFound(groupPathGeoC, '\n#include "levels/' + levelName + "/" + dirName + '/geo.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + dirName + '/geo_header.h"', "\n#endif" - ) - texscrollIncludeC = '#include "levels/' + levelName + "/" + dirName + '/texscroll.inc.c"' texscrollIncludeH = '#include "levels/' + levelName + "/" + dirName + '/texscroll.inc.h"' texscrollGroup = levelName @@ -828,7 +792,7 @@ def saveGeolayoutC( ) if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders(exportDir, matCInclude, matHInclude) + write_material_headers(Path(exportDir), matCInclude, matHInclude) return staticData.header, fileStatus diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index 1587b43fe..b671896fb 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -1,3 +1,4 @@ +from pathlib import Path import bpy, os, math, re, shutil, mathutils from collections import defaultdict from typing import NamedTuple @@ -11,29 +12,27 @@ from .sm64_f3d_writer import SM64Model, SM64GfxFormatter from .sm64_geolayout_writer import setRooms, convertObjectToGeolayout from .sm64_f3d_writer import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import cameraWarning, starSelectWarning +from .sm64_utility import ( + cameraWarning, + starSelectWarning, + to_include_descriptor, + write_includes, + write_or_delete_if_found, + write_material_headers, +) from ..utility import ( PluginError, - writeIfNotFound, getDataFromFile, saveDataToFile, unhideAllAndGetHiddenState, restoreHiddenState, overwriteData, selectSingleObject, - deleteIfFound, applyBasicTweaks, applyRotation, - prop_split, - toAlnum, - writeMaterialHeaders, raisePluginError, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, writeMaterialFiles, - getPathAndLevel, ) from ..f3d.f3d_gbi import ( @@ -71,9 +70,7 @@ def createGeoFile(levelName, filepath): + '#include "game/screen_transition.h"\n' + '#include "game/paintings.h"\n\n' + '#include "make_const_nonconst.h"\n\n' - + '#include "levels/' - + levelName - + '/header.h"\n\n' + + '#include "header.h"\n\n' ) geoFile = open(filepath, "w", newline="\n") @@ -1008,10 +1005,10 @@ def include_proto(file_name, new_line_first=False): if not customExport: if DLFormat != DLFormat.Static: # Write material headers - writeMaterialHeaders( - exportDir, - include_proto("material.inc.c"), - include_proto("material.inc.h"), + write_material_headers( + Path(exportDir), + Path("levels") / level_name / "material.inc.c", + Path("levels") / level_name / "material.inc.c", ) # Export camera triggers @@ -1082,19 +1079,26 @@ def include_proto(file_name, new_line_first=False): createHeaderFile(level_name, headerPath) # Write level data - writeIfNotFound(geoPath, include_proto("geo.inc.c", new_line_first=True), "") - writeIfNotFound(levelDataPath, include_proto("leveldata.inc.c", new_line_first=True), "") - writeIfNotFound(headerPath, include_proto("header.inc.h", new_line_first=True), "#endif") + write_includes(Path(geoPath), [Path("geo.inc.c")]) + write_includes(Path(levelDataPath), [Path("leveldata.inc.c")]) + write_includes(Path(headerPath), [Path("header.inc.h")], before_endif=True) + old_include = to_include_descriptor(Path("levels") / level_name / "texture_include.inc.c") if fModel.texturesSavedLastExport == 0: textureIncludePath = os.path.join(level_dir, "texture_include.inc.c") if os.path.exists(textureIncludePath): os.remove(textureIncludePath) # This one is for backwards compatibility purposes - deleteIfFound(os.path.join(level_dir, "texture.inc.c"), include_proto("texture_include.inc.c")) + write_or_delete_if_found( + Path(level_dir) / "texture.inc.c", + to_remove=[old_include], + ) # This one is for backwards compatibility purposes - deleteIfFound(levelDataPath, include_proto("texture_include.inc.c")) + write_or_delete_if_found( + Path(levelDataPath), + to_remove=[old_include], + ) texscrollIncludeC = include_proto("texscroll.inc.c") texscrollIncludeH = include_proto("texscroll.inc.h") diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index 951e8bcc5..0a1120447 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -1,6 +1,7 @@ import math, bpy, mathutils import os from bpy.utils import register_class, unregister_class +from bpy.types import UILayout from re import findall, sub from pathlib import Path from ..panels import SM64_Panel @@ -31,6 +32,7 @@ from .sm64_constants import ( levelIDNames, + level_enums, enumLevelNames, enumModelIDs, enumMacrosNames, @@ -65,6 +67,13 @@ ScaleNode, ) +from .animation import ( + export_animation, + export_animation_table, + get_anim_obj, + is_obj_animatable, + SM64_ArmatureAnimProperties, +) enumTerrain = [ ("Custom", "Custom", "Custom"), @@ -1443,6 +1452,8 @@ class BehaviorScriptProperty(bpy.types.PropertyGroup): _inheritable_macros = { "LOAD_COLLISION_DATA", "SET_MODEL", + "LOAD_ANIMATIONS", + "ANIMATE" # add support later maybe # "SET_HITBOX_WITH_OFFSET", # "SET_HITBOX", @@ -1496,6 +1507,18 @@ def get_inherit_args(self, context, props): if not props.export_col: raise PluginError("Can't inherit collision without exporting collision data") return props.collision_name + if self.macro == "LOAD_ANIMATIONS": + if not props.export_anim: + raise PluginError("Can't inherit animation table without exporting animation data") + if not props.anims_name: + raise PluginError("No animation name to inherit in behavior script") + return f"oAnimations, {props.anims_name}" + if self.macro == "ANIMATE": + if not props.export_anim: + raise PluginError("Can't inherit animation table without exporting animation data") + if not props.anim_object: + raise PluginError("No animation properties to inherit in behavior script") + return f"oAnimations, {props.anim_object.fast64.sm64.animation.beginning_animation}" return self.macro_args def get_args(self, context, props): @@ -1548,7 +1571,7 @@ def write_file_lines(self, path, file_lines): # exports the model ID load into the appropriate script.c location def export_script_load(self, context, props): - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path if props.export_header_type == "Level": # for some reason full_level_path doesn't work here if props.non_decomp_level: @@ -1594,7 +1617,7 @@ def export_model_id(self, context, props, offset): if props.non_decomp_level: return # check if model_ids.h exists - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path model_ids = decomp_path / "include" / "model_ids.h" if not model_ids.exists(): PluginError("Could not find model_ids.h") @@ -1711,7 +1734,7 @@ def export_level_specific_load(self, script_path, props): def export_behavior_header(self, context, props): # check if behavior_header.h exists - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path behavior_header = decomp_path / "include" / "behavior_data.h" if not behavior_header.exists(): PluginError("Could not find behavior_data.h") @@ -1745,7 +1768,7 @@ def export_behavior_script(self, context, props): raise PluginError("Behavior must have more than 0 cmds to export") # export the behavior script itself - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path behavior_data = decomp_path / "data" / "behavior_data.c" if not behavior_data.exists(): PluginError("Could not find behavior_data.c") @@ -1812,7 +1835,7 @@ def verify_context(self, context, props): raise PluginError("Operator can only be used in object mode.") if context.scene.fast64.sm64.export_type != "C": raise PluginError("Combined Object Export only supports C exporting") - if not props.col_object and not props.gfx_object and not props.bhv_object: + if not props.col_object and not props.gfx_object and not props.anim_object and not props.bhv_object: raise PluginError("No export object selected") if ( context.active_object @@ -1823,7 +1846,7 @@ def verify_context(self, context, props): def get_export_objects(self, context, props): if not props.export_all_selected: - return {props.col_object, props.gfx_object, props.bhv_object}.difference({None}) + return {props.col_object, props.gfx_object, props.anim_object, props.bhv_object}.difference({None}) def obj_root(object, context): while object.parent and object.parent in context.selected_objects: @@ -1877,6 +1900,22 @@ def execute_gfx(self, props, context, obj, index): if not props.export_all_selected: raise Exception(e) + # writes table.inc.c file, anim_header.h + # writes include into aggregate file in export location (leveldata.c/.c) + # writes name to header in aggregate file location (actor/level) + # var name is: static const struct Animation *const _anims[] (or custom name) + def execute_anim(self, props, context, obj): + try: + if props.export_anim and obj is props.anim_object: + if props.export_single_action: + export_animation(context, obj) + else: + export_animation_table(context, obj) + except Exception as exc: + # pass on multiple export, throw on singular + if not props.export_all_selected: + raise Exception(exc) from exc + def execute(self, context): props = context.scene.fast64.sm64.combined_export try: @@ -1887,6 +1926,7 @@ def execute(self, context): props.context_obj = obj self.execute_col(props, obj) self.execute_gfx(props, context, obj, index) + self.execute_anim(props, context, obj) # do not export behaviors with multiple selection if props.export_bhv and props.obj_name_bhv and not props.export_all_selected: self.export_behavior_script(context, props) @@ -1959,6 +1999,17 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): name="Export Rooms", description="Collision export will generate rooms.inc.c file" ) + # anim export options + quick_anim_read: bpy.props.BoolProperty( + name="Quick Data Read", description="Read fcurves directly, should work with the majority of rigs", default=True + ) + export_single_action: bpy.props.BoolProperty( + name="Selected Action", + description="Animation export will only export the armature's current action like in older versions of fast64", + ) + binary_level: bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") + insertable_directory: bpy.props.StringProperty(name="Directory Path", subtype="FILE_PATH") + # export options export_bhv: bpy.props.BoolProperty( name="Export Behavior", default=False, description="Export behavior with given object name" @@ -1969,6 +2020,7 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): export_gfx: bpy.props.BoolProperty( name="Export Graphics", description="Export geo layouts for linked or selected mesh that have collision data" ) + export_anim: bpy.props.BoolProperty(name="Export Animations", description="Export animation table of an armature") export_script_loads: bpy.props.BoolProperty( name="Export Script Loads", description="Exports the Model ID and adds a level script load in the appropriate place", @@ -1991,6 +2043,7 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): collision_object: bpy.props.PointerProperty(type=bpy.types.Object) graphics_object: bpy.props.PointerProperty(type=bpy.types.Object) + animation_object: bpy.props.PointerProperty(type=bpy.types.Object, poll=lambda self, obj: is_obj_animatable(obj)) # is this abuse of properties? @property @@ -2011,6 +2064,18 @@ def gfx_object(self): else: return self.graphics_object or self.context_obj or bpy.context.active_object + @property + def anim_object(self): + if not self.export_anim: + return None + obj = get_anim_obj(bpy.context) + context_obj = self.context_obj if self.context_obj and is_obj_animatable(self.context_obj) else None + if self.export_all_selected: + return context_obj or obj + else: + assert not self.animation_object or is_obj_animatable(self.animation_object) + return self.animation_object or context_obj or obj + @property def bhv_object(self): if not self.export_bhv or self.export_all_selected: @@ -2054,6 +2119,15 @@ def obj_name_bhv(self): else: return self.filter_name(self.object_name or self.bhv_object.name) + @property + def obj_name_anim(self): + if self.export_all_selected and self.anim_object: + return self.filter_name(self.anim_object.name) + if not self.object_name and not self.anim_object: + return "" + else: + return self.filter_name(self.object_name or self.anim_object.name) + @property def bhv_name(self): return "bhv" + "".join([word.title() for word in toAlnum(self.obj_name_bhv).split("_")]) @@ -2070,6 +2144,12 @@ def collision_name(self): def model_id_define(self): return f"MODEL_{toAlnum(self.obj_name_gfx)}".upper() + @property + def anims_name(self): + if not self.anim_object: + return "" + return self.anim_object.fast64.sm64.animation.get_table_name(self.obj_name_anim) + @property def export_level_name(self): if self.level_name == "Custom" or self.non_decomp_level: @@ -2104,29 +2184,42 @@ def actor_custom_path(self): return self.custom_export_path @property - def level_directory(self): + def level_directory(self) -> Path: if self.non_decomp_level: - return self.custom_level_name + return Path(self.custom_level_name) level_name = self.custom_level_name if self.level_name == "Custom" else self.level_name - return os.path.join("/levels/", level_name) + return Path("levels") / level_name @property def base_level_path(self): if self.non_decomp_level: - return bpy.path.abspath(self.custom_level_path) - return bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + return Path(bpy.path.abspath(self.custom_level_path)) + return bpy.context.scene.fast64.sm64.abs_decomp_path @property def full_level_path(self): - return os.path.join(self.base_level_path, self.level_directory) + return self.base_level_path / self.level_directory # remove user prefixes/naming that I will be adding, such as _col, _geo etc. - def filter_name(self, name): - if self.use_name_filtering: + def filter_name(self, name, force_filtering=False): + if self.use_name_filtering or force_filtering: return sub("(_col)?(_geo)?(_bhv)?(lision)?", "", name) else: return name + def draw_anim_props(self, layout: UILayout, export_type="C", is_dma=False): + col = layout.column() + col.prop(self, "quick_anim_read") + if self.quick_anim_read: + col.label(text="May Break!", icon="INFO") + if not is_dma and export_type == "C": + col.prop(self, "export_single_action") + if export_type == "Binary": + if not is_dma: + prop_split(col, self, "binary_level", "Level") + elif export_type == "Insertable Binary": + prop_split(col, self, "insertable_directory", "Directory") + def draw_export_options(self, layout): split = layout.row(align=True) @@ -2149,6 +2242,14 @@ def draw_export_options(self, layout): box.prop(self, "graphics_object", icon_only=True) if self.export_script_loads: box.prop(self, "model_id", text="Model ID") + + box = split.box().column() + box.prop(self, "export_anim", toggle=1) + if self.export_anim: + self.draw_anim_props(box) + if not self.export_all_selected: + box.prop(self, "animation_object", icon_only=True) + col = layout.column() col.prop(self, "export_all_selected") col.prop(self, "use_name_filtering") @@ -2156,8 +2257,21 @@ def draw_export_options(self, layout): col.prop(self, "export_bhv") self.draw_obj_name(layout) + @property + def actor_names(self) -> list: + return list(dict.fromkeys(filter(None, [self.obj_name_col, self.obj_name_gfx, self.obj_name_anim])).keys()) + + @property + def export_locations(self) -> str | None: + names = self.actor_names + if len(names) > 1: + return f"({'/'.join(names)})" + elif len(names) == 1: + return names[0] + return None + def draw_level_path(self, layout): - if not directory_ui_warnings(layout, bpy.path.abspath(self.base_level_path)): + if not directory_ui_warnings(layout, self.base_level_path): return if self.non_decomp_level: layout.label(text=f"Level export path: {self.full_level_path}") @@ -2166,12 +2280,24 @@ def draw_level_path(self, layout): return True def draw_actor_path(self, layout): - actor_path = Path(bpy.context.scene.fast64.sm64.decomp_path) / "actors" - if not filepath_ui_warnings(layout, (actor_path / self.actor_group_name).with_suffix(".c")): + if self.export_locations is None: return - export_locations = ",".join({self.obj_name_col, self.obj_name_gfx}) - # can this be more clear? - layout.label(text=f"Actor export path: actors/{export_locations}") + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path + if self.export_header_type == "Actor": + actor_path = decomp_path / "actors" + if not filepath_ui_warnings(layout, (actor_path / self.actor_group_name).with_suffix(".c")): + return + layout.label(text=f"Actor export path: actors/{self.export_locations}/") + elif self.export_header_type == "Level": + if not directory_ui_warnings(layout, self.full_level_path): + return + level_path = self.full_level_path if self.non_decomp_level else self.level_directory + layout.label(text=f"Actor export path: {level_path / self.export_locations}/") + elif self.export_header_type == "Custom": + custom_path = Path(bpy.path.abspath(self.custom_export_path)) + if not directory_ui_warnings(layout, custom_path): + return + layout.label(text=f"Actor export path: {custom_path / self.export_locations}/") return True def draw_col_names(self, layout): @@ -2184,6 +2310,12 @@ def draw_gfx_names(self, layout): if self.export_script_loads: layout.label(text=f"Model ID: {self.model_id_define}") + def draw_anim_names(self, layout): + anim_props = self.anim_object.fast64.sm64.animation + if anim_props.is_dma: + layout.label(text=f"Animation path: {anim_props.dma_folder}(.c)") + layout.label(text=f"Animation table name: {self.anims_name}") + def draw_obj_name(self, layout): split_1 = layout.split(factor=0.45) split_2 = split_1.split(factor=0.45) @@ -2219,7 +2351,7 @@ def draw_props(self, layout): col.separator() # object exports box = col.box() - if not self.export_col and not self.export_bhv and not self.export_gfx: + if not self.export_col and not self.export_bhv and not self.export_gfx and not self.export_anim: col = box.column() col.operator("object.sm64_export_combined_object", text="Export Object") col.enabled = False @@ -2231,7 +2363,7 @@ def draw_props(self, layout): self.draw_export_options(box) # bhv export only, so enable bhv draw only - if not self.export_col and not self.export_gfx: + if not self.export_col and not self.export_gfx and not self.export_anim: return self.draw_bhv_options(col) # pathing for gfx/col exports @@ -2266,17 +2398,13 @@ def draw_props(self, layout): "Duplicates objects will be exported! Use with Caution.", icon="ERROR", ) + return info_box = box.box() info_box.scale_y = 0.5 - if self.export_header_type == "Level": - if not self.draw_level_path(info_box): - return - - elif self.export_header_type == "Actor": - if not self.draw_actor_path(info_box): - return + if not self.draw_actor_path(info_box): + return if self.obj_name_gfx and self.export_gfx: self.draw_gfx_names(info_box) @@ -2284,6 +2412,9 @@ def draw_props(self, layout): if self.obj_name_col and self.export_col: self.draw_col_names(info_box) + if self.obj_name_anim and self.export_anim: + self.draw_anim_names(info_box) + if self.obj_name_bhv: info_box.label(text=f"Behavior name: {self.bhv_name}") @@ -2785,6 +2916,8 @@ class SM64_ObjectProperties(bpy.types.PropertyGroup): game_object: bpy.props.PointerProperty(type=SM64_GameObjectProperties) segment_loads: bpy.props.PointerProperty(type=SM64_SegmentProperties) + animation: bpy.props.PointerProperty(type=SM64_ArmatureAnimProperties) + @staticmethod def upgrade_changed_props(): for obj in bpy.data.objects: diff --git a/fast64_internal/sm64/sm64_texscroll.py b/fast64_internal/sm64/sm64_texscroll.py index f68fe995f..01d4d83b8 100644 --- a/fast64_internal/sm64/sm64_texscroll.py +++ b/fast64_internal/sm64/sm64_texscroll.py @@ -1,7 +1,8 @@ +from pathlib import Path import os, re, bpy -from ..utility import PluginError, writeIfNotFound, getDataFromFile, saveDataToFile, CScrollData, CData +from ..utility import PluginError, getDataFromFile, saveDataToFile, CScrollData, CData from .c_templates.tile_scroll import tile_scroll_c, tile_scroll_h -from .sm64_utility import getMemoryCFilePath +from .sm64_utility import END_IF_FOOTER, ModifyFoundDescriptor, getMemoryCFilePath, write_or_delete_if_found # This is for writing framework for scroll code. # Actual scroll code found in f3d_gbi.py (FVertexScrollData) @@ -78,7 +79,16 @@ def writeSegmentROMTable(baseDir): memFile.close() # Add extern definition of segment table - writeIfNotFound(os.path.join(baseDir, "src/game/memory.h"), "\nextern uintptr_t sSegmentROMTable[32];", "#endif") + write_or_delete_if_found( + Path(baseDir) / "src/game/memory.h", + [ + ModifyFoundDescriptor( + "extern uintptr_t sSegmentROMTable[32];", r"extern\h*uintptr_t\h*sSegmentROMTable\[.*?\]\h?;" + ) + ], + path_must_exist=True, + footer=END_IF_FOOTER, + ) def writeScrollTextureCall(path, include, callString): diff --git a/fast64_internal/sm64/sm64_utility.py b/fast64_internal/sm64/sm64_utility.py index c7c55de1f..97ab4e563 100644 --- a/fast64_internal/sm64/sm64_utility.py +++ b/fast64_internal/sm64/sm64_utility.py @@ -1,8 +1,15 @@ +from typing import NamedTuple, Optional +from pathlib import Path +from io import StringIO +import random +import string import os +import re + import bpy from bpy.types import UILayout -from ..utility import PluginError, filepath_checks, run_and_draw_errors, multilineLabel, prop_split +from ..utility import PluginError, filepath_checks, run_and_draw_errors, multilineLabel, prop_split, COMMENT_PATTERN from .sm64_function_map import func_map @@ -122,3 +129,264 @@ def convert_addr_to_func(addr: str): return refresh_func_map[addr.lower()] else: return addr + + +def temp_file_path(path: Path): + """Generates a temporary file path that does not exist from the given path.""" + result, size = path.with_suffix(".tmp"), 0 + for size in range(5, 15): + if not result.exists(): + return result + random_suffix = "".join(random.choice(string.ascii_letters) for _ in range(size)) + result = path.with_suffix(f".{random_suffix}.tmp") + size += 1 + raise PluginError("Cannot create unique temporary file. 10 tries exceeded.") + + +class ModifyFoundDescriptor: + string: str + regex: str + + def __init__(self, string: str, regex: str = ""): + self.string = string + if regex: + self.regex = regex.replace(r"\h", r"[^\v\S]") # /h is invalid... for some reason + else: + self.regex = re.escape(string) + r"\n?" + + +class DescriptorMatch(NamedTuple): + string: str + start: int + end: int + + +class CommentMatch(NamedTuple): + commentless_pos: int + size: int + + +def adjust_start_end(start: int, end: int, comment_map: list[CommentMatch]): + for commentless_pos, comment_size in comment_map: + if start >= commentless_pos: + start += comment_size + if end >= commentless_pos: + end += comment_size + return start, end + + +def find_descriptor_in_text( + value: ModifyFoundDescriptor, commentless: str, comment_map: list[CommentMatch], start=0, end=-1 +): + matches: list[DescriptorMatch] = [] + for match in re.finditer(value.regex, commentless[start:end]): + matches.append( + DescriptorMatch(match.group(0), *adjust_start_end(start + match.start(), start + match.end(), comment_map)) + ) + return matches + + +def get_comment_map(text: str): + comment_map: list[CommentMatch] = [] + commentless, last_pos, pos = StringIO(), 0, 0 + for match in re.finditer(COMMENT_PATTERN, text): + pos += commentless.write(text[last_pos : match.start()]) # add text before comment + match_string = match.group(0) + if match_string.startswith("/"): # actual comment + comment_map.append(CommentMatch(pos, len(match_string) - 1)) + pos += commentless.write(" ") + else: # stuff like strings + pos += commentless.write(match_string) + last_pos = match.end() + + commentless.write(text[last_pos:]) # add any remaining text after the last match + return commentless.getvalue(), comment_map + + +def find_descriptors( + text: str, + descriptors: list[ModifyFoundDescriptor], + error_if_no_header=False, + header: Optional[ModifyFoundDescriptor] = None, + error_if_no_footer=False, + footer: Optional[ModifyFoundDescriptor] = None, + ignore_comments=True, +): + """Returns: The found matches from descriptors, the footer pos (the end of the text if none)""" + if ignore_comments: + commentless, comment_map = get_comment_map(text) + else: + commentless, comment_map = text, [] + + header_matches = find_descriptor_in_text(header, commentless, comment_map) if header is not None else [] + footer_matches = find_descriptor_in_text(footer, commentless, comment_map) if footer is not None else [] + + header_pos = 0 + if len(header_matches) > 0: + _, header_pos, _ = header_matches[0] + elif header is not None and error_if_no_header: + raise PluginError(f"Header {header.string} does not exist.") + + # find first footer after the header + if footer_matches: + if header_matches: + footer_pos = next((pos for _, pos, _ in footer_matches if pos >= header_pos), footer_matches[-1].start) + else: + _, footer_pos, _ = footer_matches[-1] + else: + if footer is not None and error_if_no_footer: + raise PluginError(f"Footer {footer.string} does not exist.") + footer_pos = len(text) + + found_matches: dict[ModifyFoundDescriptor, list[DescriptorMatch]] = {} + for descriptor in descriptors: + matches = find_descriptor_in_text(descriptor, commentless, comment_map, header_pos, footer_pos) + if matches: + found_matches.setdefault(descriptor, []).extend(matches) + return found_matches, footer_pos + + +def write_or_delete_if_found( + path: Path, + to_add: Optional[list[ModifyFoundDescriptor]] = None, + to_remove: Optional[list[ModifyFoundDescriptor]] = None, + path_must_exist=False, + create_new=False, + error_if_no_header=False, + header: Optional[ModifyFoundDescriptor] = None, + error_if_no_footer=False, + footer: Optional[ModifyFoundDescriptor] = None, + ignore_comments=True, +): + changed = False + to_add, to_remove = to_add or [], to_remove or [] + + assert not (path_must_exist and create_new), "path_must_exist and create_new" + if path_must_exist: + filepath_checks(path) + if not create_new and not to_add and not to_remove: + return False + + if os.path.exists(path) and not create_new: + text = path.read_text() + if text and text[-1] not in {"\n", "\r"}: # add end new line if not there + text += "\n" + found_matches, footer_pos = find_descriptors( + text, to_add + to_remove, error_if_no_header, header, error_if_no_footer, footer, ignore_comments + ) + else: + text, found_matches, footer_pos = "", {}, 0 + + for descriptor in to_remove: + matches = found_matches.get(descriptor) + if matches is None: + continue + print(f"Removing {descriptor.string} in {str(path)}") + for match in matches: + changed = True + text = text[: match.start] + text[match.end :] # Remove match + diff = match.end - match.start + for other_match in (other_match for matches in found_matches.values() for other_match in matches): + if other_match.start > match.start: + other_match.start -= diff + other_match.end -= diff + if footer_pos > match.start: + footer_pos -= diff + + additions = "" + for descriptor in to_add: + if descriptor in found_matches: + continue + print(f"Adding {descriptor.string} in {str(path)}") + additions += f"{descriptor.string}\n" + changed = True + text = text[:footer_pos] + additions + text[footer_pos:] + + if changed or create_new: + path.write_text(text) + return True + return False + + +def to_include_descriptor(include: Path, *alternatives: Path): + base_regex = r'\n?#\h*?include\h*?"{0}"' + regex = base_regex.format(include.as_posix()) + for alternative in alternatives: + regex += f"|{base_regex.format(alternative.as_posix())}" + return ModifyFoundDescriptor(f'#include "{include.as_posix()}"', regex) + + +END_IF_FOOTER = ModifyFoundDescriptor("#endif", r"#\h*?endif") + + +def write_includes( + path: Path, includes: Optional[list[Path]] = None, path_must_exist=False, create_new=False, before_endif=False +): + to_add = [] + for include in includes or []: + to_add.append(to_include_descriptor(include)) + return write_or_delete_if_found( + path, + to_add, + path_must_exist=path_must_exist, + create_new=create_new, + footer=END_IF_FOOTER if before_endif else None, + ) + + +def update_actor_includes( + header_type: str, + group_name: str, + header_dir: Path, + dir_name: str, + level_name: str | None = None, # for backwards compatibility + data_includes: Optional[list[Path]] = None, + header_includes: Optional[list[Path]] = None, + geo_includes: Optional[list[Path]] = None, +): + if header_type == "Actor": + if not group_name: + raise PluginError("Empty group name") + data_path = header_dir / f"{group_name}.c" + header_path = header_dir / f"{group_name}.h" + geo_path = header_dir / f"{group_name}_geo.c" + elif header_type == "Level": + data_path = header_dir / "leveldata.c" + header_path = header_dir / "header.h" + geo_path = header_dir / "geo.c" + elif header_type == "Custom": + return # Custom doesn't update includes + else: + raise PluginError(f'Unknown header type "{header_type}"') + + def write_includes_with_alternate(path: Path, includes: Optional[list[Path]], before_endif=False): + if includes is None: + return False + if header_type == "Level": + path_and_alternates = [ + [ + Path(dir_name) / include, + Path("levels") / level_name / (dir_name) / include, # backwards compatability + ] + for include in includes + ] + else: + path_and_alternates = [[Path(dir_name) / include] for include in includes] + return write_or_delete_if_found( + path, + [to_include_descriptor(*paths) for paths in path_and_alternates], + path_must_exist=True, + footer=END_IF_FOOTER if before_endif else None, + ) + + if write_includes_with_alternate(data_path, data_includes): + print(f"Updated data includes at {header_path}.") + if write_includes_with_alternate(header_path, header_includes, before_endif=True): + print(f"Updated header includes at {header_path}.") + if write_includes_with_alternate(geo_path, geo_includes): + print(f"Updated geo data at {geo_path}.") + + +def write_material_headers(decomp: Path, c_include: Path, h_include: Path): + write_includes(decomp / "src/game/materials.c", [c_include]) + write_includes(decomp / "src/game/materials.h", [h_include], before_endif=True) diff --git a/fast64_internal/sm64/tools/panels.py b/fast64_internal/sm64/tools/panels.py index 5a41accbe..c22aae05c 100644 --- a/fast64_internal/sm64/tools/panels.py +++ b/fast64_internal/sm64/tools/panels.py @@ -1,11 +1,11 @@ from bpy.utils import register_class, unregister_class +from typing import TYPE_CHECKING + from ...panels import SM64_Panel from .operators import SM64_CreateSimpleLevel, SM64_AddWaterBox, SM64_AddBoneGroups, SM64_CreateMetarig -from typing import TYPE_CHECKING - if TYPE_CHECKING: from ..settings.properties import SM64_Properties diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 14f6aea59..0cda4e229 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -1,7 +1,7 @@ import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator from math import pi, ceil, degrees, radians, copysign from mathutils import * -from .utility_anim import * + from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union from bpy.types import UILayout, Scene, World @@ -423,7 +423,7 @@ def getPathAndLevel(is_custom_export, custom_export_path, custom_level_name, lev export_path = bpy.path.abspath(custom_export_path) level_name = custom_level_name else: - export_path = bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + export_path = str(bpy.context.scene.fast64.sm64.abs_decomp_path) if level_enum == "Custom": level_name = custom_level_name else: @@ -482,6 +482,7 @@ def saveDataToFile(filepath, data): def applyBasicTweaks(baseDir): + directory_path_checks(baseDir, "Empty directory path.") if bpy.context.scene.fast64.sm64.force_extended_ram: enableExtendedRAM(baseDir) @@ -510,11 +511,6 @@ def enableExtendedRAM(baseDir): segmentFile.close() -def writeMaterialHeaders(exportDir, matCInclude, matHInclude): - writeIfNotFound(os.path.join(exportDir, "src/game/materials.c"), "\n" + matCInclude, "") - writeIfNotFound(os.path.join(exportDir, "src/game/materials.h"), "\n" + matHInclude, "#endif") - - def writeMaterialFiles( exportDir, assetDir, headerInclude, matHInclude, headerDynamic, dynamic_data, geoString, customExport ): @@ -691,11 +687,17 @@ def makeWriteInfoBox(layout): def writeBoxExportType(writeBox, headerType, name, levelName, levelOption): + if not name: + writeBox.label(text="Empty actor name", icon="ERROR") + return if headerType == "Actor": writeBox.label(text="actors/" + toAlnum(name)) elif headerType == "Level": if levelOption != "Custom": levelName = levelOption + if not name: + writeBox.label(text="Empty level name", icon="ERROR") + return writeBox.label(text="levels/" + toAlnum(levelName) + "/" + toAlnum(name)) @@ -742,40 +744,6 @@ def overwriteData(headerRegex, name, value, filePath, writeNewBeforeString, isFu raise PluginError(filePath + " does not exist.") -def writeIfNotFound(filePath, stringValue, footer): - if os.path.exists(filePath): - fileData = open(filePath, "r") - fileData.seek(0) - stringData = fileData.read() - fileData.close() - if stringValue not in stringData: - if len(footer) > 0: - footerIndex = stringData.rfind(footer) - if footerIndex == -1: - raise PluginError("Footer " + footer + " does not exist.") - stringData = stringData[:footerIndex] + stringValue + "\n" + stringData[footerIndex:] - else: - stringData += stringValue - fileData = open(filePath, "w", newline="\n") - fileData.write(stringData) - fileData.close() - else: - raise PluginError(filePath + " does not exist.") - - -def deleteIfFound(filePath, stringValue): - if os.path.exists(filePath): - fileData = open(filePath, "r") - fileData.seek(0) - stringData = fileData.read() - fileData.close() - if stringValue in stringData: - stringData = stringData.replace(stringValue, "") - fileData = open(filePath, "w", newline="\n") - fileData.write(stringData) - fileData.close() - - def yield_children(obj: bpy.types.Object): yield obj if obj.children: @@ -811,6 +779,13 @@ def scale_mtx_from_vector(scale: mathutils.Vector): return mathutils.Matrix.Diagonal(scale[0:3]).to_4x4() +def attemptModifierApply(modifier): + try: + bpy.ops.object.modifier_apply(modifier=modifier.name) + except Exception as e: + print("Skipping modifier " + str(modifier.name)) + + def copy_object_and_apply(obj: bpy.types.Object, apply_scale=False, apply_modifiers=False): if apply_scale or apply_modifiers: # it's a unique mesh, use object name @@ -1302,6 +1277,11 @@ def toAlnum(name, exceptions=[]): return name +def to_valid_file_name(name: str): + """Replace any invalid characters with an underscore""" + return re.sub(r'[/\\?%*:|"<>]', " ", name) + + def get64bitAlignedAddr(address): endNibble = hex(address)[-1] if endNibble != "0" and endNibble != "8": @@ -1362,15 +1342,15 @@ def bytesToInt(value): def bytesToHex(value, byteSize=4): - return format(bytesToInt(value), "#0" + str(byteSize * 2 + 2) + "x") + return format(bytesToInt(value), f"#0{(byteSize * 2 + 2)}x") def bytesToHexClean(value, byteSize=4): - return format(bytesToInt(value), "0" + str(byteSize * 2) + "x") + return format(bytesToInt(value), f"#0{(byteSize * 2)}x") -def intToHex(value, byteSize=4): - return format(value, "#0" + str(byteSize * 2 + 2) + "x") +def intToHex(value, byte_size=4, signed=True): + return format(value if signed else cast_integer(value, byte_size * 8, False), f"#0{(byte_size * 2 + 2)}x") def intToBytes(value, byteSize): @@ -1605,6 +1585,10 @@ def bitMask(data, offset, amount): return (~(-1 << amount) << offset & data) >> offset +def is_bit_active(x: int, index: int): + return ((x >> index) & 1) == 1 + + def read16bitRGBA(data): r = bitMask(data, 11, 5) / ((2**5) - 1) g = bitMask(data, 6, 5) / ((2**5) - 1) @@ -1716,9 +1700,11 @@ def getTextureSuffixFromFormat(texFmt): return texFmt.lower() -def removeComments(text: str): - # https://stackoverflow.com/a/241506 +# https://stackoverflow.com/a/241506 +COMMENT_PATTERN = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE) + +def removeComments(text: str): def replacer(match: re.Match[str]): s = match.group(0) if s.startswith("/"): @@ -1726,9 +1712,7 @@ def replacer(match: re.Match[str]): else: return s - pattern = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE) - - return re.sub(pattern, replacer, text) + return re.sub(COMMENT_PATTERN, replacer, text) binOps = { diff --git a/fast64_internal/utility_anim.py b/fast64_internal/utility_anim.py index 982706a16..0f5218c05 100644 --- a/fast64_internal/utility_anim.py +++ b/fast64_internal/utility_anim.py @@ -1,5 +1,10 @@ import bpy, math, mathutils +from bpy.types import Object, Action, AnimData from bpy.utils import register_class, unregister_class +from bpy.props import StringProperty + +from .operators import OperatorBase +from .utility import attemptModifierApply, raisePluginError, PluginError from typing import TYPE_CHECKING @@ -23,8 +28,6 @@ class ArmatureApplyWithMeshOperator(bpy.types.Operator): # Called on demand (i.e. button press, menu item) # Can also be called from operator search menu (Spacebar) def execute(self, context): - from .utility import PluginError, raisePluginError - try: if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") @@ -46,6 +49,51 @@ def execute(self, context): return {"FINISHED"} # must return a set +class CreateAnimData(OperatorBase): + bl_idname = "scene.fast64_create_anim_data" + bl_label = "Create Animation Data" + bl_description = "Create animation data" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ANIM" + + def execute_operator(self, context): + obj = context.object + if obj is None: + raise PluginError("No selected object") + if obj.animation_data is None: + obj.animation_data_create() + + +class AddBasicAction(OperatorBase): + bl_idname = "scene.fast64_add_basic_action" + bl_label = "Add Basic Action" + bl_description = "Create animation data and add basic action" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION" + + def execute_operator(self, context): + if context.object is None: + raise PluginError("No selected object") + create_basic_action(context.object) + + +class StashAction(OperatorBase): + bl_idname = "scene.fast64_stash_action" + bl_label = "Stash Action" + bl_description = "Stash an action in an object's nla tracks if not already stashed" + context_mode = "OBJECT" + icon = "NLA" + + action: StringProperty() + + def execute_operator(self, context): + if context.object is None: + raise PluginError("No selected object") + stashActionInArmature(context.object, get_action(self.action)) + + # This code only handles root bone with no parent, which is the only bone that translates. def getTranslationRelativeToRest(bone: bpy.types.Bone, inputVector: mathutils.Vector) -> mathutils.Vector: zUpToYUp = mathutils.Quaternion((1, 0, 0), math.radians(-90.0)).to_matrix().to_4x4() @@ -63,13 +111,6 @@ def getRotationRelativeToRest(bone: bpy.types.Bone, inputEuler: mathutils.Euler) return (restRotation.inverted() @ inputEuler.to_matrix().to_4x4()).to_euler("XYZ", inputEuler) -def attemptModifierApply(modifier): - try: - bpy.ops.object.modifier_apply(modifier=modifier.name) - except Exception as e: - print("Skipping modifier " + str(modifier.name)) - - def armatureApplyWithMesh(armatureObj: bpy.types.Object, context: bpy.types.Context): for child in armatureObj.children: if child.type != "MESH": @@ -179,28 +220,62 @@ def getIntersectionInterval(): return range_get_by_choice[anim_range_choice]() -def stashActionInArmature(armatureObj: bpy.types.Object, action: bpy.types.Action): +def is_action_stashed(obj: Object, action: Action): + animation_data: AnimData | None = obj.animation_data + if animation_data is None: + return False + for track in animation_data.nla_tracks: + for strip in track.strips: + if strip.action is None: + continue + if strip.action.name == action.name: + return True + return False + + +def stashActionInArmature(obj: Object, action: Action): """ Stashes an animation (action) into an armature´s nla tracks. This prevents animations from being deleted by blender or purged by the user on accident. """ - for track in armatureObj.animation_data.nla_tracks: - for strip in track.strips: - if strip.action is None: - continue + if is_action_stashed(obj, action): + return - if strip.action.name == action.name: - return + print(f'Stashing "{action.name}" in the object "{obj.name}".') + if obj.animation_data is None: + obj.animation_data_create() + track = obj.animation_data.nla_tracks.new() + track.name = action.name + track.strips.new(action.name, int(action.frame_range[0]), action) - print(f'Stashing "{action.name}" in the object "{armatureObj.name}".') - track = armatureObj.animation_data.nla_tracks.new() - track.strips.new(action.name, int(action.frame_range[0]), action) +def create_basic_action(obj: Object, name=""): + if obj.animation_data is None: + obj.animation_data_create() + if name == "": + name = f"{obj.name} Action" + action = bpy.data.actions.new(name) + stashActionInArmature(obj, action) + obj.animation_data.action = action + return action + + +def get_action(name: str): + if name == "": + raise ValueError("Empty action name.") + if not name in bpy.data.actions: + raise IndexError(f"Action ({name}) is not in this file´s action data.") + return bpy.data.actions[name] -classes = (ArmatureApplyWithMeshOperator,) +classes = ( + ArmatureApplyWithMeshOperator, + CreateAnimData, + AddBasicAction, + StashAction, +) def utility_anim_register(): diff --git a/piranha plant/toad.insertable b/piranha plant/toad.insertable new file mode 100644 index 0000000000000000000000000000000000000000..18090db2d246a18a674e46f29f92855007931309 GIT binary patch literal 38872 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

6cg0uKd$3AJhU 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 Date: Mon, 30 Sep 2024 23:00:04 +0100 Subject: [PATCH 03/14] Remove animation docs from README until I write new ones in fast64_docs --- fast64_internal/sm64/README.md | 35 +++------------------------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/fast64_internal/sm64/README.md b/fast64_internal/sm64/README.md index 7daa84a4f..ee00fbcae 100644 --- a/fast64_internal/sm64/README.md +++ b/fast64_internal/sm64/README.md @@ -52,38 +52,7 @@ For example, for Mario you would rotate the four limb joints around the Y-axis 1 Then after applying the rest pose and skinning, you would apply those operations in reverse order then apply rest pose again. -### Importing/Exporting Binary SM64 Animations (Not Mario) -- Note: SM64 animations only allow for rotations, and translation only on the root bone. - -- Download Quad64, open the desired level, and go to Misc -> Script Dumps. -- Go to the objects header, find the object you want, and view the Behaviour Script tab. -- For most models with animation, you can will see a 27 command, and optionally a 28 command. - -For importing: - -- The last 4 bytes of the 27 command will be the animation list pointer. - - Make sure 'Is DMA Animation' is unchecked, 'Is Anim List' is checked, and 'Is Segmented Pointer' is checked. - - Set the animation importer start address as those 4 bytes. - - If a 28 command exists, then the second byte will be the anim list index. - - Otherwise, the anim list index is usually 0. - -For exporting: - -- Make sure 'Set Anim List Entry' is checked. -- Copy the addresses of the 27 command, which is the first number before the slash on that line. -- Optionally do the same for the 28 command, which may not exist. -- If a 28 command exists, then the second byte will be the anim list index. -- Otherwise, the anim list index is usually 0. - -Select an armature for the animation, and press 'Import/Export animation'. - -### Importing/Exporting Binary Mario Animations -Mario animations use a DMA table, which contains 8 byte entries of (offset from table start, animation size). Documentation about this table is here: -https://dudaw.webs.com/sm64docs/sm64_marios_animation_table.txt -Basically, Mario's DMA table starts at 0x4EC000. There is an 8 byte header, and then the animation entries afterward. Thus the 'climb up ledge' DMA entry is at 0x4EC008. The first 4 bytes at that address indicate the offset from 0x4EC000 at which the actual animation exists. Thus the 'climb up ledge' animation entry address is at 0x4EC690. Using this table you can find animations you want to overwrite. Make sure the 'Is DMA Animation' option is checked and 'Is Segmented Pointer' is unchecked when importing/exporting. Check "Overwrite DMA Entry", set the start address to 4EC000 (for Mario), and set the entry address to the DMA entry obtained previously. - -### Animating Existing Geolayouts -Often times it is hard to rig an existing SM64 geolayout, as there are many intermediate non-deform bones and bones don't point to their children. To make this easier you can use the 'Create Animatable Metarig' operator in the SM64 Armature Tools header. This will generate a metarig which can be used with IK. The metarig bones will be placed on armature layers 3 and 4. +### TODO: Link to animations docs ## Decomp To start, set your base decomp folder in SM64 General Settings. This allows the plugin to automatically add headers/includes to the correct locations. You can always choose to export to a custom location, although headers/includes won't be written. @@ -165,6 +134,8 @@ Insertable Binary exporting will generate a binary file, with a header containin 1 = Geolayout 2 = Animation 3 = Collision + 4 = Animation Table + 5 = Animation DMA Table 0x04-0x08 : Data Size (size in bytes of Data Section) 0x08-0x0C : Start Address (start address of data, relative to start of Data Section) From acacef70acdb176cbc6e7d00a08b478e235e9e0f Mon Sep 17 00:00:00 2001 From: Lila Date: Tue, 1 Oct 2024 09:52:04 +0100 Subject: [PATCH 04/14] remove accidental exports --- piranha plant/toad.insertable | Bin 38872 -> 0 bytes toad.insertable | Bin 25796 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 piranha plant/toad.insertable delete mode 100644 toad.insertable diff --git a/piranha plant/toad.insertable b/piranha plant/toad.insertable deleted file mode 100644 index 18090db2d246a18a674e46f29f92855007931309..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38872 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

6cg0uKd$3AJhU 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 Date: Tue, 1 Oct 2024 09:59:27 +0100 Subject: [PATCH 05/14] merge conflicts? --- fast64_internal/f3d_material_converter.py | 12 ++++++------ fast64_internal/oot/oot_utility.py | 10 ---------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/fast64_internal/f3d_material_converter.py b/fast64_internal/f3d_material_converter.py index 992c2c221..fffb903da 100644 --- a/fast64_internal/f3d_material_converter.py +++ b/fast64_internal/f3d_material_converter.py @@ -186,16 +186,16 @@ def convertAllBSDFtoF3D(objs, renameUV): def convertBSDFtoF3D(obj, index, material, materialDict): if not material.use_nodes: newMaterial = createF3DMat(obj, preset="Shaded Solid", index=index) - with bpy.context.temp_override(material=newMaterial): - newMaterial.f3d_mat.default_light_color = material.diffuse_color + f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial + f3dMat.default_light_color = material.diffuse_color updateMatWithName(newMaterial, material, materialDict) elif "Principled BSDF" in material.node_tree.nodes: tex0Node = material.node_tree.nodes["Principled BSDF"].inputs["Base Color"] if len(tex0Node.links) == 0: newMaterial = createF3DMat(obj, preset=getDefaultMaterialPreset("Shaded Solid"), index=index) - with bpy.context.temp_override(material=newMaterial): - newMaterial.f3d_mat.default_light_color = tex0Node.default_value + f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial + f3dMat.default_light_color = tex0Node.default_value updateMatWithName(newMaterial, material, materialDict) else: if isinstance(tex0Node.links[0].from_node, bpy.types.ShaderNodeTexImage): @@ -213,8 +213,8 @@ def convertBSDFtoF3D(obj, index, material, materialDict): else: presetName = getDefaultMaterialPreset("Shaded Texture") newMaterial = createF3DMat(obj, preset=presetName, index=index) - with bpy.context.temp_override(material=newMaterial): - newMaterial.f3d_mat.tex0.tex = tex0Node.links[0].from_node.image + f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial + f3dMat.tex0.tex = tex0Node.links[0].from_node.image updateMatWithName(newMaterial, material, materialDict) else: print("Principled BSDF material does not have an Image Node attached to its Base Color.") diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/oot/oot_utility.py index 464430478..3173cc299 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/oot/oot_utility.py @@ -496,16 +496,6 @@ def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: st return filepath -def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: str) -> str: - if isCustomExport: - filepath = exportPath - else: - filepath = os.path.join( - ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, False), folderName + ".h" - ) - return filepath - - def ootGetPath(exportPath, isCustomExport, subPath, folderName, makeIfNotExists, useFolderForCustom): if isCustomExport: path = bpy.path.abspath(os.path.join(exportPath, (folderName if useFolderForCustom else ""))) From eec5516a025c4540ee019945f468c1171ef6705e Mon Sep 17 00:00:00 2001 From: Lila Date: Tue, 1 Oct 2024 12:39:31 +0100 Subject: [PATCH 06/14] Fix level export includes dups --- fast64_internal/sm64/sm64_level_writer.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index b671896fb..e890ba8f3 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -13,10 +13,10 @@ from .sm64_geolayout_writer import setRooms, convertObjectToGeolayout from .sm64_f3d_writer import modifyTexScrollFiles, modifyTexScrollHeadersGroup from .sm64_utility import ( + END_IF_FOOTER, cameraWarning, starSelectWarning, to_include_descriptor, - write_includes, write_or_delete_if_found, write_material_headers, ) @@ -941,6 +941,14 @@ def include_proto(file_name, new_line_first=False): include += "\n" return include + def write_include(path: Path, include: Path, before_endif=False): + return write_or_delete_if_found( + path, + [to_include_descriptor(include, Path("levels") / level_name / include)], + path_must_exist=True, + footer=END_IF_FOOTER if before_endif else None, + ) + gfxFormatter = SM64GfxFormatter(ScrollMethod.Vertex) exportData = fModel.to_c(TextureExportSettings(savePNG, savePNG, f"levels/{level_name}", level_dir), gfxFormatter) staticData = exportData.staticData @@ -1079,9 +1087,9 @@ def include_proto(file_name, new_line_first=False): createHeaderFile(level_name, headerPath) # Write level data - write_includes(Path(geoPath), [Path("geo.inc.c")]) - write_includes(Path(levelDataPath), [Path("leveldata.inc.c")]) - write_includes(Path(headerPath), [Path("header.inc.h")], before_endif=True) + write_include(Path(geoPath), Path("geo.inc.c")) + write_include(Path(levelDataPath), Path("leveldata.inc.c")) + write_include(Path(headerPath), Path("header.inc.h"), before_endif=True) old_include = to_include_descriptor(Path("levels") / level_name / "texture_include.inc.c") if fModel.texturesSavedLastExport == 0: @@ -1095,10 +1103,7 @@ def include_proto(file_name, new_line_first=False): ) # This one is for backwards compatibility purposes - write_or_delete_if_found( - Path(levelDataPath), - to_remove=[old_include], - ) + write_or_delete_if_found(Path(levelDataPath), to_remove=[old_include]) texscrollIncludeC = include_proto("texscroll.inc.c") texscrollIncludeH = include_proto("texscroll.inc.h") From b97f0aaf72124479d593d5ba1515c6b3abd29ef1 Mon Sep 17 00:00:00 2001 From: Lila Date: Wed, 2 Oct 2024 18:13:53 +0100 Subject: [PATCH 07/14] Fixed comment adjust function --- fast64_internal/sm64/sm64_texscroll.py | 2 +- fast64_internal/sm64/sm64_utility.py | 44 ++++++++++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/fast64_internal/sm64/sm64_texscroll.py b/fast64_internal/sm64/sm64_texscroll.py index 01d4d83b8..5fa1dc453 100644 --- a/fast64_internal/sm64/sm64_texscroll.py +++ b/fast64_internal/sm64/sm64_texscroll.py @@ -83,7 +83,7 @@ def writeSegmentROMTable(baseDir): Path(baseDir) / "src/game/memory.h", [ ModifyFoundDescriptor( - "extern uintptr_t sSegmentROMTable[32];", r"extern\h*uintptr_t\h*sSegmentROMTable\[.*?\]\h?;" + "extern uintptr_t sSegmentROMTable[32];", r"extern\h*uintptr_t\h*sSegmentROMTable\[.*?\]\h*?;" ) ], path_must_exist=True, diff --git a/fast64_internal/sm64/sm64_utility.py b/fast64_internal/sm64/sm64_utility.py index 97ab4e563..82e30413a 100644 --- a/fast64_internal/sm64/sm64_utility.py +++ b/fast64_internal/sm64/sm64_utility.py @@ -1,3 +1,4 @@ +import dataclasses from typing import NamedTuple, Optional from pathlib import Path from io import StringIO @@ -155,48 +156,54 @@ def __init__(self, string: str, regex: str = ""): self.regex = re.escape(string) + r"\n?" -class DescriptorMatch(NamedTuple): +@dataclasses.dataclass +class DescriptorMatch: string: str start: int end: int + def __iter__(self): + return iter((self.string, self.start, self.end)) + class CommentMatch(NamedTuple): commentless_pos: int size: int -def adjust_start_end(start: int, end: int, comment_map: list[CommentMatch]): +def adjust_start_end(starting_start: int, starting_end: int, comment_map: list[CommentMatch]): + start, end = starting_start, starting_end for commentless_pos, comment_size in comment_map: - if start >= commentless_pos: + if starting_start >= commentless_pos: start += comment_size - if end >= commentless_pos: + if starting_end >= commentless_pos or starting_start > commentless_pos: end += comment_size return start, end def find_descriptor_in_text( - value: ModifyFoundDescriptor, commentless: str, comment_map: list[CommentMatch], start=0, end=-1 + value: ModifyFoundDescriptor, commentless: str, comment_map: list[CommentMatch], start=0, end=-1, adjust=True ): matches: list[DescriptorMatch] = [] for match in re.finditer(value.regex, commentless[start:end]): - matches.append( - DescriptorMatch(match.group(0), *adjust_start_end(start + match.start(), start + match.end(), comment_map)) - ) + match_start, match_end = match.start() + start, match.end() + start + if adjust: + match_start, match_end = adjust_start_end(match_start, match_end, comment_map) + matches.append(DescriptorMatch(match.group(0), match_start, match_end)) return matches def get_comment_map(text: str): comment_map: list[CommentMatch] = [] - commentless, last_pos, pos = StringIO(), 0, 0 + commentless, last_pos, commentless_pos = StringIO(), 0, 0 for match in re.finditer(COMMENT_PATTERN, text): - pos += commentless.write(text[last_pos : match.start()]) # add text before comment + commentless_pos += commentless.write(text[last_pos : match.start()]) # add text before comment match_string = match.group(0) if match_string.startswith("/"): # actual comment - comment_map.append(CommentMatch(pos, len(match_string) - 1)) - pos += commentless.write(" ") + comment_map.append(CommentMatch(commentless_pos, len(match_string) - 1)) + commentless_pos += commentless.write(" ") else: # stuff like strings - pos += commentless.write(match_string) + commentless_pos += commentless.write(match_string) last_pos = match.end() commentless.write(text[last_pos:]) # add any remaining text after the last match @@ -218,8 +225,12 @@ def find_descriptors( else: commentless, comment_map = text, [] - header_matches = find_descriptor_in_text(header, commentless, comment_map) if header is not None else [] - footer_matches = find_descriptor_in_text(footer, commentless, comment_map) if footer is not None else [] + header_matches = ( + find_descriptor_in_text(header, commentless, comment_map, adjust=False) if header is not None else [] + ) + footer_matches = ( + find_descriptor_in_text(footer, commentless, comment_map, adjust=False) if footer is not None else [] + ) header_pos = 0 if len(header_matches) > 0: @@ -243,7 +254,7 @@ def find_descriptors( matches = find_descriptor_in_text(descriptor, commentless, comment_map, header_pos, footer_pos) if matches: found_matches.setdefault(descriptor, []).extend(matches) - return found_matches, footer_pos + return found_matches, adjust_start_end(footer_pos, footer_pos, comment_map)[0] def write_or_delete_if_found( @@ -300,6 +311,7 @@ def write_or_delete_if_found( print(f"Adding {descriptor.string} in {str(path)}") additions += f"{descriptor.string}\n" changed = True + print(text[:footer_pos]) text = text[:footer_pos] + additions + text[footer_pos:] if changed or create_new: From 5812e86a036ed81e2e0c32d98c3837d2db644455 Mon Sep 17 00:00:00 2001 From: Lila Date: Sat, 5 Oct 2024 17:59:11 +0100 Subject: [PATCH 08/14] change table properties to only have elements --- fast64_internal/sm64/animation/panels.py | 12 +- fast64_internal/sm64/animation/properties.py | 110 +++++++++---------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/fast64_internal/sm64/animation/panels.py b/fast64_internal/sm64/animation/panels.py index f7a48ba02..563e9219d 100644 --- a/fast64_internal/sm64/animation/panels.py +++ b/fast64_internal/sm64/animation/panels.py @@ -73,8 +73,13 @@ def poll(cls, context: Context): def draw(self, context): sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export get_anim_props(context).draw_props( - self.layout, sm64_props.export_type, sm64_props.combined_export.export_header_type + self.layout, + sm64_props.export_type, + combined_props.export_header_type, + get_anim_actor_name(context), + combined_props.export_bhv, ) @@ -147,10 +152,7 @@ def draw(self, context): if SM64_AddNLATracksToTable.poll(context): SM64_AddNLATracksToTable.draw_props(self.layout) sm64_props: SM64_Properties = context.scene.fast64.sm64 - combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export - get_anim_props(context).draw_table( - self.layout, sm64_props.export_type, get_anim_actor_name(context), combined_props.export_bhv - ) + get_anim_props(context).draw_table(self.layout, sm64_props.export_type, get_anim_actor_name(context)) # Importing tab diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py index 4dd9f5bb1..c96984957 100644 --- a/fast64_internal/sm64/animation/properties.py +++ b/fast64_internal/sm64/animation/properties.py @@ -1056,63 +1056,9 @@ def draw_element( prev_enums, ) - def draw_table(self, layout: UILayout, export_type: str, actor_name: str, bhv_export: bool): + def draw_table(self, layout: UILayout, export_type: str, actor_name: str): col = layout.column() - if self.is_dma: - if export_type == "Binary": - string_int_prop(col, self, "dma_address", "DMA Table Address") - string_int_prop(col, self, "dma_end_address", "DMA Table End") - elif export_type == "C": - multilineLabel( - col, - "The export will follow the vanilla DMA naming\n" - "conventions (anim_xx.inc.c, anim_xx, anim_xx_values, etc).", - icon="INFO", - ) - else: - if export_type == "C": - draw_custom_or_auto(self, col, "table_name", self.get_table_name(actor_name)) - col.prop(self, "gen_enums") - if self.gen_enums: - multilineLabel( - col.box(), - f"Enum List Name: {self.get_enum_name(actor_name)}\n" - f"End Enum: {self.get_enum_end(actor_name)}", - ) - col.separator() - col.prop(self, "export_seperately_prop") - draw_forced(col, self, "override_files_prop", not self.export_seperately) - if bhv_export: - prop_split(col, self, "beginning_animation", "Beginning Animation") - elif export_type == "Binary": - string_int_prop(col, self, "address", "Table Address") - string_int_prop(col, self, "end_address", "Table End") - - box = col.box().column() - box.prop(self, "update_behavior") - if self.update_behavior: - multilineLabel( - box, - "Will update the LOAD_ANIMATIONS and ANIMATE commands.\n" - "Does not raise an error if there is no ANIMATE command", - "INFO", - ) - SM64_SearchAnimatedBhvs.draw_props(box, self, "behaviour", "Behaviour") - if self.behaviour == "Custom": - prop_split(box, self, "behavior_address_prop", "Behavior Address") - prop_split(box, self, "beginning_animation", "Beginning Animation") - - col.prop(self, "write_data_seperately") - if self.write_data_seperately: - string_int_prop(col, self, "data_address", "Data Address") - string_int_prop(col, self, "data_end_address", "Data End") - col.prop(self, "null_delimiter") - if export_type == "Insertable Binary": - draw_custom_or_auto(self, col, "file_name", self.get_table_file_name(actor_name, export_type)) - - col.separator() - op_row = col.row() op_row.label( text="Headers " + (f"({len(self.elements)})" if self.elements else "(Empty)"), @@ -1175,7 +1121,7 @@ def draw_c_settings(self, layout: UILayout, header_type: str): decompFolderMessage(col) return - def draw_props(self, layout: UILayout, export_type: str, header_type: str): + def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor_name: str, bhv_export: bool): col = layout.column() col.prop(self, "is_dma") if export_type == "C": @@ -1183,6 +1129,58 @@ def draw_props(self, layout: UILayout, export_type: str, header_type: str): elif export_type == "Binary" and not self.is_dma: col.prop(self, "update_table") + if self.is_dma: + if export_type == "Binary": + string_int_prop(col, self, "dma_address", "DMA Table Address") + string_int_prop(col, self, "dma_end_address", "DMA Table End") + elif export_type == "C": + multilineLabel( + col, + "The export will follow the vanilla DMA naming\n" + "conventions (anim_xx.inc.c, anim_xx, anim_xx_values, etc).", + icon="INFO", + ) + else: + if export_type == "C": + draw_custom_or_auto(self, col, "table_name", self.get_table_name(actor_name)) + col.prop(self, "gen_enums") + if self.gen_enums: + multilineLabel( + col.box(), + f"Enum List Name: {self.get_enum_name(actor_name)}\n" + f"End Enum: {self.get_enum_end(actor_name)}", + ) + col.separator() + col.prop(self, "export_seperately_prop") + draw_forced(col, self, "override_files_prop", not self.export_seperately) + if bhv_export: + prop_split(col, self, "beginning_animation", "Beginning Animation") + elif export_type == "Binary": + string_int_prop(col, self, "address", "Table Address") + string_int_prop(col, self, "end_address", "Table End") + + box = col.box().column() + box.prop(self, "update_behavior") + if self.update_behavior: + multilineLabel( + box, + "Will update the LOAD_ANIMATIONS and ANIMATE commands.\n" + "Does not raise an error if there is no ANIMATE command", + "INFO", + ) + SM64_SearchAnimatedBhvs.draw_props(box, self, "behaviour", "Behaviour") + if self.behaviour == "Custom": + prop_split(box, self, "behavior_address_prop", "Behavior Address") + prop_split(box, self, "beginning_animation", "Beginning Animation") + + col.prop(self, "write_data_seperately") + if self.write_data_seperately: + string_int_prop(col, self, "data_address", "Data Address") + string_int_prop(col, self, "data_end_address", "Data End") + col.prop(self, "null_delimiter") + if export_type == "Insertable Binary": + draw_custom_or_auto(self, col, "file_name", self.get_table_file_name(actor_name, export_type)) + classes = ( SM64_AnimHeaderProperties, From c4f10d63011316ef42a7eb69c09e238eabc6b00f Mon Sep 17 00:00:00 2001 From: Lila Date: Sun, 6 Oct 2024 11:12:29 +0100 Subject: [PATCH 09/14] Update properties.py --- fast64_internal/sm64/animation/properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py index c96984957..ac4d7f416 100644 --- a/fast64_internal/sm64/animation/properties.py +++ b/fast64_internal/sm64/animation/properties.py @@ -1126,7 +1126,7 @@ def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor col.prop(self, "is_dma") if export_type == "C": self.draw_c_settings(col, header_type) - elif export_type == "Binary" and not self.is_dma: + if export_type != "Insertable Binary" and not self.is_dma: col.prop(self, "update_table") if self.is_dma: From 849efe1b394ca04a608718197eefe05b1a9b0344 Mon Sep 17 00:00:00 2001 From: Lila Date: Sun, 6 Oct 2024 11:16:38 +0100 Subject: [PATCH 10/14] Rename --- fast64_internal/sm64/animation/properties.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py index ac4d7f416..668c4fd6b 100644 --- a/fast64_internal/sm64/animation/properties.py +++ b/fast64_internal/sm64/animation/properties.py @@ -1131,8 +1131,8 @@ def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor if self.is_dma: if export_type == "Binary": - string_int_prop(col, self, "dma_address", "DMA Table Address") - string_int_prop(col, self, "dma_end_address", "DMA Table End") + string_int_prop(col, self, "dma_address", "Table Address") + string_int_prop(col, self, "dma_end_address", "Table End") elif export_type == "C": multilineLabel( col, From e509c6b7a0f50ca7c83f2fa63a7ecd59f3830bb8 Mon Sep 17 00:00:00 2001 From: Lila Date: Sun, 6 Oct 2024 12:06:14 +0100 Subject: [PATCH 11/14] bounds check --- fast64_internal/sm64/animation/exporting.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fast64_internal/sm64/animation/exporting.py b/fast64_internal/sm64/animation/exporting.py index dadae5413..f5bc6e93d 100644 --- a/fast64_internal/sm64/animation/exporting.py +++ b/fast64_internal/sm64/animation/exporting.py @@ -860,9 +860,16 @@ def export_animation_binary( data, ) table_address = get64bitAlignedAddr(int_from_str(anim_props.address)) + table_end_address = int_from_str(anim_props.end_address) if anim_props.update_table: for i, header in enumerate(animation.headers): element_address = table_address + (4 * header.table_index) + if element_address > table_end_address: + raise PluginError( + f"Animation header {i + 1} sets table index {header.table_index} which is out of bounds, " + f"table is {table_end_address - table_address} bytes long, " + "update the table start/end addresses in the armature properties" + ) binary_exporter.seek(element_address) binary_exporter.write(encodeSegmentedAddr(animation_address + (i * HEADER_SIZE), segment_data)) if anim_props.update_behavior: From 40940863fee3386a8b625b6f974dbe9029175a87 Mon Sep 17 00:00:00 2001 From: Lila Date: Sun, 6 Oct 2024 13:36:21 +0100 Subject: [PATCH 12/14] keep default action name consistent with blender --- fast64_internal/utility_anim.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fast64_internal/utility_anim.py b/fast64_internal/utility_anim.py index 0f5218c05..eae6005c3 100644 --- a/fast64_internal/utility_anim.py +++ b/fast64_internal/utility_anim.py @@ -254,8 +254,7 @@ def stashActionInArmature(obj: Object, action: Action): def create_basic_action(obj: Object, name=""): if obj.animation_data is None: obj.animation_data_create() - if name == "": - name = f"{obj.name} Action" + name = name or "Action" action = bpy.data.actions.new(name) stashActionInArmature(obj, action) obj.animation_data.action = action From aa99b67614b4d00a34f391bfcd0961c0b69cd862 Mon Sep 17 00:00:00 2001 From: Lila Date: Fri, 11 Oct 2024 15:21:58 +0100 Subject: [PATCH 13/14] as_posix fix and clean up --- fast64_internal/sm64/sm64_collision.py | 10 +++---- fast64_internal/sm64/sm64_f3d_writer.py | 8 +++--- fast64_internal/sm64/sm64_geolayout_writer.py | 8 +++--- fast64_internal/sm64/sm64_level_writer.py | 13 ++++----- fast64_internal/sm64/sm64_texscroll.py | 2 +- fast64_internal/sm64/sm64_utility.py | 27 ++++++++++++------- fast64_internal/utility.py | 6 ++++- 7 files changed, 41 insertions(+), 33 deletions(-) diff --git a/fast64_internal/sm64/sm64_collision.py b/fast64_internal/sm64/sm64_collision.py index 5f342bd27..f164088c9 100644 --- a/fast64_internal/sm64/sm64_collision.py +++ b/fast64_internal/sm64/sm64_collision.py @@ -338,16 +338,14 @@ def exportCollisionC( ) if not writeRoomsFile: # TODO: Could be done better if headerType == "Actor": - group_path_c = Path(dirPath) / f"{groupName}.c" - write_or_delete_if_found(group_path_c, to_remove=[to_include_descriptor(Path(name) / "rooms.inc.c")]) + group_path_c = Path(dirPath, f"{groupName}.c") + write_or_delete_if_found(group_path_c, to_remove=[to_include_descriptor(Path(name, "rooms.inc.c"))]) elif headerType == "Level": - group_path_c = Path(dirPath) / "leveldata.c" + group_path_c = Path(dirPath, "leveldata.c") write_or_delete_if_found( group_path_c, to_remove=[ - to_include_descriptor( - Path(name) / "rooms.inc.c", Path("levels") / levelName / name / "rooms.inc.c" - ), + to_include_descriptor(Path(name, "rooms.inc.c"), Path("levels", levelName, name, "rooms.inc.c")), ], ) diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index e93b2231f..7eddc5afb 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -441,8 +441,8 @@ def sm64ExportF3DtoC( if DLFormat != DLFormat.Static: # Change this write_material_headers( Path(basePath), - Path("actors") / toAlnum(name) / "material.inc.c", - Path("actors") / toAlnum(name) / "material.inc.h", + Path("actors", toAlnum(name), "material.inc.c"), + Path("actors", toAlnum(name), "material.inc.h"), ) texscrollIncludeC = '#include "actors/' + name + '/texscroll.inc.c"' @@ -454,8 +454,8 @@ def sm64ExportF3DtoC( if DLFormat != DLFormat.Static: # Change this write_material_headers( basePath, - Path("levels") / levelName / toAlnum(name) / "material.inc.c", - Path("levels") / levelName / toAlnum(name) / "material.inc.h", + Path("actors", levelName, toAlnum(name), "material.inc.c"), + Path("actors", levelName, toAlnum(name), "material.inc.h"), ) texscrollIncludeC = '#include "levels/' + levelName + "/" + name + '/texscroll.inc.c"' diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index 3c8038612..8195c209b 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -658,12 +658,12 @@ def saveGeolayoutC( geoData = geolayoutGraph.to_c() if headerType == "Actor": - matCInclude = Path("actors") / dirName / "material.inc.c" - matHInclude = Path("actors") / dirName / "material.inc.h" + matCInclude = Path("actors", dirName, "material.inc.c") + matHInclude = Path("actors", dirName, "material.inc.h") headerInclude = '#include "actors/' + dirName + '/geo_header.h"' else: - matCInclude = Path("levels") / levelName / dirName / "material.inc.c" - matHInclude = Path("levels") / levelName / dirName / "material.inc.h" + matCInclude = Path("levels", levelName, dirName, "material.inc.c") + matHInclude = Path("levels", levelName, dirName, "material.inc.h") headerInclude = '#include "levels/' + levelName + "/" + dirName + '/geo_header.h"' modifyTexScrollFiles(exportDir, geoDirPath, scrollData) diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index e890ba8f3..2f6154367 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -944,7 +944,7 @@ def include_proto(file_name, new_line_first=False): def write_include(path: Path, include: Path, before_endif=False): return write_or_delete_if_found( path, - [to_include_descriptor(include, Path("levels") / level_name / include)], + [to_include_descriptor(include, Path("levels", level_name, include))], path_must_exist=True, footer=END_IF_FOOTER if before_endif else None, ) @@ -1015,8 +1015,8 @@ def write_include(path: Path, include: Path, before_endif=False): # Write material headers write_material_headers( Path(exportDir), - Path("levels") / level_name / "material.inc.c", - Path("levels") / level_name / "material.inc.c", + Path("levels", level_name, "material.inc.c"), + Path("levels", level_name, "material.inc.h"), ) # Export camera triggers @@ -1091,16 +1091,13 @@ def write_include(path: Path, include: Path, before_endif=False): write_include(Path(levelDataPath), Path("leveldata.inc.c")) write_include(Path(headerPath), Path("header.inc.h"), before_endif=True) - old_include = to_include_descriptor(Path("levels") / level_name / "texture_include.inc.c") + old_include = to_include_descriptor(Path("levels", level_name, "texture_include.inc.c")) if fModel.texturesSavedLastExport == 0: textureIncludePath = os.path.join(level_dir, "texture_include.inc.c") if os.path.exists(textureIncludePath): os.remove(textureIncludePath) # This one is for backwards compatibility purposes - write_or_delete_if_found( - Path(level_dir) / "texture.inc.c", - to_remove=[old_include], - ) + write_or_delete_if_found(Path(level_dir, "texture.inc.c"), to_remove=[old_include]) # This one is for backwards compatibility purposes write_or_delete_if_found(Path(levelDataPath), to_remove=[old_include]) diff --git a/fast64_internal/sm64/sm64_texscroll.py b/fast64_internal/sm64/sm64_texscroll.py index 5fa1dc453..c39de7fd0 100644 --- a/fast64_internal/sm64/sm64_texscroll.py +++ b/fast64_internal/sm64/sm64_texscroll.py @@ -80,7 +80,7 @@ def writeSegmentROMTable(baseDir): # Add extern definition of segment table write_or_delete_if_found( - Path(baseDir) / "src/game/memory.h", + Path(baseDir, "src", "game", "memory.h"), [ ModifyFoundDescriptor( "extern uintptr_t sSegmentROMTable[32];", r"extern\h*uintptr_t\h*sSegmentROMTable\[.*?\]\h*?;" diff --git a/fast64_internal/sm64/sm64_utility.py b/fast64_internal/sm64/sm64_utility.py index 82e30413a..97a9287a4 100644 --- a/fast64_internal/sm64/sm64_utility.py +++ b/fast64_internal/sm64/sm64_utility.py @@ -10,7 +10,15 @@ import bpy from bpy.types import UILayout -from ..utility import PluginError, filepath_checks, run_and_draw_errors, multilineLabel, prop_split, COMMENT_PATTERN +from ..utility import ( + filepath_checks, + run_and_draw_errors, + multilineLabel, + prop_split, + as_posix, + PluginError, + COMMENT_PATTERN, +) from .sm64_function_map import func_map @@ -247,7 +255,7 @@ def find_descriptors( else: if footer is not None and error_if_no_footer: raise PluginError(f"Footer {footer.string} does not exist.") - footer_pos = len(text) + footer_pos = len(commentless) found_matches: dict[ModifyFoundDescriptor, list[DescriptorMatch]] = {} for descriptor in descriptors: @@ -305,13 +313,14 @@ def write_or_delete_if_found( footer_pos -= diff additions = "" + if text[footer_pos - 1] not in {"\n", "\r"}: # add new line if not there + additions += "\n" for descriptor in to_add: if descriptor in found_matches: continue print(f"Adding {descriptor.string} in {str(path)}") additions += f"{descriptor.string}\n" changed = True - print(text[:footer_pos]) text = text[:footer_pos] + additions + text[footer_pos:] if changed or create_new: @@ -322,10 +331,10 @@ def write_or_delete_if_found( def to_include_descriptor(include: Path, *alternatives: Path): base_regex = r'\n?#\h*?include\h*?"{0}"' - regex = base_regex.format(include.as_posix()) + regex = base_regex.format(as_posix(include)) for alternative in alternatives: - regex += f"|{base_regex.format(alternative.as_posix())}" - return ModifyFoundDescriptor(f'#include "{include.as_posix()}"', regex) + regex += f"|{base_regex.format(as_posix(alternative))}" + return ModifyFoundDescriptor(f'#include "{as_posix(include)}"', regex) END_IF_FOOTER = ModifyFoundDescriptor("#endif", r"#\h*?endif") @@ -377,13 +386,13 @@ def write_includes_with_alternate(path: Path, includes: Optional[list[Path]], be if header_type == "Level": path_and_alternates = [ [ - Path(dir_name) / include, - Path("levels") / level_name / (dir_name) / include, # backwards compatability + Path(dir_name, include), + Path("levels", level_name, dir_name, include), # backwards compatability ] for include in includes ] else: - path_and_alternates = [[Path(dir_name) / include] for include in includes] + path_and_alternates = [[Path(dir_name, include)] for include in includes] return write_or_delete_if_found( path, [to_include_descriptor(*paths) for paths in path_and_alternates], diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 0cda4e229..a9df448a7 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -1,4 +1,4 @@ -import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator +import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator, pathlib from math import pi, ceil, degrees, radians, copysign from mathutils import * @@ -1880,3 +1880,7 @@ def create_or_get_world(scene: Scene) -> World: WORLD_WARNING_COUNT = 0 print(f'No world in this file, creating world named "Fast64".') return bpy.data.worlds.new("Fast64") + + +def as_posix(path: pathlib.Path) -> str: + return path.as_posix().replace("\\", "/") # Windows path sometimes still has backslashes? From 55c92735f5f306602b2c696f3e02291310421950 Mon Sep 17 00:00:00 2001 From: Lila Date: Mon, 14 Oct 2024 17:37:48 +0100 Subject: [PATCH 14/14] designated is include in repo settings, include in tooltip --- fast64_internal/sm64/settings/properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 10deff97d..dd153d53c 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -92,7 +92,7 @@ class SM64_Properties(PropertyGroup): # could be used for other properties outside animation designated_prop: BoolProperty( name="Designated Initialization for Animation Tables", - description="Extremely recommended but must be off when compiling with IDO", + description="Extremely recommended but must be off when compiling with IDO. Included in Repo Setting file", ) animation: PointerProperty(type=SM64_AnimProperties)