mirror of
https://github.com/ApfelTeeSaft/lightspeed64.git
synced 2026-08-26 19:33:24 +00:00
[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:
+12
-2
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -25,3 +25,6 @@ extend-exclude = '''
|
||||
/addon_updater\.py | /addon_updater_ops\.py
|
||||
)$
|
||||
'''
|
||||
|
||||
[tool.pyright]
|
||||
reportInvalidTypeForm = 'none'
|
||||
Reference in New Issue
Block a user