diff --git a/fast64_internal/gltf_extension.py b/fast64_internal/gltf_extension.py index 098a8b875..797bf373e 100644 --- a/fast64_internal/gltf_extension.py +++ b/fast64_internal/gltf_extension.py @@ -15,6 +15,7 @@ add_3_2_hooks, get_version, ) +from .sm64.gltf_extension import SM64Extensions # Original implementation from github.com/Mr-Wiseguy/gltf64-blender @@ -87,6 +88,8 @@ def __init__(self): self.sub_extensions = [] if self.settings.f3d.use: self.sub_extensions.append(F3DExtensions(self)) + if bpy.context.scene.gameEditorMode == "SM64": + self.sub_extensions.append(SM64Extensions(self)) class glTF2ExportUserExtension(GlTF2Extension): @@ -101,6 +104,28 @@ def gather_node_hook(self, gltf2_node, blender_object, export_settings): export_settings, ) + def animation_action_object_sampled( + self, gltf2_animation, blender_object, blender_action, cache_key, export_settings + ): + self.call_hooks( + "gather_any_animation_hook", + 'Animation "{args[1].name}"', + gltf2_animation, + blender_action, + blender_object, + export_settings, + ) + + def gather_animation_hook(self, gltf2_animation, blender_action, blender_object, export_settings): + self.call_hooks( + "gather_any_animation_hook", + 'Animation "{args[1].name}"', + gltf2_animation, + blender_action, + blender_object, + export_settings, + ) + def gather_mesh_hook(self, gltf2_mesh, blender_mesh, blender_object, vertex_groups, modifiers, *last_args): materials, export_settings = last_args[-2:] # 3.2 self.call_hooks( @@ -124,6 +149,24 @@ def gather_material_hook(self, gltf2_material, blender_material, export_settings export_settings, ) + def gather_joint_hook(self, gltf2_node, blender_bone, export_settings): + self.call_hooks( + "gather_joint_hook", + 'Joint "{args[1].name}"', + gltf2_node, + blender_bone, + export_settings, + ) + + def gather_scene_hook(self, gltf2_scene, blender_scene, export_settings): + self.call_hooks( + "gather_scene_hook", + 'Scene "{args[1].name}"', + gltf2_scene, + blender_scene, + export_settings, + ) + def gather_gltf_extensions_hook(self, _gltf, _export_settings): modify_f3d_nodes_for_export(True) @@ -160,6 +203,20 @@ def gather_import_mesh_after_hook(self, gltf_mesh, blender_mesh, gltf): gltf, ) + def gather_import_animation_channel_after_hook( + self, gltf_animation, gltf_node, path, channel, blender_action, gltf + ): + self.call_hooks( + "gather_import_animation_channel_after_hook", + 'Animation Channel "{args[4].name}"', + gltf_animation, + gltf_node, + path, + channel, + blender_action, + gltf, + ) + class Fast64GlTFSettings(PropertyGroup): verbose: BoolProperty( @@ -168,6 +225,7 @@ class Fast64GlTFSettings(PropertyGroup): ) f3d: PointerProperty(type=F3DGlTFSettings) game: BoolProperty(default=True, name="Export current game mode") + include_hints: BoolProperty(default=True, name="Include hints") def to_dict(self): return prop_group_to_json(self) @@ -187,6 +245,7 @@ def draw_props(self, scene, layout: UILayout): col.separator() col.prop(self, "verbose") + col.prop(self, "include_hints") game_mode = scene.gameEditorMode if game_mode == "Homebrew": diff --git a/fast64_internal/gltf_utility.py b/fast64_internal/gltf_utility.py index e8bf56296..603c0c704 100644 --- a/fast64_internal/gltf_utility.py +++ b/fast64_internal/gltf_utility.py @@ -124,6 +124,10 @@ def get_gltf_image_from_blender_image(blender_image_name: str, export_settings: class GlTF2SubExtension: required: bool = False + @property + def hints(self): + return self.extension.settings.include_hints + def post_init(self): pass @@ -136,25 +140,28 @@ def print_verbose(self, content): pprint(content) def append_extension(self, gltf_prop, name: str, data: dict | None = None, required=False, skip_if_empty=True): - if skip_if_empty and not data and data is not None: # If none, assume it shouldn´t skip + if skip_if_empty and (data is None or not any(data)): return self.print_verbose(f"Appending {name} extension") - if data: - self.print_verbose(data) - if gltf_prop.extensions is None: - gltf_prop.extensions = {} - gltf_prop.extensions[name] = self.extension.Extension( + self.print_verbose(data) + extension = self.extension.Extension( name=name, extension=data if data else {}, required=required if required else self.required, ) - return gltf_prop.extensions[name] + if isinstance(gltf_prop, dict): + gltf_prop.setdefault("extensions", {})[name] = extension + else: + if gltf_prop.extensions is None: + gltf_prop.extensions = {} + gltf_prop.extensions[name] = extension + return extension def get_extension(self, gltf_prop, name: str): if gltf_prop.extensions is None: return None data = gltf_prop.extensions.get(name, None) - if data and any(data): + if data is not None and any(data): self.print_verbose(data) return data diff --git a/fast64_internal/panels.py b/fast64_internal/panels.py index 9b3641f61..c682d5395 100644 --- a/fast64_internal/panels.py +++ b/fast64_internal/panels.py @@ -30,9 +30,9 @@ def poll(cls, context): return False elif cls.import_panel and not sm64_props.show_importing_menus: return False - elif cls.decomp_only and sm64_props.export_type != "C": + elif cls.decomp_only and sm64_props.legacy_export_type != "C": return False - elif cls.binary_only and sm64_props.export_type == "C": + elif cls.binary_only and sm64_props.legacy_export_type == "C": return False scene_goal = sm64_props.goal return scene_goal == "All" or sm64_props.goal == cls.goal or cls.goal == "All" diff --git a/fast64_internal/sm64/animation/classes.py b/fast64_internal/sm64/animation/classes.py index d11f612b6..6acc015bb 100644 --- a/fast64_internal/sm64/animation/classes.py +++ b/fast64_internal/sm64/animation/classes.py @@ -227,6 +227,18 @@ def evaluate(cls, value: str | int): else: # the value was not evaluated return value + def to_dict(self): + return {name: bool(self & flag) for name, flag in SM64_AnimFlags.props_to_flags().items()} + + @classmethod + def from_dict(cls, data: dict): + flags = cls(0) + props = cls.props_to_flags() + for prop, value in data.items(): + if value and prop in props: + flags |= props[prop] + return flags + @dataclasses.dataclass class SM64_AnimHeader: diff --git a/fast64_internal/sm64/animation/constants.py b/fast64_internal/sm64/animation/constants.py index 7ea4bbdc4..3063f6779 100644 --- a/fast64_internal/sm64/animation/constants.py +++ b/fast64_internal/sm64/animation/constants.py @@ -1,7 +1,7 @@ import struct import re -from ...utility import intToHex +from ...utility import intToHex, toAlnum from ..sm64_constants import ACTOR_PRESET_INFO, ActorPresetInfo HEADER_STRUCT = struct.Struct(">h h h h h h I I I") @@ -10,7 +10,7 @@ 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*(?P\w+))|(?PNULL|0)) # Capture element or null, element requires & (?:\s*,|) # allow no comma, techinically not correct but no other method works """, re.DOTALL | re.VERBOSE | re.MULTILINE, @@ -73,16 +73,16 @@ ] -enum_animated_behaviours = [("Custom", "Custom Behavior", "Custom"), ("", "Presets", "")] -enum_anim_tables = [("Custom", "Custom", "Custom"), ("", "Presets", "")] +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()] + [(toAlnum(name.upper()), 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()] + [(toAlnum(name.upper()), 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 index de508b3b3..a6ae08e09 100644 --- a/fast64_internal/sm64/animation/exporting.py +++ b/fast64_internal/sm64/animation/exporting.py @@ -1,12 +1,13 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Optional, Union from pathlib import Path import os import typing +import json import numpy as np import bpy from bpy.types import Object, Action, PoseBone, Context -from bpy.path import abspath +from bpy.path import abspath, clean_name from mathutils import Euler, Quaternion from ...utility import ( @@ -19,19 +20,22 @@ getExportDir, intToHex, applyBasicTweaks, + selectSingleObject, toAlnum, directory_path_checks, ) -from ...utility_anim import get_fcurves, stashActionInArmature, get_slots +from ...utility_anim import get_fcurves, stashActionInArmature from ..sm64_constants import BEHAVIOR_COMMANDS, BEHAVIOR_EXITS, defaultExtendSegment4, level_pointers from ..sm64_utility import ( ModifyFoundDescriptor, + convert_old_export_enum, find_descriptor_in_text, get_comment_map, to_include_descriptor, write_includes, update_actor_includes, + remove_actor_includes, int_from_str, write_or_delete_if_found, ) @@ -46,15 +50,18 @@ SM64_AnimPair, SM64_AnimTable, SM64_AnimTableElement, + SM64_AnimFlags, ) from .importing import import_enums, import_tables, update_table_with_table_enum from .utility import ( + add_name_to_duplicate_list, get_anim_owners, get_anim_actor_name, anim_name_to_enum_name, get_selected_action, get_action_props, duplicate_name, + remove_name_from_duplicate_list, ) from .constants import HEADER_SIZE @@ -193,7 +200,7 @@ 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) + is_owner_obj = obj.type == "MESH" if len(anim_owners) == 0: raise PluginError(f'No animation bones in armature "{obj.name}"') @@ -317,9 +324,9 @@ def to_table_element_class( export_type: str, actor_name="mario", gen_enums=False, - prev_enums: dict[str, int] | None = None, + prev_enums: set[str] | None = None, ): - prev_enums = prev_enums or {} + prev_enums = set() or prev_enums use_addresses, can_reference = export_type.endswith("Binary"), not dma element = SM64_AnimTableElement() @@ -417,7 +424,7 @@ def to_table_class( ) data_dict = {} - prev_enums = {} + prev_enums = set() element_props: SM64_AnimTableElementProperties for i, element_props in enumerate(anim_props.elements): try: @@ -567,7 +574,7 @@ def update_table_file( ) # Figure out enums on existing enum-less elements - prev_enums = {name: 0 for name in existing_table.enum_names} + prev_enums = {name for name in existing_table.enum_names} for i, element in enumerate(existing_table.elements): if element.enum_name: continue @@ -941,6 +948,329 @@ def export_animation_c( update_includes(combined_props, header_directory, actor_name, anim_props.update_table) +def _read_c_table_into_json( + anim_props: "SM64_ArmatureAnimProperties", table_c_path: Path, actor_name: str +) -> list[dict[str, Any]]: + table_name = anim_props.get_table_name(actor_name) + text = table_c_path.read_text() + comment_less, comment_map = get_comment_map(text) + tables = import_tables(comment_less, table_c_path, comment_map, table_name) + + if len(tables) != 1: + return [] + + existing = [] + prev_enums = set() + for el in tables[0].elements: + el_dict: dict[str, Any] = {"reference": {}} + if el.reference: + el_dict["reference"]["header_name"] = el.reference + enum_name = anim_name_to_enum_name(el.reference if el.reference else f"{actor_name}_anim_NULL") + auto_enum = duplicate_name(enum_name, prev_enums) + + if el.enum_name and el.enum_name != auto_enum: + el_dict["enum"] = el.enum_name + remove_name_from_duplicate_list(enum_name, prev_enums) + add_name_to_duplicate_list(el.enum_name, prev_enums) + else: + el_dict["hints"] = {"enum": auto_enum} + existing.append(el_dict) + if existing and (not existing[-1].get("action_name") and not existing[-1].get("reference", {})): + existing.pop() + return existing + + +def _get_existing_elements( + export_path: Path, anim_dir: Path, anim_props: "SM64_ArmatureAnimProperties", actor_name: str, override: bool +) -> list[dict[str, Any]]: + if override: + return [] + + if export_path.exists(): + try: + with open(export_path, "r") as f: + return json.load(f).get("elements", []) + except (json.JSONDecodeError, IOError): + pass + + table_c_path = anim_dir / "table.inc.c" + if not table_c_path.exists(): + return [] + try: + return _read_c_table_into_json(anim_props, table_c_path, actor_name) + except Exception as exc: + print(f"Error parsing {table_c_path}: {exc}") + return [] + + +def _find_all_match_indices(elements: list[dict], new_el: dict, h_name: str | None): + """Find all the indices of a newly exported element, that match by action name or variant.""" + matched_indices: list[int] = [] + new_action = new_el.get("action_name") + new_variant = new_el.get("variant", 0) + + for i, ex in enumerate(elements): + if not ex: + continue + if h_name: # Match by Header Reference + ex_ref = ex.get("reference") + if ex_ref and ex_ref.get("header_name") == h_name: + matched_indices.append(i) + continue + # Match by Action Name + Variant + if new_action and ex.get("action_name") == new_action: + if ex.get("variant", 0) == new_variant: + matched_indices.append(i) + + return matched_indices + + +def apply_new_element( + anim_props: "SM64_ArmatureAnimProperties", + actor_name: str, + merged: list[dict], + el: Union["SM64_AnimTableElementProperties", "Action"], + gen_enums: bool, + prev_enums: set[str], +): + exported_variants = [] + if isinstance(el, Action): + props = get_action_props(el) + for i, header in enumerate(props.headers): + h_name = header.get_name(actor_name, el, anim_props.is_dma) + entry: dict[str, Any] = { + "action_name": props.get_name(el, anim_props.is_dma), + "file_name": props.get_file_name(el, "GLTF", anim_props.is_dma), + } + if i > 0: + entry["variant"] = i + if gen_enums: + auto_enum = header.get_enum(actor_name, el) + deduped_enum = duplicate_name(auto_enum, prev_enums) + if deduped_enum != auto_enum: + entry["enum"] = deduped_enum + else: + entry.setdefault("hints", {})["enum"] = auto_enum + exported_variants.append((entry, h_name)) + else: # Element property + entry = el.to_dict("GLTF", gen_enums, anim_props.is_dma, actor_name, prev_enums, include_file_name=True) + h_name = entry.get("reference", {}).get("header_name") + exported_variants.append((entry, h_name)) + + for entry, h_name in exported_variants: + indices = _find_all_match_indices(merged, entry, h_name) + if not indices: + merged.append(entry) + + for idx in indices: + existing = merged[idx] + updated_entry = entry.copy() + + legacy_enum = existing.get("enum") + legacy_enum_hint = existing.get("hints", {}).get("enum") + if legacy_enum: + updated_entry["enum"] = legacy_enum + updated_entry.get("hints", {}).pop("enum", None) + elif legacy_enum_hint: + updated_entry.setdefault("hints", {})["enum"] = legacy_enum_hint + updated_entry.pop("enum", None) + variant = existing.get("variant", 0) + if variant != 0: + updated_entry["variant"] = variant + + merged[idx] = updated_entry + + +def remove_c_actor_includes( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + sm64_props: "SM64_Properties", +): + anim_dir, _, header_dir = create_and_get_paths(anim_props, combined_props, actor_name, sm64_props.abs_decomp_path) + for f in ["table.inc.c", "table_enum.h", "../anim_header.h"]: + (anim_dir / f).unlink(missing_ok=True) + + remove_actor_includes( + combined_props.export_header_type, + combined_props.actor_group_name, + header_dir, + actor_name, + combined_props.export_level_name, + [Path("anims/table.inc.c"), Path("anims/data.inc.c")], + [Path("anim_header.h")], + ) + + +def update_table_json( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + sm64_props: "SM64_Properties", + new_elements: list[Union["SM64_AnimTableElementProperties", "Action"]], + single_action: bool = False, +): + override = anim_props.override_files and not single_action + anim_dir, _, _ = create_and_get_paths(anim_props, combined_props, actor_name, sm64_props.abs_decomp_path) + export_path = anim_dir / anim_props.get_table_file_name(actor_name, "GLTF") + anim_dir.mkdir(parents=True, exist_ok=True) + + data = anim_props.to_dict(export_type="GLTF", actor_name=actor_name, include_elements=False) + merged = _get_existing_elements(export_path, anim_dir, anim_props, actor_name, override) + gen_enums = anim_props.get_gen_enums("GLTF") + + prev_enums = [] + for el in merged: + enum = el.get("enum") + if enum is None: + enum = el.get("hints", {}).get("enum") + if enum is None: + print("Can't figure out previous enums without hints") + if enum: + prev_enums.append(enum) + prev_enums = set(prev_enums) + + for el in new_elements: + try: + apply_new_element(anim_props, actor_name, merged, el, gen_enums, prev_enums) + except Exception as exc: + raise PluginError(f"Failed to apply new element: {exc}") from exc + + data["elements"] = merged + with open(export_path, "w") as f: + json.dump(data, f, indent=4) + + +def export_action_gltf( + action: Action, + obj: Object, + action_props: "SM64_ActionAnimProperty", + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + sm64_props: "SM64_Properties", +): + selectSingleObject(obj) + pre_export_frame = bpy.context.scene.frame_current + pre_export_action = obj.animation_data.action + pre_export_slot = None + if bpy.app.version >= (5, 0, 0): + pre_export_slot = obj.animation_data.action_slot + + export_dir, _, _ = create_and_get_paths(anim_props, combined_props, actor_name, sm64_props.abs_decomp_path) + file_name = action_props.get_file_name(action, "GLTF", False) + export_path = export_dir / file_name + + if action_props.reference_tables: + data = action_props.to_dict( + "GLTF", + action, + actor_name, + anim_props.get_gen_enums("GLTF"), + anim_props.is_dma, + anim_props.export_seperately, + anim_props.update_table, + include_hints=True, + file_path=export_path, + ) + json.dump(data, export_path.open("w"), indent=4) + return + try: + obj.animation_data.action = action + if bpy.app.version >= (5, 0, 0): + slot = get_action_props(action).get_slot(action) + if slot is None: + raise PluginError(f'No action slot found for action "{action.name}"') + obj.animation_data.action_slot = slot + + bpy.ops.export_scene.gltf( + filepath=str(export_path), + export_format="GLTF_SEPARATE", + use_selection=True, + # geometry + export_materials="NONE", + export_normals=False, + export_texcoords=False, + export_tangents=False, + # others + export_cameras=False, + export_lights=False, + export_extras=False, + # animation + export_animations=True, + export_force_sampling=True, + export_nla_strips=False, + export_frame_range=False, + export_skins=True, + export_morph=True, + gltf_export_id="FAST64_SM64_ACTION_EXPORT", + ) + except Exception as exc: + raise PluginError(f"GLTF export failed: {exc}") from exc + finally: + obj.animation_data.action = pre_export_action + if bpy.app.version >= (5, 0, 0): + obj.animation_data.action_slot = pre_export_slot + bpy.context.scene.frame_set(pre_export_frame) + + +def export_animation_gltf( + action: Action, + obj: Object, + action_props: "SM64_ActionAnimProperty", + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + sm64_props: "SM64_Properties", +): + export_action_gltf(action, obj, action_props, anim_props, combined_props, actor_name, sm64_props) + + if anim_props.update_table: + update_table_json(anim_props, combined_props, actor_name, sm64_props, [action], True) + remove_c_actor_includes(anim_props, combined_props, actor_name, sm64_props) + + +def export_animation_table_gltf( + obj: Object, + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + sm64_props: "SM64_Properties", +): + if not anim_props.export_seperately: + bpy.ops.export_scene.gltf( + filepath=str(create_and_get_paths(anim_props, combined_props, actor_name, sm64_props.abs_decomp_path)[0]), + export_format="GLTF_SEPARATE", + use_selection=True, + # geometry + export_materials="NONE", + export_normals=False, + export_texcoords=False, + export_tangents=False, + # others + export_cameras=False, + export_lights=False, + export_extras=False, + # animation + export_animations=True, + export_force_sampling=True, + export_nla_strips=False, + export_frame_range=False, + export_skins=True, + export_morph=True, + gltf_export_id="FAST64_SM64_ANIM_TABLE_EXPORT", + ) + remove_c_actor_includes(anim_props, combined_props, actor_name, sm64_props) + return + + for action in anim_props.actions: + action_props = get_action_props(action) + export_action_gltf(action, obj, action_props, anim_props, combined_props, actor_name, sm64_props) + + update_table_json(anim_props, combined_props, actor_name, sm64_props, anim_props.elements, False) + remove_c_actor_includes(anim_props, combined_props, actor_name, sm64_props) + + def export_animation(context: Context, obj: Object): scene = context.scene sm64_props: SM64_Properties = scene.fast64.sm64 @@ -951,6 +1281,11 @@ def export_animation(context: Context, obj: Object): action = get_selected_action(obj) action_props = get_action_props(action) stashActionInArmature(obj, action) + + if sm64_props.export_type == "GLTF": + export_animation_gltf(action, obj, action_props, anim_props, combined_props, actor_name, sm64_props) + return + bone_count = len(get_anim_owners(obj)) try: @@ -960,7 +1295,7 @@ def export_animation(context: Context, obj: Object): 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, + export_type=convert_old_export_enum(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, @@ -1006,6 +1341,10 @@ def export_animation_table(context: Context, obj: Object): if len(anim_props.elements) == 0: raise PluginError("Empty animation table") + if sm64_props.export_type == "GLTF": + export_animation_table_gltf(obj, anim_props, combined_props, actor_name, sm64_props) + return + try: print("Reading table data from fast64") table = to_table_class( @@ -1014,7 +1353,7 @@ def export_animation_table(context: Context, obj: Object): 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, + export_type=sm64_props.legacy_export_type, actor_name=actor_name, gen_enums=not anim_props.is_dma and not sm64_props.binary_export and anim_props.gen_enums, ) diff --git a/fast64_internal/sm64/animation/gltf.py b/fast64_internal/sm64/animation/gltf.py new file mode 100644 index 000000000..d86594907 --- /dev/null +++ b/fast64_internal/sm64/animation/gltf.py @@ -0,0 +1,57 @@ +import bpy + +from ..sm64_utility import get_object_actor_name +from ...gltf_utility import GlTF2SubExtension + +from .properties import SM64_ArmatureAnimProperties +from .utility import get_action_props, is_obj_animatable + + +class SM64AnimationGlTFExtension(GlTF2SubExtension): + ACTION_EXTENSION_NAME = "FAST64_animation_sm64" + OBJECT_EXTENSION_NAME = "FAST64_node_sm64_animation_table" + + HEADER_EXTENSION_NAME = "FAST64_sm64_header" + TABLE_ELEMENT_EXTENSION_NAME = "FAST64_sm64_table_element" + + def gather_any_animation_hook(self, gltf2_animation, blender_action, blender_object, export_settings): + if blender_object and is_obj_animatable(blender_object): + action_props = get_action_props(blender_action) + actor_name = get_object_actor_name(blender_object) + anim_props: SM64_ArmatureAnimProperties = blender_object.fast64.sm64.animation + data = action_props.to_dict( + export_type="GLTF", + action=blender_action, + actor_name=actor_name, + gen_enums=anim_props.get_gen_enums("GLTF"), + dma=anim_props.is_dma, + export_seperately=anim_props.export_seperately, + updates_table=anim_props.update_table, + gltf_extension=self, + ) + self.append_extension(gltf2_animation, self.ACTION_EXTENSION_NAME, data) + + def gather_node_hook(self, gltf2_node, blender_object, export_settings): + if blender_object and is_obj_animatable(blender_object): + if export_settings.get("gltf_export_id") != "FAST64_SM64_ANIM_TABLE_EXPORT": + return + anim_props = blender_object.fast64.sm64.animation + data = anim_props.to_dict( + export_type="GLTF", actor_name=get_object_actor_name(blender_object), gltf_extension=self + ) + self.append_extension(gltf2_node, self.OBJECT_EXTENSION_NAME, data) + + def gather_import_animation_channel_after_hook( + self, gltf_animation, gltf_node, path, channel, blender_action, import_settings + ): + if blender_action.get("already_imported"): + return + blender_action["already_imported"] = True + data = self.get_extension(gltf_animation, self.ACTION_EXTENSION_NAME) + if data and blender_action: + get_action_props(blender_action).from_dict(data, export_type="GLTF", gltf_extension=self) + + def gather_import_node_after_hook(self, vnode, gltf_node, blender_object, import_settings): + data = self.get_extension(gltf_node, self.OBJECT_EXTENSION_NAME) + if data and blender_object: + blender_object.fast64.sm64.animation.from_dict(data, export_type="GLTF", gltf_extension=self) diff --git a/fast64_internal/sm64/animation/panels.py b/fast64_internal/sm64/animation/panels.py index 103e783bb..5f9e2430a 100644 --- a/fast64_internal/sm64/animation/panels.py +++ b/fast64_internal/sm64/animation/panels.py @@ -3,6 +3,7 @@ from bpy.utils import register_class, unregister_class from bpy.types import Context +from ..sm64_utility import convert_old_export_enum from ...utility_anim import is_action_stashed, CreateAnimData, AddBasicAction, StashAction from ...panels import SM64_Panel @@ -51,11 +52,11 @@ def draw(self, context): sm64_props: SM64_Properties = context.scene.fast64.sm64 combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export - if sm64_props.export_type == "C": + if sm64_props.legacy_export_type == "C": if not sm64_props.hackersm64: col.prop(sm64_props, "designated_prop", text="Designated Initialization for Tables") else: - combined_props.draw_anim_props(col, sm64_props.export_type, dma_structure_context(context)) + combined_props.draw_anim_props(col, sm64_props.legacy_export_type, dma_structure_context(context)) SM64_ExportAnimTable.draw_props(col) anim_obj = get_anim_obj(context) if anim_obj is None: @@ -76,7 +77,7 @@ def draw(self, context): combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export get_anim_props(context).draw_props( self.layout, - sm64_props.export_type, + convert_old_export_enum(sm64_props.export_type), combined_props.export_header_type, get_anim_actor_name(context), combined_props.export_bhv, @@ -110,12 +111,12 @@ def draw(self, context): sm64_props: SM64_Properties = context.scene.fast64.sm64 combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export - if sm64_props.export_type != "C": + if sm64_props.legacy_export_type != "C": SM64_ExportAnim.draw_props(col) anim_props = get_anim_props(context) export_seperately = get_anim_props(context).export_seperately - if sm64_props.export_type == "C": + if sm64_props.legacy_export_type == "C": export_seperately = export_seperately or combined_props.export_single_action elif sm64_props.export_type == "Insertable Binary": export_seperately = True @@ -127,7 +128,7 @@ def draw(self, context): table_elements=anim_props.elements, updates_table=anim_props.update_table, export_seperately=export_seperately, - export_type=sm64_props.export_type, + export_type=convert_old_export_enum(sm64_props.export_type), actor_name=get_anim_actor_name(context), gen_enums=anim_props.gen_enums, dma=dma_structure_context(context), diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py index 143a30b52..704b781b2 100644 --- a/fast64_internal/sm64/animation/properties.py +++ b/fast64_internal/sm64/animation/properties.py @@ -1,5 +1,6 @@ import os -from typing import NamedTuple +from pathlib import Path +from typing import NamedTuple, Optional, TYPE_CHECKING import bpy from bpy.types import PropertyGroup, Action, UILayout, Scene, Context @@ -20,6 +21,8 @@ from bpy.path import abspath, clean_name from ...utility import ( + PluginError, + as_posix, decompFolderMessage, directory_ui_warnings, run_and_draw_errors, @@ -28,12 +31,28 @@ multilineLabel, prop_split, intToHex, + set_if_different, upgrade_old_prop, toAlnum, + prop_group_to_json, + json_to_prop_group, + filepath_checks, ) from ...utility_anim import get_slots, getFrameInterval, AddSubAction -from ..sm64_utility import import_rom_ui_warnings, int_from_str, string_int_prop, string_int_warning +from ..sm64_utility import ( + import_rom_ui_warnings, + int_from_str, + string_int_prop, + string_int_warning, + add_custom_if_not_auto, + get_custom_from_dict, + set_from_dict, + set_range_from_dict, + draw_custom_or_auto, + draw_forced, + prop_size_label, +) from ..sm64_constants import MAX_U16, MIN_S16, MAX_S16, enumLevelNames from .operators import ( @@ -50,6 +69,7 @@ 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 ( + add_name_to_duplicate_list, dma_structure_context, get_action_props, get_dma_anim_name, @@ -65,31 +85,8 @@ ) 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 +if TYPE_CHECKING: + from .gltf import SM64AnimationGlTFExtension def draw_list_op(layout: UILayout, op_cls: OperatorBase, op_name: str, index=-1, text="", icon="", **op_args): @@ -105,11 +102,6 @@ def draw_list_ops(layout: UILayout, op_cls: OperatorBase, index: int, **op_args) 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) @@ -321,6 +313,114 @@ def draw_names(self, layout: UILayout, action: Action, actor_name: str, gen_enum 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 shows_table_index( + self, export_type: str, dma: bool | None, updates_table: bool | None, export_seperately: bool | None + ): + if dma is None or updates_table is None or export_seperately is None: + return True + if export_type in {"C", "GLTF"} and not dma: + return False + elif export_seperately: + return True + else: + return (export_type == "BINARY" and updates_table) or dma + + def to_dict( + self, + export_type: str, + actor_name: str, + action: Action, + gen_enums: bool, + dma: bool, + export_seperately: bool | None, + updates_table: bool | None, + gltf_extension: Optional["SM64AnimationGlTFExtension"] = None, + include_hints: bool = False, + ): + flags_props = [ + "no_loop", + "backwards", + "no_acceleration", + "disabled", + "no_trans", + "only_vertical", + "only_horizontal", + ] + blacklist = [ + "expand_tab_in_action", + "custom_flags", + "header_variant", + "custom_enum", + "custom_name", + "start_frame", + "loop_start", + "loop_end", + "use_custom_flags", + "use_manual_loop", + "table_index", + *flags_props, + ] + data = {} + add_custom_if_not_auto(self, "name", data, blacklist) + if gen_enums: + add_custom_if_not_auto(self, "enum", data, blacklist) + + if self.use_manual_loop: + data["loop_points"] = {"start": self.start_frame, "loop_start": self.loop_start, "end": self.loop_end} + str_flags = self.get_flags(True) + if isinstance(str_flags, SM64_AnimFlags): + data["flags"] = str_flags.to_dict() + else: + data["flags"] = str_flags + if self.shows_table_index(export_type, dma, updates_table, export_seperately): + data["seperate"] = {"table_index": self.table_index} + data.update(prop_group_to_json(self, blacklist)) + + if include_hints or (gltf_extension is not None and gltf_extension.hints): + hints = {} + if not self.use_custom_name: + hints["name"] = self.get_name(actor_name, action, dma) + if not self.use_custom_enum: + hints["enum"] = self.get_enum(actor_name, action) + if not self.manual_loop_range: + loop_points = self.get_loop_points(action) + hints["loop_points"] = {"start": loop_points[0], "loop_start": loop_points[1], "end": loop_points[2]} + + if hints: + if gltf_extension is not None and gltf_extension.hints: + gltf_extension.append_extension(data, f"{gltf_extension.HEADER_EXTENSION_NAME}_hints", hints) + else: + data["hints"] = hints + return data + + def from_dict( + self, data: dict, export_type: str = "", gltf_extension: Optional["SM64AnimationGlTFExtension"] = None + ): + blacklist = [] + get_custom_from_dict(self, "name", data, blacklist) + get_custom_from_dict(self, "enum", data, blacklist) + + flags = data.get("flags") + if flags is not None: + if isinstance(flags, dict): + self.use_custom_flags = False + for prop, value in flags.items(): + if hasattr(self, prop): + setattr(self, prop, value) + else: + self.use_custom_flags = True + self.custom_flags = flags + loop_points = data.get("loop_points") + if loop_points is not None: + self.use_manual_loop = True + self.start_frame = loop_points.get("start", 0) + self.loop_start = loop_points.get("loop_start", 0) + self.loop_end = loop_points.get("end", 0) + sep = data.get("seperate") + if sep is not None: + self.table_index = sep.get("table_index", 0) + json_to_prop_group(self, data, blacklist=blacklist + ["flags", "loop_points", "seperate"]) + def draw_props( self, layout: UILayout, @@ -351,15 +451,15 @@ def draw_props( action_name=action.name, header_variant=self.header_variant, ) - if (export_type == "C" and dma) or (export_type == "Binary" and updates_table): + if self.shows_table_index(export_type, dma, updates_table, True): prop_split(col, self, "table_index", "Table Index") - if not dma and export_type == "C": + if not dma and export_type in {"C", "GLTF"}: 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")) + self.draw_flag_props(col, use_int_flags=dma or export_type.endswith("BINARY")) # workaround for garbage collector bug @@ -413,6 +513,8 @@ class SM64_ActionAnimProperty(PropertyGroup): 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") + external_data: StringProperty(name="External Data", subtype="FILE_PATH") + external_action_name: StringProperty(name="External Action Name") 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 @@ -435,7 +537,7 @@ def get_name(self, action: Action, dma=False) -> str: 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"}: + if not export_type in {"C", "GLTF", "INSERTABLE_BINARY"}: return "" if export_type == "C" and dma: return f"{self.dma_name}.inc.c" @@ -443,7 +545,14 @@ def get_file_name(self, action: Action, export_type: str, dma=False) -> str: return self.custom_file_name else: name = clean_name(f"anim_{action.name}", replace=" ") - return name + (".inc.c" if export_type == "C" else ".insertable") + if export_type == "INSERTABLE_BINARY": + return f"{name}.insertable" + elif export_type == "C": + return f"{name}.inc.c" + elif export_type == "GLTF": + if self.reference_tables and not dma: + return f"{name}.json" + return f"{name}.gltf" def get_slot(self, action: Action): if bpy.app.version < (5, 0, 0): @@ -496,15 +605,21 @@ def draw_variants( 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): + def draw_references(self, layout: UILayout, export_type: str): col = layout.column() col.prop(self, "reference_tables") if not self.reference_tables: return - if is_binary: + if export_type.endswith("BINARY"): string_int_prop(col, self, "indices_address", "Indices Table") string_int_prop(col, self, "values_address", "Value Table") else: + if export_type == "GLTF": + prop_split(col, self, "external_data", "External Data") + if self.external_data: + col.prop(self, "external_action_name") + return + col.label(text="C Fallback", icon="INFO") prop_split(col, self, "indices_table", "Indices Table") prop_split(col, self, "values_table", "Value Table") @@ -571,11 +686,11 @@ def draw_props( action_name=action.name, ) col.separator() - if export_type == "Binary" and not dma: + 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 + 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)" @@ -588,7 +703,7 @@ def draw_props( 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")) + self.draw_references(col, export_type) col.separator() if specific_variant is not None: @@ -600,6 +715,146 @@ def draw_props( else: self.draw_variants(col, action, dma, actor_name, header_args) + def to_dict( + self, + export_type: str, + action: Action, + actor_name, + gen_enums: bool, + dma: bool, + export_seperately: bool | None, + updates_table: bool | None, + gltf_extension: Optional["SM64AnimationGlTFExtension"] = None, + include_hints: bool = False, + file_path: Optional[Path] = None, + ): + blacklist = [ + "slot_identifier", + "slot_enum", + "variants_tab", + "header", + "header_variants", + "reference_tables", + "values_table", + "indices_table", + "values_address", + "indices_address", + "start_address", + "end_address", + "external_data", + "external_action_name", + ] + data = {} + add_custom_if_not_auto(self, "file_name", data, blacklist) + add_custom_if_not_auto(self, "max_frame", data, blacklist) + + if export_type in {"C", "GLTF"}: + if self.reference_tables: + if self.external_data: + external_data = Path(abspath(self.external_data)).resolve() + filepath_checks(external_data) + if external_data.suffix not in {".json", ".gltf"}: + raise PluginError("External data must be a json or gltf file.") + reference = {} + if file_path is None: + reference["abs"] = True + reference["path"] = as_posix(external_data) + else: + reference["abs"] = False + if external_data == file_path: + raise PluginError("External data cannot be the same file as the action.") + reference_path = os.path.relpath(external_data, file_path.parent) + reference["path"] = reference_path + reference["action_name"] = self.external_action_name + data["reference"] = reference + else: + data["reference"] = {"values_table": self.values_table, "indices_table": self.indices_table} + else: + blacklist.extend(["indices_table", "values_table"]) + if self.reference_tables: + data["reference"] = { + "values_table": int_from_str(self.values_address), + "indices_table": int_from_str(self.indices_address), + } + data["seperate"] = {"address": {"start": self.start_address, "end": self.end_address}} + + data.update(prop_group_to_json(self, blacklist)) + args = ( + export_type, + actor_name, + action, + gen_enums, + dma, + export_seperately, + updates_table, + gltf_extension, + include_hints, + ) + self.headers: list[SM64_AnimHeaderProperties] + data["headers"] = [v.to_dict(*args) for v in self.headers] + + if include_hints or (gltf_extension is not None and gltf_extension.hints): + hints = {} + if not self.custom_file_name: + hints["file_name"] = self.get_file_name(action, export_type, dma) + if not self.use_custom_max_frame: + hints["max_frame"] = self.get_max_frame(action) + + if hints: + if gltf_extension is not None and gltf_extension.hints: + gltf_extension.append_extension(data, f"{gltf_extension.ACTION_EXTENSION_NAME}_hints", hints) + else: + data["hints"] = hints + return data + + def from_dict( + self, data: dict, export_type: str = "", gltf_extension: Optional["SM64AnimationGlTFExtension"] = None + ): + blacklist = [] + get_custom_from_dict(self, "file_name", data, blacklist) + get_custom_from_dict(self, "max_frame", data, blacklist) + + ref = data.get("reference") + if ref is not None: + self.reference_tables = True + set_from_dict(self, "values_address", ref, "values_table", default=0x00A40CC8) + set_from_dict(self, "indices_address", ref, "indices_table", default=0x00A42150) + else: + set_from_dict(self, "values_address", {}, "values_table", default=0x00A40CC8) + set_from_dict(self, "indices_address", {}, "indices_table", default=0x00A42150) + + sep = data.get("seperate") + if sep is not None: + address = sep.get("address") + if address is not None: + set_range_from_dict( + self, "start_address", "end_address", address, start_default=0x00A40CC8, end_default=0x00A42265 + ) + else: + set_range_from_dict( + self, "start_address", "end_address", {}, start_default=0x00A40CC8, end_default=0x00A42265 + ) + else: + set_range_from_dict( + self, "start_address", "end_address", {}, start_default=0x00A40CC8, end_default=0x00A42265 + ) + + headers = data.get("headers") + if headers: + self.header.from_dict(headers[0], export_type, gltf_extension) + self.header_variants.clear() + for header_data in headers[1:]: + new_variant: SM64_AnimHeaderProperties = self.header_variants.add() + new_variant.from_dict(header_data, export_type, gltf_extension) + else: + self.header.from_dict({}, export_type, gltf_extension) + json_to_prop_group( + self, + data, + blacklist=blacklist + + ["reference", "seperate", "header", "header_variants", "slot_enum", "slot_identifier"], + ) + class ActionHeaderTuple(NamedTuple): action: Action @@ -618,12 +873,13 @@ class SM64_AnimTableElementProperties(PropertyGroup): 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]): + def get_enum(self, can_reference: bool, actor_name: str, prev_enums: set[str]): """Updates prev_enums""" enum = "" if self.use_custom_enum: self.custom_enum: str enum = self.custom_enum + add_name_to_duplicate_list(self.custom_enum, prev_enums) elif can_reference and self.reference: enum = duplicate_name(anim_name_to_enum_name(self.header_name), prev_enums) else: @@ -653,13 +909,14 @@ def set_variant(self, action: Action, variant: int): self.variant = variant def draw_reference( - self, layout: UILayout, export_type: str = "C", gen_enums: bool = False, prev_enums: dict[str, int] = None + self, layout: UILayout, export_type: str = "C", gen_enums: bool = False, prev_enums: set[str] | None = None ): - if export_type.endswith("Binary"): + if export_type.endswith("BINARY"): string_int_prop(layout, self, "header_address", "Header Address") return split = layout.split() if gen_enums: + prev_enums = set() or prev_enums draw_custom_or_auto(self, split, "enum", self.get_enum(True, "", prev_enums), factor=0.3) split.prop(self, "header_name", text="") @@ -674,7 +931,7 @@ def draw_props( export_type: str, gen_enums: bool, actor_name: str, - prev_enums: dict[str, int], + prev_enums: set[str], ): can_reference = not dma col = prop_layout.column() @@ -729,6 +986,82 @@ def draw_props( dma=dma, ) + def to_dict( + self, + export_type: str, + gen_enums: bool, + dma: bool, + actor_name: str, + prev_enums: set[str], + gltf_extension: Optional["SM64AnimationGlTFExtension"] = None, + include_hints: bool = False, + include_file_name: bool = False, + ): + can_reference = not dma + blacklist = [ + "action_prop", + "expand_tab", + "variant", + "reference", + "header_address", + "header_name", + "use_custom_enum", + "custom_enum", + ] + data = {} + if can_reference and self.reference: + reference_data = {} + if export_type in {"C", "GLTF"}: + reference_data["header_name"] = self.header_name + else: + reference_data["header_address"] = int_from_str(self.header_address) + data["reference"] = reference_data + else: + if not self.action_prop: + raise Exception(f"Header action does not exist.") + data["action_name"] = self.action_prop.name + action_props = get_action_props(self.action_prop) + if self.variant > 0: + if self.variant < 0 or self.variant >= len(action_props.headers): + raise Exception(f"Header variant {self.variant} does not exist.") + data["variant"] = self.variant + if export_type == "GLTF" and include_file_name: + data["file_name"] = action_props.get_file_name(self.action_prop, export_type, dma) + if gen_enums: + add_custom_if_not_auto(self, "enum", data, blacklist) + + data.update(prop_group_to_json(self, blacklist)) + + if include_hints or (gltf_extension is not None and gltf_extension.hints): + hints = {} + if not self.use_custom_enum: + hints["enum"] = self.get_enum(can_reference, actor_name, prev_enums) + + if hints: + if gltf_extension is not None and gltf_extension.hints: + gltf_extension.append_extension(data, f"{gltf_extension.TABLE_ELEMENT_EXTENSION_NAME}_hints", hints) + else: + data["hints"] = hints + return data + + def from_dict( + self, data: dict, export_type: str = "", gltf_extension: Optional["SM64AnimationGlTFExtension"] = None + ): + get_custom_from_dict(self, "enum", data) + + ref_data = data.get("reference") + if ref_data is not None: + self.reference = True + set_from_dict(self, "header_name", ref_data, "header_name", to_hex=False) + set_from_dict(self, "header_address", ref_data, "header_address", default=intToHex(0x0600B75C)) + elif data.get("action_name") is not None: + self.reference = False + + json_to_prop_group(self, data, blacklist=["enum", "reference", "action_name"]) + action_name = data.get("action_name") + if action_name is not None and action_name in bpy.data.actions: + self.action_prop = bpy.data.actions[action_name] + class SM64_AnimImportProperties(PropertyGroup): run_decimate: BoolProperty(name="Run Decimate (Allowed Change)", default=True) @@ -753,7 +1086,7 @@ class SM64_AnimImportProperties(PropertyGroup): items=enum_anim_tables, name="Preset", update=update_table_preset, - default="Mario", + default="MARIO", ) decomp_path: StringProperty(name="Decomp Path", subtype="FILE_PATH", default="") binary_import_type: EnumProperty( @@ -796,7 +1129,7 @@ def binary(self) -> bool: def table_index(self): if self.read_entire_table: return - elif self.preset_animation == "Custom" or not self.use_preset: + elif self.preset_animation == "CUSTOM" or not self.use_preset: return self.table_index_prop else: return int_from_str(self.preset_animation) @@ -828,7 +1161,7 @@ def table_size(self): @property def use_preset(self): - return self.import_type != "Insertable Binary" and self.preset != "Custom" + return self.import_type != "Insertable Binary" and self.preset != "CUSTOM" def upgrade_old_props(self, scene: Scene): upgrade_old_prop( @@ -871,7 +1204,7 @@ def draw_path(self, layout: UILayout): def draw_c(self, layout: UILayout, decomp: os.PathLike = ""): col = layout.column() - if self.preset == "Custom": + if self.preset == "CUSTOM": self.draw_path(col) else: col.label(text="Uses scene decomp path by default", icon="INFO") @@ -903,12 +1236,12 @@ def draw_binary(self, layout: UILayout, import_rom: os.PathLike): self.draw_import_rom(col, import_rom) col.separator() - if self.preset != "Custom": + 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": + if self.preset_animation == "CUSTOM": split.prop(self, "table_index_prop", text="Index") return col.prop(self, "ignore_bone_count") @@ -1075,7 +1408,7 @@ class SM64_ArmatureAnimProperties(PropertyGroup): 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)) + behaviour: bpy.props.EnumProperty(items=enum_animated_behaviours, default="TOAD_MESSAGE") behavior_address_prop: StringProperty(name="Behavior Address", default=intToHex(0x13002EF8)) beginning_animation: StringProperty(name="Begining Animation", default="0x00") # Mario animation table @@ -1087,7 +1420,7 @@ class SM64_ArmatureAnimProperties(PropertyGroup): @property def behavior_address(self) -> int: - if self.behaviour == "Custom": + if self.behaviour == "CUSTOM": return int_from_str(self.behavior_address_prop) return int_from_str(self.behaviour) @@ -1108,6 +1441,12 @@ def actions(self) -> list[Action]: actions.append(action) return actions + def can_gen_enums(self, export_type: str) -> bool: + return export_type in {"C", "GLTF"} and not self.is_dma + + def get_gen_enums(self, export_type: str) -> bool: + return self.can_gen_enums(export_type) and self.gen_enums + def get_table_name(self, actor_name: str) -> str: if self.use_custom_table_name: return self.custom_table_name @@ -1121,12 +1460,14 @@ def get_enum_end(self, actor_name: str): 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"}: + if not export_type in {"C", "GLTF", "INSERTABLE_BINARY"}: return "" - elif export_type == "Insertable Binary": + 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" + elif export_type == "GLTF": + return "table.json" else: return "table.inc.c" @@ -1137,7 +1478,7 @@ def draw_element( table_element: SM64_AnimTableElementProperties, export_type: str, actor_name: str, - prev_enums: dict[str, int], + prev_enums: set[str], ): col = layout.column() row = col.row() @@ -1155,7 +1496,7 @@ def draw_element( self.update_table, self.export_seperately, export_type, - export_type == "C" and self.gen_enums and not self.is_dma, + self.get_gen_enums(export_type), actor_name, prev_enums, ) @@ -1203,7 +1544,7 @@ def draw_table(self, layout: UILayout, export_type: str, actor_name: str): "INFO", ) - prev_enums = {} + prev_enums = set() element_props: SM64_AnimTableElementProperties for i, element_props in enumerate(self.elements): if i != 0: @@ -1228,16 +1569,16 @@ def draw_c_settings(self, layout: UILayout, header_type: str): def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor_name: str, bhv_export: bool): col = layout.column() col.prop(self, "is_dma") - if export_type == "C": + if export_type in {"C", "GLTF"}: self.draw_c_settings(col, header_type) - if export_type != "Insertable Binary" and not self.is_dma: + if export_type != "INSERTABLE_BINARY" and not self.is_dma: col.prop(self, "update_table") if self.is_dma: - if export_type == "Binary": + if export_type == "BINARY": string_int_prop(col, self, "dma_address", "Table Address") string_int_prop(col, self, "dma_end_address", "Table End") - elif export_type == "C": + elif export_type in {"C", "GLTF"}: multilineLabel( col, "The export will follow the vanilla DMA naming\n" @@ -1245,10 +1586,10 @@ def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor icon="INFO", ) else: - if export_type == "C": + if export_type in {"C", "GLTF"}: draw_custom_or_auto(self, col, "table_name", self.get_table_name(actor_name)) col.prop(self, "gen_enums") - if self.gen_enums: + if self.get_gen_enums(export_type): multilineLabel( col.box(), f"Enum List Name: {self.get_enum_name(actor_name)}\n" @@ -1259,7 +1600,7 @@ def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor 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": + elif export_type == "BINARY": string_int_prop(col, self, "address", "Table Address") string_int_prop(col, self, "end_address", "Table End") @@ -1273,7 +1614,7 @@ def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor "INFO", ) SM64_SearchAnimatedBhvs.draw_props(box, self, "behaviour", "Behaviour") - if self.behaviour == "Custom": + if self.behaviour == "CUSTOM": prop_split(box, self, "behavior_address_prop", "Behavior Address") prop_split(box, self, "beginning_animation", "Beginning Animation") @@ -1282,9 +1623,190 @@ def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor 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": + if export_type == "INSERTABLE_BINARY": draw_custom_or_auto(self, col, "file_name", self.get_table_file_name(actor_name, export_type)) + def to_dict( + self, + export_type: str = "", + actor_name: str = "", + gltf_extension: Optional["SM64AnimationGlTFExtension"] = None, + include_hints: bool = False, + include_elements: bool = True, + ): + blacklist = [ + "version", + "dma_folder", + "update_table", + "export_seperately_prop", + "override_files_prop", + "behavior_address_prop", + "elements", + "gen_enums", + "export_seperately", + "address", + "end_address", + "write_data_seperately", + "data_address", + "data_end_address", + "dma_address", + "dma_end_address", + "update_behavior", + "behaviour", + "beginning_animation", + "is_dma", + "use_custom_file_name", + "custom_file_name", + ] + data = {} + if self.can_gen_enums(export_type): + data["gen_enums"] = self.gen_enums + if export_type in {"C", "GLTF"}: + if self.is_dma: + data["dma_folder"] = self.dma_folder + if export_type == "C": + data["export_seperately"] = self.export_seperately + if self.export_seperately: + data["override_files"] = self.override_files + else: + if self.is_dma: + blacklist.extend(["null_delimiter"]) + + if export_type == "BINARY": + if self.is_dma: + data["dma"] = {"start": int_from_str(self.dma_address), "end": int_from_str(self.dma_end_address)} + else: + if self.update_behavior: + data["behavior"] = behavior = {"beginning_animation": self.beginning_animation} + if self.behaviour == "CUSTOM": + behavior["address"] = self.behavior_address + else: + behavior["enum"] = self.behaviour + + if self.write_data_seperately: + data["data_address"] = { + "start": int_from_str(self.data_address), + "end": int_from_str(self.data_end_address), + } + data["address"] = {"start": int_from_str(self.address), "end": int_from_str(self.end_address)} + + if export_type == "INSERTABLE_BINARY": + blacklist.remove("is_dma") + elif not self.is_dma and not export_type == "GLTF": + data["update_table"] = self.update_table + + add_custom_if_not_auto(self, "table_name", data, blacklist) + + data.update(prop_group_to_json(self, blacklist)) + + if include_elements: + table = [] + gen_enums = self.get_gen_enums(export_type) + try: + prev_enums = set() + element: SM64_AnimTableElementProperties + for i, element in enumerate(self.elements): + try: + table.append( + element.to_dict( + export_type, + gen_enums, + self.is_dma, + actor_name, + prev_enums, + gltf_extension, + include_hints, + ) + ) + except Exception as exc: # pylint: disable=broad-except + raise PluginError(f"Failed to export element {i}:\n{exc}") from exc + except Exception as exc: # pylint: disable=broad-except + raise PluginError(f"Failed to export table:\n{exc}") from exc + data["elements"] = table + + if include_hints or (gltf_extension is not None and gltf_extension.hints): + hints = {} + if export_type != "BINARY": + file_name = self.get_table_file_name(actor_name, export_type) + if file_name: + hints["file_name"] = file_name + if export_type in {"C", "GLTF"}: + hints["table_name"] = self.get_table_name(actor_name) + + if hints: + if gltf_extension is not None and gltf_extension.hints: + gltf_extension.append_extension(data, f"{gltf_extension.OBJECT_EXTENSION_NAME}_hints", hints) + else: + data["hints"] = hints + return data + + def from_dict( + self, data: dict, export_type: str = "", gltf_extension: Optional["SM64AnimationGlTFExtension"] = None + ): + blacklist = [] + get_custom_from_dict(self, "table_name", data, blacklist) + get_custom_from_dict(self, "file_name", data, blacklist) + + if dma_folder := data.get("dma_folder"): + self.dma_folder = dma_folder + self.is_dma = True + + dma = data.get("dma") + if dma is not None: + self.is_dma = True + set_range_from_dict( + self, "dma_address", "dma_end_address", dma, start_default=0x4EC000, end_default=0x4EC000 + ) + else: + set_range_from_dict( + self, "dma_address", "dma_end_address", {}, start_default=0x4EC000, end_default=0x4EC000 + ) + + behaviour = data.get("behavior") + if behaviour is not None: + self.update_behavior = True + address = behaviour.get("address") + if address is not None: + self.behaviour = "CUSTOM" + self.behavior_address_prop = intToHex(address) if isinstance(address, int) else address + elif "enum" in behaviour: + self.behaviour = behaviour["enum"] + if "beginning_animation" in behaviour: + self.beginning_animation = behaviour["beginning_animation"] + else: + self.behaviour = "TOAD_MESSAGE" + self.beginning_animation = "0x00" + + data_address = data.get("data_address") + if data_address is not None: + self.write_data_seperately = True + set_range_from_dict( + self, "data_address", "data_end_address", data_address, start_default=0x00A46738, end_default=0x00A4675C + ) + else: + set_range_from_dict( + self, "data_address", "data_end_address", {}, start_default=0x00A46738, end_default=0x00A4675C + ) + + address = data.get("address") + if address is not None: + set_range_from_dict( + self, "address", "end_address", address, start_default=0x00A46738, end_default=0x00A4675C + ) + else: + set_range_from_dict(self, "address", "end_address", {}, start_default=0x00A46738, end_default=0x00A4675C) + + json_to_prop_group(self, data, blacklist=blacklist + ["dma", "behavior", "data_address", "address"]) + + self.elements.clear() + elements = data.get("elements", []) + for i, element in enumerate(elements): + try: + element_prop = self.elements.add() + element_prop.from_dict(element, export_type, gltf_extension) + except Exception as exc: # pylint: disable=broad-except + raise PluginError(f"Failed to import element {i}:\n{exc}") from exc + classes = ( SM64_AnimHeaderProperties, diff --git a/fast64_internal/sm64/animation/schema/FAST64_animation_sm64.json b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64.json new file mode 100644 index 000000000..6e3d24a48 --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_action_anim glTF Animation Extension", + "type": "object", + "description": "Animation properties for SM64 actions", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "extensions": { + "type": "object", + "properties": { + "FAST64_sm64_action_anim_hints": { + "$ref": "FAST64_sm64_action_anim_hints.schema.json" + } + } + }, + "file_name": { + "type": "string", + "description": "Specified output file name for the exported animation data", + "default": "anim_00.inc.c", + "pattern": "^[a-zA-Z0-9_][a-zA-Z0-9._-]*$" + }, + "max_frame": { + "type": "integer", + "description": "Specified maximum number of frames in the animation data based on the end frame or detected range based on Anim Range rules", + "default": 1, + "minimum": 1 + }, + "reference": { + "oneOf": [ + { + "type": "object", + "description": "Optional references to external animation data", + "properties": { + "values_table": { + "type": "string", + "description": "Name for the animation values table array", + "default": "anim_00_values" + }, + "indices_table": { + "type": "string", + "description": "Name for the animation indices table array", + "default": "anim_00_indices" + } + } + } + { + "type": "object", + "description": "Optional references to external animation data via glTF", + "properties": { + "abs": { + "type": "boolean", + "description": "Is absolute to decomp" + }, + "path": { + "type": "string", + "description": "Path to the animation data file" + }, + "action_name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_][a-zA-Z0-9._-]*$", + "description": "Name of the action to reference" + } + } + } + ], + "additionalProperties": false + }, + "headers": { + "type": "array", + "description": "List of variants for this action", + "items": { + "$ref": "FAST64_sm64_anim_header.schema.json" + }, + "minItems": 1 + } + }, + "additionalProperties": false +} diff --git a/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_header.schema.json b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_header.schema.json new file mode 100644 index 000000000..c8dff4a76 --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_header.schema.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_anim_header glTF Animation Header", + "type": "object", + "description": "Animation header properties for SM64", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "extensions": { + "type": "object", + "properties": { + "FAST64_sm64_anim_header_hints": { + "$ref": "FAST64_sm64_anim_header_hints.schema.json" + } + } + }, + "name": { + "type": "string", + "description": "Specified name for the animation header variable", + "default": "anim_00" + }, + "enum": { + "type": "string", + "description": "Specified enum name for the animation index", + "default": "ANIM_00" + }, + "loop_points": { + "$ref": "sm64_anim_loop_points.schema.json" + }, + "flags": { + "description": "Animation behavior flags", + "oneOf": [ + { + "type": "string", + "description": "C flags field", + "default": "ANIM_FLAG_NOLOOP" + }, + { + "type": "object", + "description": "Individual boolean flags for the playback behavior", + "properties": { + "no_loop": { + "type": "boolean", + "description": "Animation does not repeat from the loop start after reaching the loop end frame", + "default": false + }, + "backwards": { + "type": "boolean", + "description": "Animation loops after reaching the loop start frame, used with acceleration to play backwards", + "default": false + }, + "no_acceleration": { + "type": "boolean", + "description": "Acceleration is not used when calculating the next animation frame", + "default": false + }, + "disabled": { + "type": "boolean", + "description": "Animation translation is not applied to shadows", + "default": false + }, + "no_trans": { + "type": "boolean", + "description": "Animation translation is not used during rendering (shadows included)", + "default": false + }, + "only_vertical": { + "type": "boolean", + "description": "Only vertical translation is applied, horizontal translation is ignored", + "default": false + }, + "only_horizontal": { + "type": "boolean", + "description": "Only horizontal translation is applied, vertical translation is ignored", + "default": false + } + }, + "additionalProperties": false + } + ] + }, + "trans_divisor": { + "type": "integer", + "description": "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", + "default": 1, + "minimum": 0, + "maximum": 65535 + } + }, + "additionalProperties": false +} diff --git a/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_header_hints.schema.json b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_header_hints.schema.json new file mode 100644 index 000000000..a04798efd --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_header_hints.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_anim_header_hints glTF Animation Header Hints", + "type": "object", + "description": "Hints from Fast64 on what the assumed values should be for animation headers", + "properties": { + "name": { + "type": "string", + "description": "The name generated from the actor and animation name", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" + }, + "enum": { + "type": "string", + "description": "The enum generated from the actor and action name", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" + }, + "loop_points": { + "$ref": "sm64_anim_loop_points.schema.json", + "description": "The loop points calculated from the Blender action's range" + } + }, + "additionalProperties": false +} diff --git a/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_hints.schema.json b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_hints.schema.json new file mode 100644 index 000000000..d4597bbdc --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_hints.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_action_anim_hints glTF Animation Extension Hints", + "type": "object", + "description": "Hints from Fast64 on what the assumed values should be for action animations", + "properties": { + "file_name": { + "type": "string", + "description": "The auto-generated file name based on the, object's name and action name", + "pattern": "^[a-zA-Z0-9_][a-zA-Z0-9._-]*$" + }, + "max_frame": { + "type": "integer", + "description": "The frame count calculated from the Blender action based on the Anim Range rules", + "minimum": 1, + "max": 65535 + } + }, + "additionalProperties": false +} diff --git a/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_loop_points.schema.json b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_loop_points.schema.json new file mode 100644 index 000000000..85c475413 --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_animation_sm64_loop_points.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "SM64 Animation Loop Points", + "type": "object", + "description": "Loop points for an SM64 animation", + "required": ["start", "loop_start", "end"], + "properties": { + "start": { + "type": "integer", + "description": "The starting frame of the animation, not the same as loop start.", + "default": 0, + "minimum": 0, + "maximum": 32767 + }, + "loop_start": { + "type": "integer", + "description": "If Backwards is not set, this will be the starting frame after each loop, otherwise this will be treated as the loop end frame.", + "default": 0, + "minimum": 0, + "maximum": 32767 + }, + "end": { + "type": "integer", + "description": "Both the end loop frame and the actual end frame. If Backwards is not set, this will be the ending frame of the animation, otherwise this will be treated as the loop start frame.", + "default": 0, + "minimum": 0, + "maximum": 32767 + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table.json b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table.json new file mode 100644 index 000000000..68cb2a31b --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_node_anim glTF Node Animation Table Extension", + "type": "object", + "description": "Node animation table properties for SM64 armatures", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "extensions": { + "type": "object", + "properties": { + "FAST64_sm64_node_anim_hints": { + "$ref": "FAST64_sm64_node_anim_hints.schema.json" + } + } + }, + "gen_enums": { + "type": "boolean", + "description": "Generate C enums for the animation table elements", + "default": true + }, + "dma_folder": { + "type": "string", + "description": "The folder where DMA data is exported", + "default": "assets/anims/" + }, + "table_name": { + "type": "string", + "description": "Specified name for the generated animation table array", + "default": "anims_table" + }, + "file_name": { + "type": "string", + "description": "Specified file name for the insertable table file", + "default": "anims_table.inc.c" + }, + "null_delimiter": { + "type": "boolean", + "description": "Add a NULL pointer at the end of the animation table", + "default": false + }, + "elements": { + "type": "array", + "description": "Variants in the table", + "items": { + "$ref": "FAST64_sm64_anim_table_element.schema.json" + } + } + }, + "additionalProperties": false +} diff --git a/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_element.schema.json b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_element.schema.json new file mode 100644 index 000000000..62703732a --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_element.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_anim_table_element glTF Animation Table Element", + "type": "object", + "description": "Animation table element properties for SM64", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "extensions": { + "type": "object", + "properties": { + "FAST64_sm64_anim_table_element_hints": { + "$ref": "FAST64_sm64_anim_table_element_hints.schema.json" + } + } + }, + "action_name": { + "type": "string", + "description": "The name of the Blender action being exported" + }, + "variant": { + "type": "integer", + "description": "The specific header variant index to use from the action", + "default": 0, + "minimum": 0 + }, + "reference": { + "type": "object", + "description": "Reference to an existing animation header in external code", + "properties": { + "header_name": { + "type": "string", + "description": "External header name to reference", + "default": "toad_seg6_anim_0600B66C" + } + }, + "additionalProperties": false + }, + "enum": { + "type": "string", + "description": "Specified enum identifier to reference the specific table index" + }, + "file_name": { + "type": "string", + "description": "The glTF file name for the animation, relative to the anims directory" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_element_hints.schema.json b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_element_hints.schema.json new file mode 100644 index 000000000..69e30396c --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_element_hints.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_anim_table_element_hints glTF Animation Table Element Hints", + "type": "object", + "description": "Hints from Fast64 on what the assumed values should be for animation table elements", + "properties": { + "enum": { + "type": "string", + "description": "The enum identifier generated from the variant/header's name", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" + } + }, + "additionalProperties": false +} diff --git a/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_hints.schema.json b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_hints.schema.json new file mode 100644 index 000000000..46a423e4d --- /dev/null +++ b/fast64_internal/sm64/animation/schema/FAST64_node_sm64_animation_table_hints.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sm64_node_anim_hints glTF Node Animation Table Extension Hints", + "type": "object", + "description": "Hints from Fast64 on what the assumed values should be for node animations", + "properties": { + "file_name": { + "type": "string", + "description": "The table file name generated from the armature name" + }, + "table_name": { + "type": "string", + "description": "The table array name generated from the armature name" + } + }, + "additionalProperties": false +} diff --git a/fast64_internal/sm64/animation/utility.py b/fast64_internal/sm64/animation/utility.py index ac17834f5..734192707 100644 --- a/fast64_internal/sm64/animation/utility.py +++ b/fast64_internal/sm64/animation/utility.py @@ -125,16 +125,32 @@ def anim_name_to_enum_name(anim_name: str) -> str: 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 != "": +def duplicate_name(base_name: str, existing_names: set[str]) -> str: + """Finds the next available name and adds it to the set.""" + if base_name == "": + return base_name + + if base_name not in existing_names: + existing_names.add(base_name) + return base_name + + current_num = 1 + while True: + new_name = f"{base_name}_D{current_num:03}" # breaks old enums but it's for the better + if new_name not in existing_names: + existing_names.add(new_name) + return new_name current_num += 1 - existing_names[name] = current_num - return f"{name}_{current_num}" - return name + + +def add_name_to_duplicate_list(name: str, existing_names: set[str]): + if name != "": + existing_names.add(name) + + +def remove_name_from_duplicate_list(name: str, existing_names: set[str]): + if name in existing_names: + existing_names.remove(name) def table_name_to_enum(name: str): @@ -157,7 +173,7 @@ def get_anim_props(context: Context) -> "SM64_ArmatureAnimProperties": 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: + if sm64_props.legacy_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) diff --git a/fast64_internal/sm64/geolayout/gltf.py b/fast64_internal/sm64/geolayout/gltf.py new file mode 100644 index 000000000..362bc7442 --- /dev/null +++ b/fast64_internal/sm64/geolayout/gltf.py @@ -0,0 +1,35 @@ +from bpy.types import PoseBone +from typing import TYPE_CHECKING + +from ...gltf_utility import GlTF2SubExtension + + +if TYPE_CHECKING: + from ..custom_cmd.properties import SM64_CustomCmdProperties + +CMD_TO_GLTF = { + "TranslateRotate": "TRANSLATE_ROTATE", + "DisplayList": "DISPLAY_LIST", + "HeldObject": "HELD_OBJECT", + "StartRenderArea": "START_RENDER_AREA", + "SwitchOption": "SWITCH_OPTION", + "DisplayListWithOffset": "ANIMATED_PART", +} + + +class SM64GeoGlTFExtension(GlTF2SubExtension): + BONE_EXTENSION_NAME = "FAST64_joint_sm64_geo" + + def gather_joint_hook(self, gltf2_node, blender_bone: PoseBone, export_settings): + bone = blender_bone.bone + data = { + "geo_cmd": CMD_TO_GLTF.get(bone.geo_cmd, bone.geo_cmd.upper()), + } + if bone.geo_cmd == "Custom": + custom: SM64_CustomCmdProperties = bone.fast64.sm64.custom + if custom.preset == "NONE": + conf_type = "NO_PRESET" + else: + conf_type = "PRESET" + data["custom"] = custom.to_dict(conf_type, bone) + self.append_extension(gltf2_node, self.BONE_EXTENSION_NAME, data) diff --git a/fast64_internal/sm64/gltf_extension.py b/fast64_internal/sm64/gltf_extension.py new file mode 100644 index 000000000..8061e4319 --- /dev/null +++ b/fast64_internal/sm64/gltf_extension.py @@ -0,0 +1,37 @@ +from ..gltf_utility import GlTF2SubExtension +from .animation.gltf import SM64AnimationGlTFExtension +from .geolayout.gltf import SM64GeoGlTFExtension + + +class SM64Extensions(GlTF2SubExtension): + SCENE_SETTING_EXTENSION_NAME = "FAST64_scene_sm64_settings" + + def post_init(self): + self.anim_ext = SM64AnimationGlTFExtension(self.extension) + self.geo_ext = SM64GeoGlTFExtension(self.extension) + + def gather_any_animation_hook(self, gltf2_animation, blender_action, blender_object, export_settings): + self.anim_ext.gather_any_animation_hook(gltf2_animation, blender_action, blender_object, export_settings) + + def gather_node_hook(self, gltf2_node, blender_object, export_settings): + self.anim_ext.gather_node_hook(gltf2_node, blender_object, export_settings) + + def gather_import_animation_channel_after_hook( + self, gltf_animation, gltf_node, path, channel, blender_action, import_settings + ): + self.anim_ext.gather_import_animation_channel_after_hook( + gltf_animation, gltf_node, path, channel, blender_action, import_settings + ) + + def gather_import_node_after_hook(self, vnode, gltf_node, blender_object, import_settings): + self.anim_ext.gather_import_node_after_hook(vnode, gltf_node, blender_object, import_settings) + + def gather_joint_hook(self, gltf2_node, blender_bone, export_settings): + self.geo_ext.gather_joint_hook(gltf2_node, blender_bone, export_settings) + + def gather_scene_hook(self, gltf2_scene, blender_scene, export_settings): + self.append_extension( + gltf2_scene, + self.SCENE_SETTING_EXTENSION_NAME, + {"blender_scale": blender_scene.fast64.sm64.blender_to_sm64_scale}, + ) diff --git a/fast64_internal/sm64/settings/constants.py b/fast64_internal/sm64/settings/constants.py index 3020ab335..5bc87e7a9 100644 --- a/fast64_internal/sm64/settings/constants.py +++ b/fast64_internal/sm64/settings/constants.py @@ -10,6 +10,7 @@ ("C", "C", "C"), ("Binary", "Binary", "Binary"), ("Insertable Binary", "Insertable Binary", "Insertable Binary"), + ("GLTF", "glTF (Animations Only)", "glTF (Animations Only)"), ] enum_compression_formats = [ diff --git a/fast64_internal/sm64/settings/panels.py b/fast64_internal/sm64/settings/panels.py index 031287b7f..57fe3a329 100644 --- a/fast64_internal/sm64/settings/panels.py +++ b/fast64_internal/sm64/settings/panels.py @@ -14,7 +14,7 @@ def draw(self, context: Context): scene = context.scene sm64_props = scene.fast64.sm64 - if sm64_props.export_type == "C": + if sm64_props.legacy_export_type == "C": # If the repo settings tab is open, we pass show_repo_settings as False # because we want to draw those specfic properties in the repo settings box box = col.box().column() diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 0d13cd291..362916e4a 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -119,6 +119,10 @@ class SM64_Properties(PropertyGroup): def binary_export(self): return self.export_type in {"Binary", "Insertable Binary"} + @property + def legacy_export_type(self): + return "C" if self.export_type == "GLTF" else self.export_type + @property def abs_decomp_path(self): return Path(abspath(self.decomp_path)) @@ -137,11 +141,11 @@ def designated(self) -> bool: @property def show_matstack_fix(self) -> bool: - return not self.hackersm64 and self.export_type == "C" + return not self.hackersm64 and self.legacy_export_type == "C" @property def use_matstack_fix(self) -> bool: - return (self.matstack_fix or self.hackersm64) and self.export_type == "C" + return (self.matstack_fix or self.hackersm64) and self.legacy_export_type == "C" @property def gfx_write_method(self): diff --git a/fast64_internal/sm64/sm64_collision.py b/fast64_internal/sm64/sm64_collision.py index d3982cc49..ae732454c 100644 --- a/fast64_internal/sm64/sm64_collision.py +++ b/fast64_internal/sm64/sm64_collision.py @@ -478,7 +478,7 @@ def execute(self, context): try: applyRotation([obj], math.radians(90), "X") - if context.scene.fast64.sm64.export_type == "C": + if context.scene.fast64.sm64.legacy_export_type == "C": export_path, level_name = getPathAndLevel( props.is_actor_custom_export, props.actor_custom_path, @@ -501,7 +501,7 @@ def execute(self, context): level_name, ) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.export_type == "Insertable Binary": + elif context.scene.fast64.sm64.legacy_export_type == "Insertable Binary": exportCollisionInsertableBinary( obj, final_transform, @@ -566,7 +566,7 @@ def execute(self, context): applyRotation([obj], math.radians(-90), "X") - if context.scene.fast64.sm64.export_type == "Binary": + if context.scene.fast64.sm64.legacy_export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -590,7 +590,7 @@ def draw(self, context): props = context.scene.fast64.sm64.combined_export col.prop(context.scene, "colIncludeChildren") - if context.scene.fast64.sm64.export_type == "Insertable Binary": + if context.scene.fast64.sm64.legacy_export_type == "Insertable Binary": col.prop(context.scene, "colInsertableBinaryPath") else: prop_split(col, context.scene, "colStartAddr", "Start Address") diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index ab2183413..e77fd3bc5 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -604,7 +604,7 @@ def execute(self, context): try: applyRotation([obj], radians(90), "X") - if context.scene.fast64.sm64.export_type == "C": + if context.scene.fast64.sm64.legacy_export_type == "C": exportPath, levelName = getPathAndLevel( props.is_actor_custom_export, props.actor_custom_path, @@ -632,7 +632,7 @@ def execute(self, context): starSelectWarning(self, fileStatus) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.export_type == "Insertable Binary": + elif context.scene.fast64.sm64.legacy_export_type == "Insertable Binary": exportF3DtoInsertableBinary( bpy.path.abspath(context.scene.DLInsertableBinaryPath), finalTransform, @@ -711,7 +711,7 @@ def execute(self, context): if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") applyRotation([obj], radians(-90), "X") - if context.scene.fast64.sm64.export_type == "Binary": + if context.scene.fast64.sm64.legacy_export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -731,7 +731,7 @@ def draw(self, context): propsDLE = col.operator(SM64_ExportDL.bl_idname) props = context.scene.fast64.sm64.combined_export - if context.scene.fast64.sm64.export_type == "C": + if context.scene.fast64.sm64.legacy_export_type == "C": col.prop(context.scene, "DLExportisStatic") prop_split(col, props, "export_header_type", "Export Type") @@ -764,7 +764,7 @@ def draw(self, context): props.level_name, ) - elif context.scene.fast64.sm64.export_type == "Insertable Binary": + elif context.scene.fast64.sm64.legacy_export_type == "Insertable Binary": col.prop(context.scene, "DLInsertableBinaryPath") else: prop_split(col, context.scene, "DLExportStart", "Start Address") diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index 9dbc7e113..1da6896da 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -2907,7 +2907,7 @@ def execute(self, context): applyRotation([obj], math.radians(90), "X") save_textures = bpy.context.scene.saveTextures - if context.scene.fast64.sm64.export_type == "C": + if context.scene.fast64.sm64.legacy_export_type == "C": export_path, level_name = getPathAndLevel( props.is_actor_custom_export, props.actor_custom_path, @@ -2932,7 +2932,7 @@ def execute(self, context): DLFormat.Static, ) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.export_type == "Insertable Binary": + elif context.scene.fast64.sm64.legacy_export_type == "Insertable Binary": exportGeolayoutObjectInsertableBinary( obj, final_transform, @@ -3026,7 +3026,7 @@ def execute(self, context): self.cleanup_temp_object_data() applyRotation([obj], math.radians(-90), "X") - if context.scene.fast64.sm64.export_type == "Binary": + if context.scene.fast64.sm64.legacy_export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -3098,7 +3098,7 @@ def execute(self, context): obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) - if context.scene.fast64.sm64.export_type == "C": + if context.scene.fast64.sm64.legacy_export_type == "C": export_path, level_name = getPathAndLevel( props.is_actor_custom_export, props.actor_custom_path, @@ -3128,7 +3128,7 @@ def execute(self, context): ) starSelectWarning(self, fileStatus) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.export_type == "Insertable Binary": + elif context.scene.fast64.sm64.legacy_export_type == "Insertable Binary": exportGeolayoutArmatureInsertableBinary( armatureObj, obj, @@ -3226,7 +3226,7 @@ def execute(self, context): applyRotation([armatureObj] + linkedArmatures, math.radians(-90), "X") - if context.scene.fast64.sm64.export_type == "Binary": + if context.scene.fast64.sm64.legacy_export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -3250,7 +3250,7 @@ def draw(self, context): propsGeoE = col.operator(SM64_ExportGeolayoutArmature.bl_idname) propsGeoE = col.operator(SM64_ExportGeolayoutObject.bl_idname) props = context.scene.fast64.sm64.combined_export - if context.scene.fast64.sm64.export_type == "Insertable Binary": + if context.scene.fast64.sm64.legacy_export_type == "Insertable Binary": col.prop(context.scene, "geoInsertableBinaryPath") else: prop_split(col, context.scene, "geoExportStart", "Start Address") diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index 082ecff62..52f0e6b02 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -1883,7 +1883,7 @@ def export_behavior_script(self, context, props): def verify_context(self, context, props): if context.mode != "OBJECT": raise PluginError("Operator can only be used in object mode.") - if context.scene.fast64.sm64.export_type != "C": + if context.scene.fast64.sm64.legacy_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.anim_object and not props.bhv_object: raise PluginError("No export object selected") @@ -2263,7 +2263,8 @@ def draw_anim_props(self, layout: UILayout, export_type="C", is_dma=False): 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": + legacy_type = "C" if export_type == "GLTF" else export_type + if not is_dma and legacy_type == "C": col.prop(self, "export_single_action") if export_type == "Binary": if not is_dma: diff --git a/fast64_internal/sm64/sm64_utility.py b/fast64_internal/sm64/sm64_utility.py index f408bc17d..4dd9877c5 100644 --- a/fast64_internal/sm64/sm64_utility.py +++ b/fast64_internal/sm64/sm64_utility.py @@ -8,16 +8,18 @@ import re import bpy -from bpy.types import UILayout +from bpy.types import Object, UILayout from ..utility import ( filepath_checks, + intToHex, run_and_draw_errors, multilineLabel, prop_split, as_posix, PluginError, COMMENT_PATTERN, + toAlnum, ) from .sm64_function_map import func_map @@ -454,3 +456,138 @@ def write_includes_with_alternate(path: Path, includes: Optional[list[Path]], be 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) + + +def get_object_actor_name(obj: Object) -> str: + sm64_props = bpy.context.scene.fast64.sm64 + return sm64_props.combined_export.filter_name(toAlnum(obj.name), True) + + +def convert_old_export_enum(export_type: str): + if export_type == "Insertable Binary": + return "INSERTABLE_BINARY" + if export_type == "Binary": + return "BINARY" + return export_type + + +def add_custom_if_not_auto(holder, prop: str, data: dict, blacklist: list[str]): + if getattr(holder, "use_custom_" + prop): + data[prop] = getattr(holder, "custom_" + prop) + blacklist.append("use_custom_" + prop) + blacklist.append("custom_" + prop) + + +def get_custom_from_dict(holder, prop: str, data: dict, blacklist: list[str] | None = None): + value = data.get(prop) + if value is not None: + setattr(holder, "use_custom_" + prop, True) + setattr(holder, "custom_" + prop, value) + if blacklist is not None: + blacklist.append("use_custom_" + prop) + blacklist.append("custom_" + prop) + + +def set_from_dict(holder, dest_prop: str, data: dict, key: str, to_hex: bool = True, default=None): + value = data.get(key) + if value is not None: + setattr(holder, dest_prop, intToHex(value) if to_hex and isinstance(value, int) else value) + elif default is not None: + setattr(holder, dest_prop, intToHex(default) if to_hex and isinstance(default, int) else default) + + +def set_range_from_dict( + holder, start_prop: str, end_prop: str, data: dict, to_hex: bool = True, start_default=None, end_default=None +): + start = data.get("start") + if start is not None: + setattr(holder, start_prop, intToHex(start) if to_hex and isinstance(start, int) else start) + elif start_default is not None: + setattr( + holder, start_prop, intToHex(start_default) if to_hex and isinstance(start_default, int) else start_default + ) + + end = data.get("end") + if end is not None: + setattr(holder, end_prop, intToHex(end) if to_hex and isinstance(end, int) else end) + elif end_default is not None: + setattr(holder, end_prop, intToHex(end_default) if to_hex and isinstance(end_default, int) else end_default) + + +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 remove_actor_includes( + header_type: str, + group_name: str, + header_dir: Path, + dir_name: str, + level_name: str, + data_includes: Optional[list[str | Path]] = None, + header_includes: Optional[list[str | Path]] = None, + geo_includes: Optional[list[str | 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 + 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), + ] + for include in includes + ] + else: + path_and_alternates = [[Path(dir_name, include)] for include in includes] + return write_or_delete_if_found( + path, + to_remove=[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"Removed data includes from {data_path}.") + if write_includes_with_alternate(header_path, header_includes, before_endif=True): + print(f"Removed header includes from {header_path}.") + if write_includes_with_alternate(geo_path, geo_includes): + print(f"Removed geo includes from {geo_path}.") diff --git a/fast64_internal/utility_anim.py b/fast64_internal/utility_anim.py index 74a3aca64..c3e45bfcf 100644 --- a/fast64_internal/utility_anim.py +++ b/fast64_internal/utility_anim.py @@ -308,7 +308,10 @@ def get_action(name: str): def get_slots(action: Action): - return {str(slot.identifier): slot for slot in action.slots if slot.target_id_type == "OBJECT"} + if bpy.app.version >= (5, 0, 0): + return {str(slot.identifier): slot for slot in action.slots if slot.target_id_type == "OBJECT"} + else: + return {} def get_fcurves(action: bpy.types.Action, action_slot: Optional["ActionSlot"] = None) -> "ActionFCurves":