From 8dd43138c123da34a3c29ddbe27e5899d062c809 Mon Sep 17 00:00:00 2001 From: Lila Date: Mon, 30 Sep 2024 22:03:26 +0100 Subject: [PATCH] [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 zcmeI*b+}bkzcB2z0Vyd3q&oyuLP=>U2?Z6A?vzHt1_28bTfr_YQdCeBL`4))!UCnc zyW_ooo4q}p?>*YWaRB zkr-X^TsSGAwohcWG@rb&CMj8x&on8^X;VH^x1~$86UBCk&U{Laq;`pZd`*Eg?GnTJ zDb2ZX;+WEDQ{9pt(!{x00%ise%l*zFW-<`Wcu?pInpE&*E5Wv)MdH#C1_>e z(T<;WMtG+aiEDW8R3cG&`oDqnvZx z#tc@mmRWRWx9z?8&2f$RQdliX%U0*eBHd=pH+-8VZ5#jcfmbZm<_DCY}tcZ?1kVtG+!~fiyNZiTW z6!!gZR_#wDvL94l_B(Spk(hE+o;$XxIHVVcW#TYF9LkBqJI--PJhF(xCWblp``k|* za&p4@9sJENe8sy=pdXE>LkZT2_m9-He7$35%8+Zc%#|0aH(F3Pl`%v1M4}9TTE3Jb z)-SQWmi_Lw|BH@&O`Kkh_=)F2@s08vF8-zEWhrSHEpHb~>$TEaOkU)Z-V)M#we+@^ zN8_aVWBcs1Z&}CnlYj3EW4o}52=hAmI#~D*O8<+_^|m-qbl&RX{4LeQ;}Pbuhi?ERq9fI|O3P0BO5cORuP(o8$iwdPFI^UK5Vuz1@uzdXBHWU~ zY$41V_8%f&Rtmcfd*sy%Tt^m`%fD9`OhXEDMjSS=9LY-T6z03sX14U?riV1WB~5Fc zD<2)DW2(F^$W-SZ#vneRn00gMZ=cKTdw|a!dxtPe2hkbu^ zOfGH~#-qYoBHRMpshl@UXGc~^YX??J?+Ixw%V}vm%u-%w7>y{vQE6Sx5+WbY^04)@ z99Km+ql9^kG*Op)?zirguyfGeK82{ydo;B!r|qXXW8W;|R#ZH%6yI9nU0oWANy~oT zmbU8B`3}2Sq{FzCKFV;FI<;0gKC3R}llPC!qeuy`I5esOWxdpwbt#9PvKiTK$)@r8Xp5a;Mm=S8J)T=Z|*_|^A5%&>i& z{n7|4>ceXBpSVYTP}kqvo=BYJXUiAv(qC%>jrSVlmB|8SGFm$8NM}Mizjo|+@o3=O zr{v}PRyP&)a_6X{Jw71buWO?Z%Y$mt@dhikmGC?3*iTk@QPFEL@--YCBVQFG1pD~y-)(>WJL3vJf`yR-roh##w z%KkR~Vxn-Kw{EU&-{(&xCP$d|FVAtyf+QCDK9Zt*ZF?7v+V>Ii@`7X2bB%B!yf1`%5ksBh_c$Nz zo&Sh9-6np`#dm~!UvNg>!(?@=8i!8VpPe-Iy|!&L?OR?r?>Of!c~MpVWTce5IVo;O zoOg=t3xyr?tk)c~$FYymmG%td4K`5T_WOvrOj-L!JU$`jK%$vE0qrauC#7SXG}IS% zDRGZELMHn(6So(SE6=0KT6;;aeZ9Zm+-9G=p_krbY->uTl=+_zLk!tAudljev|U7CakB$HG}v}lBV|J zbiMO5&{y;qxAI)ez04z>^-USedwk3bG>-4K_Ylv1N7Ntb$tgXzOT&`AiNr&D)WO}- zxJ#YdX>LG_kCT0$V*SPTSs=|*r1wk59uY+r_K2I5rU1O7d@$xVMys-qKP) z{(hHF{UbLm)cL-6U%w6t76+@_Q`JF zsgCKOk4-C_$AtHra6iqjA1t7s_c_k@gDGw77yEWpmp&K98F_q__{1D*hP=(IoTsWA z?%@&o}Sr?_A`+Rhs)6?|RGM9{SC$@~@M+)lNUy#&~^GB5^}2?f*LS z;O4?>s$XfQPw+ka4f@p^&9m8SpJFZbUDhA7y@dUiH&G@mWQt>V+plVS{cmS!=wjaF zTzje5EsGI7WGtEi=4A0e>?ADRHq9z`R$xf%ab>pf3EylCO&_Q<8pcS zPA_%1k2)_O5B8Hbc{)JeMn3;MT-^AK$qc71r*G3g(1;&~$wTfQYTOy3UJSPXK=n*G zeS6wRx(7M$bS8Dw?tK5=`9E>~cb$KT^KVmro5i7lINZokUS%~U#p!l_r;ac`+crsj-gexR z`{nfk{jtvn_sK`$wRQfg;&g}b?iSzg>|24igk6lcr0)tom+qEqcfQD{3j8JC9;XTE z`B}c!NuxfIFRl4iI@g;Q7=zL)?+o%VlRhT1_Hm&;D~m9)8vps(@{QK@&*(;uV{S|m zpR?*%LY@gLvvv7|b0xzq-!JUR*5ASiW|P6X-uy&O`%I;@{a>Yk^FK!pafs{ZxK?cI zTpzND9mKh>v;HporDd4o)(E3MF9>tHbJXG$aVy9-;`SzoEys0w6UVQSw&<(#@Qbt- zx30FZ+SnfPt!rOZGT}6d-=yy=77)vE->1Cw{h7{j>f1lADZk?c#e`9vkM#r7d4mV( zObJ#FRu|bhNWL+8p!x6sVG1WBQ-S2G<)>9gLC{uD?| z$=t&7(dqz$ZF~71$K9=8vhR;~O2ZxcxZBN-M(IOG8ncA+g?_7@_P0TtT8Ymj@t7_S z&HKskUfOf_MB=5c;@CyJI-4(Q<7Ydl>)dDzn4vGN&X%_32z*6J-_P={?RD+_S<+{U1s;&8Ei&7mB! zDKmAy-i69Vefds(8OuP+aUUVBRbI`0Za&|^G5x4X4mMq+-7}Dad@j5j*ku3FR3Pr> zMI{)97829rFSA0hl)kl}mR!W%@mR7&H+4md8y_fxGiO-|zZ_H=MYZHTg zZzj&eoU2(bVN2JK(llRr2g{vVV*4f3wtqdx78lNDaegVSeoLFluKjF2du|Rt z@3i`U%JtVt=iq}A>OSvtt?y^~)AnEN_vabc29Apx!r6qAQF#0KQrOYA)!+lm85m%F zTuaZh??T1D+;MRod^1Ocb(C?ItJ&7mz9Yo5sWd#U9>le473KOVZ&QHE^8X_1*Vz8E zx=@IB<$YP+a$J5MllHUHJ&fqbZYBD+N2tVh`7oR4|E39ZG^d4oHI0N_PQ5rL+g)`|02`5X5De{}w7 z)-7`GSDZWQ;Bsn<`%|nSqqtNeu7$SSHrl>hgwaEpWRdUNl<)Rq>dH~q27FK43#)Tn zJZwA0CCWYSe`Qp5pDCyN9ot>mHWp4<>5sgg@BH1J?=R;mB@d&VlEi&1|NNKkKYuQf z*41QMcdFC6T2JfgKw39G(k60|hx`;us9GtC{7eEgk7%w;-L zc!SA2$CFH8Joj=t!x%^(y3?7spWcSn+?XPkQ?<3+g?(j2&XqaQ|A>pV{gJGq zmOdcvF-5=KR5)?2XLyHCS`r@^Qic*AXn3tA>51V zoIF2_b=6T_lb>7V^I3zIbYTFdT8Ys-$YVUkbG*o8Ugj0H?lpGpGdJT~;$G9G2eg5M z)*o_hqHV=}>K@v~Bzg3^yvZpqTF8TYq&@1w;)t_~dYyJVRfQ|1Kc1yOY_C5osIr~V zA0E{o=E|Y$we#8L1$FYch7|F*@A-2P$7|o6ORIBb#pw#=ccpoCHGO+6{c%10LIZO} zDp~$rVl!T4+`C+zETayp*i$PxPZfQTj45B!xoW$vtYckWiA0_4L^EnomV#s_Eyv~8KDH6} z4gcm3eq|X;S;Ar#@eN<|6)`9If_S!)DxR6f_U~EFTH+qrPSy}%%;7WU5@9SL!d*y& z8)1FIOg`d0-r_Z)e!sx8e61h;go(uXpP9Hms!Kd~t53`$;#uMo%Kub4ZI|_F)pd5N z8!=W^WRDE}hIfejR0C;EZOU*78Bo;{d)UlcR`X|yzkIG|JF04OA;lc77*%LaFGlk; zFY`QO8AYsrgcpf;%;Zz%63;|pj+QFE$ND*Z!YrnfC&qOasMj4>VQz8-x9WH&s_$=R zIX4fBd)edUS6_M6Tt3CU=S$>KdinFK{OBgE&()`R#*|yTs;)J=?y6~j`z3Ir zHhD_htjkCH%&W*w1}gD5#cYdf_kH%gQrn&7*pb?8+_PPx&Gt|S<9_~o&YN94e&D!x zY_%@>p$*KFm8*R}E?&PWw=j}pzW3)>+p6<|{f^qd4W9_N5EI4!PW$xZYWCUhNec72 z{R{B4{o|f@^v}^AZey1;cjaes>`5t(%Y$E;&9mH2CmM1Ec}PP%)B1}aiDy&q6VJFF zV+7u#Jw8v8 z)%p_DB%XU_Q1(Za{SSzG1d-Z05MjE7<*%kjBPPS#<&>c;6ucC{Q}W%e?*MyF}^S750>%>(Pw?EY`Sn* zx;qfhr!OMPWgZ{$Fs-OU4Vu%3SU-(FNI0f6F^=})PNtVs9^6wxTPQBCin{J7Bpn6B zFR!|k+n8^xD5*WIGJZTQ&pODXs`90xyl5>xQ{{DY^*f&V-mZ-lRqr#Z|LL@aT*kGU zI*;3TyDr~lnQwM#gOniy7f_PkEMvHRI@I2V@?pyn@aM#eLr=~aP4=GxV^&|hA^1Bc#@C!g<}-8Z|C{i!fo=6H0&O# z?1m_#LGo*WxZf(gKIR*})SK?Y?V^w9s9v=*Znlxft%ZAov8<)?zD{4#LO9J`?-?xxB3#li{ z=au~Om!?!I?D|YOwl1T5OY4vA`)M)x%}hpEm#v)pkiz=G@<{vEQ|5UoMQ1+aGV4F0 ziu28-l6{|I1(7dV?>aZ%pKz!C@D6dK-0jM7l(A`qx-{IlI?TTEZ1G@aCC{FfXKDH? zzkbHwzS?nbeTclP-cujk!#U((6?|ojry%t#y9yn zR6c$x|B5vd$7{3`dAG2RzCixXtmzr_)$*l=d4YT^&5Wx0VV_@Ef0KREk-wJl+V@KK z8*1CTRisCr-*crpUP-(t$Bn#98tZT8FhlIOj#`zaiSzG4%~~T|(yi5pvYGMgj2G+m zT^qD5PHdE4n_UBJG5^|XZoW-B-L6jT(C*d$HM`UW^?!!?e}ncjhuqr1L~hwHKMyFs zL(1`}JU(eWI;$O|Ggrvyyjj)li;T&+wbT5?rC{(Mp=uus3s~VsSjzyFRighIc( z9T%E6WYM2X-vfNh#uSTu#=V7x@!dYdiMj6GoN{d3Ux;Ve&j_nDUkdjY_BzKHaxv98 zV?OZK1@b{0E+*zmw}{(!;yFWn-{%I)Qy54kPFfe&7%viie|_>0^UEK2oBN3VJJMa8 zj3i(4_A$5P`o7v}KY4JgzP!IQ4m{V_3>>2V53~Pp_X|g8U!&}=y$!faUww~o@6{iT zHinEbzTKyPCFfXem)caP0*&cFCz?^3L-w7^-PGekHVWqlzTq$OTGxtOxt}k%+_7hP zY`i|@e)YuX4(n&x|9z(N2@6?Hgn5K4_9;Ov+AxgA`GD`)$ocIk#-Db?I~#d8pe`@w zW%_d!hw?ceoyox4d8M7%dE_%AFEK7?2kmm%?_%Sk%C;)I_M6T8RUJ93PE1k{W-Ifa zY1CEaw%~+0*b#l=0sYi&^=(x`6{jWo0{i7$?B3KO{r`8a&AyfY-#B)maqDY!c)_`TcHCFS z{V%0)o@*gyeql^Gs(!MHa>vwpvL09Wi1(yoJ}~;Eeo!0xi-*;-tEA~q;yH~hNS^;) zoTTm&^WAu#DCXoJvxF_gvy#slMg?R?LJ|}2@Fa>np$a6w13B5}r~FNX+n0mHGoP5Z z$Mto*uUbvKt|8vp>*D)w#2DS1*I7j-@hnait|6}9TBf+pXHyyw?~YWZ5^;}cT{(4v zsb%${W%SdR>O)Ga+a-;g#l^p{(| zFPO$#OkyHqxq~|y&kKA^yc3n=_=@yo62FpNxQ*#Y)V=SlyMu%3-$3=Rlm4j=4|9;N z)_qQH{n#*mp{#x5I%WzRDd1c;@&I$#K>=YlWdxJ?=f9l)`8~6X6Bno_c_>RwuA>V> z8OO89*WrrR=exd7VJ=I~TffHlwd^NtQj)i!lUz+ECGxoanwN6a^8I>R(#rQ6=)s5x z`hUD<7WH?P`t%6p`BC}DoIC14dN!#;aorx*(s2zPeQ#VtPhmQ-KJGI;%Prc>Anj$Y z`k2r92&arNClTZ6jn4HtJGoMfh7<1z#Wh+kbvy3YtmRjB(|n(D+iySpM!a7f?eryK z&lT2c<64@G`dwvPTlrMeADveJda8@9gg=ORTqf)(lokFP{KW~fJH8MlQWW!9m`jNF zv3J-f?hC~IfSS(pdV1ldQ{Su?o#c0Q;+pbj;u+>Pj&Xvx&o!O9iLouNr8BWiJa1w| zcIQ^_DvSGj!tWvM`ofKS8jIv%PU#$~gP0~ySIEP=^rgS5%XiwRIlo9xDSC^?RN?1l zs{Nkg3F2PeE5z9PDGP~fpWpb4xMurEn!d_l+;i>?($!L$Hwbg7FuQXFnTdDAHWKgf zt|acOMtY;p#C6Ui6rxuNZLPR;7E=$3$g@JqE5AHfCXXwdd!&DaG{^kmMdw~7O_iMU zSLf(F-~ai~s)VdbzCU=m&pJd|b|cDj2)ELin~3@pdGG{3pwcJ*7uW7_T^aF=dB$ga zz)Osy7Y!&uM&jD@Z+<2E^*zM3%5PjWOr8x?hx-bnhd!aJIbj#`Gt2u#Ciie@xmz0; zr(EvW2aZ+dcW5^Q^wIWBOp8@$Xbyh*H!@MB&T_dnuZM%0tvh4-m+#rq+J%zu`TR1V@>LmIY?)<;Og zo=1fHwEUbbj5m#+Q`D_@^#iY&b3CJLrS;yS|9x%|bM&R;TdPixZ=L)i+j?UXpIcrZ z>se>N89uXbaIa^JdFL*5>99OLgZ5qglsIb(kDj%!GL3ooX!SIn?afhl;<-sFPFs#^ z=lG36U4FDr2A}1Lx#05@>F4^bzr0k2@2Wthq`irBzbV}}jgmh+%2wYeS%0hTne6w8 zV?K8L|8xFSO8>G#z5bBK{F2Wn%kru(G_kyptz6HG>i@0kf4mnT&lG=3oSSoOIx23* zwSDPrB;7@%GoIJB)d<>4#|jRxj`)2;ynkB6@qL*~+-r$(`qy&eT}Im|;eJy=V|Z@y z*3PnL)`!?{q~qS>Ma!!l6Z7`9Yw2@yWG|>b9*ZMY|?cBoTv`ySqO-htWmy{?=xpYkumobbv zz9%m5>n74%&?J$TW=xE3CpHOh4ho_}`QsoFw|nMYcT_iQ^oO*9nw$k zR<2uJ&uo%E80;e4T(epEPLz#J%|o z#XCPSS1ZY-Tuud|zLuc~k-pRR-^};C%UIfR87JjiyifK7k-xcFAs?TjExOUx4Ja@f*eaUgw!6oT&dls{?Tl zydm+dA_LJrk8z3%e2;rWrKw3gmmbWc#2oPlc92C_aShOt{ya#eDbiiS`EExONIc{G zZNxFkAbv{|_jdjkw=cvoelr;Nk2liM82vtv=TLvm|2i0N8f#Y{=Tj#Nxkf9hJO#Xi zH#kxJ-2L(kiz&k*>SJN^KKor&P`MP4PRGUl^VY`Hv&Oj>a;Xc#oF?q}&CCMr^_HyC zYJ3}gp*qFAmS3{&Tid>5zvVU7y=mL^#=@=Q8NV+)B>sb>W0Q2XmA;>(ImYFM+SPT+ zYPYiaRC%2?Zb!cn<4b+%U*q`R!fazay-FP7H#?^&Ax>9Po_I&?v~_X)`Zn>~p30nX z%v$G&dnEDvDjS=Hdx9v-$$ZN<#M~&JfAl5h_%WZ4zAh^riT}p)5#3pAUCblnm|enq znR^(`13byA%wPd=jkkh$*1DL~>%BKD$FtH&#Bw}KjeBtM95$D*8xzlK7LeWfZbeom z&+8}2#|rZEUZUT)kLU*)lbcnxzrYP-;48=7#Wg7A#2R5$)t#wgz{%H&05yjo=99>;_Fv!?r`ZrCW&V$7TEnC_@z$=9cSg_D=Ko{qK| z_g50c^OXJ^SFb+iXIe!1Y&(H!k^Js!^jqIaPm=hT)2=G3qq(JRAyu@gX2O2V{&Da3 z1NE`0^o^Gvd-mvCq;H9_CJ!I*ddip+X}-nw_^nE-{o0-V@0NcD<>^p)kV`m!$%mQ3 z94G9C;u7hfFCH(5$1CCx7lrY@+?DEX1NHi=i-n;LcA=5w_-)R$GGUFpZzP^iihGP< zalIPP=IRLha_6{BnU!?Tmg0Q@gME+d*O~UuBJ6mV?o?iVPJa1QKzk%tL1QqNlbLv4 z^B`C7qy2i4md^9FKj%wT%yLoUS#3e`lb7iK&;D@hd;`1K;o^pD~jUd571T#B)sKQ63`N=@{-}6vG%uAEFL*qzyOFoJQ2;YAREK zvXrCyyW+NLD6wCi8hNv>bhYHlppuJT3Z+ONhE#fS6kq zqZDPioJv&TYGNMOkf@I_ZnP%mX3-~gp$BnoJAk2#}?I_)jePa!U&22E&9 zSNb!Ydl=8dJi?Od}w%^h{&Fi#t72)fq`i&;)Kx6sXP@j8^HeTQT&${xVwl-Bu`PGzHHPpxI+IKZ` zYnoclZ~go_(%C>h3S&nL^~E^~I`8TZ+E7<{)I(XTgcJH{YyHo)qnK}pD7O*Z#t;T_ zE4}DOl=m&PAOGCmEPqMd zLs`uE_2Vz!Q|rez%TYh}a)784Q4gXHL|usQaZe`dPwGBVf1=(*ojFeIdy3Th5_M*( zekx2Oo?*@66XJb?uZZzF=2X8B?;XUwr44LlC#m;!qQ5^w8ev{Qw9V|~B0ohb&E;H4 zb?T7X-db6X{EocuPK-I{=kHkGA0qPfStb*CIfZG==5rSC9ZQIIwu+eZrM9u%mLs2{ zeMR1+=HdDI@>n->8+Oo1Cw7*f)u*LBO*NR~m;#w+Zl zBtO|_ggW@H{ku?=Qk0-9SEPLA^tn-eok&i)JI6@pYTR3&;M|kMp`N&GRKM@*qEGAW zygJyr>inX1_G@eYrQH?M?grdsf1U1A+T3yNL zz}u8Dmkrn6d4c#%+Y(l$IR75fLY?x(R8Ol9(If8?f#&&IEo4nqtKj6tN>IgL%O*`wh*uKub8Fp*q!r3dl z!_G6&`7Su9Jb0V<{d+p`D8dz#rx>yBH`^vsk@p<8!SQ2-vs9Slg?+8_e7DKEjq-4V z_3O=@cxjz9@%P$u_x;}SIfo4tu)ZBn(kH#Lra6@M`2cH_r11@$Oh9;SC~wFTIxdjdm@% zaW{|i3=p0oM^^QxES-%5G)iM(IU{g$7Qf1BjvZMKzD{wI$r zQ~SPmQasg*=o5w}^n*$Io1UJ>^ipor>Z8r7=WT9PrrJnV_3{WEw3|tM!&>6EO?%kH zGU8pER~bwEUa%SQE=eBJuvZvsSjHm0VqS{*DeHdo8T*`}oMYNBnz#9m^&BDHU~>@C z5N&!1PjfZX9Q)P)ExB{A(!h-cEb2JmYy$ysLP1W_iYH%kkThhP=oZ)?JxZ8RXE9 z<#N8f>OcW)prE;Q`UXQ zGxVVz#mGWhGE#)=m_$CuY~y+1H_I;na;jhAGh00B6>@B0*8nOV07D`kIGf>g%2hv|$=hh5}^efOhdYU05U^Po*)xp_6?5N!^^p zbX4Qy`-R)Bf76xHR3NlX}7)(VDsXNoTiTDl9 zLe6lX?^SG1vj0*Zc6@PRJyx6|TQTz^idz2B zws;QKSX~;U4cx1pSxkPvwukX5bYhig2sE5D_Motf#>;xxMv;DGd6ON3sizU#J%;3R3@H7$35ko?6*(chwjW* z@^z#5#eu6U zqwzOGdhjk=h~KI-rW->Tp7PnpXIo<1l@uZa2OYDX75qTl!yQfq;@zBhSEeof_?6GB zzg%B&r2_7&54kz7x}@wwAN@)VeMx40;4k9#syG)F|BIxfJe4fRyZG_9Z}v$;jLWSr zbbls`wnRpCXCR9yB8;I#znji^8x!vk%_i=pM*rS|5sW9UQ%f?zwob~Wjru?CkiJhw zKBMmEk#83{$4qg0U!A;SuQqI3XWQa^fVe+2PW6 zC*E^x%Qud%!c67XfHmUsCqMHoas3t7VsG*sPZRydyY`*oxE8`{DctxS{c++q^`nS! zyMw%par_WvO@29YZn%F zapP|(=PsjOl+(vjg$`xSJ*=BrTK{Kzr4s5vG5tmn`B=!Y!aggEVsxH=?*3j$c@*~+ zQ^ohV-xv4!auI*$Nt6@&SjTd{;S=8HRi5TS?qU#KxrwG+Lrtns0Z~kT79tsu#N_@= z)laBG|LvZe( zlT7AaV$7LG)Sq97dKTC5+lYG{as3|G?lA_%SQGcYW^wFheO7C2P@)&zs6O4GyqLi} z-`}<^*DdyKr_4JlhtAqh7iHL0{&iCayBkA$sQbNy-PiTT0QF{w_zqLgxR%31&F3k_ zbla*iXRz|;Q<+|cSp($>pEFE&ZERNp8tM8-0O+4^dk9tC9SYq@;%iTeZI%X z%waZDcnr}?erGH0Z?vJaN)gZfFFT=sFqXyh&RLYVT*P?!zU}c`^t3iNO?&IC(#N&m z-IiNv?>o|-d#+JSLCc!t{yOdfEhOIA+C;p&lfk*-8n6u2Xv|G?rWbwanesi#xeGBy zM7e&&aiT6Q_BntErvx3DN|a^P%}zx9eUvA;A6b}u|M*g$y3<75qj`eonZhEDQd9Z8 zKn|JoE{%GsyUOy~-mY(y=W{Z+vGRIkq_V!peZTSQ)5Ge8likE`mNPscy!*u0_8uZ~ z{0`;9a29d#ozg-4UU}#3>OZ|k8AFA+eS|XM;BbAzde@BerO{{HD~W6I%<{Y;BY2Qr z#C3k<*rt3QQBDh`?|J#XNZD;Ys+}J&7VpxYwrC$~_5Xh=w_oMa3h83ON@Kh45Bw&a z-?b55|3kUhugEI>Chx2^mi?vQ{9C`xqifU`VSPsVb@G_FXK@P^Nw7kkVr+=#>^~FF zZsOiS{B5)o8MVnw>QQF()MroY#;Qk0#e4h-{f>C&C*DDczd6vr822IZ-rQ;ZYUZgQgmG4#D zW!ndi`9b-vODWq69bf4-^}?|Wgj0+4!uo{s%RR2)2a<(nPN?&2Kdz5qnB_6n$1}6& z+cHVx6ViRT{CGL?Cx>|DRMxqTlX=wDeCNiDTZ|o93&~?X;2`A-OBc&W-2f+mugSl?+W% zH>Ba6GvZHKRj80{@g7^eqtTUt3{Ls%mEg!Ej@2+KvR|6BgV-(ehlv1cvnSx@z=6k9Df&805qH~tqhu9r4GmobMeuFtoB z`t0&cexJ2({4I*#iEG{3);+;q+6&`nTG;nz+9;cK#Jk&jh~L2d^I!h|{Ms^~Ys-9z z!c?Ob{TRz6KIW(7zEd{)-15(IzQoplmTmjzIy0ZeBu@vE5(OxjlrK?;s>J)$ed))o zzV~N5@s|JpeQl{|lgHj6$~LYW9;72(=*>{ZFp((xFNyau50b+%<%v0L^!F2ZkGNjg zNQ~R6b@JbGj`v5dqACr!p6iJBvE#a}39&w|-D-0+)rtKpr>K(hJ=WJGuI=KOYgxQc zpLf7D#u4ez<{CL~Jhwk1)$we+6$`9Wq{;8o#@w*uY3Dwr-?RT&?RAN8o;|An5yoYQ ztvjfW3UlFp+sM@I! z>MXUmmPOj(kF4Y`)}Cjb;64<1?r7YQi17#~-9so-xmfzi$xlW^@+6 zZp1Z0FXH>n+{i7&J3X;Iw#B{8ekq@^e_U^Nq7B#6h^whU3G$GY@ANs}<`ss15KOXwlG`2@T&DM>wn(Uk1w#_I+Z@X<0|QoIv0>P`mWFQVOte;wxZ5a%=wDz zvr4FECG9ic^@VxMpj(C2N8F8ZbGqqwy1Iw#^Idv%SGRg-3$|73W&G-`joSC#zOKjm zDPPAP6h?u8=Hr8;Z?J7c^lwAWrG_cD+s>UQ{+8hs%lmBm+P+mCGs3Y?liBh|3>+W~ z+e!&1mvFM$H>+?i7ES@jmKMg9#>v_vrDK#ja=ZMvL%Qx%uQ`8woVw2Wn8d>jrz7<# zMMk!-6@NbDF$QxJ)hJ8`4yY7s`GtkdWi~UZqb`&o+WL>gJ990`#8(M@7{&PI?74eh zQ5Tn;G44~Ec#mi_@jmJX{lSa0CjQ3IT75&j#~OdjZ9G@9LY~CmZ@Yk(YY}rKh1avVGS^-%E~OopcE9XWb+W86pqxmh z{@46JAD5dNmm4Rp_jg8~aSgr3=cP%_%yya?HEc`M%)i_q=_zCI3;!%P^G4`@t>5_1 zax-t``CCqD{VgXdrfueqU9-epeCvDmbV-RET$HYvUzj#abmSd5{XdV(F;>JibUcH9 zfEcqn5%1OALA+ZR*M5tMXZ3OI9@pvZxQEx6&)*zDH=mH*$zx7QpRzA;ITg5)tEfU% zs!{!a5!*uS7yDNxj*T!XQIYaoL4+Sg6z5W83B{NEE|B6(6s0h#XY${qQ2fdBg@~i- zk$l~p-Dg%VBomPa)ie>A7GwYaHm^vXQ(R-8xECDriI_{o9OTLrsq>K#+?h`j6 z<|0jrFq#u{l$fi8n74$M|1YKPfBt7`SgG@$2tUNRQpLGL#3NPAb5cbdL%e60D&ijg zx4F=674~ix%e%O@7x!+*X~QM8;hEZQ6>T^EUP9d?<`IWns~$9F9ME6yx4h5z%lmtc zy;SES4(zcn-!hx`c#Ri{zu)joidc?yukaQh@G3<{agG`^r2_*Q z%~QP1XT&>#>o`P4aV$h78q=OZ+{ZJ#!zV06*Ooj-iF-9&C_*~oK1eq5Q;u5PK-~Wr z&GSrS5z)2|5Mf1~ErRaJUvxXy2BJMg+lV$2Z6&pRMcX-F>bBIj6vv$3o+|iWo@h@I z_W5lp&KK=U_n1g+R}l|gXmY#Kr6%%{ht#%pvE>|OBei{HwjAv%1BQU)Hg^7eH12iB zeDi;jItPt?&Yy!W^F4L``JLsMQ!XI>=G>RW-=7O#{C|{s{P~{|ZmKxX`R4ncD&mnU zem|Ki;+QJt%c;J#oGQ|CzVqkQPs+##WMuq1Fkf+sxc~hJanCxB?LRY7r`9%$K2QOy z<3)whAiHM@#NRRciZjHs+N-HhhG6k~ke%bmpbq4X#IK4>S}a2*Y)NhK~N z-ub?W^qh3Qy=>$+eqbKcd6Rf<`w(|AkgnWFJnyYWSqhS!G#rtZ9jxOIqVL|Oy}U{b zvT^~rxQr%5JA0V8K3L8PiaD+s!+C{8=q8e{6DsM`8W7ib?deKSdZm1J^BL~|#Jlo0 z(2UqW?uW+nj(Dz9mXZ{wSjzHcKCk3z>T@kIuWUm{y3>dL3?}Z8#5`$u%4d8ZK*S~f zjzpZ#;Gg_Ao1z`QE?sf2JnpZ?ed9M+#!)(}C#m9hy*JQ;cqY;~MbngZt$fCHPdhr% zg{~=|aSasLLa|RX;`j#Cr(TL{d^Y4-;v8|VxGw5IH+s{bAq;0Ew{r)hxQ(dSaedX3 zIDdO0j0h{<;fS!~8Y|9Qhg!t-R*e*~T$_kXYW!MSj&LF#kq42!SdM&&bHugT935Ny zPGf}5H-5Jfb$C22ba3^oyD67;BC|iWPm+Caam;KTVt$j3u{y}@CLk?}8cT|)mpab_ zI?*Rgj?(E++wue-F`M^OzW4TdB}eQxo1t96PRCB;Zd!9S$pna0D_1I96stJ!1y;}qs~`xfF^$7SP@A^IEM5>^uuEEo2Cwo$?NG5kt- z`#s1yYB=^uHd9@A53!QCChf%YEM`BM#337pEx*e$`>ha0ypL6LfbgWN3AK2H`1?eI ziM)zyaFcchi*ylqMrPg}Ibjyu?@r(UF!kqF#zvZbf(QB=-NB z1C$fi06rt~YcQ+1afEczXQXmr2JKm6T_=v&=ltu{sq3VZr@5P+G@~MyaC)t@v6Rnv zn`d}{ySRP=9d8iF zzeL=txSDv6Al^;O%7G;HpAWc)rex)Zgz=r2^To5kxJMgfW!#I+P5dq@4gJmE<5^Np zTA0TVHjf{BO#3)0e~+lMhlP8HgWCN8`LbVMvrpaLt1U2jkA8{iyUp!sPJ13@HT7+a zd3p=`?qZT-+X$neu(tELaGw-@Pv@!Ze5aY`-0?e^_*<^8kXyX&BCa#y@6kO3KRasojP0P_w^!Eu+RoTV zO-j>r{$(AvhL?rYLwMzddzz)r@v3tTan8EVpIJP9VWK#-5VteDF0Lm@ zC*Gj}o5&$e@%NzyN$-X7Al@lGzYWGcqqtABgq5siC-L6XQQ|q$HdZj3$LUOIwieVM z)0sT1D4>1PiTKS@T2|+G?Zh(-=N4*FoGcv8C!Vb2Tjns0cX*A-yvPf@#LK+S6s9wm z7}MkUjT|DqaPw1xTq;(wTUfAB4SVimvgBXQp=e(M$DJ+F9& zd)o!tH}PJ~b7{p%yPOgAP<@gkPi*yN!+`8p7{3+niJ2! z*0U+6yvi-zm$(;_S3jqmo|Nx3aWA78<38Pee8yhi zOD83{zv*vQBvp0AQ!UYj=i<=h`v9u_KkuAI@x(o;t$U4WmgCwu{x;B^>PQC5@%MB( z&o>_2rqAP7J}2g2F`uYG5;4z+_iCmwA=)3+IWky(!F{x(6lsY0;2*^N z@OL(GoZPlmryEbQkdssu)^K9{IKtJ=a}V=LD-I19%a=0EKjN6Zsr-nELYMEZu` zB|Y3sMb6wQEbgEz%kEGQ$j+48)g4xj(vC^urIE%ER*i5Du4314`NzGq=Ne+pT!)tQ z=P4F)nmUeokl!dLtg)=2mhdNWijK}ZmptMzgzvaS{JQZL>&PkowdlYg&L2DCZ{mEy zSNy~VPElyBYeR+-bGEeAY_G$uK7KTKo&NR$eNTGV$r;Rt`IC5mZlm>Q)2gRQ z+Np4Ro-!5+>)(IZ-gqV-M$6k6U#~T`7BjZ~`|sMDzpGDwsw=OkYjK_W@4suWO{yD* z`S;(o*Kg%D2IeydBi>v4_usX59ht*55;KVxQEuW*{{46DU5BV6#nqWSdCc)HanCK6 zxnxe)4V1pvJWM_X%+btx_kD%@ z_(mRm;=ay(?&q9vKj&B<&tyHr>PY-fFP5(&4@YcU!ACqmXR45u&5m8b8$8d>__V&0 z}lx{p$VhdH(0-eHKn8E4yT7)~x;RcaI2ykP#$q7}EH1{r)fP zLnvT$FgBPInB_AMFd7&O%m>ULEE236*if($V6(yIgKYrY40aUkBv?9F7MLekN3a;M z8DQ(c4uRbSD@IWGYk9?4>ZIhu)(0@;*ZuK7_M`9qu>V(o?DZe^|LRXF{losqALWpr z{`r2JN5Af;!j}6Ff7@c~{vUsS-EVjMSN}L@Z5%Sy+GZ5FZ~Zq8d4R;Bp>if$Q1H0B#xYg#Mv}KP@A>j19jP~ z`(E#Nf15=3{0kCi+ZUN^dku-RV}UJd^t06aH~v%XULkRmPGBc~UZy>U#Mw^+dw`(S z(8e}@FDIVb3W=wl{N9GfzE7pGkomO5$N<_WB%a<1nN2^5%w|yGd;hh*$1|27@r*A> zJTn&T4ie95kHoRg|FoU)Y%k;QoJ&8j>;L7x$GJKnajr|j z-XL*qU6CnnXOK8|o1eBb&V43){uqgC(gvB*Z@&2teRpfFMch2uV1WqLvpX`&a{v=4F=cF-<YgHb(|>8~Yv@gG2^y|KI6* z;N73L^H2L81jptl*L!48XCyLc8Ccp+%Zv=FLn4C%z^43N-yMP>M*ifuy9eKFJZ^Bz z+=Jm<_%Ft~KRfR3!JmKH&OhtBdocdfzPpFGBJLq=!3O-aO!ts!@cBBhBY)rbkc_|Y z`}gDS9s-qPvkeQz0xxVMY~oBY!<-CM4L&kusZas1u& zZpnB45jrD{=z?^CjA%Nt97#g9ARCcYNIWtT8HjZH2WTv}Gtw3EDL&Ez>1joGga>;p zrFMb+-C!TT<-Khj!e1Y$ua@a=*0MWT^dWE3(6GO~eI?WaxX$lD2ZC)EAfT+NK_K9iKoO}BAd8C>>?HtF+@wk9+%)n_(^;vJ{)g@GtA%2 zx4YroSngo%0B&zCkK2jcp4*xm%=PE`a=p2p+~#23@UA7dBexHC0(TEr?L6OE@3PU= z-ZjDXzN^Aj1NOo7uImNYMAwn7p{_PA6)rh0$6dC#tOjg%x#aT7)zx*r>jO6rw;66} zZiM?__v=kun;h`)^LXC$XtRyY4|qQD4)b}~;*kIG!1p1Etq!+q-oY-icefWk&-VRg z(1&3iMqQlX6SrrkdVYt+DG8i}-U*`;CMHY<8VR$7kTf z@Sb=C-WG3#hvKdAc6bN8GtR?%;eGHZUOzw-uQ#s;kH_oG>&R=*Yr_lWh46xSfq-Dx zZp({+&&Tmr^Um@f^R#>~ejomH{#yPH{sI0`{we-B{ssOe{$;RBuyu}qhJOJNXCsC-~?2SNK=K&hbP2KFf zuIlo+3$OG3$g0S|PV*x&IvP55?vNP%G`x3sr|>r6A>l!Qknq;wVc`+sUBkPF_Y8+C z1kS`uL=0Ys?}DB+#arNW2@*m=Gs!*e=1@N}CvuKBML9>hY;%oqz2th;b-Am*%L|tj zmnAO4T*6$OoHfp`opYRzJ106%a~|N_-r3*T&$)%OFTe-jZH1TfAJMqi^0|-Ivju#^ z58w|700cS*0fGS`&Mg64Jm@eBSn#-NwK6cq)DV{q}inTq=lr#q@|=~ zRxBYcf<3cHainqJGl0Y=wI}(Lnvj^78T*Po!p>qb=tv}x2*O3gRHP8ygbqQjBTJBc zL=V>o9SMPmwh6g`JVHtlF~nA1ViUgFq&2$eXL^-VtJcty<-`f>V7<5Kesf<@%xS532wfyQ;FWK)eP-o*Yu%7;Pi`-=B(#2L~ne{npi38}eJ z%ddS>7g4vcZgt(bI_KJ}wO%#LYbZ5MY6E3{ih=bf)w};MW7mJHuHBd&nPnSISw!`s zk*Rm=+uO>?)6wQ8oqn)Zth;6m#rvTf$$RqQMEfhFfL-+b-sy+sC8f`6OINIhq@Jr@Hrrrjjo~&)r5TJ5V7-bc;ccj9v zPpZ)`<3CA0rdOG&o>t)>Cx0yc)auiqk6YfyzuEXYqx{udbLHBquAf~b-@fgwsjrjB z2P!%$UMhPk*D3cYH!3G6gB5bcEyYH~48>^0AVn`lH$^A#=l~X>h*Wfe_dM&7MiCNX zi%@}cazfmY=7=A}uJ(}i^n%PL78wsYRy>k`tV9+A;}QeRQZvMssE7RF4e^eUA~r}{ zV5BZ1QZy7@jOJn})*IV~Js{bVx#v{fn##=@K^n-$ULS7b+oIzfr43vk?L3g1S(Cg?qbR#+tF(BI@zK${` zsg26TO0#NK!zyjPVT4Uj zW%F~B)TlFJ297>NH%fb2OVu3HjMMZ1jMl8soYj9Qc z9CtX}cR)Ge>hqAC5Y0FVqqZv+OPc>m+WmjfilOwj+$tH z$gT(31KDh>)AUq)lhI^b>KAK18r$M4{>stH(l*hJ+^EcBsq3#kx;*%z;-w<66JB9@7eK@OTn93d7FeF+a-hUepZ@oA7*x|p@*cjo(M zky&WIV=g!AaR=NHw>4|bQgabcPt1ie>MEoh<)T4oARqwsNBvM=xT3t!=BNkij=I2S4k!zy zqxPsRN`fJ+8Lm_3T1myl;j7(oCp?}0Rh=+_(~jStcSY7Rz1 zA^L?wS=}J^L?Hu^!7%L(ur%tW#XbWd(43Rv5aVdIJ^^`Z*z{@-)V%yY}RMG-oDDVlx&Y?BA!Gf z{t(}fC%`yiEIt$;i1)+$W>lFZq$f&M0ygg=4&RZp|dVl6Q^EYe_A1v&zH5UYgv0!9;6

6cg0uKd$3AJhU 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