[SM64] Animations Rewrite/Rework (#467)

* [SM64] Animations Rewrite/Rework

* [SM64] Animations Rewrite/Rework

* Remove animation docs from README until I write new ones in fast64_docs

* remove accidental exports

* merge conflicts?

* Fix level export includes dups

* Fixed comment adjust function

* change table properties to only have elements

* Update properties.py

* Rename

* bounds check

* keep default action name consistent with blender

* as_posix fix and clean up

* designated is include in repo settings, include in tooltip

* Fix empty text files

* fix peach's address

* Some attempts at tasteful comments

Verbose documentation like docstrings is.. not my strong suite

* allow str, use path in f3d writer

* better valid filename func

* fix things!

* review and a personal nit

* Add docs link
This commit is contained in:
Lila
2025-08-21 21:39:33 +01:00
committed by GitHub
parent 5877a949ab
commit 8853a484fd
23 changed files with 5678 additions and 1434 deletions
+12 -2
View File
@@ -15,7 +15,7 @@ from .fast64_internal.repo_settings import (
repo_settings_operators_unregister,
)
from .fast64_internal.sm64 import sm64_register, sm64_unregister
from .fast64_internal.sm64 import sm64_register, sm64_unregister, SM64_ActionProperty
from .fast64_internal.sm64.sm64_constants import sm64_world_defaults
from .fast64_internal.sm64.settings.properties import SM64_Properties
from .fast64_internal.sm64.sm64_geolayout_bone import SM64_BoneProperties
@@ -246,6 +246,14 @@ class Fast64_Properties(bpy.types.PropertyGroup):
renderSettings: bpy.props.PointerProperty(type=Fast64RenderSettings_Properties, name="Fast64 Render Settings")
class Fast64_ActionProperties(bpy.types.PropertyGroup):
"""
Properties in Action.fast64.
"""
sm64: bpy.props.PointerProperty(type=SM64_ActionProperty, name="SM64 Properties")
class Fast64_BoneProperties(bpy.types.PropertyGroup):
"""
Properties in bone.fast64 (bpy.types.Bone)
@@ -315,6 +323,7 @@ classes = (
Fast64RenderSettings_Properties,
ManualUpdatePreviewOperator,
Fast64_Properties,
Fast64_ActionProperties,
Fast64_BoneProperties,
Fast64_ObjectProperties,
F3D_GlobalSettingsPanel,
@@ -464,7 +473,7 @@ def register():
bpy.types.Scene.fast64 = bpy.props.PointerProperty(type=Fast64_Properties, name="Fast64 Properties")
bpy.types.Bone.fast64 = bpy.props.PointerProperty(type=Fast64_BoneProperties, name="Fast64 Bone Properties")
bpy.types.Object.fast64 = bpy.props.PointerProperty(type=Fast64_ObjectProperties, name="Fast64 Object Properties")
bpy.types.Action.fast64 = bpy.props.PointerProperty(type=Fast64_ActionProperties, name="Fast64 Action Properties")
bpy.app.handlers.load_post.append(after_load)
@@ -492,6 +501,7 @@ def unregister():
del bpy.types.Scene.fast64
del bpy.types.Bone.fast64
del bpy.types.Object.fast64
del bpy.types.Action.fast64
repo_settings_operators_unregister()
+59 -5
View File
@@ -1,8 +1,20 @@
import bpy, mathutils, math
from bpy.types import Operator, Context, UILayout
from cProfile import Profile
from pstats import SortKey, Stats
from typing import Optional
import bpy, mathutils
from bpy.types import Operator, Context, UILayout, EnumProperty
from bpy.utils import register_class, unregister_class
from .utility import *
from .f3d.f3d_material import *
from .utility import (
cleanupTempMeshes,
get_mode_set_from_context_mode,
raisePluginError,
parentObject,
store_original_meshes,
store_original_mtx,
)
from .f3d.f3d_material import createF3DMat
def addMaterialByName(obj, matName, preset):
@@ -14,6 +26,9 @@ def addMaterialByName(obj, matName, preset):
material.name = matName
PROFILE_ENABLED = False
class OperatorBase(Operator):
"""Base class for operators, keeps track of context mode and sets it back after running
execute_operator() and catches exceptions for raisePluginError()"""
@@ -21,13 +36,19 @@ class OperatorBase(Operator):
context_mode: str = ""
icon = "NONE"
@classmethod
def is_enabled(cls, context: Context, **op_values):
return True
@classmethod
def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, **op_values):
"""Op args are passed to the operator via setattr()"""
icon = icon if icon else cls.icon
layout = layout.column()
op = layout.operator(cls.bl_idname, icon=icon, text=text)
for key, value in op_values.items():
setattr(op, key, value)
layout.enabled = cls.is_enabled(bpy.context, **op_values)
return op
def execute_operator(self, context: Context):
@@ -40,7 +61,12 @@ class OperatorBase(Operator):
try:
if self.context_mode and self.context_mode != starting_mode_set:
bpy.ops.object.mode_set(mode=self.context_mode)
self.execute_operator(context)
if PROFILE_ENABLED:
with Profile() as profile:
self.execute_operator(context)
print(Stats(profile).strip_dirs().sort_stats(SortKey.CUMULATIVE).print_stats())
else:
self.execute_operator(context)
return {"FINISHED"}
except Exception as exc:
raisePluginError(self, exc)
@@ -53,6 +79,34 @@ class OperatorBase(Operator):
bpy.ops.object.mode_set(mode=starting_mode_set)
class SearchEnumOperatorBase(OperatorBase):
bl_description = "Search Enum"
bl_label = "Search"
bl_property = None
bl_options = {"UNDO"}
@classmethod
def draw_props(cls, layout: UILayout, data, prop: str, name: str):
row = layout.row()
if name:
row.label(text=name)
row.prop(data, prop, text="")
row.operator(cls.bl_idname, icon="VIEWZOOM", text="")
def update_enum(self, context: Context):
raise NotImplementedError()
def execute_operator(self, context: Context):
assert self.bl_property
self.report({"INFO"}, f"Selected: {getattr(self, self.bl_property)}")
self.update_enum(context)
context.region.tag_redraw()
def invoke(self, context: Context, _):
context.window_manager.invoke_search_popup(self)
return {"RUNNING_MODAL"}
class AddWaterBox(OperatorBase):
bl_idname = "object.add_water_box"
bl_label = "Add Water Box"
+3 -32
View File
@@ -52,38 +52,7 @@ For example, for Mario you would rotate the four limb joints around the Y-axis 1
Then after applying the rest pose and skinning, you would apply those operations in reverse order then apply rest pose again.
### Importing/Exporting Binary SM64 Animations (Not Mario)
- Note: SM64 animations only allow for rotations, and translation only on the root bone.
- Download Quad64, open the desired level, and go to Misc -> Script Dumps.
- Go to the objects header, find the object you want, and view the Behaviour Script tab.
- For most models with animation, you can will see a 27 command, and optionally a 28 command.
For importing:
- The last 4 bytes of the 27 command will be the animation list pointer.
- Make sure 'Is DMA Animation' is unchecked, 'Is Anim List' is checked, and 'Is Segmented Pointer' is checked.
- Set the animation importer start address as those 4 bytes.
- If a 28 command exists, then the second byte will be the anim list index.
- Otherwise, the anim list index is usually 0.
For exporting:
- Make sure 'Set Anim List Entry' is checked.
- Copy the addresses of the 27 command, which is the first number before the slash on that line.
- Optionally do the same for the 28 command, which may not exist.
- If a 28 command exists, then the second byte will be the anim list index.
- Otherwise, the anim list index is usually 0.
Select an armature for the animation, and press 'Import/Export animation'.
### Importing/Exporting Binary Mario Animations
Mario animations use a DMA table, which contains 8 byte entries of (offset from table start, animation size). Documentation about this table is here:
https://dudaw.webs.com/sm64docs/sm64_marios_animation_table.txt
Basically, Mario's DMA table starts at 0x4EC000. There is an 8 byte header, and then the animation entries afterward. Thus the 'climb up ledge' DMA entry is at 0x4EC008. The first 4 bytes at that address indicate the offset from 0x4EC000 at which the actual animation exists. Thus the 'climb up ledge' animation entry address is at 0x4EC690. Using this table you can find animations you want to overwrite. Make sure the 'Is DMA Animation' option is checked and 'Is Segmented Pointer' is unchecked when importing/exporting. Check "Overwrite DMA Entry", set the start address to 4EC000 (for Mario), and set the entry address to the DMA entry obtained previously.
### Animating Existing Geolayouts
Often times it is hard to rig an existing SM64 geolayout, as there are many intermediate non-deform bones and bones don't point to their children. To make this easier you can use the 'Create Animatable Metarig' operator in the SM64 Armature Tools header. This will generate a metarig which can be used with IK. The metarig bones will be placed on armature layers 3 and 4.
### [Animations](https://fast64.readthedocs.io/en/latest/sm64/animations/index.html)
## Decomp
To start, set your base decomp folder in SM64 General Settings. This allows the plugin to automatically add headers/includes to the correct locations. You can always choose to export to a custom location, although headers/includes won't be written.
@@ -165,6 +134,8 @@ Insertable Binary exporting will generate a binary file, with a header containin
1 = Geolayout
2 = Animation
3 = Collision
4 = Animation Table
5 = Animation DMA Table
0x04-0x08 : Data Size (size in bytes of Data Section)
0x08-0x0C : Start Address (start address of data, relative to start of Data Section)
+24 -9
View File
@@ -1,3 +1,7 @@
from bpy.types import PropertyGroup
from bpy.props import PointerProperty
from bpy.utils import register_class, unregister_class
from .settings import (
settings_props_register,
settings_props_unregister,
@@ -83,14 +87,23 @@ from .sm64_f3d_writer import (
sm64_dl_writer_unregister,
)
from .sm64_anim import (
sm64_anim_panel_register,
sm64_anim_panel_unregister,
sm64_anim_register,
sm64_anim_unregister,
from .animation import (
anim_panel_register,
anim_panel_unregister,
anim_register,
anim_unregister,
SM64_ActionAnimProperty,
)
class SM64_ActionProperty(PropertyGroup):
"""
Properties in Action.fast64.sm64.
"""
animation: PointerProperty(type=SM64_ActionAnimProperty, name="SM64 Properties")
def sm64_panel_register():
settings_panels_register()
tools_panels_register()
@@ -103,7 +116,7 @@ def sm64_panel_register():
sm64_spline_panel_register()
sm64_dl_writer_panel_register()
sm64_dl_parser_panel_register()
sm64_anim_panel_register()
anim_panel_register()
def sm64_panel_unregister():
@@ -118,12 +131,13 @@ def sm64_panel_unregister():
sm64_spline_panel_unregister()
sm64_dl_writer_panel_unregister()
sm64_dl_parser_panel_unregister()
sm64_anim_panel_unregister()
anim_panel_unregister()
def sm64_register(register_panels: bool):
tools_operators_register()
tools_props_register()
anim_register()
sm64_col_register()
sm64_bone_register()
sm64_cam_register()
@@ -134,8 +148,8 @@ def sm64_register(register_panels: bool):
sm64_spline_register()
sm64_dl_writer_register()
sm64_dl_parser_register()
sm64_anim_register()
settings_props_register()
register_class(SM64_ActionProperty)
if register_panels:
sm64_panel_register()
@@ -144,6 +158,7 @@ def sm64_register(register_panels: bool):
def sm64_unregister(unregister_panels: bool):
tools_operators_unregister()
tools_props_unregister()
anim_unregister()
sm64_col_unregister()
sm64_bone_unregister()
sm64_cam_unregister()
@@ -154,8 +169,8 @@ def sm64_unregister(unregister_panels: bool):
sm64_spline_unregister()
sm64_dl_writer_unregister()
sm64_dl_parser_unregister()
sm64_anim_unregister()
settings_props_unregister()
unregister_class(SM64_ActionProperty)
if unregister_panels:
sm64_panel_unregister()
@@ -0,0 +1,15 @@
from .operators import anim_ops_register, anim_ops_unregister
from .properties import anim_props_register, anim_props_unregister, SM64_ArmatureAnimProperties, SM64_ActionAnimProperty
from .panels import anim_panel_register, anim_panel_unregister
from .exporting import export_animation, export_animation_table
from .utility import get_anim_obj, is_obj_animatable
def anim_register():
anim_ops_register()
anim_props_register()
def anim_unregister():
anim_ops_unregister()
anim_props_unregister()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,88 @@
import struct
import re
from ...utility import intToHex
from ..sm64_constants import ACTOR_PRESET_INFO, ActorPresetInfo
HEADER_STRUCT = struct.Struct(">h h h h h h I I I")
HEADER_SIZE = HEADER_STRUCT.size
TABLE_ELEMENT_PATTERN = re.compile( # strict but only in the sense that it requires valid c code
r"""
(?:\[\s*(?P<enum>\w+)\s*\]\s*=\s*)? # Don´t capture brackets or equal, works with nums
(?:(?:&\s*(?P<element>\w+))|(?P<null>NULL)) # Capture element or null, element requires &
(?:\s*,|) # allow no comma, techinically not correct but no other method works
""",
re.DOTALL | re.VERBOSE | re.MULTILINE,
)
TABLE_PATTERN = re.compile(
r"""
const\s+struct\s*Animation\s*\*const\s*(?P<name>\w+)\s*
(?:\[.*?\])? # Optional size, don´t capture
\s*=\s*\{
(?P<content>[\s\S]*) # Capture any character including new lines
(?=\}\s*;) # Look ahead for the end
""",
re.DOTALL | re.VERBOSE | re.MULTILINE,
)
TABLE_ENUM_PATTERN = re.compile( # strict but only in the sense that it requires valid c code
r"""
(?P<name>\w+)\s*
(?:\s*=\s*(?P<num>\w+)\s*)?
(?=,|) # lookahead, allow no comma, techinically not correct but no other method works
""",
re.DOTALL | re.VERBOSE | re.MULTILINE,
)
TABLE_ENUM_LIST_PATTERN = re.compile(
r"""
enum\s*(?P<name>\w+)\s*\{
(?P<content>[\s\S]*) # Capture any character including new lines, lazy
(?=\}\s*;)
""",
re.DOTALL | re.VERBOSE | re.MULTILINE,
)
enumAnimExportTypes = [
("Actor", "Actor Data", "Includes are added to a group in actors/"),
("Level", "Level Data", "Includes are added to a specific level in levels/"),
(
"DMA",
"DMA (Mario)",
"No headers or includes are genarated. Mario animation converter order is used (headers, indicies, values)",
),
("Custom", "Custom Path", "Exports to a specific path"),
]
enum_anim_import_types = [
("C", "C", "Import a decomp folder or a specific animation"),
("Binary", "Binary", "Import from ROM"),
("Insertable Binary", "Insertable Binary", "Import from an insertable binary file"),
]
enum_anim_binary_import_types = [
("DMA", "DMA (Mario)", "Import a DMA animation from a DMA table from a ROM"),
("Table", "Table", "Import animations from an animation table from a ROM"),
("Animation", "Animation", "Import one animation from a ROM"),
]
enum_animated_behaviours = [("Custom", "Custom Behavior", "Custom"), ("", "Presets", "")]
enum_anim_tables = [("Custom", "Custom", "Custom"), ("", "Presets", "")]
for actor_name, preset_info in ACTOR_PRESET_INFO.items():
if not preset_info.animation:
continue
behaviours = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.behaviours)
enum_animated_behaviours.extend(
[(intToHex(address), name, intToHex(address)) for name, address in behaviours.items()]
)
tables = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.address)
enum_anim_tables.extend(
[(name, name, f"{intToHex(address)}, {preset_info.level}") for name, address in tables.items()]
)
File diff suppressed because it is too large Load Diff
+808
View File
@@ -0,0 +1,808 @@
from typing import TYPE_CHECKING, Optional
from pathlib import Path
import dataclasses
import functools
import os
import re
import numpy as np
import bpy
from bpy.path import abspath
from bpy.types import Object, Action, Context, PoseBone
from mathutils import Quaternion
from ...f3d.f3d_parser import math_eval
from ...utility import PluginError, decodeSegmentedAddr, filepath_checks, path_checks, intToHex
from ...utility_anim import create_basic_action
from ..sm64_constants import AnimInfo, level_pointers
from ..sm64_level_parser import parseLevelAtPointer
from ..sm64_utility import CommentMatch, get_comment_map, adjust_start_end, import_rom_checks
from ..sm64_classes import RomReader
from .utility import (
animation_operator_checks,
get_action_props,
get_anim_owners,
get_scene_anim_props,
get_anim_actor_name,
anim_name_to_enum_name,
table_name_to_enum,
)
from .classes import (
SM64_Anim,
CArrayDeclaration,
SM64_AnimHeader,
SM64_AnimTable,
SM64_AnimTableElement,
)
from .constants import ACTOR_PRESET_INFO, TABLE_ENUM_LIST_PATTERN, TABLE_ENUM_PATTERN, TABLE_PATTERN
if TYPE_CHECKING:
from .properties import (
SM64_AnimImportProperties,
SM64_ArmatureAnimProperties,
SM64_AnimHeaderProperties,
SM64_ActionAnimProperty,
SM64_AnimTableElementProperties,
)
from ..settings.properties import SM64_Properties
def get_preset_anim_name_list(preset_name: str):
assert preset_name in ACTOR_PRESET_INFO, "Selected preset not in actor presets"
preset = ACTOR_PRESET_INFO[preset_name]
assert preset.animation is not None and isinstance(
preset.animation, AnimInfo
), "Selected preset's actor has not animation information"
return preset.animation.names
def flip_euler(euler: np.ndarray) -> np.ndarray:
euler = euler.copy()
euler[1] = -euler[1]
euler += np.pi
return euler
def naive_flip_diff(a1: np.ndarray, a2: np.ndarray) -> np.ndarray:
diff = a1 - a2
mask = np.abs(diff) > np.pi
return a2 + mask * np.sign(diff) * 2 * np.pi
@dataclasses.dataclass
class FramesHolder:
frames: np.ndarray = dataclasses.field(default_factory=list)
def populate_action(self, action: Action, pose_bone: PoseBone, path: str):
for property_index in range(3):
f_curve = action.fcurves.new(
data_path=pose_bone.path_from_id(path),
index=property_index,
action_group=pose_bone.name,
)
for time, frame in enumerate(self.frames):
f_curve.keyframe_points.insert(time, frame[property_index], options={"FAST"})
def euler_to_quaternion(euler_angles: np.ndarray):
"""
Fast vectorized euler to quaternion function, euler_angles is an array of shape (-1, 3)
"""
phi = euler_angles[:, 0]
theta = euler_angles[:, 1]
psi = euler_angles[:, 2]
half_phi = phi / 2.0
half_theta = theta / 2.0
half_psi = psi / 2.0
cos_half_phi = np.cos(half_phi)
sin_half_phi = np.sin(half_phi)
cos_half_theta = np.cos(half_theta)
sin_half_theta = np.sin(half_theta)
cos_half_psi = np.cos(half_psi)
sin_half_psi = np.sin(half_psi)
q_w = cos_half_phi * cos_half_theta * cos_half_psi + sin_half_phi * sin_half_theta * sin_half_psi
q_x = sin_half_phi * cos_half_theta * cos_half_psi - cos_half_phi * sin_half_theta * sin_half_psi
q_y = cos_half_phi * sin_half_theta * cos_half_psi + sin_half_phi * cos_half_theta * sin_half_psi
q_z = cos_half_phi * cos_half_theta * sin_half_psi - sin_half_phi * sin_half_theta * cos_half_psi
quaternions = np.vstack((q_w, q_x, q_y, q_z)).T # shape (-1, 4)
return quaternions
@dataclasses.dataclass
class RotationFramesHolder(FramesHolder):
@property
def quaternion(self):
return euler_to_quaternion(self.frames) # We make this code path as optiomal as it can be
def get_euler(self, order: str):
if order == "XYZ":
return self.frames
return [Quaternion(x).to_euler(order) for x in self.quaternion]
@property
def axis_angle(self):
result = []
for x in self.quaternion:
x = Quaternion(x).to_axis_angle()
result.append([x[1]] + list(x[0]))
return result
def populate_action(self, action: Action, pose_bone: PoseBone, path: str = ""):
rotation_mode = pose_bone.rotation_mode
rotation_mode_name = {
"QUATERNION": "rotation_quaternion",
"AXIS_ANGLE": "rotation_axis_angle",
}.get(rotation_mode, "rotation_euler")
data_path = pose_bone.path_from_id(rotation_mode_name)
size = 4
if rotation_mode == "QUATERNION":
rotations = self.quaternion
elif rotation_mode == "AXIS_ANGLE":
rotations = self.axis_angle
else:
rotations = self.get_euler(rotation_mode)
size = 3
for property_index in range(size):
f_curve = action.fcurves.new(
data_path=data_path,
index=property_index,
action_group=pose_bone.name,
)
for frame, rotation in enumerate(rotations):
f_curve.keyframe_points.insert(frame, rotation[property_index], options={"FAST"})
@dataclasses.dataclass
class IntermidiateAnimationBone:
translation: FramesHolder = dataclasses.field(default_factory=FramesHolder)
rotation: RotationFramesHolder = dataclasses.field(default_factory=RotationFramesHolder)
def read_pairs(self, pairs: list["SM64_AnimPair"]):
pair_count = len(pairs)
max_length = max(len(pair.values) for pair in pairs)
result = np.empty((max_length, pair_count), dtype=np.int16)
for i, pair in enumerate(pairs):
current_length = len(pair.values)
result[:current_length, i] = pair.values
result[current_length:, i] = pair.values[-1]
return result
def read_translation(self, pairs: list["SM64_AnimPair"], scale: float):
self.translation.frames = self.read_pairs(pairs) / scale
def continuity_filter(self, frames: np.ndarray) -> np.ndarray:
if len(frames) <= 1:
return frames
# There is no way to fully vectorize this function
prev = frames[0]
for frame, euler in enumerate(frames):
euler = naive_flip_diff(prev, euler)
flipped_euler = naive_flip_diff(prev, flip_euler(euler))
if np.all((prev - flipped_euler) ** 2 < (prev - euler) ** 2):
euler = flipped_euler
frames[frame] = prev = euler
return frames
def read_rotation(self, pairs: list["SM64_AnimPair"], continuity_filter: bool):
frames = self.read_pairs(pairs).astype(np.uint16).astype(np.float32)
frames *= 360.0 / (2**16)
frames = np.radians(frames)
if continuity_filter:
frames = self.continuity_filter(frames)
self.rotation.frames = frames
def populate_action(self, action: Action, pose_bone: PoseBone):
self.translation.populate_action(action, pose_bone, "location")
self.rotation.populate_action(action, pose_bone, "")
def from_header_class(
header_props: "SM64_AnimHeaderProperties",
header: SM64_AnimHeader,
action: Action,
actor_name: str,
use_custom_name: bool,
):
if isinstance(header.reference, str) and header.reference != header_props.get_name(actor_name, action):
header_props.custom_name = header.reference
if use_custom_name:
header_props.use_custom_name = True
if header.enum_name and header.enum_name != header_props.get_enum(actor_name, action):
header_props.custom_enum = header.enum_name
header_props.use_custom_enum = True
correct_loop_points = header.start_frame, header.loop_start, header.loop_end
header_props.start_frame, header_props.loop_start, header_props.loop_end = correct_loop_points
if correct_loop_points != header_props.get_loop_points(action): # check if auto loop points don´t match
header_props.use_manual_loop = True
header_props.trans_divisor = header.trans_divisor
header_props.set_flags(header.flags)
header_props.table_index = header.table_index
def from_anim_class(
action_props: "SM64_ActionAnimProperty",
action: Action,
animation: SM64_Anim,
actor_name: str,
use_custom_name: bool,
import_type: str,
):
main_header = animation.headers[0]
is_from_binary = import_type.endswith("Binary")
if animation.action_name:
action_name = animation.action_name
elif main_header.file_name:
action_name = main_header.file_name.removesuffix(".c").removesuffix(".inc")
elif is_from_binary:
action_name = intToHex(main_header.reference)
action.name = action_name.removeprefix("anim_")
print(f'Populating action "{action.name}" properties.')
indice_reference, values_reference = main_header.indice_reference, main_header.values_reference
if is_from_binary:
action_props.indices_address, action_props.values_address = intToHex(indice_reference), intToHex(
values_reference
)
else:
action_props.indices_table, action_props.values_table = indice_reference, values_reference
if animation.data:
file_name = animation.data.indices_file_name
action_props.custom_max_frame = max([1] + [len(x.values) for x in animation.data.pairs])
if action_props.get_max_frame(action) != action_props.custom_max_frame:
action_props.use_custom_max_frame = True
else:
file_name = main_header.file_name
action_props.reference_tables = True
if file_name:
action_props.custom_file_name = file_name
if use_custom_name and action_props.get_file_name(action, import_type) != action_props.custom_file_name:
action_props.use_custom_file_name = True
if is_from_binary:
start_addresses = [x.reference for x in animation.headers]
end_addresses = [x.end_address for x in animation.headers]
if animation.data:
start_addresses.append(animation.data.start_address)
end_addresses.append(animation.data.end_address)
action_props.start_address = intToHex(min(start_addresses))
action_props.end_address = intToHex(max(end_addresses))
print("Populating header properties.")
for i, header in enumerate(animation.headers):
if i:
action_props.header_variants.add()
header_props = action_props.headers[-1]
header.action = action # Used in table class to prop
from_header_class(header_props, header, action, actor_name, use_custom_name)
action_props.update_variant_numbers()
def from_table_element_class(
element_props: "SM64_AnimTableElementProperties",
element: SM64_AnimTableElement,
use_custom_name: bool,
actor_name: str,
prev_enums: dict[str, int],
):
if element.header:
assert element.header.action
element_props.set_variant(element.header.action, element.header.header_variant)
else:
element_props.reference = True
if isinstance(element.reference, int):
element_props.header_address = intToHex(element.reference)
else:
element_props.header_name = element.c_name
element_props.header_address = intToHex(0)
if element.enum_name:
element_props.custom_enum = element.enum_name
if use_custom_name and element.enum_name != element_props.get_enum(True, actor_name, prev_enums):
element_props.use_custom_enum = True
def from_anim_table_class(
anim_props: "SM64_ArmatureAnimProperties",
table: SM64_AnimTable,
clear_table: bool,
use_custom_name: bool,
actor_name: str,
):
if clear_table:
anim_props.elements.clear()
anim_props.null_delimiter = table.has_null_delimiter
prev_enums: dict[str, int] = {}
for i, element in enumerate(table.elements):
if anim_props.null_delimiter and i == len(table.elements) - 1:
break
anim_props.elements.add()
from_table_element_class(anim_props.elements[-1], element, use_custom_name, actor_name, prev_enums)
if isinstance(table.reference, int): # Binary
anim_props.dma_address = intToHex(table.reference)
anim_props.dma_end_address = intToHex(table.end_address)
anim_props.address = intToHex(table.reference)
anim_props.end_address = intToHex(table.end_address)
# Data
start_addresses = []
end_addresses = []
for element in table.elements:
if element.header and element.header.data:
start_addresses.append(element.header.data.start_address)
end_addresses.append(element.header.data.end_address)
if start_addresses and end_addresses:
anim_props.write_data_seperately = True
anim_props.data_address = intToHex(min(start_addresses))
anim_props.data_end_address = intToHex(max(end_addresses))
elif isinstance(table.reference, str) and table.reference: # C
if use_custom_name:
anim_props.custom_table_name = table.reference
if anim_props.get_table_name(actor_name) != anim_props.custom_table_name:
anim_props.use_custom_table_name = True
def animation_import_to_blender(
obj: Object,
blender_to_sm64_scale: float,
anim_import: SM64_Anim,
actor_name: str,
use_custom_name: bool,
import_type: str,
force_quaternion: bool,
continuity_filter: bool,
):
action = create_basic_action(obj, "")
try:
if anim_import.data:
print("Converting pairs to intermidiate data.")
bones = get_anim_owners(obj)
bones_data: list[IntermidiateAnimationBone] = []
pairs = anim_import.data.pairs
for pair_num in range(3, len(pairs), 3):
bone = IntermidiateAnimationBone()
if pair_num == 3:
bone.read_translation(pairs[0:3], blender_to_sm64_scale)
bone.read_rotation(pairs[pair_num : pair_num + 3], continuity_filter)
bones_data.append(bone)
print("Populating action keyframes.")
for pose_bone, bone_data in zip(bones, bones_data):
if force_quaternion:
pose_bone.rotation_mode = "QUATERNION"
bone_data.populate_action(action, pose_bone)
from_anim_class(get_action_props(action), action, anim_import, actor_name, use_custom_name, import_type)
return action
except PluginError as exc:
bpy.data.actions.remove(action)
raise exc
def update_table_with_table_enum(table: SM64_AnimTable, enum_table: SM64_AnimTable):
for element, enum_element in zip(table.elements, enum_table.elements):
if element.enum_name:
enum_element = next(
(
other_enum_element
for other_enum_element in enum_table.elements
if element.enum_name == other_enum_element.enum_name
),
enum_element,
)
element.enum_name = enum_element.enum_name
element.enum_val = enum_element.enum_val
element.enum_start = enum_element.enum_start
element.enum_end = enum_element.enum_end
table.enum_list_reference = enum_table.enum_list_reference
table.enum_list_start = enum_table.enum_list_start
table.enum_list_end = enum_table.enum_list_end
def import_enums(c_data: str, path: Path, comment_map: list[CommentMatch], specific_name=""):
tables = []
for list_match in re.finditer(TABLE_ENUM_LIST_PATTERN, c_data):
name, content = list_match.group("name"), list_match.group("content")
if name is None and content is None: # comment
continue
if specific_name and name != specific_name:
continue
list_start, list_end = adjust_start_end(c_data.find(content, list_match.start()), list_match.end(), comment_map)
content = c_data[list_start:list_end]
table = SM64_AnimTable(
file_name=path.name,
enum_list_reference=name,
enum_list_start=list_start,
enum_list_end=list_end,
)
for element_match in re.finditer(TABLE_ENUM_PATTERN, content):
name, num = (element_match.group("name"), element_match.group("num"))
if name is None and num is None: # comment
continue
enum_start, enum_end = adjust_start_end(
list_start + element_match.start(), list_start + element_match.end(), comment_map
)
table.elements.append(
SM64_AnimTableElement(
enum_name=name, enum_val=num, enum_start=enum_start - list_start, enum_end=enum_end - list_start
)
)
tables.append(table)
return tables
def import_tables(
c_data: str,
path: Path,
comment_map: list[CommentMatch],
specific_name="",
header_decls: Optional[list[CArrayDeclaration]] = None,
values_decls: Optional[list[CArrayDeclaration]] = None,
indices_decls: Optional[list[CArrayDeclaration]] = None,
):
read_headers = {}
header_decls, values_decls, indices_decls = (
header_decls or [],
values_decls or [],
indices_decls or [],
)
tables: list[SM64_AnimTable] = []
for table_match in re.finditer(TABLE_PATTERN, c_data):
table_elements = []
name, content = table_match.group("name"), table_match.group("content")
if name is None and content is None: # comment
continue
if specific_name and name != specific_name:
continue
table = SM64_AnimTable(name, file_name=path.name, elements=table_elements)
table.read_c(
c_data,
c_data.find(content, table_match.start()),
table_match.end(),
comment_map,
read_headers,
header_decls,
values_decls,
indices_decls,
)
tables.append(table)
return tables
DECL_PATTERN = re.compile(
r"(static\s+const\s+struct\s+Animation|static\s+const\s+u16|static\s+const\s+s16)\s+"
r"(\w+)\s*?(?:\[.*?\])?\s*?=\s*?\{(.*?)\s*?\};",
re.DOTALL,
)
VALUE_SPLIT_PATTERN = re.compile(r"\s*(?:(?:\.(?P<var>\w+)|\[\s*(?P<designator>.*?)\s*\])\s*=\s*)?(?P<val>.+?)(?:,|\Z)")
def find_decls(c_data: str, path: Path, decl_list: dict[str, list[CArrayDeclaration]]):
"""At this point a generilized c parser would be better"""
matches = DECL_PATTERN.findall(c_data)
for decl_type, name, value_text in matches:
values = []
for match in VALUE_SPLIT_PATTERN.finditer(value_text):
var, designator, val = match.group("var"), match.group("designator"), match.group("val")
assert val is not None
if designator is not None:
designator = math_eval(designator, object())
if isinstance(designator, int):
if isinstance(values, dict):
raise PluginError("Invalid mix of designated initializers")
first_val = values[0] if values else "0"
values.extend([first_val] * (designator + 1 - len(values)))
else:
if not values:
values = {}
elif isinstance(values, list):
raise PluginError("Invalid mix of designated initializers")
values[designator] = val
elif var is not None:
if not values:
values = {}
elif isinstance(values, list):
raise PluginError("Mix of designated and positional variable assignment")
values[var] = val
else:
if isinstance(values, dict):
raise PluginError("Mix of designated and positional variable assignment")
values.append(val)
decl_list[decl_type].append(CArrayDeclaration(name, path, path.name, values))
def import_c_animations(path: Path) -> tuple[SM64_AnimTable | None, dict[str, SM64_AnimHeader]]:
path_checks(path)
if path.is_file():
file_paths = [path]
elif path.is_dir():
file_paths = sorted([f for f in path.rglob("*") if f.suffix in {".c", ".h"}])
else:
raise PluginError("Path is neither a file or a folder but it exists, somehow.")
print("Reading from:\n" + "\n".join([f.name for f in file_paths]))
c_files = {file_path: get_comment_map(file_path.read_text()) for file_path in file_paths}
decl_lists = {"static const struct Animation": [], "static const u16": [], "static const s16": []}
header_decls, indices_decls, value_decls = (
decl_lists["static const struct Animation"],
decl_lists["static const u16"],
decl_lists["static const s16"],
)
tables: list[SM64_AnimTable] = []
enum_lists: list[SM64_AnimTable] = []
for file_path, (comment_less, _comment_map) in c_files.items():
find_decls(comment_less, file_path, decl_lists)
for file_path, (comment_less, comment_map) in c_files.items():
tables.extend(import_tables(comment_less, file_path, comment_map, "", header_decls, value_decls, indices_decls))
enum_lists.extend(import_enums(comment_less, file_path, comment_map))
if len(tables) > 1:
raise ValueError("More than 1 table declaration")
elif len(tables) == 1:
table: SM64_AnimTable = tables[0]
if enum_lists:
enum_table = next( # find enum with the same name or use the first
(
enum_table
for enum_table in enum_lists
if enum_table.reference == table_name_to_enum(table.reference)
),
enum_lists[0],
)
update_table_with_table_enum(table, enum_table)
read_headers = {header.reference: header for header in table.header_set}
return table, read_headers
else:
read_headers: dict[str, SM64_AnimHeader] = {}
for table_index, header_decl in enumerate(sorted(header_decls, key=lambda h: h.name)):
SM64_AnimHeader().read_c(header_decl, value_decls, indices_decls, read_headers, table_index)
return None, read_headers
def import_binary_animations(
data_reader: RomReader,
import_type: str,
read_headers: dict[str, SM64_AnimHeader],
table: SM64_AnimTable,
table_index: Optional[int] = None,
bone_count: Optional[int] = None,
table_size: Optional[int] = None,
):
if import_type == "Table":
table.read_binary(data_reader, read_headers, table_index, bone_count, table_size)
elif import_type == "DMA":
table.read_dma_binary(data_reader, read_headers, table_index, bone_count)
elif import_type == "Animation":
SM64_AnimHeader.read_binary(
data_reader,
read_headers,
False,
bone_count,
table_size,
)
else:
raise PluginError("Unimplemented binary import type.")
def import_insertable_binary_animations(
reader: RomReader,
read_headers: dict[str, SM64_AnimHeader],
table: SM64_AnimTable,
table_index: Optional[int] = None,
bone_count: Optional[int] = None,
table_size: Optional[int] = None,
):
if reader.insertable.data_type == "Animation":
SM64_AnimHeader.read_binary(
reader,
read_headers,
False,
bone_count,
)
elif reader.insertable.data_type == "Animation Table":
table.read_binary(reader, read_headers, table_index, bone_count, table_size)
elif reader.insertable.data_type == "Animation DMA Table":
table.read_dma_binary(reader, read_headers, table_index, bone_count)
def import_animations(context: Context):
animation_operator_checks(context, False)
scene = context.scene
obj: Object = context.object
sm64_props: SM64_Properties = scene.fast64.sm64
import_props: SM64_AnimImportProperties = sm64_props.animation.importing
anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation
update_table_preset(import_props, context)
read_headers: dict[str, SM64_AnimHeader] = {}
table = SM64_AnimTable()
print("Reading animation data.")
if import_props.binary:
rom_path = Path(abspath(import_props.rom if import_props.rom else sm64_props.import_rom))
binary_args = (
read_headers,
table,
import_props.table_index,
None if import_props.ignore_bone_count else len(get_anim_owners(obj)),
import_props.table_size,
)
if import_props.import_type == "Binary":
import_rom_checks(rom_path)
address = import_props.address
with rom_path.open("rb") as rom_file:
if import_props.binary_import_type == "DMA":
segment_data = None
else:
segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData
if import_props.is_segmented_address:
address = decodeSegmentedAddr(address.to_bytes(4, "big"), segment_data)
import_binary_animations(
RomReader(rom_file, start_address=address, segment_data=segment_data),
import_props.binary_import_type,
*binary_args,
)
elif import_props.import_type == "Insertable Binary":
insertable_path = Path(abspath(import_props.path))
filepath_checks(insertable_path)
with insertable_path.open("rb") as insertable_file:
if import_props.read_from_rom:
import_rom_checks(rom_path)
with rom_path.open("rb") as rom_file:
segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData
import_insertable_binary_animations(
RomReader(rom_file, insertable_file=insertable_file, segment_data=segment_data),
*binary_args,
)
else:
import_insertable_binary_animations(RomReader(insertable_file=insertable_file), *binary_args)
elif import_props.import_type == "C":
table, read_headers = import_c_animations(Path(abspath(import_props.path)))
table = table or SM64_AnimTable()
else:
raise NotImplementedError(f"Unimplemented animation import type {import_props.import_type}")
if not table.elements:
print("No table was read. Automatically creating table.")
table.elements = [SM64_AnimTableElement(header=header) for header in read_headers.values()]
seperate_anims = table.get_seperate_anims()
actor_name: str = get_anim_actor_name(context)
if import_props.use_preset and import_props.preset in ACTOR_PRESET_INFO:
preset_animation_names = get_preset_anim_name_list(import_props.preset)
for animation in seperate_anims:
if len(animation.headers) == 0:
continue
names, indexes = [], []
for header in animation.headers:
if header.table_index >= len(preset_animation_names):
continue
name = preset_animation_names[header.table_index]
header.enum_name = header.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}")
names.append(name)
indexes.append(str(header.table_index))
animation.action_name = f"{'/'.join(indexes)} - {'/'.join(names)}"
for i, element in enumerate(table.elements[: len(preset_animation_names)]):
name = preset_animation_names[i]
element.enum_name = element.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}")
print("Importing animations into blender.")
actions = []
for animation in seperate_anims:
actions.append(
animation_import_to_blender(
obj,
sm64_props.blender_to_sm64_scale,
animation,
actor_name,
import_props.use_custom_name,
import_props.import_type,
import_props.force_quaternion,
import_props.continuity_filter if not import_props.force_quaternion else True,
)
)
if import_props.run_decimate:
print("Decimating imported actions's fcurves")
old_area = bpy.context.area.type
old_action = obj.animation_data.action
try:
if obj.type == "ARMATURE":
bpy.ops.object.posemode_toggle() # Select all bones
bpy.ops.pose.select_all(action="SELECT")
bpy.context.area.type = "GRAPH_EDITOR"
for action in actions:
print(f"Decimating {action.name}.")
obj.animation_data.action = action
bpy.ops.graph.select_all(action="SELECT")
bpy.ops.graph.decimate(mode="ERROR", factor=1, remove_error_margin=import_props.decimate_margin)
finally:
bpy.context.area.type = old_area
obj.animation_data.action = old_action
if import_props.binary:
anim_props.is_dma = import_props.binary_import_type == "DMA"
if table:
print("Importing animation table into properties.")
from_anim_table_class(anim_props, table, import_props.clear_table, import_props.use_custom_name, actor_name)
@functools.cache
def cached_enum_from_import_preset(preset: str):
animation_names = get_preset_anim_name_list(preset)
enum_items: list[tuple[str, str, str, int]] = []
enum_items.append(("Custom", "Custom", "Pick your own animation index", 0))
if animation_names:
enum_items.append(("", "Presets", "", 1))
for i, name in enumerate(animation_names):
enum_items.append((str(i), f"{i} - {name}", f'"{preset}" Animation {i}', i + 2))
return enum_items
def get_enum_from_import_preset(_import_props: "SM64_AnimImportProperties", context):
try:
return cached_enum_from_import_preset(get_scene_anim_props(context).importing.preset)
except Exception as exc: # pylint: disable=broad-except
print(str(exc))
return [("Custom", "Custom", "Pick your own animation index", 0)]
def update_table_preset(import_props: "SM64_AnimImportProperties", context):
if not import_props.use_preset:
return
preset = ACTOR_PRESET_INFO[import_props.preset]
assert preset.animation is not None and isinstance(
preset.animation, AnimInfo
), "Selected preset's actor has not animation information"
if import_props.preset_animation == "":
# If the previously selected animation isn't in this preset, select animation 0
import_props.preset_animation = "0"
# C
decomp_path = import_props.decomp_path if import_props.decomp_path else context.scene.fast64.sm64.decomp_path
directory = preset.animation.directory if preset.animation.directory else f"{preset.decomp_path}/anims"
import_props.path = os.path.join(decomp_path, directory)
# Binary
import_props.ignore_bone_count = preset.animation.ignore_bone_count
import_props.level = preset.level
if preset.animation.dma:
import_props.dma_table_address = intToHex(preset.animation.address)
import_props.binary_import_type = "DMA"
import_props.is_segmented_address_prop = False
else:
import_props.table_address = intToHex(preset.animation.address)
import_props.binary_import_type = "Table"
import_props.is_segmented_address_prop = True
if preset.animation.size is None:
import_props.check_null = True
else:
import_props.check_null = False
import_props.table_size_prop = preset.animation.size
+346
View File
@@ -0,0 +1,346 @@
from typing import TYPE_CHECKING
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Context, Scene, Action
from bpy.props import EnumProperty, StringProperty, IntProperty
from bpy.app.handlers import persistent
from ...operators import OperatorBase, SearchEnumOperatorBase
from ...utility import copyPropertyGroup
from ...utility_anim import get_action
from .importing import import_animations, get_enum_from_import_preset
from .exporting import export_animation, export_animation_table
from .utility import (
animation_operator_checks,
get_action_props,
get_anim_obj,
get_scene_anim_props,
get_anim_props,
get_anim_actor_name,
)
from .constants import enum_anim_tables, enum_animated_behaviours
if TYPE_CHECKING:
from .properties import SM64_AnimProperties, SM64_AnimHeaderProperties
@persistent
def emulate_no_loop(scene: Scene):
if scene.gameEditorMode != "SM64":
return
anim_props: SM64_AnimProperties = scene.fast64.sm64.animation
played_action: Action = anim_props.played_action
if not played_action:
return
if not bpy.context.screen.is_animation_playing or anim_props.played_header >= len(
get_action_props(played_action).headers
):
anim_props.played_action = None
return
frame = scene.frame_current
header_props = get_action_props(played_action).headers[anim_props.played_header]
_start, loop_start, end = header_props.get_loop_points(played_action)
if header_props.backwards:
if frame < loop_start:
if header_props.no_loop:
scene.frame_set(loop_start)
else:
scene.frame_set(end - 1)
elif frame >= end:
if header_props.no_loop:
scene.frame_set(end - 1)
else:
scene.frame_set(loop_start)
class SM64_PreviewAnim(OperatorBase):
bl_idname = "scene.sm64_preview_animation"
bl_label = "Preview Animation"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "PLAY"
played_header: IntProperty(name="Header", min=0, default=0)
played_action: StringProperty(name="Action")
def execute_operator(self, context):
animation_operator_checks(context)
played_action = get_action(self.played_action)
scene = context.scene
anim_props = scene.fast64.sm64.animation
context.object.animation_data.action = played_action
action_props = get_action_props(played_action)
if self.played_header >= len(action_props.headers):
raise ValueError("Invalid Header Index")
header_props: SM64_AnimHeaderProperties = action_props.headers[self.played_header]
start_frame = header_props.get_loop_points(played_action)[0]
scene.frame_set(start_frame)
scene.render.fps = 30
if bpy.context.screen.is_animation_playing:
bpy.ops.screen.animation_play() # in case it was already playing, stop it
bpy.ops.screen.animation_play()
anim_props.played_header = self.played_header
anim_props.played_action = played_action
class SM64_AnimTableOps(OperatorBase):
bl_idname = "scene.sm64_table_operations"
bl_label = "Table Operations"
bl_description = "Move, remove, clear or add table elements"
bl_options = {"UNDO"}
index: IntProperty()
op_name: StringProperty()
action_name: StringProperty()
header_variant: IntProperty()
@classmethod
def is_enabled(cls, context: Context, op_name: str, index: int, **_kwargs):
table_elements = get_anim_props(context).elements
if op_name == "MOVE_UP" and index == 0:
return False
elif op_name == "MOVE_DOWN" and index >= len(table_elements) - 1:
return False
elif op_name == "CLEAR" and len(table_elements) == 0:
return False
return True
def execute_operator(self, context):
table_elements = get_anim_props(context).elements
if self.op_name == "MOVE_UP":
table_elements.move(self.index, self.index - 1)
elif self.op_name == "MOVE_DOWN":
table_elements.move(self.index, self.index + 1)
elif self.op_name == "ADD":
if self.index != -1:
table_element = table_elements[self.index]
table_elements.add()
if self.action_name: # set based on action variant
table_elements[-1].set_variant(bpy.data.actions[self.action_name], self.header_variant)
elif self.index != -1: # copy from table
copyPropertyGroup(table_element, table_elements[-1])
if self.index != -1:
table_elements.move(len(table_elements) - 1, self.index + 1)
elif self.op_name == "ADD_ALL":
action = bpy.data.actions[self.action_name]
for header_variant in range(len(get_action_props(action).headers)):
table_elements.add()
table_elements[-1].set_variant(action, header_variant)
elif self.op_name == "REMOVE":
table_elements.remove(self.index)
elif self.op_name == "CLEAR":
table_elements.clear()
else:
raise NotImplementedError(f"Unimplemented table op {self.op_name}")
class SM64_AnimVariantOps(OperatorBase):
bl_idname = "scene.sm64_header_variant_operations"
bl_label = "Header Variant Operations"
bl_description = "Move, remove, clear or add variants"
bl_options = {"UNDO"}
index: IntProperty()
op_name: StringProperty()
action_name: StringProperty()
@classmethod
def is_enabled(cls, context: Context, action_name: str, op_name: str, index: int, **_kwargs):
action_props = get_action_props(get_action(action_name))
headers = action_props.headers
if op_name == "REMOVE" and index == 0:
return False
elif op_name == "MOVE_UP" and index <= 0:
return False
elif op_name == "MOVE_DOWN" and index >= len(headers) - 1:
return False
elif op_name == "CLEAR" and len(headers) <= 1:
return False
return True
def execute_operator(self, context):
action = get_action(self.action_name)
action_props = get_action_props(action)
headers = action_props.headers
variants = action_props.header_variants
variant_position = self.index - 1
if self.op_name == "MOVE_UP":
if self.index - 1 == 0:
variants.add()
copyPropertyGroup(headers[0], variants[-1])
copyPropertyGroup(headers[self.index], headers[0])
copyPropertyGroup(variants[-1], headers[self.index])
variants.remove(len(variants) - 1)
else:
variants.move(variant_position, variant_position - 1)
elif self.op_name == "MOVE_DOWN":
if self.index == 0:
variants.add()
copyPropertyGroup(headers[0], variants[-1])
copyPropertyGroup(headers[1], headers[0])
copyPropertyGroup(variants[-1], headers[1])
variants.remove(len(variants) - 1)
else:
variants.move(variant_position, variant_position + 1)
elif self.op_name == "ADD":
variants.add()
added_variant = variants[-1]
copyPropertyGroup(action_props.headers[self.index], added_variant)
variants.move(len(variants) - 1, variant_position + 1)
action_props.update_variant_numbers()
added_variant.action = action
added_variant.expand_tab = True
added_variant.use_custom_name = False
added_variant.use_custom_enum = False
added_variant.custom_name = added_variant.get_name(get_anim_actor_name(context), action)
elif self.op_name == "REMOVE":
variants.remove(variant_position)
elif self.op_name == "CLEAR":
variants.clear()
else:
raise NotImplementedError(f"Unimplemented table op {self.op_name}")
action_props.update_variant_numbers()
class SM64_AddNLATracksToTable(OperatorBase):
bl_idname = "scene.sm64_add_nla_tracks_to_table"
bl_label = "Add Existing NLA Tracks To Animation Table"
bl_description = "Adds all NLA tracks in the selected armature to the animation table"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "NLA"
@classmethod
def poll(cls, context):
if get_anim_obj(context) is None or get_anim_obj(context).animation_data is None:
return False
actions = get_anim_props(context).actions
for track in context.object.animation_data.nla_tracks:
for strip in track.strips:
if strip.action is not None and strip.action not in actions:
return True
return False
def execute_operator(self, context):
assert self.__class__.poll(context)
anim_props = get_anim_props(context)
for track in context.object.animation_data.nla_tracks:
for strip in track.strips:
action = strip.action
if action is None or action in anim_props.actions:
continue
for header_variant in range(len(get_action_props(action).headers)):
anim_props.elements.add()
anim_props.elements[-1].set_variant(action, header_variant)
class SM64_ExportAnimTable(OperatorBase):
bl_idname = "scene.sm64_export_anim_table"
bl_label = "Export Animation Table"
bl_description = "Exports the animation table of the selected armature"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "EXPORT"
@classmethod
def poll(cls, context):
return get_anim_obj(context) is not None
def execute_operator(self, context):
animation_operator_checks(context)
export_animation_table(context, context.object)
self.report({"INFO"}, "Exported animation table successfully!")
class SM64_ExportAnim(OperatorBase):
bl_idname = "scene.sm64_export_anim"
bl_label = "Export Individual Animation"
bl_description = "Exports the select action of the selected armature"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "ACTION"
@classmethod
def poll(cls, context):
return get_anim_obj(context) is not None
def execute_operator(self, context):
animation_operator_checks(context)
export_animation(context, context.object)
self.report({"INFO"}, "Exported animation successfully!")
class SM64_ImportAnim(OperatorBase):
bl_idname = "scene.sm64_import_anim"
bl_label = "Import Animation(s)"
bl_description = "Imports animations into the call context's animation propreties, scene or object"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "IMPORT"
def execute_operator(self, context):
import_animations(context)
class SM64_SearchAnimPresets(SearchEnumOperatorBase):
bl_idname = "scene.search_mario_anim_enum_operator"
bl_property = "preset_animation"
preset_animation: EnumProperty(items=get_enum_from_import_preset)
def update_enum(self, context: Context):
get_scene_anim_props(context).importing.preset_animation = self.preset_animation
class SM64_SearchAnimTablePresets(SearchEnumOperatorBase):
bl_idname = "scene.search_anim_table_enum_operator"
bl_property = "preset"
preset: EnumProperty(items=enum_anim_tables)
def update_enum(self, context: Context):
get_scene_anim_props(context).importing.preset = self.preset
class SM64_SearchAnimatedBhvs(SearchEnumOperatorBase):
bl_idname = "scene.search_animated_behavior_enum_operator"
bl_property = "behaviour"
behaviour: EnumProperty(items=enum_animated_behaviours)
def update_enum(self, context: Context):
get_anim_props(context).behaviour = self.behaviour
classes = (
SM64_ExportAnimTable,
SM64_ExportAnim,
SM64_PreviewAnim,
SM64_AnimTableOps,
SM64_AnimVariantOps,
SM64_AddNLATracksToTable,
SM64_ImportAnim,
SM64_SearchAnimPresets,
SM64_SearchAnimatedBhvs,
SM64_SearchAnimTablePresets,
)
def anim_ops_register():
for cls in classes:
register_class(cls)
bpy.app.handlers.frame_change_pre.append(emulate_no_loop)
def anim_ops_unregister():
for cls in reversed(classes):
unregister_class(cls)
+200
View File
@@ -0,0 +1,200 @@
from typing import TYPE_CHECKING
from bpy.utils import register_class, unregister_class
from bpy.types import Context
from ...utility_anim import is_action_stashed, CreateAnimData, AddBasicAction, StashAction
from ...panels import SM64_Panel
from .utility import (
get_action_props,
get_anim_actor_name,
get_anim_props,
get_selected_action,
dma_structure_context,
get_anim_obj,
)
from .operators import SM64_ExportAnim, SM64_ExportAnimTable, SM64_AddNLATracksToTable
if TYPE_CHECKING:
from ..settings.properties import SM64_Properties
from ..sm64_objects import SM64_CombinedObjectProperties
from .properties import SM64_AnimImportProperties
# Base
class AnimationPanel(SM64_Panel):
bl_label = "SM64 Animation Inspector"
goal = "Object/Actor/Anim"
# Base panels
class SceneAnimPanel(AnimationPanel):
bl_idname = "SM64_PT_anim"
bl_parent_id = bl_idname
class ObjAnimPanel(AnimationPanel):
bl_idname = "OBJECT_PT_SM64_anim"
bl_context = "object"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_parent_id = bl_idname
# Main tab
class SceneAnimPanelMain(SceneAnimPanel):
bl_parent_id = ""
def draw(self, context):
col = self.layout.column()
sm64_props: SM64_Properties = context.scene.fast64.sm64
combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export
if sm64_props.export_type == "C":
if not sm64_props.hackersm64:
col.prop(sm64_props, "designated_prop", text="Designated Initialization for Tables")
else:
combined_props.draw_anim_props(col, sm64_props.export_type, dma_structure_context(context))
SM64_ExportAnimTable.draw_props(col)
anim_obj = get_anim_obj(context)
if anim_obj is None:
col.box().label(text="No selected armature/animated object")
else:
col.box().label(text=f'Armature "{anim_obj.name}"')
class ObjAnimPanelMain(ObjAnimPanel):
bl_parent_id = "OBJECT_PT_context_object"
@classmethod
def poll(cls, context: Context):
return get_anim_obj(context) is not None
def draw(self, context):
sm64_props: SM64_Properties = context.scene.fast64.sm64
combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export
get_anim_props(context).draw_props(
self.layout,
sm64_props.export_type,
combined_props.export_header_type,
get_anim_actor_name(context),
combined_props.export_bhv,
)
# Action tab
class AnimationPanelAction(AnimationPanel):
bl_label = "Action Inspector"
def draw(self, context):
col = self.layout.column()
if context.object.animation_data is None:
col.box().label(text="Select object has no animation data")
CreateAnimData.draw_props(col)
action = None
else:
col.prop(context.object.animation_data, "action", text="Selected Action")
action = get_selected_action(context.object, False)
if action is None:
AddBasicAction.draw_props(col)
return
if not is_action_stashed(context.object, action):
warn_col = col.column()
StashAction.draw_props(warn_col, action=action.name)
warn_col.alert = True
sm64_props: SM64_Properties = context.scene.fast64.sm64
combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export
if sm64_props.export_type != "C":
SM64_ExportAnim.draw_props(col)
export_seperately = get_anim_props(context).export_seperately
if sm64_props.export_type == "C":
export_seperately = export_seperately or combined_props.export_single_action
elif sm64_props.export_type == "Insertable Binary":
export_seperately = True
get_action_props(action).draw_props(
layout=col,
action=action,
specific_variant=None,
in_table=False,
updates_table=get_anim_props(context).update_table,
export_seperately=export_seperately,
export_type=sm64_props.export_type,
actor_name=get_anim_actor_name(context),
gen_enums=get_anim_props(context).gen_enums,
dma=dma_structure_context(context),
)
class SceneAnimPanelAction(AnimationPanelAction, SceneAnimPanel):
bl_idname = "SM64_PT_anim_panel_action"
@classmethod
def poll(cls, context: Context):
return get_anim_obj(context) is not None and SceneAnimPanel.poll(context)
class ObjAnimPanelAction(AnimationPanelAction, ObjAnimPanel):
bl_idname = "OBJECT_PT_SM64_anim_action"
class ObjAnimPanelTable(ObjAnimPanel):
bl_label = "Table"
bl_idname = "OBJECT_PT_SM64_anim_table"
def draw(self, context):
if SM64_AddNLATracksToTable.poll(context):
SM64_AddNLATracksToTable.draw_props(self.layout)
sm64_props: SM64_Properties = context.scene.fast64.sm64
get_anim_props(context).draw_table(self.layout, sm64_props.export_type, get_anim_actor_name(context))
# Importing tab
class AnimationPanelImport(AnimationPanel):
bl_label = "Importing"
import_panel = True
def draw(self, context):
sm64_props: SM64_Properties = context.scene.fast64.sm64
importing: SM64_AnimImportProperties = sm64_props.animation.importing
importing.draw_props(self.layout, sm64_props.import_rom, sm64_props.decomp_path)
class SceneAnimPanelImport(SceneAnimPanel, AnimationPanelImport):
bl_idname = "SM64_PT_anim_panel_import"
@classmethod
def poll(cls, context: Context):
return get_anim_obj(context) is not None and AnimationPanelImport.poll(context)
class ObjAnimPanelImport(ObjAnimPanel, AnimationPanelImport):
bl_idname = "OBJECT_PT_SM64_anim_panel_import"
classes = (
ObjAnimPanelMain,
ObjAnimPanelTable,
ObjAnimPanelAction,
SceneAnimPanelMain,
SceneAnimPanelAction,
SceneAnimPanelImport,
)
def anim_panel_register():
for cls in classes:
register_class(cls)
def anim_panel_unregister():
for cls in reversed(classes):
unregister_class(cls)
File diff suppressed because it is too large Load Diff
+165
View File
@@ -0,0 +1,165 @@
from typing import TYPE_CHECKING
import functools
import re
from bpy.types import Context, Object, Action, PoseBone
from ...utility import findStartBones, PluginError, toAlnum
from ..sm64_geolayout_bone import animatableBoneTypes
if TYPE_CHECKING:
from .properties import SM64_ActionAnimProperty, SM64_AnimProperties, SM64_ArmatureAnimProperties
def is_obj_animatable(obj: Object) -> bool:
if obj.type == "ARMATURE" or (obj.type == "MESH" and obj.geo_cmd_static in animatableBoneTypes):
return True
return False
def get_anim_obj(context: Context) -> Object | None:
obj = context.object
if obj is None and len(context.selected_objects) > 0:
obj = context.selected_objects[0]
if obj is not None and is_obj_animatable(obj):
return obj
def animation_operator_checks(context: Context, requires_animation=True, specific_obj: Object | None = None):
if specific_obj is None:
if len(context.selected_objects) > 1:
raise PluginError("Multiple objects selected at once.")
obj = get_anim_obj(context)
else:
obj = specific_obj
if is_obj_animatable(obj):
raise PluginError(f'Selected object "{obj.name}" is not an armature.')
if requires_animation and obj.animation_data is None:
raise PluginError(f'Armature "{obj.name}" has no animation data.')
def get_selected_action(obj: Object, raise_exc=True) -> Action:
assert obj is not None
if not is_obj_animatable(obj):
if raise_exc:
raise ValueError(f'Object "{obj.name}" is not animatable in SM64.')
elif obj.animation_data is not None and obj.animation_data.action is not None:
return obj.animation_data.action
if raise_exc:
raise ValueError(f'No action selected in object "{obj.name}".')
def get_anim_owners(obj: Object):
"""Get SM64 animation bones from an armature or return the obj if it's an animated cmd mesh"""
def check_children(children: list[Object] | None):
if children is None:
return
for child in children:
if child.geo_cmd_static in animatableBoneTypes:
raise PluginError("Cannot have child mesh with animation, use an armature")
check_children(child.children)
if obj.type == "MESH": # Object will be treated as a bone
if obj.geo_cmd_static in animatableBoneTypes:
check_children(obj.children)
return [obj]
else:
raise PluginError("Mesh is not animatable")
assert obj.type == "ARMATURE", "Obj is neither mesh or armature"
bones_to_process: list[str] = findStartBones(obj)
current_bone = obj.data.bones[bones_to_process[0]]
anim_bones: list[PoseBone] = []
# Get animation bones in order
while len(bones_to_process) > 0:
bone_name = bones_to_process[0]
current_bone = obj.data.bones[bone_name]
current_pose_bone = obj.pose.bones[bone_name]
bones_to_process = bones_to_process[1:]
# Only handle 0x13 bones for animation
if current_bone.geo_cmd in animatableBoneTypes:
anim_bones.append(current_pose_bone)
# Traverse children in alphabetical order.
children_names = sorted([bone.name for bone in current_bone.children])
bones_to_process = children_names + bones_to_process
return anim_bones
def num_to_padded_hex(num: int):
hex_str = hex(num)[2:].upper() # remove the '0x' prefix
return hex_str.zfill(2)
@functools.cache
def get_dma_header_name(index: int):
return f"anim_{num_to_padded_hex(index)}"
def get_dma_anim_name(header_indices: list[int]):
return f'anim_{"_".join([f"{num_to_padded_hex(num)}" for num in header_indices])}'
@functools.cache
def action_name_to_anim_name(action_name: str) -> str:
return re.sub(r"^_(\d+_)+(?=\w)", "", toAlnum(action_name), flags=re.MULTILINE)
@functools.cache
def anim_name_to_enum_name(anim_name: str) -> str:
enum_name = anim_name.upper()
enum_name: str = re.sub(r"(?<=_)_|_$", "", toAlnum(enum_name), flags=re.MULTILINE)
if anim_name == enum_name:
enum_name = f"{enum_name}_ENUM"
return enum_name
def duplicate_name(name: str, existing_names: dict[str, int]) -> str:
"""Updates existing_names"""
current_num = existing_names.get(name)
if current_num is None:
existing_names[name] = 0
elif name != "":
current_num += 1
existing_names[name] = current_num
return f"{name}_{current_num}"
return name
def table_name_to_enum(name: str):
return name.title().replace("_", "")
def get_action_props(action: Action) -> "SM64_ActionAnimProperty":
return action.fast64.sm64.animation
def get_scene_anim_props(context: Context) -> "SM64_AnimProperties":
return context.scene.fast64.sm64.animation
def get_anim_props(context: Context) -> "SM64_ArmatureAnimProperties":
obj = get_anim_obj(context)
assert obj is not None
return obj.fast64.sm64.animation
def get_anim_actor_name(context: Context) -> str | None:
sm64_props = context.scene.fast64.sm64
if sm64_props.export_type == "C" and sm64_props.combined_export.export_anim:
return toAlnum(sm64_props.combined_export.obj_name_anim)
elif context.object:
return sm64_props.combined_export.filter_name(toAlnum(context.object.name), True)
else:
return None
def dma_structure_context(context: Context) -> bool:
if get_anim_obj(context) is None:
return False
return get_anim_props(context).is_dma
+31 -9
View File
@@ -19,6 +19,7 @@ from ..sm64_constants import defaultExtendSegment4, OLD_BINARY_LEVEL_ENUMS
from ..sm64_objects import SM64_CombinedObjectProperties
from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings
from ..tools import SM64_AddrConvProperties
from ..animation.properties import SM64_AnimProperties
from .constants import (
enum_refresh_versions,
@@ -40,13 +41,15 @@ class SM64_Properties(PropertyGroup):
"""Global SM64 Scene Properties found under scene.fast64.sm64"""
version: IntProperty(name="SM64_Properties Version", default=0)
cur_version = 5 # version after property migration
cur_version = 6 # version after property migration
# UI Selection
show_importing_menus: BoolProperty(name="Show Importing Menus", default=False)
export_type: EnumProperty(items=enum_export_type, name="Export Type", default="C")
goal: EnumProperty(items=enum_sm64_goal_type, name="Goal", default="All")
combined_export: bpy.props.PointerProperty(type=SM64_CombinedObjectProperties)
animation: PointerProperty(type=SM64_AnimProperties)
address_converter: PointerProperty(type=SM64_AddrConvProperties)
blender_to_sm64_scale: FloatProperty(
name="Blender To SM64 Scale",
@@ -55,6 +58,7 @@ class SM64_Properties(PropertyGroup):
)
import_rom: StringProperty(name="Import ROM", subtype="FILE_PATH")
# binary
export_rom: StringProperty(name="Export ROM", subtype="FILE_PATH")
output_rom: StringProperty(name="Output ROM", subtype="FILE_PATH")
extend_bank_4: BoolProperty(
@@ -64,7 +68,6 @@ class SM64_Properties(PropertyGroup):
f"{hex(defaultExtendSegment4[1])}) and copies data from old bank",
)
address_converter: PointerProperty(type=SM64_AddrConvProperties)
# C
decomp_path: StringProperty(
name="Decomp Folder",
@@ -93,6 +96,11 @@ class SM64_Properties(PropertyGroup):
name="Write All",
description="Write single load geo and set othermode commands instead of writting the difference to defaults. Can result in smaller displaylists but may introduce issues",
)
# could be used for other properties outside animation
designated_prop: BoolProperty(
name="Designated Initialization for Animation Tables",
description="Extremely recommended but must be off when compiling with IDO. Included in Repo Setting file",
)
@property
def binary_export(self):
@@ -102,6 +110,14 @@ class SM64_Properties(PropertyGroup):
def abs_decomp_path(self) -> Path:
return Path(abspath(self.decomp_path))
@property
def hackersm64(self) -> bool:
return self.refresh_version.startswith("HackerSM64")
@property
def designated(self) -> bool:
return self.designated_prop or self.hackersm64
@property
def gfx_write_method(self):
from ...f3d.f3d_gbi import GfxMatWriteMethod
@@ -128,19 +144,17 @@ class SM64_Properties(PropertyGroup):
"group_name": {"geoGroupName", "colGroupName", "animGroupName", "DLGroupName"},
"level_name": {"levelOption", "geoLevelOption", "colLevelOption", "animLevelOption", "DLLevelOption"},
"non_decomp_level": {"levelCustomExport"},
"export_header_type": {
"geoExportHeaderType",
"colExportHeaderType",
"animExportHeaderType",
"DLExportHeaderType",
},
"custom_include_directory": {"geoTexDir", "DLTexDir"},
"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"}
old_custom_props = {"animCustomExport", "colCustomExport", "geoCustomExport", "DLCustomExport"}
for scene in bpy.data.scenes:
sm64_props: SM64_Properties = scene.fast64.sm64
sm64_props.address_converter.upgrade_changed_props(scene)
sm64_props.animation.upgrade_changed_props(scene)
if sm64_props.version == SM64_Properties.cur_version:
continue
upgrade_old_prop(
@@ -161,6 +175,11 @@ class SM64_Properties(PropertyGroup):
combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export
for new, old in old_export_props_to_new.items():
upgrade_old_prop(combined_props, new, scene, old)
insertable_directory = get_first_set_prop(scene, "animInsertableBinaryPath")
if insertable_directory is not None: # Ignores file name
combined_props.insertable_directory = os.path.split(insertable_directory)[1]
if get_first_set_prop(combined_props, old_custom_props):
combined_props.export_header_type = "Custom"
upgrade_old_prop(combined_props, "level_name", scene, binary_level_names, old_enum=OLD_BINARY_LEVEL_ENUMS)
@@ -175,6 +194,8 @@ class SM64_Properties(PropertyGroup):
if self.matstack_fix:
data["lighting_engine_presets"] = self.lighting_engine_presets
data["write_all"] = self.write_all
if not self.hackersm64:
data["designated"] = self.designated_prop
return data
def from_repo_settings(self, data: dict):
@@ -184,6 +205,7 @@ class SM64_Properties(PropertyGroup):
set_prop_if_in_data(self, "matstack_fix", data, "matstack_fix")
set_prop_if_in_data(self, "lighting_engine_presets", data, "lighting_engine_presets")
set_prop_if_in_data(self, "write_all", data, "write_all")
set_prop_if_in_data(self, "designated_prop", data, "designated")
def draw_repo_settings(self, layout: UILayout):
col = layout.column()
File diff suppressed because it is too large Load Diff
+290
View File
@@ -0,0 +1,290 @@
from io import BufferedReader, StringIO
from typing import BinaryIO
from pathlib import Path
import dataclasses
import shutil
import struct
import os
import numpy as np
from ..utility import intToHex, decodeSegmentedAddr, PluginError, toAlnum
from .sm64_constants import insertableBinaryTypes, SegmentData
from .sm64_utility import export_rom_checks, temp_file_path
@dataclasses.dataclass
class InsertableBinaryData:
data_type: str = ""
data: bytearray = dataclasses.field(default_factory=bytearray)
start_address: int = 0
ptrs: list[int] = dataclasses.field(default_factory=list)
def write(self, path: Path):
path.write_bytes(self.to_binary())
def to_binary(self):
data = bytearray()
data.extend(insertableBinaryTypes[self.data_type].to_bytes(4, "big")) # 0-4
data.extend(len(self.data).to_bytes(4, "big")) # 4-8
data.extend(self.start_address.to_bytes(4, "big")) # 8-12
data.extend(len(self.ptrs).to_bytes(4, "big")) # 12-16
for ptr in self.ptrs: # 16-(16 + len(ptr) * 4)
data.extend(ptr.to_bytes(4, "big"))
data.extend(self.data)
return data
def read(self, file: BufferedReader, expected_type: list = None):
print(f"Reading insertable binary data from {file.name}")
reader = RomReader(file)
type_num = reader.read_int(4)
if type_num not in insertableBinaryTypes.values():
raise ValueError(f"Unknown data type: {intToHex(type_num)}")
self.data_type = next(k for k, v in insertableBinaryTypes.items() if v == type_num)
if expected_type and self.data_type not in expected_type:
raise ValueError(f"Unexpected data type: {self.data_type}")
data_size = reader.read_int(4)
self.start_address = reader.read_int(4)
pointer_count = reader.read_int(4)
self.ptrs = []
for _ in range(pointer_count):
self.ptrs.append(reader.read_int(4))
actual_start = reader.address + self.start_address
self.data = reader.read_data(data_size, actual_start)
return self
@dataclasses.dataclass
class RomReader:
"""
Helper class that simplifies reading data continously from a starting address.
Can read insertable binary files, in which it can also read data from ROM if provided.
"""
rom_file: BufferedReader = None
insertable_file: BufferedReader = None
start_address: int = 0
segment_data: SegmentData = dataclasses.field(default_factory=dict)
insertable: InsertableBinaryData = None
address: int = dataclasses.field(init=False)
def __post_init__(self):
self.address = self.start_address
if self.insertable_file and not self.insertable:
self.insertable = InsertableBinaryData().read(self.insertable_file)
assert self.insertable or self.rom_file
def branch(self, start_address=-1):
start_address = self.address if start_address == -1 else start_address
if self.read_int(1, specific_address=start_address) is None:
if self.insertable and self.rom_file:
return RomReader(self.rom_file, start_address=start_address, segment_data=self.segment_data)
return None
return RomReader(
self.rom_file,
self.insertable_file,
start_address,
self.segment_data,
self.insertable,
)
def skip(self, size: int):
self.address += size
def read_data(self, size=-1, specific_address=-1):
if specific_address == -1:
address = self.address
self.skip(size)
else:
address = specific_address
if self.insertable:
data = self.insertable.data[address : address + size]
else:
self.rom_file.seek(address)
data = self.rom_file.read(size)
if size > 0 and not data:
raise IndexError(f"Value at {intToHex(address)} not present in data.")
return data
def read_ptr(self, specific_address=-1):
address = self.address if specific_address == -1 else specific_address
ptr = self.read_int(4, specific_address=specific_address)
if self.insertable and address in self.insertable.ptrs:
return ptr
if ptr and self.segment_data:
return decodeSegmentedAddr(ptr.to_bytes(4, "big"), self.segment_data)
return ptr
def read_int(self, size=4, signed=False, specific_address=-1):
return int.from_bytes(self.read_data(size, specific_address), "big", signed=signed)
def read_float(self, size=4, specific_address=-1):
return struct.unpack(">f", self.read_data(size, specific_address))[0]
def read_str(self, specific_address=-1):
ptr = self.read_ptr() if specific_address == -1 else specific_address
if not ptr:
return None
branch = self.branch(ptr)
text_data = bytearray()
while True:
byte = branch.read_data(1)
if byte == b"\x00" or not byte:
break
text_data.append(ord(byte))
text = text_data.decode("utf-8")
return text
@dataclasses.dataclass
class BinaryExporter:
export_rom: Path
output_rom: Path
rom_file_output: BinaryIO = dataclasses.field(init=False)
temp_rom: Path = dataclasses.field(init=False)
@property
def tell(self):
return self.rom_file_output.tell()
def __enter__(self):
export_rom_checks(self.export_rom)
print(f"Binary export started, exporting to {self.output_rom}")
self.temp_rom = temp_file_path(self.output_rom)
print(f'Copying "{self.export_rom}" to temporary file "{self.temp_rom}".')
shutil.copy(self.export_rom, self.temp_rom)
self.rom_file_output = self.temp_rom.open("rb+")
return self
def write_to_range(self, start_address: int, end_address: int, data: bytes | bytearray):
address_range_str = f"[{intToHex(start_address)}, {intToHex(end_address)}]"
if end_address < start_address:
raise PluginError(f"Start address is higher than the end address: {address_range_str}")
if start_address + len(data) > end_address:
raise PluginError(
f"Data ({len(data) / 1000.0} kb) does not fit in range {address_range_str} "
f"({(end_address - start_address) / 1000.0} kb).",
)
print(f"Writing {len(data) / 1000.0} kb to {address_range_str} ({(end_address - start_address) / 1000.0} kb))")
self.write(data, start_address)
def seek(self, offset: int, whence: int = 0):
self.rom_file_output.seek(offset, whence)
def read(self, n=-1, offset=-1):
if offset != -1:
self.seek(offset)
return self.rom_file_output.read(n)
def write(self, s: bytes, offset=-1):
if offset != -1:
self.seek(offset)
return self.rom_file_output.write(s)
def __exit__(self, exc_type, exc_value, traceback):
if self.temp_rom.exists():
print(f"Closing temporary file {self.temp_rom}.")
self.rom_file_output.close()
else:
raise FileNotFoundError(f"Temporary file {self.temp_rom} does not exist?")
if exc_value:
print("Deleting temporary file because of exception.")
os.remove(self.temp_rom)
print("Type:", exc_type, "\nValue:", exc_value, "\nTraceback:", traceback)
else:
print(f"Moving temporary file to {self.output_rom}.")
if os.path.exists(self.output_rom):
os.remove(self.output_rom)
self.temp_rom.rename(self.output_rom)
@dataclasses.dataclass
class DMATableElement:
offset: int = 0
size: int = 0
address: int = 0
end_address: int = 0
@dataclasses.dataclass
class DMATable:
address_place_holder: int = 0
entries: list[DMATableElement] = dataclasses.field(default_factory=list)
data: bytearray = dataclasses.field(default_factory=bytearray)
address: int = 0
end_address: int = 0
def to_binary(self):
print(
f"Generating DMA table with {len(self.entries)} entries",
f"and {len(self.data)} bytes of data",
)
data = bytearray()
data.extend(len(self.entries).to_bytes(4, "big", signed=False))
data.extend(self.address_place_holder.to_bytes(4, "big", signed=False))
entries_offset = 8
entries_length = len(self.entries) * 8
entrie_data_offset = entries_offset + entries_length
for entrie in self.entries:
offset = entrie_data_offset + entrie.offset
data.extend(offset.to_bytes(4, "big", signed=False))
data.extend(entrie.size.to_bytes(4, "big", signed=False))
data.extend(self.data)
return data
def read_binary(self, reader: RomReader):
print("Reading DMA table at", intToHex(reader.start_address))
self.address = reader.start_address
num_entries = reader.read_int(4) # numEntries
self.address_place_holder = reader.read_int(4) # addrPlaceholder
table_size = 0
for _ in range(num_entries):
offset = reader.read_int(4)
size = reader.read_int(4)
address = self.address + offset
self.entries.append(DMATableElement(offset, size, address, address + size))
end_of_entry = offset + size
if end_of_entry > table_size:
table_size = end_of_entry
self.end_address = self.address + table_size
print(f"Found {len(self.entries)} DMA entries")
return self
@dataclasses.dataclass
class IntArray:
data: np.ndarray
name: str = ""
wrap: int = 6
wrap_start: int = 0 # -6 To replicate decomp animation index table formatting
def to_binary(self):
return self.data.astype(">i2").tobytes()
def to_c(self, c_data: StringIO | None = None, new_lines=1):
assert self.name, "Array must have a name"
data = self.data
byte_count = data.itemsize
data_type = f"{'s' if data.dtype == np.int16 else 'u'}{byte_count * 8}"
print(f'Generating {data_type} array "{self.name}" with {len(self.data)} elements')
c_data = c_data or StringIO()
c_data.write(f"// {len(self.data)}\n")
c_data.write(f"static const {data_type} {toAlnum(self.name)}[] = {{\n\t")
i = self.wrap_start
for value in self.data:
c_data.write(f"{intToHex(value, byte_count, False)}, ")
i += 1
if i >= self.wrap:
c_data.write("\n\t")
i = 0
c_data.write("\n};" + ("\n" * new_lines))
return c_data
+134 -246
View File
@@ -30,6 +30,28 @@ marioVanishOffsets = {
"metal": 0x9EC,
}
NULL = 0x00000000
MIN_U8 = 0
MAX_U8 = (2**8) - 1
MIN_S8 = -(2**7)
MAX_S8 = (2**7) - 1
MIN_S16 = -(2**15)
MAX_S16 = (2**15) - 1
MIN_U16 = 0
MAX_U16 = 2**16 - 1
MIN_S32 = -(2**31)
MAX_S32 = 2**31 - 1
MIN_U32 = 0
MAX_U32 = 2**32 - 1
SegmentData = dict[int, tuple[int, int]]
commonGeolayoutPointers = {
"Dorrie": [2039136, "HMC"],
"Bowser": [1809204, "BFB"],
@@ -302,7 +324,14 @@ level_pointers = {
"intro": 0x269EB0,
}
insertableBinaryTypes = {"Display List": 0, "Geolayout": 1, "Animation": 2, "Collision": 3}
insertableBinaryTypes = {
"Display List": 0,
"Geolayout": 1,
"Animation": 2,
"Collision": 3,
"Animation Table": 4,
"Animation DMA Table": 5,
}
enumBehaviourPresets = [
("Custom", "Custom", "Custom"),
("1300407c", "1 Up", "1 Up"),
@@ -2128,6 +2157,7 @@ groups_seg8 = [
("Custom", "Custom", "Custom"),
]
# groups you can use for the combined object export
groups_obj_export = [
("common0", "common0", "chuckya, boxes, blue coin switch"),
@@ -2153,8 +2183,77 @@ groups_obj_export = [
("Custom", "Custom", "Custom"),
]
BEHAVIOR_EXITS = [
"RETURN",
"GOTO",
"END_LOOP",
"BREAK",
"BREAK_UNUSED",
"DEACTIVATE",
]
BEHAVIOR_COMMANDS = [
# Name, Size
("BEGIN", 1), # bhv_cmd_begin
("DELAY", 1), # bhv_cmd_delay
("CALL", 1), # bhv_cmd_call
("RETURN", 1), # bhv_cmd_return
("GOTO", 1), # bhv_cmd_goto
("BEGIN_REPEAT", 1), # bhv_cmd_begin_repeat
("END_REPEAT", 1), # bhv_cmd_end_repeat
("END_REPEAT_CONTINUE", 1), # bhv_cmd_end_repeat_continue
("BEGIN_LOOP", 1), # bhv_cmd_begin_loop
("END_LOOP", 1), # bhv_cmd_end_loop
("BREAK", 1), # bhv_cmd_break
("BREAK_UNUSED", 1), # bhv_cmd_break_unused
("CALL_NATIVE", 2), # bhv_cmd_call_native
("ADD_FLOAT", 1), # bhv_cmd_add_float
("SET_FLOAT", 1), # bhv_cmd_set_float
("ADD_INT", 1), # bhv_cmd_add_int
("SET_INT", 1), # bhv_cmd_set_int
("OR_INT", 1), # bhv_cmd_or_int
("BIT_CLEAR", 1), # bhv_cmd_bit_clear
("SET_INT_RAND_RSHIFT", 2), # bhv_cmd_set_int_rand_rshift
("SET_RANDOM_FLOAT", 2), # bhv_cmd_set_random_float
("SET_RANDOM_INT", 2), # bhv_cmd_set_random_int
("ADD_RANDOM_FLOAT", 2), # bhv_cmd_add_random_float
("ADD_INT_RAND_RSHIFT", 2), # bhv_cmd_add_int_rand_rshift
("NOP_1", 1), # bhv_cmd_nop_1
("NOP_2", 1), # bhv_cmd_nop_2
("NOP_3", 1), # bhv_cmd_nop_3
("SET_MODEL", 1), # bhv_cmd_set_model
("SPAWN_CHILD", 3), # bhv_cmd_spawn_child
("DEACTIVATE", 1), # bhv_cmd_deactivate
("DROP_TO_FLOOR", 1), # bhv_cmd_drop_to_floor
("SUM_FLOAT", 1), # bhv_cmd_sum_float
("SUM_INT", 1), # bhv_cmd_sum_int
("BILLBOARD", 1), # bhv_cmd_billboard
("HIDE", 1), # bhv_cmd_hide
("SET_HITBOX", 2), # bhv_cmd_set_hitbox
("NOP_4", 1), # bhv_cmd_nop_4
("DELAY_VAR", 1), # bhv_cmd_delay_var
("BEGIN_REPEAT_UNUSED", 1), # bhv_cmd_begin_repeat_unused
("LOAD_ANIMATIONS", 2), # bhv_cmd_load_animations
("ANIMATE", 1), # bhv_cmd_animate
("SPAWN_CHILD_WITH_PARAM", 3), # bhv_cmd_spawn_child_with_param
("LOAD_COLLISION_DATA", 2), # bhv_cmd_load_collision_data
("SET_HITBOX_WITH_OFFSET", 3), # bhv_cmd_set_hitbox_with_offset
("SPAWN_OBJ", 3), # bhv_cmd_spawn_obj
("SET_HOME", 1), # bhv_cmd_set_home
("SET_HURTBOX", 2), # bhv_cmd_set_hurtbox
("SET_INTERACT_TYPE", 2), # bhv_cmd_set_interact_type
("SET_OBJ_PHYSICS", 5), # bhv_cmd_set_obj_physics
("SET_INTERACT_SUBTYPE", 2), # bhv_cmd_set_interact_subtype
("SCALE", 1), # bhv_cmd_scale
("PARENT_BIT_CLEAR", 2), # bhv_cmd_parent_bit_clear
("ANIMATE_TEXTURE", 1), # bhv_cmd_animate_texture
("DISABLE_RENDERING", 1), # bhv_cmd_disable_rendering
("SET_INT_UNUSED", 2), # bhv_cmd_set_int_unused
("SPAWN_WATER_DROPLET", 2), # bhv_cmd_spawn_water_droplet
]
T = TypeVar("T")
DictOrVal = T | dict[T] | None
DictOrVal = T | dict[str, T] | None
ListOrVal = T | list[T] | None
@@ -2191,7 +2290,7 @@ class AnimInfo:
ignore_bone_count: bool = False
dma: bool = False
directory: str | None = None
names: list[str] | None = None
names: list[str] = dataclasses.field(default_factory=list)
def __post_init__(self):
assert isinstance(self.address, int)
@@ -2263,26 +2362,26 @@ class ActorPresetInfo:
assert validate_dict(self.models, ModelInfo)
assert validate_dict(self.collision, CollisionInfo)
group_to_level = {
"common0": "HH",
"common1": "HH",
"group0": "HH",
"group1": "WF",
"group2": "LLL",
"group3": "BOB",
"group4": "JRB",
"group5": "SSL",
"group6": "TTM",
"group7": "CCM",
"group8": "VC",
"group9": "HH",
"group10": "CG",
"group11": "THI",
"group12": "BFB",
"group13": "WDW",
"group14": "BOB",
"group15": "IC",
"group16": "CCM",
"group17": "HMC",
"common0": "bbh",
"common1": "bbh",
"group0": "bbh",
"group1": "wf",
"group2": "lll",
"group3": "bob",
"group4": "jrb",
"group5": "ssl",
"group6": "ttm",
"group7": "ccm",
"group8": "vcutm",
"group9": "bbh",
"group10": "castle_grounds",
"group11": "thi",
"group12": "bowser_1",
"group13": "wdw",
"group14": "bob",
"group15": "castle_inside",
"group16": "ccm",
"group17": "hmc",
}
if self.level is None and self.group is not None:
self.level = group_to_level[self.group]
@@ -2957,17 +3056,17 @@ ACTOR_PRESET_INFO = {
"Shivering return to idle ",
"Shivering",
"Climb down on ledge",
"Credits - Waving",
"Credits - Look up",
"Credits - Return from look up",
"Credits - Raising hand",
"Credits - Lowering hand",
"Credits - Taking off cap",
"Credits - Start walking and look up",
"Credits - Look back then run",
"Waving (Credits)",
"Look up (Credits)",
"Return from look up (Credits)",
"Raising hand (Credits)",
"Lowering hand (Credits)",
"Taking off cap (Credits)",
"Start walking and look up (Credits)",
"Look back then run (Credits)",
"Final Bowser - Raise hand and spin",
"Final Bowser - Wing cap take off",
"Credits - Peach sign",
"Peach sign (Credits)",
"Stand up from lava boost",
"Fire/Lava burn",
"Wing cap flying",
@@ -3239,9 +3338,11 @@ ACTOR_PRESET_INFO = {
decomp_path="actors/peach",
group="group10",
animation=AnimInfo(
address=0x501C50C,
address=0x501C504,
behaviours={"Peach (Beginning)": 0x13005638, "Peach (End)": 0x13000EAC},
names=[
"Listen Everybody",
"Turning Away",
"Walking away",
"Walking away 2",
"Descend",
@@ -3718,219 +3819,6 @@ ACTOR_PRESET_INFO = {
),
}
marioAnimations = [
# ( Adress, "Animation name" ),
(5162640, "0 - Slow ledge climb up"),
(5165520, "1 - Fall over backwards"),
(5165544, "2 - Backward air kb"),
(5172396, "3 - Dying on back"),
(5177044, "4 - Backflip"),
(5179584, "5 - Climbing up pole"),
(5185656, "6 - Grab pole short"),
(5186824, "7 - Grab pole swing part 1"),
(5186848, "8 - Grab pole swing part 2"),
(5191920, "9 - Handstand idle"),
(5194740, "10 - Handstand jump"),
(5194764, "11 - Start handstand"),
(5188592, "12 - Return from handstand"),
(5196388, "13 - Idle on pole"),
(5197436, "14 - A pose"),
(5197792, "15 - Skid on ground"),
(5197816, "16 - Stop skid"),
(5199596, "17 - Crouch from fast longjump"),
(5201048, "18 - Crouch from a slow longjump"),
(5202644, "19 - Fast longjump"),
(5204600, "20 - Slow longjump"),
(5205980, "21 - Airborne on stomach"),
(5207188, "22 - Walk with light object"),
(5211916, "23 - Run with light object"),
(5215136, "24 - Slow walk with light object"),
(5219864, "25 - Shivering and warming hands"),
(5225496, "26 - Shivering return to idle "),
(5226920, "27 - Shivering"),
(5230056, "28 - Climb down on ledge"),
(5231112, "29 - Credits - Waving"),
(5232768, "30 - Credits - Look up"),
(5234576, "31 - Credits - Return from look up"),
(5235700, "32 - Credits - Raising hand"),
(5243100, "33 - Credits - Lowering hand"),
(5245988, "34 - Credits - Taking off cap"),
(5248016, "35 - Credits - Start walking and look up"),
(5256508, "36 - Credits - Look back then run"),
(5266160, "37 - Final Bowser - Raise hand and spin"),
(5274456, "38 - Final Bowser - Wing cap take off"),
(5282084, "39 - Credits - Peach sign"),
(5291340, "40 - Stand up from lava boost"),
(5292628, "41 - Fire/Lava burn"),
(5293488, "42 - Wing cap flying"),
(5295016, "43 - Hang on owl"),
(5296876, "44 - Land on stomach"),
(5296900, "45 - Air forward kb"),
(5302796, "46 - Dying on stomach"),
(5306100, "47 - Suffocating"),
(5313796, "48 - Coughing"),
(5319500, "49 - Throw catch key"),
(5330436, "50 - Dying fall over"),
(5338604, "51 - Idle on ledge"),
(5341720, "52 - Fast ledge grab"),
(5343296, "53 - Hang on ceiling"),
(5347276, "54 - Put cap on"),
(5351252, "55 - Take cap off then on"),
(5358356, "56 - Quickly put cap on"),
(5359476, "57 - Head stuck in ground"),
(5372172, "58 - Ground pound landing"),
(5372824, "59 - Triple jump ground-pound"),
(5374304, "60 - Start ground-pound"),
(5374328, "61 - Ground-pound"),
(5375380, "62 - Bottom stuck in ground"),
(5387148, "63 - Idle with light object"),
(5390520, "64 - Jump land with light object"),
(5391892, "65 - Jump with light object"),
(5392704, "66 - Fall land with light object"),
(5393936, "67 - Fall with light object"),
(5394296, "68 - Fall from sliding with light object"),
(5395224, "69 - Sliding on bottom with light object"),
(5395248, "70 - Stand up from sliding with light object"),
(5396716, "71 - Riding shell"),
(5397832, "72 - Walking"),
(5403208, "73 - Forward flip"),
(5404784, "74 - Jump riding shell"),
(5405676, "75 - Land from double jump"),
(5407340, "76 - Double jump fall"),
(5408288, "77 - Single jump"),
(5408312, "78 - Land from single jump"),
(5411044, "79 - Air kick"),
(5412900, "80 - Double jump rise"),
(5413596, "81 - Start forward spinning"),
(5414876, "82 - Throw light object"),
(5416032, "83 - Fall from slide kick"),
(5418280, "84 - Bend kness riding shell"),
(5419872, "85 - Legs stuck in ground"),
(5431416, "86 - General fall"),
(5431440, "87 - General land"),
(5433276, "88 - Being grabbed"),
(5434636, "89 - Grab heavy object"),
(5437964, "90 - Slow land from dive"),
(5441520, "91 - Fly from cannon"),
(5442516, "92 - Moving right while hanging"),
(5444052, "93 - Moving left while hanging"),
(5445472, "94 - Missing cap"),
(5457860, "95 - Pull door walk in"),
(5463196, "96 - Push door walk in"),
(5467492, "97 - Unlock door"),
(5480428, "98 - Start reach pocket"),
(5481448, "99 - Reach pocket"),
(5483352, "100 - Stop reach pocket"),
(5484876, "101 - Ground throw"),
(5486852, "102 - Ground kick"),
(5489076, "103 - First punch"),
(5489740, "104 - Second punch"),
(5490356, "105 - First punch fast"),
(5491396, "106 - Second punch fast"),
(5492732, "107 - Pick up light object"),
(5493948, "108 - Pushing"),
(5495508, "109 - Start riding shell"),
(5497072, "110 - Place light object"),
(5498484, "111 - Forward spinning"),
(5498508, "112 - Backward spinning"),
(5498884, "113 - Breakdance"),
(5501240, "114 - Running"),
(5501264, "115 - Running (unused)"),
(5505884, "116 - Soft back kb"),
(5508004, "117 - Soft front kb"),
(5510172, "118 - Dying in quicksand"),
(5515096, "119 - Idle in quicksand"),
(5517836, "120 - Move in quicksand"),
(5528568, "121 - Electrocution"),
(5532480, "122 - Shocked"),
(5533160, "123 - Backward kb"),
(5535796, "124 - Forward kb"),
(5538372, "125 - Idle heavy object"),
(5539764, "126 - Stand against wall"),
(5544580, "127 - Side step left"),
(5548480, "128 - Side step right"),
(5553004, "129 - Start sleep idle"),
(5557588, "130 - Start sleep scratch"),
(5563636, "131 - Start sleep yawn"),
(5568648, "132 - Start sleep sitting"),
(5573680, "133 - Sleep idle"),
(5574280, "134 - Sleep start laying"),
(5577460, "135 - Sleep laying"),
(5579300, "136 - Dive"),
(5579324, "137 - Slide dive"),
(5580860, "138 - Ground bonk"),
(5584116, "139 - Stop slide light object"),
(5587364, "140 - Slide kick"),
(5588288, "141 - Crouch from slide kick"),
(5589652, "142 - Slide motionless"),
(5589676, "143 - Stop slide"),
(5591572, "144 - Fall from slide"),
(5592860, "145 - Slide"),
(5593404, "146 - Tiptoe"),
(5599280, "147 - Twirl land"),
(5600160, "148 - Twirl"),
(5600516, "149 - Start twirl"),
(5601072, "150 - Stop crouching"),
(5602028, "151 - Start crouching"),
(5602720, "152 - Crouching"),
(5605756, "153 - Crawling"),
(5613048, "154 - Stop crawling"),
(5613968, "155 - Start crawling"),
(5614876, "156 - Summon star"),
(5620036, "157 - Return star approach door"),
(5622256, "158 - Backwards water kb"),
(5626540, "159 - Swim with object part 1"),
(5627592, "160 - Swim with object part 2"),
(5628260, "161 - Flutter kick with object"),
(5629456, "162 - Action end with object in water"),
(5631180, "163 - Stop holding object in water"),
(5634048, "164 - Holding object in water"),
(5635976, "165 - Drowning part 1"),
(5641400, "166 - Drowning part 2"),
(5646324, "167 - Dying in water"),
(5649660, "168 - Forward kb in water"),
(5653848, "169 - Falling from water"),
(5655852, "170 - Swimming part 1"),
(5657100, "171 - Swimming part 2"),
(5658128, "172 - Flutter kick"),
(5660112, "173 - Action end in water"),
(5662248, "174 - Pick up object in water"),
(5663480, "175 - Grab object in water part 2"),
(5665916, "176 - Grab object in water part 1"),
(5666632, "177 - Throw object in water"),
(5669328, "178 - Idle in water"),
(5671428, "179 - Star dance in water"),
(5678200, "180 - Return from in water star dance"),
(5680324, "181 - Grab bowser"),
(5680348, "182 - Swing bowser"),
(5682008, "183 - Release bowser"),
(5685264, "184 - Holding bowser"),
(5686316, "185 - Heavy throw"),
(5688660, "186 - Walk panting"),
(5689924, "187 - Walk with heavy object"),
(5694332, "188 - Turning part 1"),
(5694356, "189 - Turning part 2"),
(5696160, "190 - Side flip land"),
(5697196, "191 - Side flip"),
(5699408, "192 - Triple jump land"),
(5702136, "193 - Triple jump"),
(5704880, "194 - First person"),
(5710580, "195 - Idle head left"),
(5712800, "196 - Idle head right"),
(5715020, "197 - Idle head center"),
(5717240, "198 - Handstand left"),
(5719184, "199 - Handstand right"),
(5722304, "200 - Wake up from sleeping"),
(5724228, "201 - Wake up from laying"),
(5726444, "202 - Start tiptoeing"),
(5728720, "203 - Slide jump"),
(5728744, "204 - Start wallkick"),
(5730404, "205 - Star dance"),
(5735864, "206 - Return from star dance"),
(5737600, "207 - Forwards spinning flip"),
(5740584, "208 - Triple jump fly"),
]
sm64_world_defaults = {
"geometryMode": {
"zBuffer": True,
+126 -7
View File
@@ -2,6 +2,7 @@ import math, bpy, mathutils
import os
import traceback
from bpy.utils import register_class, unregister_class
from bpy.types import UILayout
from re import findall, sub
from pathlib import Path
from ..panels import SM64_Panel
@@ -68,6 +69,13 @@ from .sm64_geolayout_classes import (
ScaleNode,
)
from .animation import (
export_animation,
export_animation_table,
get_anim_obj,
is_obj_animatable,
SM64_ArmatureAnimProperties,
)
enumTerrain = [
("Custom", "Custom", "Custom"),
@@ -1455,6 +1463,8 @@ class BehaviorScriptProperty(bpy.types.PropertyGroup):
_inheritable_macros = {
"LOAD_COLLISION_DATA",
"SET_MODEL",
"LOAD_ANIMATIONS",
"ANIMATE"
# add support later maybe
# "SET_HITBOX_WITH_OFFSET",
# "SET_HITBOX",
@@ -1508,6 +1518,18 @@ class BehaviorScriptProperty(bpy.types.PropertyGroup):
if not props.export_col:
raise PluginError("Can't inherit collision without exporting collision data")
return props.collision_name
if self.macro == "LOAD_ANIMATIONS":
if not props.export_anim:
raise PluginError("Can't inherit animation table without exporting animation data")
if not props.anims_name:
raise PluginError("No animation name to inherit in behavior script")
return f"oAnimations, {props.anims_name}"
if self.macro == "ANIMATE":
if not props.export_anim:
raise PluginError("Can't inherit animation table without exporting animation data")
if not props.anim_object:
raise PluginError("No animation properties to inherit in behavior script")
return f"oAnimations, {props.anim_object.fast64.sm64.animation.beginning_animation}"
return self.macro_args
def get_args(self, context, props):
@@ -1824,7 +1846,7 @@ class SM64_ExportCombinedObject(ObjectDataExporter):
raise PluginError("Operator can only be used in object mode.")
if context.scene.fast64.sm64.export_type != "C":
raise PluginError("Combined Object Export only supports C exporting")
if not props.col_object and not props.gfx_object and not props.bhv_object:
if not props.col_object and not props.gfx_object and not props.anim_object and not props.bhv_object:
raise PluginError("No export object selected")
if (
context.active_object
@@ -1835,7 +1857,7 @@ class SM64_ExportCombinedObject(ObjectDataExporter):
def get_export_objects(self, context, props):
if not props.export_all_selected:
return {props.col_object, props.gfx_object, props.bhv_object}.difference({None})
return {props.col_object, props.gfx_object, props.anim_object, props.bhv_object}.difference({None})
def obj_root(object, context):
while object.parent and object.parent in context.selected_objects:
@@ -1889,6 +1911,22 @@ class SM64_ExportCombinedObject(ObjectDataExporter):
if not props.export_all_selected or not PluginError.check_exc_warn(exc):
raise Exception(exc)
# writes table.inc.c file, anim_header.h
# writes include into aggregate file in export location (leveldata.c/<group>.c)
# writes name to header in aggregate file location (actor/level)
# var name is: static const struct Animation *const <props.anim_obj>_anims[] (or custom name)
def execute_anim(self, props, context, obj):
try:
if props.export_anim and obj is props.anim_object:
if props.export_single_action:
export_animation(context, obj)
else:
export_animation_table(context, obj)
except Exception as exc:
# pass on multiple export, throw on singular
if not props.export_all_selected:
raise Exception(exc) from exc
def execute(self, context):
props = context.scene.fast64.sm64.combined_export
try:
@@ -1899,6 +1937,7 @@ class SM64_ExportCombinedObject(ObjectDataExporter):
props.context_obj = obj
self.execute_col(props, obj)
self.execute_gfx(props, context, obj, index)
self.execute_anim(props, context, obj)
# do not export behaviors with multiple selection
if props.export_bhv and props.obj_name_bhv and not props.export_all_selected:
self.export_behavior_script(context, props)
@@ -1973,6 +2012,16 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
name="Export Rooms", description="Collision export will generate rooms.inc.c file"
)
# anim export options
quick_anim_read: bpy.props.BoolProperty(
name="Quick Data Read", description="Read fcurves directly, should work with the majority of rigs", default=True
)
export_single_action: bpy.props.BoolProperty(
name="Selected Action",
description="Animation export will only export the armature's current action like in older versions of fast64",
)
insertable_directory: bpy.props.StringProperty(name="Directory Path", subtype="FILE_PATH")
# export options
export_bhv: bpy.props.BoolProperty(
name="Export Behavior", default=False, description="Export behavior with given object name"
@@ -1983,6 +2032,7 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
export_gfx: bpy.props.BoolProperty(
name="Export Graphics", description="Export geo layouts for linked or selected mesh that have collision data"
)
export_anim: bpy.props.BoolProperty(name="Export Animations", description="Export animation table of an armature")
export_script_loads: bpy.props.BoolProperty(
name="Export Script Loads",
description="Exports the Model ID and adds a level script load in the appropriate place",
@@ -2005,6 +2055,7 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
collision_object: bpy.props.PointerProperty(type=bpy.types.Object)
graphics_object: bpy.props.PointerProperty(type=bpy.types.Object)
animation_object: bpy.props.PointerProperty(type=bpy.types.Object, poll=lambda self, obj: is_obj_animatable(obj))
# is this abuse of properties?
@property
@@ -2025,6 +2076,18 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
else:
return self.graphics_object or self.context_obj or bpy.context.active_object
@property
def anim_object(self):
if not self.export_anim:
return None
obj = get_anim_obj(bpy.context)
context_obj = self.context_obj if self.context_obj and is_obj_animatable(self.context_obj) else None
if self.export_all_selected:
return context_obj or obj
else:
assert not self.animation_object or is_obj_animatable(self.animation_object)
return self.animation_object or context_obj or obj
@property
def bhv_object(self):
if not self.export_bhv or self.export_all_selected:
@@ -2068,6 +2131,15 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
else:
return self.filter_name(self.object_name or self.bhv_object.name)
@property
def obj_name_anim(self):
if self.export_all_selected and self.anim_object:
return self.filter_name(self.anim_object.name)
if not self.object_name and not self.anim_object:
return ""
else:
return self.filter_name(self.object_name or self.anim_object.name)
@property
def bhv_name(self):
return "bhv" + "".join([word.title() for word in toAlnum(self.obj_name_bhv).split("_")])
@@ -2084,6 +2156,12 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
def model_id_define(self):
return f"MODEL_{toAlnum(self.obj_name_gfx)}".upper()
@property
def anims_name(self):
if not self.anim_object:
return ""
return self.anim_object.fast64.sm64.animation.get_table_name(self.obj_name_anim)
@property
def export_level_name(self):
if self.level_name == "Custom" or self.non_decomp_level:
@@ -2135,12 +2213,25 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
return self.base_level_path / self.level_directory
# remove user prefixes/naming that I will be adding, such as _col, _geo etc.
def filter_name(self, name):
if self.use_name_filtering:
def filter_name(self, name, force_filtering=False):
if self.use_name_filtering or force_filtering:
return sub("(_col)?(_geo)?(_bhv)?(lision)?", "", name)
else:
return name
def draw_anim_props(self, layout: UILayout, export_type="C", is_dma=False):
col = layout.column()
col.prop(self, "quick_anim_read")
if self.quick_anim_read:
col.label(text="May Break!", icon="INFO")
if not is_dma and export_type == "C":
col.prop(self, "export_single_action")
if export_type == "Binary":
if not is_dma:
prop_split(col, self, "level_name", "Level")
elif export_type == "Insertable Binary":
prop_split(col, self, "insertable_directory", "Directory")
def draw_export_options(self, layout):
split = layout.row(align=True)
@@ -2163,6 +2254,14 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
box.prop(self, "graphics_object", icon_only=True)
if self.export_script_loads:
box.prop(self, "model_id", text="Model ID")
box = split.box().column()
box.prop(self, "export_anim", toggle=1)
if self.export_anim:
self.draw_anim_props(box)
if not self.export_all_selected:
box.prop(self, "animation_object", icon_only=True)
col = layout.column()
col.prop(self, "export_all_selected")
col.prop(self, "use_name_filtering")
@@ -2172,7 +2271,16 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
@property
def actor_names(self) -> list:
return list(dict.fromkeys(filter(None, [self.obj_name_col, self.obj_name_gfx])).keys())
return list(dict.fromkeys(filter(None, [self.obj_name_col, self.obj_name_gfx, self.obj_name_anim])).keys())
@property
def export_locations(self) -> str | None:
names = self.actor_names
if len(names) > 1:
return f"({'/'.join(names)})"
elif len(names) == 1:
return names[0]
return None
@property
def export_locations(self) -> str | None:
@@ -2223,6 +2331,12 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
if self.export_script_loads:
layout.label(text=f"Model ID: {self.model_id_define}")
def draw_anim_names(self, layout):
anim_props = self.anim_object.fast64.sm64.animation
if anim_props.is_dma:
layout.label(text=f"Animation path: {anim_props.dma_folder}(.c)")
layout.label(text=f"Animation table name: {self.anims_name}")
def draw_obj_name(self, layout):
split_1 = layout.split(factor=0.45)
split_2 = split_1.split(factor=0.45)
@@ -2258,7 +2372,7 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
col.separator()
# object exports
box = col.box().column()
if not self.export_col and not self.export_bhv and not self.export_gfx:
if not self.export_col and not self.export_bhv and not self.export_gfx and not self.export_anim:
col = box.column()
col.operator("object.sm64_export_combined_object", text="Export Object")
col.enabled = False
@@ -2270,7 +2384,7 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
self.draw_export_options(box)
# bhv export only, so enable bhv draw only
if not self.export_col and not self.export_gfx:
if not self.export_col and not self.export_gfx and not self.export_anim:
return self.draw_bhv_options(col)
# pathing for gfx/col exports
@@ -2332,6 +2446,9 @@ class SM64_CombinedObjectProperties(bpy.types.PropertyGroup):
if self.obj_name_col and self.export_col:
self.draw_col_names(info_box)
if self.obj_name_anim and self.export_anim:
self.draw_anim_names(info_box)
if self.obj_name_bhv:
info_box.label(text=f"Behavior name: {self.bhv_name}")
@@ -2861,6 +2978,8 @@ class SM64_ObjectProperties(bpy.types.PropertyGroup):
game_object: bpy.props.PointerProperty(type=SM64_GameObjectProperties)
segment_loads: bpy.props.PointerProperty(type=SM64_SegmentProperties)
animation: bpy.props.PointerProperty(type=SM64_ArmatureAnimProperties)
@staticmethod
def upgrade_changed_props():
for obj in bpy.data.objects:
+14
View File
@@ -2,6 +2,8 @@ import dataclasses
from typing import NamedTuple, Optional
from pathlib import Path
from io import StringIO
import random
import string
import os
import re
@@ -138,6 +140,18 @@ def convert_addr_to_func(addr: str):
return addr
def temp_file_path(path: Path):
"""Generates a temporary file path that does not exist from the given path."""
result, size = path.with_suffix(".tmp"), 0
for size in range(5, 15):
if not result.exists():
return result
random_suffix = "".join(random.choice(string.ascii_letters) for _ in range(size))
result = path.with_suffix(f".{random_suffix}.tmp")
size += 1
raise PluginError("Cannot create unique temporary file. 10 tries exceeded.")
class ModifyFoundDescriptor:
string: str
regex: str
+2 -2
View File
@@ -1,11 +1,11 @@
from bpy.utils import register_class, unregister_class
from typing import TYPE_CHECKING
from ...panels import SM64_Panel
from .operators import SM64_CreateSimpleLevel, SM64_AddWaterBox, SM64_AddBoneGroups, SM64_CreateMetarig
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..settings.properties import SM64_Properties
+23 -5
View File
@@ -2,7 +2,7 @@ from pathlib import Path
import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator, inspect
from math import pi, ceil, degrees, radians, copysign
from mathutils import *
from .utility_anim import *
from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union
from bpy.types import UILayout, Scene, World
@@ -506,6 +506,7 @@ def saveDataToFile(filepath, data):
def applyBasicTweaks(baseDir):
directory_path_checks(baseDir, "Empty directory path.")
if bpy.context.scene.fast64.sm64.force_extended_ram:
enableExtendedRAM(baseDir)
@@ -714,11 +715,17 @@ def makeWriteInfoBox(layout):
def writeBoxExportType(writeBox, headerType, name, levelName, levelOption):
if not name:
writeBox.label(text="Empty actor name", icon="ERROR")
return
if headerType == "Actor":
writeBox.label(text="actors/" + toAlnum(name))
elif headerType == "Level":
if levelOption != "Custom":
levelName = levelOption
if not name:
writeBox.label(text="Empty level name", icon="ERROR")
return
writeBox.label(text="levels/" + toAlnum(levelName) + "/" + toAlnum(name))
@@ -803,6 +810,13 @@ def scale_mtx_from_vector(scale: mathutils.Vector):
return mathutils.Matrix.Diagonal(scale[0:3]).to_4x4()
def attemptModifierApply(modifier):
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except Exception as e:
print("Skipping modifier " + str(modifier.name))
def copy_object_and_apply(obj: bpy.types.Object, apply_scale=False, apply_modifiers=False):
if apply_scale or apply_modifiers:
# it's a unique mesh, use object name
@@ -1346,15 +1360,15 @@ def bytesToInt(value):
def bytesToHex(value, byteSize=4):
return format(bytesToInt(value), "#0" + str(byteSize * 2 + 2) + "x")
return format(bytesToInt(value), f"#0{(byteSize * 2 + 2)}x")
def bytesToHexClean(value, byteSize=4):
return format(bytesToInt(value), "0" + str(byteSize * 2) + "x")
return format(bytesToInt(value), f"#0{(byteSize * 2)}x")
def intToHex(value, byteSize=4):
return format(value, "#0" + str(byteSize * 2 + 2) + "x")
def intToHex(value, byte_size=4, signed=True):
return format(value if signed else cast_integer(value, byte_size * 8, False), f"#0{(byte_size * 2 + 2)}x")
def intToBytes(value, byteSize):
@@ -1614,6 +1628,10 @@ def bitMask(data, offset, amount):
return (~(-1 << amount) << offset & data) >> offset
def is_bit_active(x: int, index: int):
return ((x >> index) & 1) == 1
def read16bitRGBA(data):
r = bitMask(data, 11, 5) / ((2**5) - 1)
g = bitMask(data, 6, 5) / ((2**5) - 1)
+95 -21
View File
@@ -1,5 +1,10 @@
import bpy, math, mathutils
from bpy.types import Object, Action, AnimData
from bpy.utils import register_class, unregister_class
from bpy.props import StringProperty
from .operators import OperatorBase
from .utility import attemptModifierApply, raisePluginError, PluginError
from typing import TYPE_CHECKING
@@ -23,8 +28,6 @@ class ArmatureApplyWithMeshOperator(bpy.types.Operator):
# Called on demand (i.e. button press, menu item)
# Can also be called from operator search menu (Spacebar)
def execute(self, context):
from .utility import PluginError, raisePluginError
try:
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
@@ -46,6 +49,51 @@ class ArmatureApplyWithMeshOperator(bpy.types.Operator):
return {"FINISHED"} # must return a set
class CreateAnimData(OperatorBase):
bl_idname = "scene.fast64_create_anim_data"
bl_label = "Create Animation Data"
bl_description = "Create animation data"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "ANIM"
def execute_operator(self, context):
obj = context.object
if obj is None:
raise PluginError("No selected object")
if obj.animation_data is None:
obj.animation_data_create()
class AddBasicAction(OperatorBase):
bl_idname = "scene.fast64_add_basic_action"
bl_label = "Add Basic Action"
bl_description = "Create animation data and add basic action"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "ACTION"
def execute_operator(self, context):
if context.object is None:
raise PluginError("No selected object")
create_basic_action(context.object)
class StashAction(OperatorBase):
bl_idname = "scene.fast64_stash_action"
bl_label = "Stash Action"
bl_description = "Stash an action in an object's nla tracks if not already stashed"
context_mode = "OBJECT"
icon = "NLA"
action: StringProperty()
def execute_operator(self, context):
if context.object is None:
raise PluginError("No selected object")
stashActionInArmature(context.object, get_action(self.action))
# This code only handles root bone with no parent, which is the only bone that translates.
def getTranslationRelativeToRest(bone: bpy.types.Bone, inputVector: mathutils.Vector) -> mathutils.Vector:
zUpToYUp = mathutils.Quaternion((1, 0, 0), math.radians(-90.0)).to_matrix().to_4x4()
@@ -63,13 +111,6 @@ def getRotationRelativeToRest(bone: bpy.types.Bone, inputEuler: mathutils.Euler)
return (restRotation.inverted() @ inputEuler.to_matrix().to_4x4()).to_euler("XYZ", inputEuler)
def attemptModifierApply(modifier):
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except Exception as e:
print("Skipping modifier " + str(modifier.name))
def armatureApplyWithMesh(armatureObj: bpy.types.Object, context: bpy.types.Context):
from .utility import selectSingleObject
@@ -179,28 +220,61 @@ def getFrameInterval(action: bpy.types.Action):
return range_get_by_choice[anim_range_choice]()
def stashActionInArmature(armatureObj: bpy.types.Object, action: bpy.types.Action):
def is_action_stashed(obj: Object, action: Action):
animation_data: AnimData | None = obj.animation_data
if animation_data is None:
return False
for track in animation_data.nla_tracks:
for strip in track.strips:
if strip.action is None:
continue
if strip.action.name == action.name:
return True
return False
def stashActionInArmature(obj: Object, action: Action):
"""
Stashes an animation (action) into an armature´s nla tracks.
This prevents animations from being deleted by blender or
purged by the user on accident.
"""
for track in armatureObj.animation_data.nla_tracks:
for strip in track.strips:
if strip.action is None:
continue
if is_action_stashed(obj, action):
return
if strip.action.name == action.name:
return
print(f'Stashing "{action.name}" in the object "{armatureObj.name}".')
track = armatureObj.animation_data.nla_tracks.new()
print(f'Stashing "{action.name}" in the object "{obj.name}".')
if obj.animation_data is None:
obj.animation_data_create()
track = obj.animation_data.nla_tracks.new()
track.name = action.name
track.strips.new(action.name, int(action.frame_range[0]), action)
classes = (ArmatureApplyWithMeshOperator,)
def create_basic_action(obj: Object, name=""):
if obj.animation_data is None:
obj.animation_data_create()
name = name or "Action"
action = bpy.data.actions.new(name)
stashActionInArmature(obj, action)
obj.animation_data.action = action
return action
def get_action(name: str):
if name == "":
raise ValueError("Empty action name.")
if not name in bpy.data.actions:
raise IndexError(f"Action ({name}) is not in this file´s action data.")
return bpy.data.actions[name]
classes = (
ArmatureApplyWithMeshOperator,
CreateAnimData,
AddBasicAction,
StashAction,
)
def utility_anim_register():
+3
View File
@@ -25,3 +25,6 @@ extend-exclude = '''
/addon_updater\.py | /addon_updater_ops\.py
)$
'''
[tool.pyright]
reportInvalidTypeForm = 'none'