From 5d69e6fa6b75d96e97503f27518c3d0e4fa00d1a Mon Sep 17 00:00:00 2001 From: Lila Date: Wed, 28 Jan 2026 00:04:59 +0000 Subject: [PATCH 1/3] [Codebase] Implement early blender 5.0 support This fixes mesh exports, material warnings and implements action slots into sm64 --- fast64_internal/f3d/f3d_material_helpers.py | 1 + fast64_internal/sm64/animation/exporting.py | 17 ++- fast64_internal/sm64/animation/importing.py | 48 ++++---- fast64_internal/sm64/animation/operators.py | 39 ++++++- fast64_internal/sm64/animation/properties.py | 101 +++++++++++++++-- fast64_internal/sm64/animation/utility.py | 11 ++ fast64_internal/sm64/settings/properties.py | 1 - fast64_internal/sm64/sm64_geolayout_writer.py | 2 +- fast64_internal/utility.py | 4 +- fast64_internal/utility_anim.py | 103 +++++++++++++----- 10 files changed, 261 insertions(+), 66 deletions(-) diff --git a/fast64_internal/f3d/f3d_material_helpers.py b/fast64_internal/f3d/f3d_material_helpers.py index da54ca9d8..56b9a5207 100644 --- a/fast64_internal/f3d/f3d_material_helpers.py +++ b/fast64_internal/f3d/f3d_material_helpers.py @@ -88,6 +88,7 @@ def unlock_material(self): "socket_type", "in_out", "item_type", + "inferred_structure_type", "default_input", # poorly documented, what does it do? ) diff --git a/fast64_internal/sm64/animation/exporting.py b/fast64_internal/sm64/animation/exporting.py index 4705a4a52..35cb5fdef 100644 --- a/fast64_internal/sm64/animation/exporting.py +++ b/fast64_internal/sm64/animation/exporting.py @@ -22,7 +22,7 @@ toAlnum, directory_path_checks, ) -from ...utility_anim import stashActionInArmature +from ...utility_anim import get_fcurve, stashActionInArmature, get_slots from ..sm64_constants import BEHAVIOR_COMMANDS, BEHAVIOR_EXITS, defaultExtendSegment4, level_pointers from ..sm64_utility import ( @@ -99,7 +99,8 @@ def get_entire_fcurve_data( default_values = list(getattr(anim_owner, prop)) populated = [False] * len(default_values) - for fcurve in action.fcurves: + fcurves = get_fcurve(action, get_action_props(action).get_slot(action)) + for fcurve in fcurves: if fcurve.data_path == data_path: array_index = fcurve.array_index for frame in range(max_frame): @@ -149,6 +150,9 @@ def to_xyz(row): def read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, is_owner_obj): pre_export_frame = bpy.context.scene.frame_current pre_export_action = obj.animation_data.action + pre_export_slot = None + if bpy.app.version >= (5, 0, 0): + pre_export_slot = obj.animation_data.action_slot was_playing = bpy.context.screen.is_animation_playing try: @@ -157,6 +161,11 @@ def read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, i for action, action_trans, action_rot, max_frame in zip(actions, trans_values, rot_values, max_frames): print(f'Reading animation data from action "{action.name}".') obj.animation_data.action = action + 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 for frame in range(max_frame): bpy.context.scene.frame_set(frame) @@ -173,6 +182,8 @@ def read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, i action_rot[index : index + 3, frame] = list(local_matrix.to_euler()) 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) if was_playing != bpy.context.screen.is_animation_playing: bpy.ops.screen.animation_play() @@ -327,7 +338,7 @@ def to_table_element_class( return element # Not reference - header_props, action = element_props.get_header(can_reference), element_props.get_action(can_reference) + action, header_props = element_props.get_action_header(can_reference) if not action: raise PluginError("Action is not set.") if not header_props: diff --git a/fast64_internal/sm64/animation/importing.py b/fast64_internal/sm64/animation/importing.py index c9d453546..da70d74f0 100644 --- a/fast64_internal/sm64/animation/importing.py +++ b/fast64_internal/sm64/animation/importing.py @@ -13,7 +13,7 @@ from ...f3d.f3d_parser import math_eval from ...utility import PluginError, decodeSegmentedAddr, filepath_checks, path_checks, intToHex -from ...utility_anim import create_basic_action +from ...utility_anim import create_basic_action, get_fcurve from ..sm64_constants import AnimInfo, level_pointers from ..sm64_level_parser import parseLevelAtPointer @@ -39,6 +39,9 @@ from .constants import ACTOR_PRESET_INFO, TABLE_ENUM_LIST_PATTERN, TABLE_ENUM_PATTERN, TABLE_PATTERN if TYPE_CHECKING: + if bpy.app.version >= (5, 0, 0): + from bpy.types import ActionSlot + from .properties import ( SM64_AnimImportProperties, SM64_ArmatureAnimProperties, @@ -75,15 +78,16 @@ def naive_flip_diff(a1: np.ndarray, a2: np.ndarray) -> np.ndarray: class FramesHolder: frames: np.ndarray = dataclasses.field(default_factory=list) - def populate_action(self, action: Action, pose_bone: PoseBone, path: str): - for property_index in range(3): - f_curve = action.fcurves.new( - data_path=pose_bone.path_from_id(path), - index=property_index, - action_group=pose_bone.name, - ) + def populate_action(self, action: Action, action_slot: "ActionSlot", pose_bone: PoseBone, path: str): + fcurves = get_fcurve(action, action_slot) + for index in range(3): + data_path = pose_bone.path_from_id(path) + if bpy.app.version >= (5, 0, 0): + f_curve = fcurves.new(data_path, index=index, group_name=pose_bone.name) + else: + f_curve = fcurves.new(data_path, index=index, action_group=pose_bone.name) for time, frame in enumerate(self.frames): - f_curve.keyframe_points.insert(time, frame[property_index], options={"FAST"}) + f_curve.keyframe_points.insert(time, frame[index], options={"FAST"}) def euler_to_quaternion(euler_angles: np.ndarray): @@ -133,7 +137,7 @@ def axis_angle(self): result.append([x[1]] + list(x[0])) return result - def populate_action(self, action: Action, pose_bone: PoseBone, path: str = ""): + def populate_action(self, action: Action, action_slot: "ActionSlot", pose_bone: PoseBone, path: str = ""): rotation_mode = pose_bone.rotation_mode rotation_mode_name = { "QUATERNION": "rotation_quaternion", @@ -149,14 +153,14 @@ def populate_action(self, action: Action, pose_bone: PoseBone, path: str = ""): else: rotations = self.get_euler(rotation_mode) size = 3 - for property_index in range(size): - f_curve = action.fcurves.new( - data_path=data_path, - index=property_index, - action_group=pose_bone.name, - ) + fcurves = get_fcurve(action, action_slot) + for index in range(size): + if bpy.app.version >= (5, 0, 0): + f_curve = fcurves.new(data_path, index=index, group_name=pose_bone.name) + else: + f_curve = fcurves.new(data_path, index=index, action_group=pose_bone.name) for frame, rotation in enumerate(rotations): - f_curve.keyframe_points.insert(frame, rotation[property_index], options={"FAST"}) + f_curve.keyframe_points.insert(frame, rotation[index], options={"FAST"}) @dataclasses.dataclass @@ -201,9 +205,9 @@ def read_rotation(self, pairs: list["SM64_AnimPair"], continuity_filter: bool): frames = self.continuity_filter(frames) self.rotation.frames = frames - def populate_action(self, action: Action, pose_bone: PoseBone): - self.translation.populate_action(action, pose_bone, "location") - self.rotation.populate_action(action, pose_bone, "") + def populate_action(self, action: Action, action_slot: "ActionSlot", pose_bone: PoseBone): + self.translation.populate_action(action, action_slot, pose_bone, "location") + self.rotation.populate_action(action, action_slot, pose_bone, "") def from_header_class( @@ -371,7 +375,7 @@ def animation_import_to_blender( force_quaternion: bool, continuity_filter: bool, ): - action = create_basic_action(obj, "") + action, action_slot = create_basic_action(obj) try: if anim_import.data: print("Converting pairs to intermidiate data.") @@ -388,7 +392,7 @@ def animation_import_to_blender( for pose_bone, bone_data in zip(bones, bones_data): if force_quaternion: pose_bone.rotation_mode = "QUATERNION" - bone_data.populate_action(action, pose_bone) + bone_data.populate_action(action, action_slot, pose_bone) from_anim_class(get_action_props(action), action, anim_import, actor_name, use_custom_name, import_type) return action diff --git a/fast64_internal/sm64/animation/operators.py b/fast64_internal/sm64/animation/operators.py index 6e861edca..7dc15f4c7 100644 --- a/fast64_internal/sm64/animation/operators.py +++ b/fast64_internal/sm64/animation/operators.py @@ -16,6 +16,7 @@ animation_operator_checks, check_for_headers_in_table, get_action_props, + get_active_diff_slot, get_anim_obj, get_scene_anim_props, get_anim_props, @@ -43,7 +44,11 @@ def emulate_no_loop(scene: Scene): frame = scene.frame_current header_props = get_action_props(played_action).headers[anim_props.played_header] - _start, loop_start, end = header_props.get_loop_points(played_action) + _start, loop_start, end = ( + anim_props.played_cached_start, + anim_props.played_cached_loop_start, + anim_props.played_cached_loop_end, + ) if header_props.backwards: if frame < loop_start: if header_props.no_loop: @@ -72,14 +77,16 @@ def execute_operator(self, context): played_action = get_action(self.played_action) scene = context.scene anim_props = scene.fast64.sm64.animation + action_props = get_action_props(played_action) context.object.animation_data.action = played_action - action_props = get_action_props(played_action) + if bpy.app.version >= (5, 0, 0): + context.object.animation_data.action_slot = action_props.get_slot(played_action) if self.played_header >= len(action_props.headers): raise ValueError("Invalid Header Index") header_props: SM64_AnimHeaderProperties = action_props.headers[self.played_header] - start_frame = header_props.get_loop_points(played_action)[0] + start_frame, loop_start, end = header_props.get_loop_points(played_action) scene.frame_set(start_frame) scene.render.fps = 30 @@ -89,6 +96,9 @@ def execute_operator(self, context): anim_props.played_header = self.played_header anim_props.played_action = played_action + anim_props.played_cached_start = start_frame + anim_props.played_cached_loop_start = loop_start + anim_props.played_cached_loop_end = end # TODO: update these to use CollectionOperatorBase @@ -247,6 +257,28 @@ def execute_operator(self, context): anim_props.elements[-1].set_variant(action, header_variant) +class SM64_SetActionSlotFromObj(OperatorBase): + bl_idname = "scene.sm64_set_action_slot_from_object" + bl_label = "Set to active slot" + bl_description = "Sets the action slot to the object's active slot" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION_SLOT" + + action_name: StringProperty(name="Action Name", default="") + + @classmethod + def is_enabled(cls, context: Context, action_name: str, **_kwargs): + return get_active_diff_slot(context, get_action(action_name)) is not None + + def execute_operator(self, context): + animation_operator_checks(context) + obj = get_anim_obj(context) + action = get_action(self.action_name) + action_props = get_action_props(action) + action_props.slot_identifier = obj.animation_data.action_slot.identifier + + class SM64_ExportAnimTable(OperatorBase): bl_idname = "scene.sm64_export_anim_table" bl_label = "Export Animation Table" @@ -332,6 +364,7 @@ def update_enum(self, context: Context): SM64_AnimTableOps, SM64_AnimVariantOps, SM64_AddNLATracksToTable, + SM64_SetActionSlotFromObj, SM64_ImportAnim, SM64_SearchAnimPresets, SM64_SearchAnimatedBhvs, diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py index 9e7ad3820..143a30b52 100644 --- a/fast64_internal/sm64/animation/properties.py +++ b/fast64_internal/sm64/animation/properties.py @@ -1,7 +1,12 @@ import os +from typing import NamedTuple import bpy from bpy.types import PropertyGroup, Action, UILayout, Scene, Context + +if bpy.app.version >= (5, 0, 0): + from bpy.types import ActionSlot + from bpy.utils import register_class, unregister_class from bpy.props import ( BoolProperty, @@ -26,7 +31,7 @@ upgrade_old_prop, toAlnum, ) -from ...utility_anim import getFrameInterval +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_constants import MAX_U16, MIN_S16, MAX_S16, enumLevelNames @@ -37,6 +42,7 @@ SM64_AnimTableOps, SM64_AnimVariantOps, SM64_ImportAnim, + SM64_SetActionSlotFromObj, SM64_SearchAnimPresets, SM64_SearchAnimatedBhvs, SM64_SearchAnimTablePresets, @@ -55,6 +61,7 @@ table_name_to_enum, check_for_action_in_table, check_for_headers_in_table, + get_active_diff_slot, ) from .importing import get_enum_from_import_preset, update_table_preset @@ -223,7 +230,7 @@ def manual_loop_range(self) -> tuple[int, int, int]: def get_loop_points(self, action: Action): if self.use_manual_loop: return self.manual_loop_range - loop_start, loop_end = getFrameInterval(action) + loop_start, loop_end = getFrameInterval(action, get_action_props(action).get_slot(action)) return (0, loop_start, loop_end + 1) def get_name(self, actor_name: str, action: Action, dma=False) -> str: @@ -355,9 +362,49 @@ def draw_props( self.draw_flag_props(col, use_int_flags=dma or export_type.endswith("Binary")) +# workaround for garbage collector bug +get_slot_enum_items_cache = [] + + +def get_slot_enum(self, context): + """Generate enum items from the current action’s slots.""" + global get_slot_enum_items_cache + + action = self.id_data + + get_slot_enum_items_cache.clear() + for i, slot in enumerate(get_slots(action).values()): + get_slot_enum_items_cache.append( + ( + str(slot.identifier), + str(slot.name_display), + f"Slot {i}", + "OBJECT_DATA", + i, + ), + ) + + return get_slot_enum_items_cache + + +def get_current_slot(self): + action = self.id_data + slot_keys = list(get_slots(action).keys()) + if self.slot_identifier in slot_keys: + return slot_keys.index(self.slot_identifier) + return 0 + + +def set_current_slot(self, value): + self.slot_identifier = list(get_slots(self.id_data).keys())[value] + + class SM64_ActionAnimProperty(PropertyGroup): """Properties in Action.fast64.sm64.animation""" + slot_identifier: StringProperty(name="Slot Identifier") + slot_enum: EnumProperty(name="Action Slot", items=get_slot_enum, get=get_current_slot, set=set_current_slot) + header: PointerProperty(type=SM64_AnimHeaderProperties) variants_tab: BoolProperty(name="Header Variants") header_variants: CollectionProperty(type=SM64_AnimHeaderProperties) @@ -398,10 +445,18 @@ def get_file_name(self, action: Action, export_type: str, dma=False) -> str: name = clean_name(f"anim_{action.name}", replace=" ") return name + (".inc.c" if export_type == "C" else ".insertable") + def get_slot(self, action: Action): + if bpy.app.version < (5, 0, 0): + return None + slots = get_slots(action) + if len(slots) == 1: + return next(iter(slots.values())) + return slots.get(get_action_props(action).slot_identifier) + def get_max_frame(self, action: Action) -> int: if self.use_custom_max_frame: return self.custom_max_frame - loop_ends: list[int] = [getFrameInterval(action)[1]] + loop_ends: list[int] = [getFrameInterval(action, self.get_slot(action))[1]] header_props: SM64_AnimHeaderProperties for header_props in self.headers: loop_ends.append(header_props.get_loop_points(action)[2]) @@ -474,6 +529,26 @@ def draw_props( col = layout.column() if specific_variant is not None: col.label(text="Action Properties", icon="ACTION") + + if bpy.app.version >= (5, 0, 0): + slots = get_slots(action) + if len(slots) > 1: + prop_split(col, self, "slot_enum", "Action Slot", icon="ACTION_SLOT") + slot = get_active_diff_slot(bpy.context, action) + text = None + if slot is not None: + text = f"Set to active slot ({slot.name_display})" + SM64_SetActionSlotFromObj.draw_props(col, text=text, action_name=action.name) + elif len(slots) == 1: + col.label(text=f"Action Slot: {list(slots.values())[0].name_display}", icon="ACTION_SLOT") + else: + box = col.box() + box.alert = True + box.label(text="Action has no object slots.", icon="ERROR") + box.alert = False + AddSubAction.draw_props(box, action_name=action.name) + col.separator() + if not in_table: if check_for_action_in_table(action, table_elements, dma): if not check_for_headers_in_table(self.headers, table_elements, dma): @@ -526,9 +601,15 @@ def draw_props( self.draw_variants(col, action, dma, actor_name, header_args) +class ActionHeaderTuple(NamedTuple): + action: Action + header: SM64_AnimHeaderProperties + + class SM64_AnimTableElementProperties(PropertyGroup): expand_tab: BoolProperty() action_prop: PointerProperty(name="Action", type=Action) + variant: IntProperty(name="Variant", min=0) reference: BoolProperty(name="Reference") # Toad example @@ -555,16 +636,17 @@ def get_action_header(self, can_reference: bool): self.variant: int self.action_prop: Action if (not can_reference or not self.reference) and self.action_prop: - headers = get_action_props(self.action_prop).headers + action_props = get_action_props(self.action_prop) + headers = action_props.headers if self.variant < len(headers): - return (self.action_prop, headers[self.variant]) - return (None, None) + return ActionHeaderTuple(self.action_prop, headers[self.variant]) + return ActionHeaderTuple(None, None) def get_action(self, can_reference: bool) -> Action | None: - return self.get_action_header(can_reference)[0] + return self.get_action_header(can_reference).action def get_header(self, can_reference: bool) -> SM64_AnimHeaderProperties | None: - return self.get_action_header(can_reference)[1] + return self.get_action_header(can_reference).header def set_variant(self, action: Action, variant: int): self.action_prop = action @@ -899,6 +981,9 @@ class SM64_AnimProperties(PropertyGroup): played_header: IntProperty(min=0) played_action: PointerProperty(name="Action", type=Action) + played_cached_start: IntProperty(min=0) + played_cached_loop_start: IntProperty(min=0) + played_cached_loop_end: IntProperty(min=0) importing: PointerProperty(type=SM64_AnimImportProperties) diff --git a/fast64_internal/sm64/animation/utility.py b/fast64_internal/sm64/animation/utility.py index a618e8e91..ac17834f5 100644 --- a/fast64_internal/sm64/animation/utility.py +++ b/fast64_internal/sm64/animation/utility.py @@ -201,3 +201,14 @@ def check_for_action_in_table( action: "SM64_ActionAnimProperty", table_elements: list["SM64_AnimTableElementProperties"], dma: bool ) -> bool: return any(element.get_action(not dma) == action for element in table_elements) + + +def get_active_diff_slot(context: Context, action: Action = None): + obj = get_anim_obj(context) + if obj is None or action is None: + return None + if obj.animation_data is None or obj.animation_data.action_slot is None: + return None + if obj.animation_data.action != action: + return None + return obj.animation_data.action_slot diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index cb9a57c5d..0a6ea3328 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -157,7 +157,6 @@ def upgrade_changed_props(): "non_decomp_level": {"levelCustomExport"}, "export_header_type": {"geoExportHeaderType", "colExportHeaderType", "animExportHeaderType"}, "custom_include_directory": {"geoTexDir"}, - "binary_level": {"levelAnimExport"}, # as the others binary props get carried over to here we need to update the cur_version again } binary_level_names = {"levelAnimExport", "colExportLevel", "levelDLExport", "levelGeoExport"} diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index c678dbd78..9dbc7e113 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -1328,7 +1328,7 @@ def processMesh( rotate = mathutils.Quaternion() scale = mathutils.Vector((1, 1, 1)) elif obj.get("original_mtx"): # object is instanced or a transformation - orig_mtx = mathutils.Matrix(obj["original_mtx"]) + orig_mtx = mathutils.Matrix(obj.get("original_mtx")) translate, rotate, scale = orig_mtx.decompose() translate = translate_blender_to_n64(translate) rotate = rotate_quat_blender_to_n64(rotate) diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index d045ba14a..8844625a3 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -820,9 +820,9 @@ def store_original_mtx(): # negative scales produce a rotation, we need to remove that since # scales will be applied to the transform for each object loc, rot, _scale = obj.matrix_local.decompose() - obj["original_mtx"] = Matrix.LocRotScale(loc, rot, None) + obj["original_mtx"] = list(Matrix.LocRotScale(loc, rot, None)) loc, rot, scale = obj.matrix_world.decompose() - obj["original_mtx_world"] = Matrix.LocRotScale(loc, rot, scale) + obj["original_mtx_world"] = list(Matrix.LocRotScale(loc, rot, scale)) def rotate_bounds(bounds, mtx: mathutils.Matrix): diff --git a/fast64_internal/utility_anim.py b/fast64_internal/utility_anim.py index 26837e9b5..e2015b39a 100644 --- a/fast64_internal/utility_anim.py +++ b/fast64_internal/utility_anim.py @@ -2,13 +2,16 @@ from bpy.types import Object, Action, AnimData from bpy.utils import register_class, unregister_class from bpy.props import StringProperty +from bpy_extras import anim_utils from .operators import OperatorBase from .utility import attemptModifierApply, raisePluginError, PluginError -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: + if bpy.app.version >= (5, 0, 0): + from bpy.types import ActionSlot from .. import Fast64_Properties from .. import Fast64Settings_Properties @@ -94,6 +97,22 @@ def execute_operator(self, context): stashActionInArmature(context.object, get_action(self.action)) +class AddSubAction(OperatorBase): + bl_idname = "scene.fast64_add_sub_action" + bl_label = "Add Sub Action" + bl_description = "Add a sub action" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION_SLOT" + + action_name: StringProperty() + + def execute_operator(self, context): + if context.object is None: + raise PluginError("No selected object") + assign_action(context.object, get_action(self.action_name)) + + # This code only handles root bone with no parent, which is the only bone that translates. def getTranslationRelativeToRest(bone: bpy.types.Bone, inputVector: mathutils.Vector) -> mathutils.Vector: zUpToYUp = mathutils.Quaternion((1, 0, 0), math.radians(-90.0)).to_matrix().to_4x4() @@ -182,7 +201,7 @@ def saveTranslationFrame(frameData, translation): frameData[i].frames.append(min(int(round(translation[i])), 2**16 - 1)) -def getFrameInterval(action: bpy.types.Action): +def getFrameInterval(action: bpy.types.Action, slot: Optional[bpy.types.ActionSlot] = None): scene = bpy.context.scene fast64_props = scene.fast64 # type: Fast64_Properties @@ -190,29 +209,35 @@ def getFrameInterval(action: bpy.types.Action): anim_range_choice = fast64settings_props.anim_range_choice + def get_action_frame_range(): + if slot is not None: + fcurves = get_fcurve(action, slot) + + min_frame = 0 + max_frame = 0 + for fcu in fcurves: + for kp in fcu.keyframe_points: + f = kp.co.x + min_frame = min(min_frame, f) + max_frame = max(max_frame, f) + + return int(round(min_frame)), int(round(max_frame)) + return int(round(action.frame_range[0])), int(round(action.frame_range[1])) + def getIntersectionInterval(): """ intersect action range and scene range Note: this doesn't handle correctly the case where the two ranges don't intersect, not a big deal """ + frame_start, frame_last = get_action_frame_range() - frame_start = max( - scene.frame_start, - int(round(action.frame_range[0])), - ) + start = max(scene.frame_start, frame_start) + end = min(scene.frame_end, frame_last) - frame_last = max( - min( - scene.frame_end, - int(round(action.frame_range[1])), - ), - frame_start, - ) - - return frame_start, frame_last + return start, max(end, start) range_get_by_choice = { - "action": lambda: (int(round(action.frame_range[0])), int(round(action.frame_range[1]))), + "action": lambda: get_action_frame_range(), "scene": lambda: (int(round(scene.frame_start)), int(round(scene.frame_end))), "intersect_action_and_scene": getIntersectionInterval, } @@ -251,14 +276,27 @@ def stashActionInArmature(obj: Object, action: Action): track.strips.new(action.name, int(action.frame_range[0]), action) -def create_basic_action(obj: Object, name=""): +def assign_action(obj: any, action: Action, create_slot=True): if obj.animation_data is None: obj.animation_data_create() - name = name or "Action" + obj.animation_data.action = action + if create_slot and bpy.app.version >= (5, 0, 0): + slot = action.slots.new(obj.id_type, "Default") + obj.animation_data.action_slot = slot + return slot + return None + + +def create_basic_action_in_data(obj: any, name="Action"): action = bpy.data.actions.new(name) + slot = assign_action(obj, action) + return action, slot + + +def create_basic_action(obj: Object, name="Action"): + action, slot = create_basic_action_in_data(obj, name) stashActionInArmature(obj, action) - obj.animation_data.action = action - return action + return action, slot def get_action(name: str): @@ -269,12 +307,25 @@ def get_action(name: str): return bpy.data.actions[name] -classes = ( - ArmatureApplyWithMeshOperator, - CreateAnimData, - AddBasicAction, - StashAction, -) +def get_slots(action: Action): + return {str(slot.identifier): slot for slot in action.slots if slot.target_id_type == "OBJECT"} + + +def get_fcurve( + action: bpy.types.Action, action_slot: Optional["ActionSlot"] = None, error=True +) -> list[bpy.types.FCurve]: + if bpy.app.version >= (5, 0, 0): + if action_slot is None: + if error: + raise PluginError(f'No action slot provided for action "{action.name}"') + return [] + channelbag = anim_utils.action_ensure_channelbag_for_slot(action, action_slot) + return channelbag.fcurves + else: + return action.fcurves + + +classes = (ArmatureApplyWithMeshOperator, CreateAnimData, AddBasicAction, StashAction, AddSubAction) def utility_anim_register(): From 12c1fc8c891e840c0d8316f5c64d6e45cee5ec9c Mon Sep 17 00:00:00 2001 From: Lila Date: Wed, 28 Jan 2026 00:42:04 +0000 Subject: [PATCH 2/3] casually fix oot, oops --- fast64_internal/f3d/flipbook.py | 13 ++++++--- fast64_internal/sm64/animation/exporting.py | 4 +-- fast64_internal/sm64/animation/importing.py | 16 ++++------- fast64_internal/utility_anim.py | 24 ++++++++++------- .../z64/animation/importer/functions.py | 27 +++++++++++++------ 5 files changed, 51 insertions(+), 33 deletions(-) diff --git a/fast64_internal/f3d/flipbook.py b/fast64_internal/f3d/flipbook.py index 2377dd069..e8192e695 100644 --- a/fast64_internal/f3d/flipbook.py +++ b/fast64_internal/f3d/flipbook.py @@ -256,6 +256,8 @@ def ootFlipbookAnimUpdate(self, armatureObj: bpy.types.Object, segment: str, ind # we use a handler since update functions are not called when a property is animated. @persistent def flipbookAnimHandler(dummy): + from ..utility_anim import get_fcurves + if bpy.context.scene.gameEditorMode in {"OOT", "MM"}: for obj in bpy.data.objects: if obj.type == "ARMATURE": @@ -263,10 +265,15 @@ def flipbookAnimHandler(dummy): # this somewhat mitigates the issue of two skeletons using the same flipbook material. if obj.animation_data is None or obj.animation_data.action is None: continue - action = obj.animation_data.action + action_slot = None + if bpy.app.version >= (5, 0, 0): + action_slot = obj.animation_data.action_slot + if action_slot is None: + continue + + fcurves = get_fcurves(obj.animation_data.action, action_slot) if not ( - action.fcurves.find("ootLinkTextureAnim.eyes") is None - or action.fcurves.find("ootLinkTextureAnim.mouth") is None + fcurves.find("ootLinkTextureAnim.eyes") is None or fcurves.find("ootLinkTextureAnim.mouth") is None ): ootFlipbookAnimUpdate(obj.data, obj, "8", obj.ootLinkTextureAnim.eyes) ootFlipbookAnimUpdate(obj.data, obj, "9", obj.ootLinkTextureAnim.mouth) diff --git a/fast64_internal/sm64/animation/exporting.py b/fast64_internal/sm64/animation/exporting.py index 35cb5fdef..de508b3b3 100644 --- a/fast64_internal/sm64/animation/exporting.py +++ b/fast64_internal/sm64/animation/exporting.py @@ -22,7 +22,7 @@ toAlnum, directory_path_checks, ) -from ...utility_anim import get_fcurve, stashActionInArmature, get_slots +from ...utility_anim import get_fcurves, stashActionInArmature, get_slots from ..sm64_constants import BEHAVIOR_COMMANDS, BEHAVIOR_EXITS, defaultExtendSegment4, level_pointers from ..sm64_utility import ( @@ -99,7 +99,7 @@ def get_entire_fcurve_data( default_values = list(getattr(anim_owner, prop)) populated = [False] * len(default_values) - fcurves = get_fcurve(action, get_action_props(action).get_slot(action)) + fcurves = get_fcurves(action, get_action_props(action).get_slot(action)) for fcurve in fcurves: if fcurve.data_path == data_path: array_index = fcurve.array_index diff --git a/fast64_internal/sm64/animation/importing.py b/fast64_internal/sm64/animation/importing.py index da70d74f0..4027697e3 100644 --- a/fast64_internal/sm64/animation/importing.py +++ b/fast64_internal/sm64/animation/importing.py @@ -13,7 +13,7 @@ from ...f3d.f3d_parser import math_eval from ...utility import PluginError, decodeSegmentedAddr, filepath_checks, path_checks, intToHex -from ...utility_anim import create_basic_action, get_fcurve +from ...utility_anim import create_basic_action, get_fcurves, create_new_fcurve from ..sm64_constants import AnimInfo, level_pointers from ..sm64_level_parser import parseLevelAtPointer @@ -79,13 +79,10 @@ class FramesHolder: frames: np.ndarray = dataclasses.field(default_factory=list) def populate_action(self, action: Action, action_slot: "ActionSlot", pose_bone: PoseBone, path: str): - fcurves = get_fcurve(action, action_slot) + fcurves = get_fcurves(action, action_slot) for index in range(3): data_path = pose_bone.path_from_id(path) - if bpy.app.version >= (5, 0, 0): - f_curve = fcurves.new(data_path, index=index, group_name=pose_bone.name) - else: - f_curve = fcurves.new(data_path, index=index, action_group=pose_bone.name) + f_curve = create_new_fcurve(fcurves, data_path, index=index, action_group=pose_bone.name) for time, frame in enumerate(self.frames): f_curve.keyframe_points.insert(time, frame[index], options={"FAST"}) @@ -153,12 +150,9 @@ def populate_action(self, action: Action, action_slot: "ActionSlot", pose_bone: else: rotations = self.get_euler(rotation_mode) size = 3 - fcurves = get_fcurve(action, action_slot) + fcurves = get_fcurves(action, action_slot) for index in range(size): - if bpy.app.version >= (5, 0, 0): - f_curve = fcurves.new(data_path, index=index, group_name=pose_bone.name) - else: - f_curve = fcurves.new(data_path, index=index, action_group=pose_bone.name) + f_curve = create_new_fcurve(fcurves, data_path, index=index, action_group=pose_bone.name) for frame, rotation in enumerate(rotations): f_curve.keyframe_points.insert(frame, rotation[index], options={"FAST"}) diff --git a/fast64_internal/utility_anim.py b/fast64_internal/utility_anim.py index e2015b39a..76ed91a01 100644 --- a/fast64_internal/utility_anim.py +++ b/fast64_internal/utility_anim.py @@ -1,5 +1,5 @@ import bpy, math, mathutils -from bpy.types import Object, Action, AnimData +from bpy.types import Object, Action, AnimData, FCurve from bpy.utils import register_class, unregister_class from bpy.props import StringProperty from bpy_extras import anim_utils @@ -11,7 +11,7 @@ if TYPE_CHECKING: if bpy.app.version >= (5, 0, 0): - from bpy.types import ActionSlot + from bpy.types import ActionSlot, ActionFCurves from .. import Fast64_Properties from .. import Fast64Settings_Properties @@ -211,7 +211,7 @@ def getFrameInterval(action: bpy.types.Action, slot: Optional[bpy.types.ActionSl def get_action_frame_range(): if slot is not None: - fcurves = get_fcurve(action, slot) + fcurves = get_fcurves(action, slot) min_frame = 0 max_frame = 0 @@ -311,20 +311,26 @@ def get_slots(action: Action): return {str(slot.identifier): slot for slot in action.slots if slot.target_id_type == "OBJECT"} -def get_fcurve( - action: bpy.types.Action, action_slot: Optional["ActionSlot"] = None, error=True -) -> list[bpy.types.FCurve]: +def get_fcurves(action: bpy.types.Action, action_slot: Optional["ActionSlot"] = None) -> "ActionFCurves": + """If action_slot is None in blender 5.0 an exception will still be raised""" if bpy.app.version >= (5, 0, 0): if action_slot is None: - if error: - raise PluginError(f'No action slot provided for action "{action.name}"') - return [] + raise PluginError(f'No action slot provided for action "{action.name}"') channelbag = anim_utils.action_ensure_channelbag_for_slot(action, action_slot) return channelbag.fcurves else: return action.fcurves +def create_new_fcurve( + fcurves: "ActionFCurves", data_path: str, *, index: int | None = 0, action_group: str = "" +) -> FCurve: + if bpy.app.version >= (5, 0, 0): + return fcurves.new(data_path=data_path, index=index, group_name=action_group) + else: + return fcurves.new(data_path=data_path, index=index, action_group=action_group) + + classes = (ArmatureApplyWithMeshOperator, CreateAnimData, AddBasicAction, StashAction, AddSubAction) diff --git a/fast64_internal/z64/animation/importer/functions.py b/fast64_internal/z64/animation/importer/functions.py index da5635915..55f509f79 100644 --- a/fast64_internal/z64/animation/importer/functions.py +++ b/fast64_internal/z64/animation/importer/functions.py @@ -10,6 +10,9 @@ getTranslationRelativeToRest, getRotationRelativeToRest, stashActionInArmature, + create_basic_action, + get_fcurves, + create_new_fcurve, ) from ...utility import ( @@ -101,7 +104,8 @@ def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCu # print(str(frameData) + "\n" + str(jointIndices)) bpy.context.scene.frame_end = frameCount - anim = bpy.data.actions.new(animName) + anim, slot = create_basic_action(armatureObj, animName) + anim_fcurves = get_fcurves(anim, slot) startBoneName = getStartBone(armatureObj) boneStack = [startBoneName] @@ -113,7 +117,8 @@ def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCu for jointIndex in jointIndices: if isRootTranslation: fcurves = [ - anim.fcurves.new( + create_new_fcurve( + anim_fcurves, data_path='pose.bones["' + startBoneName + '"].location', index=propertyIndex, action_group=startBoneName, @@ -142,7 +147,8 @@ def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCu bone, boneStack = getNextBone(boneStack, armatureObj) fcurves = [ - anim.fcurves.new( + create_new_fcurve( + anim_fcurves, data_path='pose.bones["' + bone.name + '"].rotation_euler', index=propertyIndex, action_group=bone.name, @@ -227,7 +233,8 @@ def ootImportLinkAnimationC( print(f"{frameDataName}: {frameCount} frames, {len(frameData)} values.") bpy.context.scene.frame_end = frameCount - anim = bpy.data.actions.new(animHeaderName) + anim, slot = create_basic_action(armatureObj, animHeaderName) + anim_fcurves = get_fcurves(anim, slot) # get ordered list of bone names # create animation curves for each bone @@ -237,11 +244,13 @@ def ootImportLinkAnimationC( boneCurveTranslation = None boneStack = [startBoneName] - eyesCurve = anim.fcurves.new( + eyesCurve = create_new_fcurve( + anim_fcurves, data_path="ootLinkTextureAnim.eyes", action_group="Texture Animations", ) - mouthCurve = anim.fcurves.new( + mouthCurve = create_new_fcurve( + anim_fcurves, data_path="ootLinkTextureAnim.mouth", action_group="Texture Animations", ) @@ -253,7 +262,8 @@ def ootImportLinkAnimationC( if boneCurveTranslation is None: boneCurveTranslation = [ - anim.fcurves.new( + create_new_fcurve( + anim_fcurves, data_path='pose.bones["' + bone.name + '"].location', index=propertyIndex, action_group=startBoneName, @@ -263,7 +273,8 @@ def ootImportLinkAnimationC( boneCurvesRotation.append( [ - anim.fcurves.new( + create_new_fcurve( + anim_fcurves, data_path='pose.bones["' + bone.name + '"].rotation_euler', index=propertyIndex, action_group=bone.name, From 173dd27ebf37db8e99c4f4cf81db0ec517132332 Mon Sep 17 00:00:00 2001 From: Lila Date: Wed, 28 Jan 2026 11:54:03 +0000 Subject: [PATCH 3/3] safe get_attr --- addon_updater_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon_updater_ops.py b/addon_updater_ops.py index b2fd80b1c..590bc7c49 100644 --- a/addon_updater_ops.py +++ b/addon_updater_ops.py @@ -91,7 +91,7 @@ def make_annotations(cls): if bl_props: if '__annotations__' not in cls.__dict__: setattr(cls, '__annotations__', {}) - annotations = cls.__dict__['__annotations__'] + annotations = getattr(cls, "__annotations__", {}) for k, v in bl_props.items(): annotations[k] = v delattr(cls, k)