[SM64] Custom level script and special commands and improvements to custom geo commands (#457)

* implement prop

* first iter of custom cmd exporting

* let rovert fly

* Update sm64_objects.py

* Update sm64_objects.py

* Update sm64_objects.py

* upgrade existing custom geo into new custom type

* Hack for geo because

* [SM64] Add depth arg and remove comma from existing cmds

* [SM64] add depth arg to level and collision objects

* change unused depth to _depth and add my stashed changes.. oops

* some code clean up and ui improvement

* Allow custom geo to have children

* command preset system (WIP)

allows you to create presets for your repo.
missing auto updates to presets

* detect changes via a hash

* fix last remaining bugs

* fix preset edit preview

* add draw layers

* basic example macro

* boolean support and fixes

* world's most over enginered way to handle numbers

* bugs and visual fixes

* make color serializable

* dont use PRESET_EDIT if presumably there is no edit data

* use degrees

* inital support for bones

* dont require transform to be inherented, fixes bone using scale arg

* remove sys, last few bits of code tomorrow

* move custom args into a folder

* lock

* typing fixes

* use dict for exports

* early binary support

* Missing dl ext and animated

* dl ext impl

* make is animatable work

* update updateBone to suit my new needs

* finish upgrade code

* dl ext now dl cmd, example macros for dl cmds

* SM64_CustomArgProperties

* enum property support to really hammer in the scope creep

* Nice move by SpongeBob! This match is just about over.

checked for mesh, removed one indent, changed Special to Collision

* some fixes to the ops

* make binary more flexiable by introducing eval expressions

Now behavior that would require a macro can be written in an eval statement, this is peak over engineering. It's also present in c under a toggle because SCALE GEO CMD COMPATABILITY!!!

* make hard defined int types for binary

* one last fix

* allow color quant for background node compat, fix enum ops

* impl last translate/rotate name in custom

* undo export color

* fix area root UI

* fix commas

* add order, fix up a couple of nits

* lila did a stupid

* fix transforms

* conventional rotation in macros is not scaled to s16

* let eval run on tuples

* implement generators

* add cast_integer

* Update properties.py

* round to conventional for scale

* document collection operator base more, fix bug with scale rounding

also add copy_on_add

* undo repo settings ver change since there has been no breaking changes

* remove unused

* implement top level level script commands

* make clean color into tuple, run eval on lists

* update base displaylist node

* little thing i noticed missing

* unnecessary names removed

* Fix transforms in level

* fix parameter

* show animatable toggle in presets

* More control of the level script section

* copy enum example

* over engineered description code

* update preset instead of changing to NONE

* fix number updates

* typo

* fix crash

* VERY IMPORTANT FIX

* link to docs

* add comment about updating existing animation pr ops

* fix animation command checks
This commit is contained in:
Lila
2025-08-21 22:00:20 +01:00
committed by GitHub
parent 8853a484fd
commit 17a6155f5c
24 changed files with 2467 additions and 360 deletions
+2
View File
@@ -61,6 +61,8 @@ Selecting F3DEX3 as your microcode unlocks a large number of additional presets
For cel shading, it is recommended to start with one of the cel shading presets, then modify the settings under the `Use Cel Shading` panel. Hover over each UI control for additional information about how that setting works.
### [Repo Settings](https://fast64.readthedocs.io/en/latest/common/repo_settings/repo_settings.html)
### Fast64 Development
If you'd like to develop in VSCode, follow this tutorial to get proper autocomplete. Skip the linter for now, we'll need to make sure the entire project gets linted before enabling autosave linting because the changes will be massive.
https://b3d.interplanety.org/en/using-microsoft-visual-studio-code-as-external-ide-for-writing-blender-scripts-add-ons/
+4 -1
View File
@@ -6,7 +6,7 @@ from bpy.path import abspath
from . import addon_updater_ops
from .fast64_internal.game_data import game_data
from .fast64_internal.utility import prop_split, multilineLabel, set_prop_if_in_data
from .fast64_internal.utility import prop_split, multilineLabel, set_prop_if_in_data, Matrix4x4Property
from .fast64_internal.repo_settings import (
draw_repo_settings,
@@ -338,6 +338,7 @@ def upgrade_changed_props():
SM64_Properties.upgrade_changed_props()
MK64_Properties.upgrade_changed_props()
SM64_ObjectProperties.upgrade_changed_props()
SM64_BoneProperties.upgrade_changed_props()
OOT_ObjectProperties.upgrade_changed_props()
for scene in bpy.data.scenes:
settings: Fast64Settings_Properties = scene.fast64.settings
@@ -433,6 +434,7 @@ def register():
register_class(ExampleAddonPreferences)
addon_updater_ops.register(bl_info)
register_class(Matrix4x4Property)
initOOTActorProperties()
utility_anim_register()
mat_register()
@@ -490,6 +492,7 @@ def unregister():
mat_unregister()
bsdf_conv_unregister()
bsdf_conv_panel_unregsiter()
unregister_class(Matrix4x4Property)
del bpy.types.Scene.fullTraceback
del bpy.types.Scene.ignoreTextureRestrictions
-2
View File
@@ -376,8 +376,6 @@ def math_eval(s, f3d):
elif isinstance(node, ast.Name):
if hasattr(f3d, node.id):
return getattr(f3d, node.id)
else:
return node.id
elif isinstance(node, ast.Num):
return node.n
elif isinstance(node, ast.UnaryOp):
+107 -1
View File
@@ -1,10 +1,11 @@
from cProfile import Profile
from pstats import SortKey, Stats
from typing import Optional
from typing import TypeVar, Iterable, Optional
import bpy, mathutils
from bpy.types import Operator, Context, UILayout, EnumProperty
from bpy.utils import register_class, unregister_class
from bpy.props import IntProperty, StringProperty
from .utility import (
cleanupTempMeshes,
@@ -13,6 +14,7 @@ from .utility import (
parentObject,
store_original_meshes,
store_original_mtx,
deselectAllObjects,
)
from .f3d.f3d_material import createF3DMat
@@ -79,6 +81,110 @@ class OperatorBase(Operator):
bpy.ops.object.mode_set(mode=starting_mode_set)
CollectionMember = TypeVar("CollectionMember")
class CollectionOperatorBase(OperatorBase):
"""
A basic collection operator, implements basic add/remove/move/clear operations,
but can support more by the subclass implementing the .lower equivelent of the op_name.
See some examples in sm64/custom_cmd/operators.py
"""
# index -1 means no index, so on an add that would mean adding at the end with no copy of the previous element
index: IntProperty(default=-1)
op_name: StringProperty()
copy_on_add: bool = False
object_name: str = "item" # simple name to be used in descriptions
@classmethod
def description(cls, context: Context, properties: dict) -> str:
op_name: str = properties.get("op_name", "")
description = op_name.capitalize()
index = properties.get("index", -1)
if index != -1:
description += f" (copy of {index})"
object_name = cls.object_name
if op_name == "CLEAR":
object_name += "s"
description += f" {object_name}"
return description
@classmethod
def collection(cls, context: Context, op_values: dict) -> Iterable[CollectionMember]:
"""Abstract method for getting the collection from the context"""
raise NotImplementedError()
@classmethod
def is_enabled(cls, context: Context, **op_values) -> bool:
"""Checks if the operation being drawn should be enabled in the UI, for example clear requires the collection to not be empty"""
collection = cls.collection(context, op_values)
op_name: str = op_values.get("op_name", "")
match op_name:
case "MOVE_UP":
return op_values.get("index") > 0
case "MOVE_DOWN":
return op_values.get("index") < len(collection) - 1
case "CLEAR":
return len(collection) > 0
case _:
lower = op_name.lower() + "_enabled"
if hasattr(cls, lower):
return getattr(cls, lower)(context, collection)
return True
@classmethod
def draw_row(cls, row: UILayout, index: int, **op_values):
"""Draw add/remove/move/clear operations, clear only draws in a element-less index (-1)"""
def draw_op(icon: str, op_name: str):
cls.draw_props(row, icon, "", op_name=op_name, index=index, **op_values)
draw_op("ADD", "ADD")
if index == -1:
draw_op("TRASH", "CLEAR")
else:
draw_op("REMOVE", "REMOVE")
draw_op("TRIA_DOWN", "MOVE_DOWN")
draw_op("TRIA_UP", "MOVE_UP")
def add(
self, _context: Context, collection: Iterable[CollectionMember]
) -> tuple[CollectionMember | None, CollectionMember]:
"""Returns the previous element and the newly created element"""
collection.add()
old_arg: CollectionMember | None = None
new_arg: CollectionMember = collection[-1]
if self.index != -1:
collection.move(len(collection) - 1, self.index + 1)
old_arg = collection[self.index]
new_arg = collection[self.index + 1]
if self.copy_on_add:
copyPropertyGroup(old_arg, new_arg)
return old_arg, new_arg
def execute_operator(self, context: Context):
collection = self.__class__.collection(context, self.properties)
match self.op_name:
case "ADD":
self.add(context, collection)
case "REMOVE":
collection.remove(self.index)
case "MOVE_UP":
collection.move(self.index, self.index - 1)
case "MOVE_DOWN":
collection.move(self.index, self.index + 1)
case "CLEAR":
collection.clear()
case _:
lower = self.op_name.lower()
if hasattr(self, lower):
getattr(self, lower)(context, collection)
else:
raise NotImplementedError(f'Unimplemented internal op "{self.op_name}"')
class SearchEnumOperatorBase(OperatorBase):
bl_description = "Search Enum"
bl_label = "Search"
+1 -1
View File
@@ -98,7 +98,7 @@ def save_repo_settings(scene: Scene, path: os.PathLike):
data["sm64"] = save_sm64_repo_settings(scene)
with open(abspath(path), "w", encoding="utf-8") as json_file:
json.dump(data, json_file, indent=2)
json.dump(data, json_file, indent="\t")
def draw_repo_settings(layout: UILayout, context: Context):
+2 -3
View File
@@ -57,9 +57,6 @@ Then after applying the rest pose and skinning, you would apply those operations
## 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.
## Repo settings
Fast64 can save and load repo settings files. By default, they're named fast64.json. These files have RDP defaults, microcode, and more. They also have game-specific settings (OOT will support these in the future). Fast64 will set the path for the settings and auto-load them if auto-load is enabled as soon as the user picks an sm64 decomp path.
### Decomp Export Types
Most exports will let you choose an export type.
@@ -154,6 +151,8 @@ To resolve pointer addresses, for each pointer address,
# Convert offset to segmented address
data[pointer_address] = encode_segmented_address(export_address + current_offset)
### [Custom Commands](https://fast64.readthedocs.io/en/latest/sm64/custom_commands/custom_commands.html)
### Common Issues
Game crashes: Invalid function address for switch/function/held object bones.
Animation root translation/rotation not exporting: Make sure you are animating the root bone, not the armature object.
+4
View File
@@ -95,6 +95,8 @@ from .animation import (
SM64_ActionAnimProperty,
)
from .custom_cmd import custom_cmd_register, custom_cmd_unregister
class SM64_ActionProperty(PropertyGroup):
"""
@@ -135,6 +137,7 @@ def sm64_panel_unregister():
def sm64_register(register_panels: bool):
custom_cmd_register()
tools_operators_register()
tools_props_register()
anim_register()
@@ -156,6 +159,7 @@ def sm64_register(register_panels: bool):
def sm64_unregister(unregister_panels: bool):
custom_cmd_unregister()
tools_operators_unregister()
tools_props_unregister()
anim_unregister()
@@ -90,6 +90,7 @@ class SM64_PreviewAnim(OperatorBase):
anim_props.played_action = played_action
# TODO: update these to use CollectionOperatorBase
class SM64_AnimTableOps(OperatorBase):
bl_idname = "scene.sm64_table_operations"
bl_label = "Table Operations"
+5 -5
View File
@@ -5,14 +5,14 @@ import re
from bpy.types import Context, Object, Action, PoseBone
from ...utility import findStartBones, PluginError, toAlnum
from ..sm64_geolayout_bone import animatableBoneTypes
from ..sm64_geolayout_utility import is_bone_animatable
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):
if obj.type == "ARMATURE" or (obj.type == "MESH" and obj.geo_cmd_static == "DisplayListWithOffset"):
return True
return False
@@ -56,12 +56,12 @@ def get_anim_owners(obj: Object):
if children is None:
return
for child in children:
if child.geo_cmd_static in animatableBoneTypes:
if child.geo_cmd_static == "DisplayListWithOffset":
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:
if obj.geo_cmd_static == "DisplayListWithOffset":
check_children(obj.children)
return [obj]
else:
@@ -81,7 +81,7 @@ def get_anim_owners(obj: Object):
bones_to_process = bones_to_process[1:]
# Only handle 0x13 bones for animation
if current_bone.geo_cmd in animatableBoneTypes:
if is_bone_animatable(current_bone):
anim_bones.append(current_pose_bone)
# Traverse children in alphabetical order.
@@ -0,0 +1,12 @@
from .properties import props_register, props_unregister
from .operators import operators_register, operators_unregister
def custom_cmd_register():
props_register()
operators_register()
def custom_cmd_unregister():
props_unregister()
operators_unregister()
@@ -0,0 +1,406 @@
import dataclasses
import math
import operator
import struct
import ast
from io import StringIO
from typing import Iterable, NamedTuple, Optional, TypeVar, Union
from ...utility import (
PluginError,
get_clean_color,
quantize_color,
cast_integer,
to_s16,
cast_integer,
encodeSegmentedAddr,
)
from ..sm64_constants import SegmentData
from ..sm64_geolayout_utility import BaseDisplayListNode
from .utility import getDrawLayerName
BIT_COUNTS = {"CHAR": 8, "SHORT": 16, "INT": 32, "LONG": 64, "FLOAT": 32, "DOUBLE": 64}
T = TypeVar("T")
def flatten(iterable: Iterable[T]) -> tuple[T]:
if not isinstance(iterable, Iterable) or isinstance(iterable, str):
return (iterable,)
flat = []
for x in iterable:
if isinstance(x, Iterable):
flat.extend(flatten(x))
else:
flat.append(x)
return tuple(flat)
bin_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Mod: operator.mod,
ast.LShift: operator.lshift,
ast.RShift: operator.rshift,
ast.BitOr: operator.or_,
ast.BitAnd: operator.and_,
ast.BitXor: operator.xor,
ast.Pow: operator.pow,
ast.FloorDiv: operator.floordiv,
ast.USub: operator.neg,
ast.UAdd: lambda a: a,
ast.Not: operator.not_,
ast.NotEq: operator.ne,
ast.And: operator.and_,
ast.Or: operator.or_,
ast.In: operator.contains,
ast.NotIn: lambda a, b: not operator.contains(a, b),
ast.Is: operator.is_,
ast.IsNot: operator.is_not,
ast.Eq: operator.eq,
ast.Lt: operator.lt,
ast.LtE: operator.le,
ast.Gt: operator.gt,
ast.GtE: operator.ge,
ast.Invert: operator.invert,
}
builtins_map = {
"round": round,
"abs": abs,
"tuple": tuple,
"list": list,
"set": set,
"dict": dict,
"len": len,
"range": range,
"min": min,
"max": max,
"sum": sum,
"sorted": sorted,
"all": all,
"any": any,
"enumerate": enumerate,
"flatten": flatten,
"cast_integer": cast_integer,
}
collection_constructors = {ast.List: list, ast.Tuple: tuple, ast.Set: set}
def math_eval(s, start_scope: dict[str, object] | None = None):
if start_scope is None:
start_scope = {}
if isinstance(s, int):
return s
s = s.strip()
node = ast.parse(s, mode="eval")
def _eval(node: ast.expr, scope: dict[str, object]):
scope = scope.copy()
def eval_comprehension(elt_node: ast.expr, generators: list[ast.comprehension], scope: dict[str, object]):
if not generators:
result = [_eval(elt_node, scope)]
else:
result = []
first_comp, rest_comps = generators[0], generators[1:]
for value in _eval(first_comp.iter, scope):
new_scope = scope.copy()
if isinstance(first_comp.target, ast.Name):
new_scope[first_comp.target.id] = value
elif isinstance(first_comp.target, (ast.Tuple, ast.List, ast.Set)):
for i, elt in enumerate(first_comp.target.elts):
new_scope[elt.id] = value[i]
if all(_eval(if_node, new_scope) for if_node in first_comp.ifs):
sub_results = eval_comprehension(elt_node, rest_comps, new_scope)
result.extend(sub_results)
return result
if isinstance(node, ast.Name):
if node.id in scope:
return scope[node.id]
elif hasattr(math, node.id):
return getattr(math, node.id)
else:
return builtins_map.get(node.id, node.id)
elif isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.UnaryOp):
return bin_ops[type(node.op)](_eval(node.operand, scope))
elif isinstance(node, ast.BinOp):
return bin_ops[type(node.op)](_eval(node.left, scope), _eval(node.right, scope))
elif isinstance(node, ast.Call):
args = [_eval(x, scope) for x in node.args]
funcName = _eval(node.func, scope)
return funcName(*args)
elif isinstance(node, ast.ListComp):
return eval_comprehension(node.elt, node.generators, scope)
elif isinstance(node, ast.SetComp):
return set(eval_comprehension(node.elt, node.generators, scope))
elif isinstance(node, ast.GeneratorExp):
return eval_comprehension(node.elt, node.generators, scope)
elif isinstance(node, tuple(collection_constructors.keys())):
return collection_constructors[type(node)](_eval(x, scope) for x in node.elts)
elif isinstance(node, ast.Expression):
return _eval(node.body, scope)
elif isinstance(node, ast.Subscript):
return _eval(node.value, scope)[_eval(node.slice, scope)]
elif isinstance(node, ast.Slice):
lower, upper, step = 0, None, None
if node.lower is not None:
lower = _eval(node.lower, scope)
if node.upper is not None:
upper = _eval(node.upper, scope)
if node.step is not None:
step = _eval(node.step, scope)
return slice(lower, upper, step)
elif isinstance(node, ast.IfExp):
if _eval(node.test, scope):
return _eval(node.body, scope)
else:
return _eval(node.orelse, scope)
elif isinstance(node, ast.Compare):
left = _eval(node.left, scope)
for op, right in zip(node.ops, node.comparators):
right = _eval(right, scope)
if not bin_ops[type(op)](left, right):
return False
left = right
return True
else:
raise Exception(f"Unsupported AST node: {ast.dump(node)}")
return _eval(node.body, start_scope)
class ArgExport(NamedTuple):
value: float | int | bool | str
bit_count: int = 32
signed: bool = True
@dataclasses.dataclass
class CustomCmd(BaseDisplayListNode):
data: dict
draw_layer: int | str | None = 0
hasDL: bool = False
dlRef: str = None
name: str = ""
bleed_independently: bool = False
fMesh: "FMesh" = None
DLmicrocode: Union["GfxList", None] = None
# exists to get the override DL from an fMesh
override_hash: tuple | None = None
def __post_init__(self):
self.hasDL &= self.data.get("dl_option") != "NONE"
self.group_children = self.data.get("group_children", True)
@property
def drawLayer(self):
"""HACK: drawLayer's default is usually per bone/object, but in the custom cmd system defaults are per argument.
We instead store a layer that can be none, and set it to a real value if the setter is called.
"""
if self.draw_layer is None:
return 0
return self.draw_layer
@drawLayer.setter
def drawLayer(self, value):
self.draw_layer = value
@property
def args(self):
yield from self.data["args"]
if self.hasDL and "dl_command" in self.data:
yield {"name": "Displaylist", "arg_type": "DL"}
def do_export_checks(self, children_count: int):
name = "" or self.data.get("name") or self.data.get("str_cmd")
name = f" ({name})" if name else ""
children_requirements = self.data.get("children_requirements", "ANY")
if children_requirements == "MUST" and children_count == 0:
raise PluginError(f"Command{name} must have at least one child node")
elif children_requirements == "NONE" and children_count > 0:
raise PluginError(f"Command{name} must have no children")
if self.data.get("dl_option") == "REQUIRED":
if self.DLmicrocode is None:
raise PluginError(f"Command{name} requires a displaylist")
def to_arg(self, data: dict, binary=False) -> Iterable[ArgExport]:
def run_eval(value, bit_count=32, signed=True):
if (
(not self.data["skip_eval"] or binary)
and isinstance(value, (int, float, complex, tuple, list))
and (not isinstance(value, bool) or binary)
and "eval_expression" in data
):
evaluated = math_eval(data["eval_expression"], {"x": value})
yield from tuple(ArgExport(x, bit_count, signed) for x in flatten(evaluated))
else:
yield from tuple(ArgExport(x, bit_count, signed) for x in flatten(value))
arg_type = data.get("arg_type")
round_to_sm64 = data.get("round_to_sm64", True)
match arg_type:
case "COLOR":
if round_to_sm64:
bit_counts = data.get("color_bits", (8, 8, 8, 8))
color = get_clean_color(data["color"], True, False, True)
yield from run_eval(quantize_color(color, bit_counts), sum(bit_counts), False)
else:
yield from run_eval(get_clean_color(data["color"], True, True, True), 32, False)
case "PARAMETER":
if binary:
value = math_eval(data["parameter"], {})
if isinstance(value, str):
raise PluginError("Strings not supported in binary")
yield from run_eval(value)
else:
yield from run_eval(data["parameter"])
case "ENUM":
if data["enum"] >= len(data["enum_options"]):
option = {"int_value": 0, "str_value": "INVALID"}
else:
option = data["enum_options"][data["enum"]]
if binary:
yield from run_eval(option["int_value"])
else:
yield from run_eval(option["str_value"])
case "LAYER":
layer = data["layer"] if self.draw_layer is None or not data.get("inherit", True) else self.draw_layer
if binary:
layer = int(data["layer"])
if "dl_command" in self.data:
layer = (1 << 7) | layer
yield from run_eval(layer, 8, False)
else:
yield from run_eval(getDrawLayerName(layer))
case "BOOLEAN":
yield from run_eval(data["boolean"], 8)
case "NUMBER":
yield from run_eval(data["value"], 32)
case "TRANSLATION":
translation = data["translation"]
if round_to_sm64:
yield from run_eval(tuple(round(x) for x in translation), 16)
else:
yield from run_eval(tuple(x for x in translation), 32)
case "SCALE" | "MATRIX":
scale_matrix = data.get(arg_type.lower())
if round_to_sm64 and arg_type == "SCALE":
yield from run_eval(round(scale_matrix * 0x10000))
yield from run_eval(scale_matrix)
case "ROTATION":
rot_type = data["rot_type"]
rot = data.get(rot_type.lower())
if round_to_sm64 and rot_type == "EULER":
yield from run_eval(tuple(to_s16(round(x)) for x in rot), 16)
else:
yield from run_eval(rot, 32)
case "DL":
has_dl, dl_ref = self.hasDL, self.dlRef
self.hasDL, self.dlRef = True, (data.get("dl") or None)
if binary:
yield from run_eval(self.get_dl_address(), 32)
else:
yield from run_eval(self.get_dl_name(), 32)
self.hasDL, self.dlRef = has_dl, dl_ref
case _:
raise PluginError(f"Unknown arg type {arg_type}")
def to_c(self, depth: int = 0, max_length: int = 150) -> str:
data = StringIO()
dl_command = self.data.get("dl_command")
data.write(dl_command if dl_command is not None and self.hasDL else self.data["str_cmd"])
data.write("(")
groups = []
for i, arg_data in enumerate(self.args):
group = []
try:
for value, _, _ in self.to_arg(arg_data):
if value is None:
value = "NULL"
elif isinstance(value, bool):
value = str(value).upper()
group.append(str(value))
group_str = ", ".join(group)
if "name" in arg_data and arg_data["name"]:
group_str = f"/*{arg_data['name']}*/ {group_str}"
groups.append(group_str)
except Exception as exc:
raise PluginError(f'Failed to export arg "{arg_data.get("name", f"Arg {i}")}": {exc}') from exc
if len("".join(groups)) > max_length:
separator = ",\n" + ("\t" * (depth + 1))
data.write(separator.join(groups))
else:
data.write(", ".join(groups))
data.write(")")
return data.getvalue()
def to_binary_groups(self, segment_data: Optional[SegmentData] = None):
groups = []
groups.append(("Command Index (𝗔𝘂𝘁𝗼𝗺𝗮𝘁𝗶𝗰)", self.data["int_cmd"].to_bytes(1, "big")))
for i, arg_data in enumerate(self.args):
name = arg_data.get("name", f"Arg {i}")
try:
group = bytearray(0)
for value, bit_count, signed in self.to_arg(arg_data, True):
if value is None:
value = 0
signed = arg_data.get("signed", signed)
if "value_type" in arg_data:
bit_count = BIT_COUNTS[arg_data["value_type"]]
if arg_data["value_type"] in {"FLOAT", "DOUBLE"}:
value = float(value)
else:
value = int(value)
if arg_data.get("seg_addr", False) and segment_data is not None:
value = encodeSegmentedAddr(value, segment_data)
if isinstance(value, bytes):
group += value
elif isinstance(value, float):
group += struct.pack("f" if bit_count == 32 else "d", value)
elif isinstance(value, int):
value = cast_integer(value, bit_count, signed)
group += value.to_bytes(math.ceil(bit_count / 8), "big", signed=signed)
else:
raise PluginError(f"{type(value)} not supported in binary")
groups.append((name, group))
except Exception as exc:
raise PluginError(f'Failed to export arg "{name}": \n{exc}') from exc
size = sum(len(data) for _, data in groups)
padding = size % 4
if padding != 0:
groups.append(("Trailing Padding (𝗔𝘂𝘁𝗼𝗺𝗮𝘁𝗶𝗰)", bytes(4 - padding)))
return groups
def to_binary(self, segment_data: Optional[SegmentData] = None):
return bytearray(b for _, data in self.to_binary_groups(segment_data) for b in data)
def size(self, segment_data: Optional[SegmentData] = None):
return sum(len(data) for _, data in self.to_binary_groups(segment_data))
def get_ptr_offsets(self):
return []
def to_text_dump(self, segment_data: Optional[SegmentData] = None):
data = StringIO()
data.write(f"Size: {self.size(segment_data)} bytes.")
if segment_data is None:
data.write("\nNo segment range provided, won't encode to a respective segment")
for name, bytes in self.to_binary_groups(segment_data):
bytes_str = ", ".join(f"0x{byte:02x}" for byte in bytes)
if name:
data.write(f'\n\t"{name}": {bytes_str}')
else:
data.write(f"\n\t{bytes_str}")
return data.getvalue()
@@ -0,0 +1,182 @@
from typing import TYPE_CHECKING, Iterable
from bpy.utils import register_class, unregister_class
from bpy.props import StringProperty, IntProperty, EnumProperty
from bpy.types import Context, Scene
from ...operators import OperatorBase, CollectionOperatorBase, SearchEnumOperatorBase
from ...utility import PluginError
from .utility import custom_cmd_preset_update, duplicate_name, get_custom_cmd_preset_enum, get_custom_prop
if TYPE_CHECKING:
from .properties import SM64_CustomCmdProperties, SM64_CustomArgProperties, SM64_CustomEnumProperties
def get_conf_type(context: Context):
custom = get_custom_prop(context).custom
return "PRESET_EDIT" if custom is None or custom.preset != "NONE" else "NO_PRESET"
class SM64_CustomCmdOps(CollectionOperatorBase):
bl_idname = "scene.sm64_custom_cmd_ops"
bl_label = ""
bl_options = {"UNDO"}
object_name = "custom command preset"
index: IntProperty(default=-1)
op_name: StringProperty()
example_name: StringProperty(default="")
@classmethod
def description(cls, context: Context, properties: dict) -> str:
op_name: str = properties.get("op_name", "")
if op_name == "COPY_EXAMPLE":
return "Copy example defines"
return super().description(context, properties)
@classmethod
def collection(cls, context: Context, op_values: dict) -> Iterable["SM64_CustomCmdProperties"]:
return context.scene.fast64.sm64.custom_cmds
def execute_operator(self, context):
presets = context.scene.fast64.sm64.custom_cmds
custom, owner = get_custom_prop(context)
conf_type = get_conf_type(context)
match self.op_name:
case "ADD":
presets.add()
new_preset: "SM64_CustomCmdProperties" = presets[-1]
old_preset: "SM64_CustomCmdProperties" | None = None
if self.index == -1:
if custom is not None:
old_preset = custom
else:
old_preset = presets[self.index]
if old_preset is not None:
new_preset.from_dict(old_preset.to_dict(conf_type, owner, include_defaults=True), set_defaults=True)
old_name = old_preset.name
else:
old_name = None
existing_names = {preset.name for preset in presets if preset != new_preset}
new_preset.name = duplicate_name(new_preset.name, existing_names, old_name)
new_preset.tab = True
if self.index != -1:
presets.move(len(presets) - 1, self.index + 1)
if custom is not None:
custom.preset = new_preset.name
for area in context.screen.areas: # HACK: redraw everything
area.tag_redraw()
case "REMOVE":
presets.remove(self.index)
case "COPY_EXAMPLE":
preset = presets[self.index] if custom is None else custom
context.window_manager.clipboard = preset.get_examples(owner, conf_type)[self.example_name][1]
case _:
raise NotImplementedError(f'Unimplemented internal custom command preset op "{self.op_name}"')
custom_cmd_preset_update(self, context)
def get_args(context: Context, command_index: int) -> Iterable["SM64_CustomArgProperties"]:
owner = get_custom_prop(context).owner
if isinstance(owner, Scene):
return context.scene.fast64.sm64.custom_cmds[command_index].args
elif owner is not None:
return owner.fast64.sm64.custom.args
else:
raise PluginError("Invalid context")
class SM64_CustomArgsOps(CollectionOperatorBase):
bl_idname = "scene.sm64_custom_args_ops"
bl_label = ""
bl_options = {"UNDO"}
object_name = "arg"
command_index: IntProperty(default=0) # for scene command presets
@classmethod
def collection(cls, context: Context, op_values: dict):
return get_args(context, op_values.get("command_index", 0))
@classmethod
def description(cls, context: Context, properties: dict) -> str:
op_name: str = properties.get("op_name", "")
if op_name == "COPY_EXAMPLE":
return "Copy example enum list"
return super().description(context, properties)
def add(self, context: Context, collection: Iterable["SM64_CustomArgProperties"]):
old, new = super().add(context, collection)
old_name = None
if old is not None:
old_name = old.name
new.from_dict(
old.to_dict(get_conf_type(context), owner=get_custom_prop(context).owner, include_defaults=True),
set_defaults=True,
)
existing_names = {arg.name for arg in collection if arg != new}
new.name = duplicate_name(new.name, existing_names, old_name)
def copy_example(self, context: Context, collection: Iterable["SM64_CustomArgProperties"]):
"""Copy example of enum list to clipboard"""
arg: "SM64_CustomArgProperties" = collection[self.index]
context.window_manager.clipboard = arg.get_enum_list_example()
def execute_operator(self, context: Context):
super().execute_operator(context)
custom_cmd_preset_update(self, context)
class SM64_CustomEnumOps(CollectionOperatorBase):
bl_idname = "scene.sm64_custom_enum_ops"
bl_label = ""
bl_options = {"UNDO"}
object_name = "enum option"
command_index: IntProperty(default=0) # for scene command presets
arg_index: IntProperty(default=0)
@classmethod
def collection(cls, context: Context, op_values: dict) -> Iterable["SM64_CustomEnumProperties"]:
args = get_args(context, op_values.get("command_index", 0))
return args[op_values.get("arg_index", 0)].enum_options
def add(self, context: Context, collection: Iterable["SM64_CustomArgProperties"]):
old, new = CollectionOperatorBase.add(self, context, collection)
old_name = None
if old is not None:
old_name = old.name
new.from_dict(old.to_dict())
existing_names = {enum.name for enum in collection if enum != new}
new.name = duplicate_name(new.name, existing_names, old_name)
class SM64_SearchCustomCmds(SearchEnumOperatorBase):
bl_idname = "scene.sm64_search_custom_cmds"
bl_label = "Search Custom Commands"
bl_options = {"REGISTER", "UNDO"}
bl_property = "preset"
preset: EnumProperty(items=get_custom_cmd_preset_enum)
def update_enum(self, context):
context.object.fast64.sm64.custom.preset = self.preset
classes = (
SM64_CustomEnumOps,
SM64_CustomArgsOps,
SM64_CustomCmdOps,
SM64_SearchCustomCmds,
)
def operators_register():
for cls in classes:
register_class(cls)
def operators_unregister():
for cls in reversed(classes):
unregister_class(cls)
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
from typing import Literal, NamedTuple, Optional, TYPE_CHECKING
from re import fullmatch
import mathutils
from bpy.types import Object, Bone, Context, SpaceView3D, Scene
from ...utility import z_up_to_y_up_matrix
from ..sm64_geolayout_utility import updateBone
if TYPE_CHECKING:
from .properties import SM64_CustomCmdProperties
AvailableOwners = Object | Bone | Scene
CustomCmdConf = Literal["PRESET", "PRESET_EDIT", "NO_PRESET"] # type of configuration
def getDrawLayerName(drawLayer):
from ..sm64_geolayout_classes import getDrawLayerName
return getDrawLayerName(drawLayer)
def duplicate_name(name, existing_names: set, old_name: Optional[str] = None):
if not name in existing_names:
return name
num = 0
if old_name is not None:
number_match = fullmatch(r"(.*?)\.(\d+)$", old_name)
if number_match is not None: # if name already a duplicate/copy, add number
name, num = number_match.group(1), int(number_match.group(2))
else:
name, num = old_name, 0
for i in range(1, len(existing_names) + 1):
new_name = f"{name}.{num+i:03}"
if new_name not in existing_names: # only use name if it's unique
return new_name
assert False, "Failed to generate unique name"
def get_custom_prop(context: Context):
"""If owner is a scene, custom is always None"""
class CustomContext(NamedTuple):
custom: Optional["SM64_CustomCmdProperties"]
owner: Optional[AvailableOwners]
if isinstance(context.space_data, SpaceView3D):
return CustomContext(None, context.scene)
else:
if hasattr(context, "bone") and context.bone is not None:
return CustomContext(context.bone.fast64.sm64.custom, context.bone)
if hasattr(context, "object") and context.object is not None:
return CustomContext(context.object.fast64.sm64.custom, context.object)
return CustomContext(None, None)
def get_custom_cmd_preset(
custom_cmd: "SM64_CustomCmdProperties", context: Context
) -> Optional["SM64_CustomCmdProperties"]:
if custom_cmd.preset == "":
return None
presets: dict["SM64_CustomCmdProperties"] = {
custom.name: custom for custom in context.scene.fast64.sm64.custom_cmds
}
return presets[custom_cmd.preset]
def check_preset_hashes(owner: AvailableOwners, context: Context):
if owner.fast64.sm64.custom.locked:
return
custom_cmd: "SM64_CustomCmdProperties" = owner.fast64.sm64.custom
if custom_cmd.preset == "NONE":
return
preset_cmd = get_custom_cmd_preset(custom_cmd, context)
if preset_cmd is None:
custom_cmd.preset = "NONE"
elif custom_cmd.saved_hash != preset_cmd.preset_hash:
custom_cmd.from_dict(
preset_cmd.to_dict("PRESET_EDIT", owner, *get_transforms(owner), include_defaults=False), set_defaults=False
)
custom_cmd.saved_hash = preset_cmd.preset_hash
def custom_cmd_preset_update(_self, context: Context):
owner = get_custom_prop(context).owner
if isinstance(owner, Scene): # current context is scene, check all
for obj in context.scene.objects:
check_preset_hashes(obj, context)
if obj.type == "ARMATURE":
for bone in obj.data.bones:
check_preset_hashes(bone, context)
elif owner is not None:
check_preset_hashes(owner, context)
if isinstance(owner, Bone):
updateBone(owner, context)
def get_custom_cmd_preset_enum(_self, context: Context):
if isinstance(get_custom_prop(context)[1], Bone):
allowed_types = {"Geo"}
else:
allowed_types = {"Level", "Geo", "Special"}
return [("NONE", "No Preset", "No preset selected")] + [
(preset.name, preset.name, f"{preset.name} ({preset.cmd_type})")
for preset in (context.scene.fast64.sm64.custom_cmds)
if preset.cmd_type in allowed_types
]
def better_round(value: float): # round, but handle inf
return round(max(-(2**31), min(2**31 - 1, value)))
def get_transforms(owner: Optional[AvailableOwners] = None):
if isinstance(owner, Object):
return tuple(
z_up_to_y_up_matrix @ x @ z_up_to_y_up_matrix.inverted() for x in [owner.matrix_world, owner.matrix_local]
)
elif isinstance(owner, Bone):
relative = owner.matrix_local
if owner.parent is not None:
relative = owner.parent.matrix_local.inverted() @ relative
return tuple(z_up_to_y_up_matrix @ x @ z_up_to_y_up_matrix.inverted() for x in [owner.matrix_local, relative])
else:
return (z_up_to_y_up_matrix @ mathutils.Matrix.Identity(4) @ z_up_to_y_up_matrix.inverted(),) * 2
+20 -1
View File
@@ -2,7 +2,15 @@ import os
from pathlib import Path
import bpy
from bpy.types import PropertyGroup, UILayout, Context
from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty, PointerProperty
from bpy.props import (
BoolProperty,
StringProperty,
EnumProperty,
IntProperty,
FloatProperty,
PointerProperty,
CollectionProperty,
)
from bpy.path import abspath
from bpy.utils import register_class, unregister_class
@@ -17,6 +25,7 @@ from ...utility import (
)
from ..sm64_constants import defaultExtendSegment4, OLD_BINARY_LEVEL_ENUMS
from ..sm64_objects import SM64_CombinedObjectProperties
from ..custom_cmd.properties import SM64_CustomCmdProperties, draw_custom_cmd_presets
from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings
from ..tools import SM64_AddrConvProperties
from ..animation.properties import SM64_AnimProperties
@@ -49,6 +58,8 @@ class SM64_Properties(PropertyGroup):
goal: EnumProperty(items=enum_sm64_goal_type, name="Goal", default="All")
combined_export: bpy.props.PointerProperty(type=SM64_CombinedObjectProperties)
animation: PointerProperty(type=SM64_AnimProperties)
custom_cmds: CollectionProperty(type=SM64_CustomCmdProperties)
custom_cmds_tab: BoolProperty(default=True, name="Custom Commands")
address_converter: PointerProperty(type=SM64_AddrConvProperties)
blender_to_sm64_scale: FloatProperty(
@@ -196,6 +207,8 @@ class SM64_Properties(PropertyGroup):
data["write_all"] = self.write_all
if not self.hackersm64:
data["designated"] = self.designated_prop
if self.custom_cmds:
data["custom_cmds"] = [preset.to_dict("PRESET_EDIT") for preset in self.custom_cmds]
return data
def from_repo_settings(self, data: dict):
@@ -206,6 +219,11 @@ class SM64_Properties(PropertyGroup):
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")
if "custom_cmds" in data:
self.custom_cmds.clear()
for preset_data in data.get("custom_cmds", []):
self.custom_cmds.add()
self.custom_cmds[-1].from_dict(preset_data)
def draw_repo_settings(self, layout: UILayout):
col = layout.column()
@@ -218,6 +236,7 @@ class SM64_Properties(PropertyGroup):
if self.matstack_fix:
col.prop(self, "lighting_engine_presets")
col.prop(self, "write_all")
draw_custom_cmd_presets(self, col.box())
def draw_props(self, layout: UILayout, show_repo_settings: bool = True):
col = layout.column()
+2 -2
View File
@@ -135,11 +135,11 @@ class Collision:
if len(self.specials) > 0:
data.source += "\tCOL_SPECIAL_INIT(" + str(len(self.specials)) + "),\n"
for special in self.specials:
data.source += "\t" + special.to_c()
data.source += "\t" + special.to_c(1) + ",\n"
if len(self.water_boxes) > 0:
data.source += "\tCOL_WATER_BOX_INIT(" + str(len(self.water_boxes)) + "),\n"
for waterBox in self.water_boxes:
data.source += "\t" + waterBox.to_c()
data.source += "\t" + waterBox.to_c(1) + ",\n"
data.source += "\tCOL_END()\n" + "};\n"
return data
+26 -35
View File
@@ -1,10 +1,11 @@
import bpy
from bpy.ops import object
from bpy.types import Bone, Object, Panel, Operator, Armature, Mesh, Material, PropertyGroup
from bpy.types import Bone, Object, Context, Panel, Operator, Armature, Mesh, Material, PropertyGroup
from bpy.utils import register_class, unregister_class
from ..utility import PluginError, prop_split, obj_scale_is_unified
from ..utility import PluginError, get_first_set_prop, prop_split, obj_scale_is_unified, upgrade_old_prop
from ..f3d.f3d_material import sm64EnumDrawLayers
from .sm64_geolayout_utility import createBoneGroups, addBoneToGroup
from .sm64_geolayout_utility import updateBone
from .custom_cmd.properties import SM64_CustomCmdProperties
from bpy.props import (
StringProperty,
@@ -34,12 +35,10 @@ enumBoneType = [
("Ignore", "Ignore", "Ignore bones when exporting."),
("SwitchOption", "Switch Option", "Switch Option"),
("DisplayListWithOffset", "Animated Part (0x13)", "Animated Part (Animatable Bone)"),
("CustomAnimated", "Custom Animated", "Custom Bone used for animation"),
("CustomNonAnimated", "Custom (Non-animated)", "Custom geolayout bone, non animated"),
("", "", ""),
("Custom", "Custom", "Custom bone using command presets"),
]
animatableBoneTypes = {"DisplayListWithOffset", "CustomAnimated"}
enumGeoStaticType = [
("Billboard", "Billboard (0x14)", "Billboard"),
("DisplayListWithOffset", "Animated Part (0x13)", "Animated Part (Animatable Bone)"),
@@ -81,13 +80,14 @@ enumMatOverrideOptions = [
]
def drawGeoInfo(panel: Panel, bone: Bone):
bone_props: "SM64_BoneProperties" = bone.fast64.sm64
def drawGeoInfo(panel: Panel, context: Context):
panel.layout.box().label(text="Geolayout Inspector")
bone = context.bone
if bone is None:
panel.layout.label(text="Edit geolayout properties in Pose mode.")
return
bone_props: "SM64_BoneProperties" = bone.fast64.sm64
sm64_props: "SM64_Properties" = context.scene.fast64.sm64
col = panel.layout.column()
prop_split(col, bone, "geo_cmd", "Geolayout Command")
@@ -109,7 +109,6 @@ def drawGeoInfo(panel: Panel, bone: Bone):
"DisplayList",
"Scale",
"DisplayListWithOffset",
"CustomAnimated",
]:
drawLayerWarningBox(col, bone, "draw_layer")
if bpy.context.scene.exportInlineF3D:
@@ -149,14 +148,10 @@ def drawGeoInfo(panel: Panel, bone: Bone):
infoBoxRenderArea.label(text="See the object properties window for the armature instead.")
prop_split(col, bone, "culling_radius", "Culling Radius")
elif bone.geo_cmd in {"CustomAnimated", "CustomNonAnimated"}:
prop_split(col, bone_props, "custom_geo_cmd_macro", "Geo Command Macro")
if bone.geo_cmd == "CustomNonAnimated":
prop_split(col, bone_props, "custom_geo_cmd_args", "Geo Command Args")
else: # It's animated
infobox = col.box()
infobox.label(text="Command's args will be filled with layer, translate, and rotate", icon="INFO")
infobox.label(text="e.g. `GEO_CUSTOM(layer, tX, tY, tZ, rX, rY, rZ, displayList)`")
elif bone.geo_cmd == "Custom":
bone_props.custom.draw_props(
col, sm64_props.binary_export, context.bone, "NO_PRESET", sm64_props.blender_to_sm64_scale
)
# if bone.geo_cmd == 'SwitchOption':
# prop_split(col, bone, 'switch_bone', 'Switch Bone')
@@ -180,7 +175,7 @@ class GeolayoutBonePanel(Panel):
return context.scene.gameEditorMode == "SM64"
def draw(self, context):
drawGeoInfo(self, context.bone)
drawGeoInfo(self, context)
class GeolayoutArmaturePanel(Panel):
@@ -458,23 +453,9 @@ def getSwitchOptionBone(switchArmature):
return optionBones[0]
def updateBone(bone, context):
armatureObj = context.object
createBoneGroups(armatureObj)
if bone.geo_cmd not in animatableBoneTypes:
addBoneToGroup(armatureObj, bone.name, bone.geo_cmd)
object.mode_set(mode="POSE")
else:
addBoneToGroup(armatureObj, bone.name, None)
object.mode_set(mode="POSE")
class SM64_BoneProperties(PropertyGroup):
version: IntProperty(name="SM64_BoneProperties Version", default=0)
custom_geo_cmd_macro: StringProperty(name="Geo Command Macro", default="GEO_BONE")
custom_geo_cmd_args: StringProperty(name="Geo Command Args", default="")
custom: PointerProperty(type=SM64_CustomCmdProperties)
revert_previous_mat: BoolProperty(name="Revert Previous Material", default=False)
revert_after_mat: BoolProperty(
name="Revert After Material",
@@ -483,6 +464,16 @@ class SM64_BoneProperties(PropertyGroup):
)
revert_before_func: BoolProperty(name="Revert Before Function", default=True)
def upgrade_bone(self, bone):
self.custom.upgrade_bone(bone)
@staticmethod
def upgrade_changed_props():
for obj in bpy.data.objects:
if obj.type == "ARMATURE":
for bone in obj.data.bones:
bone.fast64.sm64.upgrade_bone(bone)
sm64_bone_classes = (
AddSwitchOption,
+82 -150
View File
@@ -50,6 +50,8 @@ from .sm64_geolayout_constants import (
GEO_SETUP_OBJ_RENDER,
GEO_SET_BG,
)
from .sm64_geolayout_utility import BaseDisplayListNode
from .custom_cmd.exporting import CustomCmd
from .sm64_utility import convert_addr_to_func
drawLayerNames = {
@@ -279,45 +281,6 @@ class Geolayout:
return drawLayers
class BaseDisplayListNode:
"""Base displaylist node with common helper functions dealing with displaylists"""
dl_ext = "WITH_DL" # add dl_ext to geo command if command has a displaylist
override_layer = False
dlRef: str | GfxList | None
def get_dl_address(self):
assert not isinstance(self.dlRef, str), "dlRef string not supported in binary"
if isinstance(self.dlRef, GfxList):
return self.dlRef.startAddress
if self.hasDL and self.DLmicrocode is not None:
return self.DLmicrocode.startAddress
return None
def get_dl_name(self):
if isinstance(self.dlRef, GfxList):
return self.dlRef.name
if self.hasDL and (self.dlRef or self.DLmicrocode is not None):
return self.dlRef or self.DLmicrocode.name
return "NULL"
def get_c_func_macro(self, base_cmd: str):
return f"{base_cmd}_{self.dl_ext}" if self.hasDL else base_cmd
def c_func_macro(self, base_cmd: str, *args: str):
"""
Supply base command and all arguments for command.
if self.hasDL:
this will add self.dl_ext to the command, and
adds the name of the displaylist to the end of the command
Example return: 'GEO_YOUR_COMMAND_WITH_DL(arg, arg2),'
"""
all_args = list(args)
if self.hasDL:
all_args.append(self.get_dl_name())
return f'{self.get_c_func_macro(base_cmd)}({", ".join(all_args)}),'
class TransformNode:
def __init__(self, node):
self.node = node
@@ -329,6 +292,19 @@ class TransformNode:
self.revert_previous_mat = False
self.revert_after_mat = False
def do_export_checks(self):
if self.node is not None:
if hasattr(self.node, "do_export_checks"):
self.node.do_export_checks(len(self.children))
@property
def groups(self):
if isinstance(self.node, tuple(nodeGroupClasses)):
return True
if hasattr(self.node, "group_children"):
return self.node.group_children
return False
def convertToDynamic(self):
if self.node.hasDL:
funcNode = FunctionNode(self.node.DLmicrocode.name, self.node.drawLayer)
@@ -364,7 +340,7 @@ class TransformNode:
if self.node is not None:
if getattr(self.node, "hasDL", False):
return True
if type(self.node) in (JumpNode, SwitchNode, FunctionNode, ShadowNode, CustomNode, CustomAnimatedNode):
if type(self.node) in (JumpNode, SwitchNode, FunctionNode, ShadowNode, CustomCmd):
return True
for child in self.children:
if child.has_data():
@@ -373,7 +349,7 @@ class TransformNode:
def size(self):
size = self.node.size() if self.node is not None else 0
if len(self.children) > 0 and type(self.node) in nodeGroupClasses:
if len(self.children) > 0 and self.groups:
size += 8 # node open/close
for child in self.children:
size += child.size()
@@ -383,6 +359,7 @@ class TransformNode:
# Function commands usually effect the following command, so it is similar
# to a parent child relationship.
def to_binary(self, segmentData):
self.do_export_checks()
if self.node is not None:
data = self.node.to_binary(segmentData)
else:
@@ -391,37 +368,39 @@ class TransformNode:
if type(self.node) is FunctionNode:
raise PluginError("An FunctionNode cannot have children.")
if type(self.node) in nodeGroupClasses:
if self.groups:
data.extend(bytearray([GEO_NODE_OPEN, 0x00, 0x00, 0x00]))
for child in self.children:
data.extend(child.to_binary(segmentData))
if type(self.node) in nodeGroupClasses:
if self.groups:
data.extend(bytearray([GEO_NODE_CLOSE, 0x00, 0x00, 0x00]))
elif type(self.node) is SwitchNode:
raise PluginError("A switch bone must have at least one child bone.")
return data
def to_c(self, depth):
self.do_export_checks()
if self.node is not None:
nodeC = self.node.to_c()
nodeC = self.node.to_c(depth)
if nodeC is not None: # Should only be the case for DisplayListNode with no DL
data = depth * "\t" + self.node.to_c() + "\n"
data = ("\t" * depth) + f"{nodeC},\n"
else:
data = ""
else:
data = ""
if len(self.children) > 0:
if type(self.node) in nodeGroupClasses:
data += depth * "\t" + "GEO_OPEN_NODE(),\n"
if self.groups:
data += ("\t" * depth) + "GEO_OPEN_NODE(),\n"
for child in self.children:
data += child.to_c(depth + (1 if type(self.node) in nodeGroupClasses else 0))
if type(self.node) in nodeGroupClasses:
data += depth * "\t" + "GEO_CLOSE_NODE(),\n"
data += child.to_c(depth + (1 if self.groups else 0))
if self.groups:
data += ("\t" * depth) + "GEO_CLOSE_NODE(),\n"
elif type(self.node) is SwitchNode:
raise PluginError("A switch bone must have at least one child bone.")
return data
def toTextDump(self, nodeLevel, segmentData):
self.do_export_checks()
data = ""
if self.node is not None:
command = self.node.to_binary(segmentData)
@@ -434,11 +413,11 @@ class TransformNode:
data += "\n"
if len(self.children) > 0:
if type(self.node) in nodeGroupClasses:
if self.groups:
data += "\t" * nodeLevel + "04 00 00 00\n"
for child in self.children:
data += child.toTextDump(nodeLevel + (1 if type(self.node) in nodeGroupClasses else 0), segmentData)
if type(self.node) in nodeGroupClasses:
data += child.toTextDump(nodeLevel + (1 if self.groups else 0), segmentData)
if self.groups:
data += "\t" * nodeLevel + "05 00 00 00\n"
elif type(self.node) is SwitchNode:
raise PluginError("A switch bone must have at least one child bone.")
@@ -488,9 +467,9 @@ class JumpNode:
command.extend(startAddress)
return command
def to_c(self):
def to_c(self, _depth=0):
geo_name = self.geoRef or self.geolayout.name
return "GEO_BRANCH(" + ("1, " if self.storeReturn else "0, ") + geo_name + "),"
return "GEO_BRANCH(" + ("1, " if self.storeReturn else "0, ") + geo_name + ")"
LastMaterials = dict[int, tuple[FMaterial | None, list[tuple[GfxList, dict[type, GbiMacro]]]]]
@@ -604,6 +583,12 @@ class FunctionNode:
self.func_param = func_param
self.hasDL = False
def do_export_checks(self, children_count: int):
if children_count > 0:
raise PluginError(
"Function bones cannot have children. They instead affect the next sibling bone in alphabetical order."
)
def size(self):
return 8
@@ -614,8 +599,8 @@ class FunctionNode:
addFuncAddress(command, self.geo_func)
return command
def to_c(self):
return "GEO_ASM(" + str(self.func_param) + ", " + convert_addr_to_func(self.geo_func) + "),"
def to_c(self, _depth=0):
return "GEO_ASM(" + str(self.func_param) + ", " + convert_addr_to_func(self.geo_func) + ")"
class HeldObjectNode:
@@ -634,7 +619,7 @@ class HeldObjectNode:
addFuncAddress(command, self.geo_func)
return command
def to_c(self):
def to_c(self, _depth=0):
return (
"GEO_HELD_OBJECT(0, "
+ str(convertFloatToShort(self.translate[0]))
@@ -644,7 +629,7 @@ class HeldObjectNode:
+ str(convertFloatToShort(self.translate[2]))
+ ", "
+ convert_addr_to_func(self.geo_func)
+ "),"
+ ")"
)
@@ -659,8 +644,8 @@ class StartNode:
command = bytearray([GEO_START, 0x00, 0x00, 0x00])
return command
def to_c(self):
return "GEO_NODE_START(),"
def to_c(self, _depth=0):
return "GEO_NODE_START()"
class EndNode:
@@ -674,8 +659,8 @@ class EndNode:
command = bytearray([GEO_END, 0x00, 0x00, 0x00])
return command
def to_c(self):
return "GEO_END(),"
def to_c(self, _depth=0):
return "GEO_END()"
# Geolayout node hierarchy is first generated without material/draw layer
@@ -699,8 +684,8 @@ class SwitchNode:
addFuncAddress(command, self.switchFunc)
return command
def to_c(self):
return "GEO_SWITCH_CASE(" + str(self.defaultCase) + ", " + convert_addr_to_func(self.switchFunc) + "),"
def to_c(self, _depth=0):
return "GEO_SWITCH_CASE(" + str(self.defaultCase) + ", " + convert_addr_to_func(self.switchFunc) + ")"
class TranslateRotateNode(BaseDisplayListNode):
@@ -771,7 +756,7 @@ class TranslateRotateNode(BaseDisplayListNode):
command.extend(bytearray([0x00] * 4))
return command
def to_c(self):
def to_c(self, _depth=0):
if self.fieldLayout == 0:
return self.c_func_macro(
"GEO_TRANSLATE_ROTATE",
@@ -838,7 +823,7 @@ class TranslateNode(BaseDisplayListNode):
command.extend(bytearray([0x00] * 4))
return command
def to_c(self):
def to_c(self, _depth=0):
return self.c_func_macro(
"GEO_TRANSLATE_NODE",
getDrawLayerName(self.drawLayer),
@@ -881,7 +866,7 @@ class RotateNode(BaseDisplayListNode):
command.extend(bytearray([0x00] * 4))
return command
def to_c(self):
def to_c(self, _depth=0):
return self.c_func_macro(
"GEO_ROTATION_NODE",
getDrawLayerName(self.drawLayer),
@@ -923,7 +908,7 @@ class BillboardNode(BaseDisplayListNode):
command.extend(bytearray([0x00] * 4))
return command
def to_c(self):
def to_c(self, _depth=0):
return self.c_func_macro(
"GEO_BILLBOARD_WITH_PARAMS",
getDrawLayerName(self.drawLayer),
@@ -958,11 +943,11 @@ class DisplayListNode(BaseDisplayListNode):
command.extend(bytearray([0x00] * 4))
return command
def to_c(self):
def to_c(self, _depth=0):
if not self.hasDL:
return None
args = [getDrawLayerName(self.drawLayer), self.get_dl_name()]
return f"GEO_DISPLAY_LIST({join_c_args(args)}),"
return f"GEO_DISPLAY_LIST({join_c_args(args)})"
class ShadowNode:
@@ -982,9 +967,9 @@ class ShadowNode:
command.extend(self.shadowScale.to_bytes(2, "big"))
return command
def to_c(self):
def to_c(self, _depth=0):
return (
"GEO_SHADOW(" + str(self.shadowType) + ", " + str(self.shadowSolidity) + ", " + str(self.shadowScale) + "),"
"GEO_SHADOW(" + str(self.shadowType) + ", " + str(self.shadowSolidity) + ", " + str(self.shadowScale) + ")"
)
@@ -1016,7 +1001,7 @@ class ScaleNode(BaseDisplayListNode):
command.extend(bytearray([0x00] * 4))
return command
def to_c(self):
def to_c(self, _depth=0):
return self.c_func_macro(
"GEO_SCALE", getDrawLayerName(self.drawLayer), str(int(round(self.scaleValue * 0x10000)))
)
@@ -1035,12 +1020,12 @@ class StartRenderAreaNode:
command.extend(convertFloatToShort(self.cullingRadius).to_bytes(2, "big"))
return command
def to_c(self):
def to_c(self, _depth=0):
cullingRadius = convertFloatToShort(self.cullingRadius)
# if abs(cullingRadius) > 2**15 - 1:
# raise PluginError("A render area node has a culling radius that does not fit an s16.\n Radius is " +\
# str(cullingRadius) + ' when converted to SM64 units.')
return "GEO_CULLING_RADIUS(" + str(convertFloatToShort(self.cullingRadius)) + "),"
return "GEO_CULLING_RADIUS(" + str(convertFloatToShort(self.cullingRadius)) + ")"
class RenderRangeNode:
@@ -1058,13 +1043,13 @@ class RenderRangeNode:
command.extend(convertFloatToShort(self.maxDist).to_bytes(2, "big"))
return command
def to_c(self):
def to_c(self, _depth=0):
minDist = convertFloatToShort(self.minDist)
maxDist = convertFloatToShort(self.maxDist)
# if (abs(minDist) > 2**15 - 1) or (abs(maxDist) > 2**15 - 1):
# raise PluginError("A render range (LOD) node has a range that does not fit an s16.\n Range is " +\
# str(minDist) + ', ' + str(maxDist) + ' when converted to SM64 units.')
return "GEO_RENDER_RANGE(" + str(minDist) + ", " + str(maxDist) + "),"
return "GEO_RENDER_RANGE(" + str(minDist) + ", " + str(maxDist) + ")"
class DisplayListWithOffsetNode(BaseDisplayListNode):
@@ -1095,7 +1080,7 @@ class DisplayListWithOffsetNode(BaseDisplayListNode):
command.extend(bytearray([0x00] * 4))
return command
def to_c(self):
def to_c(self, _depth=0):
args = [
getDrawLayerName(self.drawLayer),
str(convertFloatToShort(self.translate[0])),
@@ -1103,7 +1088,7 @@ class DisplayListWithOffsetNode(BaseDisplayListNode):
str(convertFloatToShort(self.translate[2])),
self.get_dl_name(), # This node requires 'NULL' if there is no DL
]
return f"GEO_ANIMATED_PART({join_c_args(args)}),"
return f"GEO_ANIMATED_PART({join_c_args(args)})"
class ScreenAreaNode:
@@ -1129,10 +1114,10 @@ class ScreenAreaNode:
command.extend(dimensions[1].to_bytes(2, "big", signed=True))
return command
def to_c(self):
def to_c(self, _depth=0):
if self.useDefaults:
return (
"GEO_NODE_SCREEN_AREA(10, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2),"
"GEO_NODE_SCREEN_AREA(10, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2)"
)
else:
return (
@@ -1146,7 +1131,7 @@ class ScreenAreaNode:
+ str(self.dimensions[0])
+ ", "
+ str(self.dimensions[1])
+ "),"
+ ")"
)
@@ -1164,8 +1149,8 @@ class OrthoNode:
command.extend(bytearray(pack(">f", self.scale)))
return command
def to_c(self):
return "GEO_NODE_ORTHO(" + format(self.scale, ".4f") + "),"
def to_c(self, _depth=0):
return "GEO_NODE_ORTHO(" + format(self.scale, ".4f") + ")"
class FrustumNode:
@@ -1189,9 +1174,9 @@ class FrustumNode:
command.extend(bytes.fromhex("8029AA3C"))
return command
def to_c(self):
def to_c(self, _depth=0):
if not self.useFunc:
return "GEO_CAMERA_FRUSTUM(" + format(self.fov, ".4f") + ", " + str(self.near) + ", " + str(self.far) + "),"
return "GEO_CAMERA_FRUSTUM(" + format(self.fov, ".4f") + ", " + str(self.near) + ", " + str(self.far) + ")"
else:
return (
"GEO_CAMERA_FRUSTUM_WITH_FUNC("
@@ -1200,7 +1185,7 @@ class FrustumNode:
+ str(self.near)
+ ", "
+ str(self.far)
+ ", geo_camera_fov),"
+ ", geo_camera_fov)"
)
@@ -1216,8 +1201,8 @@ class ZBufferNode:
command = bytearray([GEO_SET_Z_BUF, 0x01 if self.enable else 0x00, 0x00, 0x00])
return command
def to_c(self):
return "GEO_ZBUFFER(" + ("1" if self.enable else "0") + "),"
def to_c(self, _depth=0):
return "GEO_ZBUFFER(" + ("1" if self.enable else "0") + ")"
class CameraNode:
@@ -1243,7 +1228,7 @@ class CameraNode:
addFuncAddress(command, self.geo_func)
return command
def to_c(self):
def to_c(self, _depth=0):
return (
"GEO_CAMERA("
+ str(self.camType)
@@ -1261,7 +1246,7 @@ class CameraNode:
+ str(self.lookAt[2])
+ ", "
+ convert_addr_to_func(self.geo_func)
+ "),"
+ ")"
)
@@ -1277,8 +1262,8 @@ class RenderObjNode:
command = bytearray([GEO_SETUP_OBJ_RENDER, 0x00, 0x00, 0x00])
return command
def to_c(self):
return "GEO_RENDER_OBJ(),"
def to_c(self, _depth=0):
return "GEO_RENDER_OBJ()"
class BackgroundNode:
@@ -1300,61 +1285,11 @@ class BackgroundNode:
addFuncAddress(command, self.geo_func)
return command
def to_c(self):
def to_c(self, _depth=0):
if self.isColor:
return "GEO_BACKGROUND_COLOR(0x" + format(self.backgroundValue, "04x").upper() + "),"
return "GEO_BACKGROUND_COLOR(0x" + format(self.backgroundValue, "04x").upper() + ")"
else:
return "GEO_BACKGROUND(" + str(self.backgroundValue) + ", " + convert_addr_to_func(self.geo_func) + "),"
class CustomNode:
def __init__(self, command: str, args: str):
self.command = command
self.args = args or "" # command may not have args
self.hasDL = False
def size(self):
return 8
def to_binary(self, segmentData):
raise PluginError("Custom Geo Nodes are not supported for binary exports.")
def to_c(self):
return f"{self.command}({self.args}),"
class CustomAnimatedNode(BaseDisplayListNode):
def __init__(self, command: str, drawLayer, translate, rotate, dlRef: str = None):
self.command = command
self.drawLayer = drawLayer
self.hasDL = True
self.translate = translate
self.rotate = rotate
self.fMesh = None
self.DLmicrocode = None
self.dlRef = dlRef
# exists to get the override DL from an fMesh
self.override_hash = None
def size(self):
return 16
def get_ptr_offsets(self):
return []
def to_binary(self, segmentData):
raise PluginError("Custom Geo Nodes are not supported for binary exports.")
def to_c(self):
args = [
getDrawLayerName(self.drawLayer),
str(convertFloatToShort(self.translate[0])),
str(convertFloatToShort(self.translate[1])),
str(convertFloatToShort(self.translate[2])),
*(str(radians_to_s16(r)) for r in self.rotate.to_euler("XYZ")),
self.get_dl_name(), # This node requires 'NULL' if there is no DL
]
return f"{self.command}({join_c_args(args)}),"
return "GEO_BACKGROUND(" + str(self.backgroundValue) + ", " + convert_addr_to_func(self.geo_func) + ")"
nodeGroupClasses = [
@@ -1374,8 +1309,6 @@ nodeGroupClasses = [
ZBufferNode,
CameraNode,
RenderRangeNode,
CustomNode,
CustomAnimatedNode,
]
DLNodes = [
@@ -1386,5 +1319,4 @@ DLNodes = [
ScaleNode,
DisplayListNode,
DisplayListWithOffsetNode,
CustomAnimatedNode,
]
+6 -15
View File
@@ -4,7 +4,8 @@ from ..f3d.f3d_parser import createBlankMaterial, parseF3DBinary
from ..panels import SM64_Panel
from .sm64_level_parser import parse_level_binary
from .sm64_constants import enumLevelNames
from .sm64_geolayout_bone import enumShadowType, animatableBoneTypes, enumBoneType
from .sm64_geolayout_bone import enumShadowType
from .sm64_geolayout_utility import is_bone_animatable
from .sm64_geolayout_constants import getGeoLayoutCmdLength, nodeGroupCmds, GEO_BRANCH_STORE
from .sm64_utility import import_rom_checks
@@ -169,13 +170,6 @@ def parseGeoLayout(
if bpy.app.version < (4, 0, 0) and useArmature:
armatureObj.data.layers[1] = True
"""
if useMetarig:
metaBones = [bone for bone in armatureObj.data.bones if \
bone.layers[boneLayers['meta']] or bone.layers[boneLayers['visual']]]
for bone in metaBones:
addBoneToGroup(armatureObj, bone.name, 'Ignore')
"""
return armatureMeshGroups, armatureObj
@@ -536,11 +530,9 @@ def traverseArmatureForMetarig(armatureObj, boneName, parentName):
if bpy.app.version >= (4, 0, 0):
if "Ignore" in bone.collections:
return
nonAnimatableBoneTypes = set([item[0] for item in enumBoneType]) - animatableBoneTypes
isAnimatableBone = not any([item in bone.collections for item in nonAnimatableBoneTypes])
if isAnimatableBone:
if is_bone_animatable(bone):
processBoneMeta(armatureObj, boneName, parentName)
nextParentName = boneName if isAnimatableBone else parentName
nextParentName = boneName if is_bone_animatable(bone) else parentName
bone = armature.bones[boneName] # re-obtain reference after edit mode changes
childrenNames = [child.name for child in bone.children]
@@ -730,9 +722,8 @@ def createBone(armatureObj, parentBoneName, boneName, currentTransform, boneGrou
bone.tail += currentTransform.to_quaternion() @ mathutils.Vector((0, 1, 0)) * 0.02
boneName = bone.name
addBoneToGroup(armatureObj, bone.name, boneGroup)
bone = armatureObj.data.bones[boneName]
bone.geo_cmd = boneGroup if boneGroup is not None else "DisplayListWithOffset"
addBoneToGroup(armatureObj, bone.name)
return boneName
@@ -802,7 +793,7 @@ def createSwitchOption(
bMesh = bmesh.new()
bMesh.from_mesh(mesh)
addBoneToGroup(switchArmature, boneName, "SwitchOption")
addBoneToGroup(switchArmature, boneName)
return boneName, (switchArmature, bMesh, obj), finalTransform, finalNextParentTransform
+95 -37
View File
@@ -1,7 +1,20 @@
import bpy
from bpy.types import Object, Armature, Bone, PoseBone
from ..f3d.f3d_gbi import GfxList
from ..utility import PluginError
def is_bone_animatable(bone: Bone):
bone_props: "SM64_BoneProperties" = bone.fast64.sm64
geo_cmd: str = bone.geo_cmd
if geo_cmd == "DisplayListWithOffset":
return True
elif geo_cmd == "Custom" and bone_props.custom.is_animated:
return True
return False
def getBoneGroupByName(armatureObj, name):
for boneGroup in armatureObj.pose.bone_groups:
if boneGroup.name == name:
@@ -42,6 +55,8 @@ boneNodeProperties = {
"StartRenderArea": BoneNodeProperties(True, "THEME13"), # 0x20
"Ignore": BoneNodeProperties(False, "THEME08"), # Used for rigging
"SwitchOption": BoneNodeProperties(False, "THEME11"),
"DisplayListWithOffset": BoneNodeProperties(True, "THEME00"),
"Custom": BoneNodeProperties(True, "THEME15"),
}
boneLayers = {"anim": 0, "other": 1, "meta": 2, "visual": 3}
@@ -66,51 +81,94 @@ def createBoneGroups(armatureObj):
boneGroup.color_set = properties.theme
def addBoneToGroup(armatureObj, boneName, groupName):
armature = armatureObj.data
if groupName is None:
if bpy.context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
posebone = armatureObj.pose.bones[boneName]
bone = armature.bones[boneName]
bone.use_deform = True
def addBoneToGroup(armature_obj: Object, name: str):
armature: Armature = armature_obj.data
pose_bone: PoseBone = armature_obj.pose.bones[name]
bone: Bone = armature.bones[name]
geo_cmd: str = bone.geo_cmd
if geo_cmd not in boneNodeProperties:
raise PluginError(f"Bone group {geo_cmd} doesn't exist.")
lock_location, lock_rotation, lock_scale = False, False, False
if is_bone_animatable(bone):
if bpy.app.version >= (4, 0, 0):
if not "anim" in armature.collections:
armature.collections.new(name="anim")
armature.collections["anim"].assign(bone)
else:
posebone.bone_group = None
pose_bone.bone_group = None
bone.layers = createBoneLayerMask([boneLayers["anim"]])
posebone.lock_location = (False, False, False)
posebone.lock_rotation = (False, False, False)
posebone.lock_scale = (False, False, False)
return
elif groupName not in boneNodeProperties:
raise PluginError("Bone group " + groupName + " doesn't exist.")
if bpy.context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
posebone = armatureObj.pose.bones[boneName]
bone = armatureObj.data.bones[boneName]
if bpy.app.version >= (4, 0, 0):
armature.collections[groupName].assign(bone)
armature.collections[geo_cmd].assign(bone)
else:
posebone.bone_group_index = getBoneGroupIndex(armatureObj, groupName)
pose_bone.bone_group_index = getBoneGroupIndex(armature_obj, geo_cmd)
if groupName != "Ignore":
bone.use_deform = boneNodeProperties[groupName].deform
if groupName != "DisplayList":
if bpy.app.version >= (4, 0, 0):
if not "other" in armature.collections:
armature.collections.new(name="other")
armature.collections["other"].assign(bone)
else:
bone.layers = createBoneLayerMask([boneLayers["other"]])
if geo_cmd == "Custom":
custom = bone.fast64.sm64.custom
bone.use_deform = custom.dl_option != "NONE"
if not custom.is_animated:
lock_location = lock_rotation = lock_scale = True
elif geo_cmd != "Ignore":
bone.use_deform = boneNodeProperties[geo_cmd].deform
if geo_cmd != "SwitchOption":
lock_location = True
lock_rotation = lock_scale = True
if geo_cmd not in {"Ignore", "DisplayList"}:
if bpy.app.version >= (4, 0, 0):
if not "other" in armature.collections:
armature.collections.new(name="other")
armature.collections["other"].assign(bone)
else:
bone.layers = createBoneLayerMask([boneLayers["other"]])
if groupName != "SwitchOption":
posebone.lock_location = (True, True, True)
posebone.lock_rotation = (True, True, True)
posebone.lock_scale = (True, True, True)
pose_bone.lock_location = (lock_location, lock_location, lock_location)
pose_bone.lock_rotation = (lock_rotation, lock_rotation, lock_rotation)
pose_bone.lock_scale = (lock_scale, lock_scale, lock_scale)
def updateBone(bone, context):
armatureObj = context.object
createBoneGroups(armatureObj)
addBoneToGroup(armatureObj, bone.name)
class BaseDisplayListNode:
"""Base displaylist node with common helper functions dealing with displaylists"""
dl_ext = "WITH_DL" # add dl_ext to geo command if command has a displaylist
override_layer = False
dlRef: str | GfxList | None
def get_dl_address(self):
assert not isinstance(self.dlRef, str), "dlRef string not supported in binary"
if isinstance(self.dlRef, GfxList):
return self.dlRef.startAddress
if self.hasDL and self.DLmicrocode is not None:
return self.DLmicrocode.startAddress
return None
def get_dl_name(self):
if isinstance(self.dlRef, GfxList):
return self.dlRef.name
if self.hasDL and (self.dlRef or self.DLmicrocode is not None):
return self.dlRef or self.DLmicrocode.name
return "NULL"
def get_c_func_macro(self, base_cmd: str):
return f"{base_cmd}_{self.dl_ext}" if self.hasDL else base_cmd
def c_func_macro(self, base_cmd: str, *args: str):
"""
Supply base command and all arguments for command.
if self.hasDL:
this will add self.dl_ext to the command, and
adds the name of the displaylist to the end of the command
Example return: 'GEO_YOUR_COMMAND_WITH_DL(arg, arg2),'
"""
all_args = list(args)
if self.hasDL:
all_args.append(self.get_dl_name())
return f'{self.get_c_func_macro(base_cmd)}({", ".join(all_args)})'
+75 -39
View File
@@ -9,7 +9,7 @@ from io import BytesIO
from ..operators import ObjectDataExporter
from ..panels import SM64_Panel
from .sm64_objects import InlineGeolayoutObjConfig, inlineGeoLayoutObjects
from .sm64_geolayout_bone import getSwitchOptionBone, animatableBoneTypes
from .sm64_geolayout_bone import getSwitchOptionBone
from .sm64_camera import saveCameraSettingsToGeolayout
from .sm64_f3d_writer import SM64Model, SM64GfxFormatter
from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup
@@ -20,6 +20,7 @@ from .sm64_utility import export_rom_checks, starSelectWarning, update_actor_inc
from ..utility import (
PluginError,
VertexWeightError,
z_up_to_y_up_matrix,
setOrigin,
raisePluginError,
findStartBones,
@@ -113,13 +114,11 @@ from .sm64_geolayout_classes import (
RotateNode,
TranslateRotateNode,
FunctionNode,
CustomNode,
BillboardNode,
ScaleNode,
RenderRangeNode,
ShadowNode,
DisplayListWithOffsetNode,
CustomAnimatedNode,
HeldObjectNode,
Geolayout,
)
@@ -130,6 +129,17 @@ if typing.TYPE_CHECKING:
from .sm64_geolayout_bone import SM64_BoneProperties
def get_custom_cmd_with_transform(node: "CustomNode", parentTransformNode, translate, rotate):
types = {a["arg_type"] for a in node.data["args"]}
has_translation, has_rotation, has_scale = "TRANSLATION" in types, "ROTATION" in types, "SCALE" in types
if (not has_translation and not isZeroTranslation(translate)) or (not has_rotation and not isZeroRotation(rotate)):
field = 0 if not (has_translation or has_rotation) else (1 if has_rotation else 2)
parentTransformNode = addParentNode(
parentTransformNode, TranslateRotateNode(node.drawLayer, field, False, translate, rotate)
)
return node, parentTransformNode
def appendSecondaryGeolayout(geoDirPath, geoName1, geoName2, additionalNode=""):
geoPath = os.path.join(geoDirPath, "geo.inc.c")
geoFile = open(geoPath, "a", newline="\n")
@@ -490,6 +500,7 @@ def convertArmatureToGeolayout(armatureObj, obj, convertTransformMatrix, camera,
None,
None,
None,
None,
meshGeolayout.nodes[i],
[],
name,
@@ -1148,7 +1159,7 @@ def duplicateNode(transformNode, parentNode, index):
def partOfGeolayout(obj):
useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj.sm64_obj_type)
useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj)
return obj.type == "MESH" or useGeoEmpty
@@ -1219,8 +1230,6 @@ def processPreInlineGeo(
node = JumpNode(True, None, obj.geoReference)
elif inlineGeoConfig.name == "Geo Displaylist":
node = DisplayListNode(int(obj.draw_layer_static), obj.dlReference)
elif inlineGeoConfig.name == "Custom Geo Command":
node = CustomNode(obj.customGeoCommand, obj.customGeoCommandArgs)
addParentNode(parentTransformNode, node) # Allow this node to be translated/rotated
@@ -1242,7 +1251,23 @@ def processInlineGeoNode(
elif inlineGeoConfig.name == "Geo Rotation Node":
node = RotateNode(obj.draw_layer_static, obj.useDLReference, rotate, obj.dlReference)
elif inlineGeoConfig.name == "Geo Scale":
node = ScaleNode(obj.draw_layer_static, scale, obj.useDLReference, obj.dlReference)
node = ScaleNode(obj.draw_layer_static, scale[0], obj.useDLReference, obj.dlReference)
elif inlineGeoConfig.name == "Custom":
local_matrix = (
mathutils.Matrix.Translation(translate)
@ rotate.to_matrix().to_4x4()
@ mathutils.Matrix.Diagonal(scale).to_4x4()
)
node = obj.fast64.sm64.custom.get_final_cmd(
obj,
bpy.context.scene.fast64.sm64.blender_to_sm64_scale,
z_up_to_y_up_matrix @ mathutils.Matrix(obj.get("original_mtx_world")) @ z_up_to_y_up_matrix.inverted(),
local_matrix,
obj.draw_layer_static,
obj.useDLReference,
obj.dlReference,
)
node, parentTransformNode = get_custom_cmd_with_transform(node, parentTransformNode, translate, rotate)
else:
raise PluginError(f"Ooops! Didnt implement inline geo exporting for {inlineGeoConfig.name}")
@@ -1263,11 +1288,11 @@ def processMesh(
):
# final_transform = copy.deepcopy(transformMatrix)
useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj.sm64_obj_type)
useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj)
useSwitchNode = obj.type == "EMPTY" and obj.sm64_obj_type == "Switch"
useInlineGeo = obj.type == "EMPTY" and checkIsSM64InlineGeoLayout(obj.sm64_obj_type)
useInlineGeo = obj.type == "EMPTY" and checkIsSM64InlineGeoLayout(obj)
addRooms = isRoot and obj.type == "EMPTY" and obj.sm64_obj_type == "Area Root" and obj.enableRoomSwitch
@@ -1277,7 +1302,7 @@ def processMesh(
inlineGeoConfig: InlineGeolayoutObjConfig = inlineGeoLayoutObjects.get(obj.sm64_obj_type)
processed_inline_geo = False
isPreInlineGeoLayout = checkIsSM64PreInlineGeoLayout(obj.sm64_obj_type)
isPreInlineGeoLayout = checkIsSM64PreInlineGeoLayout(obj)
if useInlineGeo and isPreInlineGeoLayout:
processed_inline_geo = True
processPreInlineGeo(inlineGeoConfig, obj, parentTransformNode)
@@ -1356,7 +1381,7 @@ def processMesh(
else:
if useInlineGeo and not processed_inline_geo:
node, parentTransformNode = processInlineGeoNode(
inlineGeoConfig, obj, parentTransformNode, translate, rotate, scale[0]
inlineGeoConfig, obj, parentTransformNode, translate, rotate, scale
)
processed_inline_geo = True
@@ -1563,6 +1588,7 @@ def processBone(
transformMatrix,
lastTranslateName,
lastRotateName,
last_scale_name,
lastDeformName,
parentTransformNode,
materialOverrides,
@@ -1598,40 +1624,38 @@ def processBone(
rotateParent = None
rotate = bone.matrix_local.decompose()[1]
# Get scale
if last_scale_name is not None:
scaleParent = armatureObj.data.bones[last_scale_name]
scale = (scaleParent.matrix_local.inverted() @ bone.matrix_local).decompose()[2]
else:
scaleParent = None
scale = bone.matrix_local.decompose()[2]
translation = mathutils.Matrix.Translation(translate)
rotation = rotate.to_matrix().to_4x4()
zeroTranslation = isZeroTranslation(translate)
zeroRotation = isZeroRotation(rotate)
zero_scale = isZeroScaleChange(scale)
# hasDL = bone.use_deform
hasDL = True
if bone.geo_cmd in animatableBoneTypes:
if bone.geo_cmd == "CustomAnimated":
if not bone.fast64.sm64.custom_geo_cmd_macro:
raise PluginError(f'Bone "{boneName}" on armature "{armatureObj.name}" needs a geo command macro.')
node = CustomAnimatedNode(bone.fast64.sm64.custom_geo_cmd_macro, int(bone.draw_layer), translate, rotate)
if bone.geo_cmd == "DisplayListWithOffset":
if not zeroRotation:
node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, mathutils.Vector((0, 0, 0)))
parentTransformNode = addParentNode(
parentTransformNode, TranslateRotateNode(1, 0, False, translate, rotate)
)
lastTranslateName = boneName
lastRotateName = boneName
else: # DisplayListWithOffset
if not zeroRotation:
node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, mathutils.Vector((0, 0, 0)))
parentTransformNode = addParentNode(
parentTransformNode, TranslateRotateNode(1, 0, False, translate, rotate)
)
lastTranslateName = boneName
lastRotateName = boneName
else:
node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, translate)
lastTranslateName = boneName
else:
node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, translate)
lastTranslateName = boneName
final_transform = transformMatrix @ translation
elif bone.geo_cmd == "CustomNonAnimated":
if bone.fast64.sm64.custom_geo_cmd_macro == "":
raise PluginError(f'Bone "{boneName}" on armature "{armatureObj.name}" needs a geo command macro.')
node = CustomNode(bone.fast64.sm64.custom_geo_cmd_macro, bone.fast64.sm64.custom_geo_cmd_args)
elif bone.geo_cmd == "Function":
if bone.geo_func == "":
raise PluginError("Function bone " + boneName + " function value is empty.")
@@ -1704,6 +1728,21 @@ def processBone(
final_transform = transformMatrix @ mathutils.Matrix.Scale(node.scaleValue, 4)
elif bone.geo_cmd == "StartRenderArea":
node = StartRenderAreaNode(bone.culling_radius)
elif bone.geo_cmd == "Custom":
local_matrix = mathutils.Matrix.LocRotScale(translate, rotate, scale)
world_matrix = z_up_to_y_up_matrix @ bone.matrix_local @ z_up_to_y_up_matrix.inverted()
node = bone_props.custom.get_final_cmd(
bone, bpy.context.scene.fast64.sm64.blender_to_sm64_scale, world_matrix, local_matrix, None, hasDL
)
node, parentTransformNode = get_custom_cmd_with_transform(node, parentTransformNode, translate, rotate)
if not has_scale and not zero_scale:
parentTransformNode = addParentNode(parentTransformNode, ScaleNode(node.drawLayer, scale[0], False))
if has_translation:
lastTranslateName = boneName
elif has_rotation:
lastRotateName = boneName
elif has_scale:
last_scale_name = boneName
else:
raise PluginError("Invalid geometry command: " + bone.geo_cmd)
@@ -1809,12 +1848,6 @@ def processBone(
if not isinstance(transformNode.node, SwitchNode):
# print(boneGroup.name if boneGroup is not None else "Offset")
if len(bone.children) > 0:
# print("\tHas Children")
if bone.geo_cmd == "Function":
raise PluginError(
"Function bones cannot have children. They instead affect the next sibling bone in alphabetical order."
)
# Handle child nodes
# nonDeformTransformData should be modified to be sent to children,
# otherwise it should not be modified for parent.
@@ -1829,6 +1862,7 @@ def processBone(
final_transform,
lastTranslateName,
lastRotateName,
last_scale_name,
lastDeformName,
transformNode,
materialOverrides,
@@ -1864,6 +1898,7 @@ def processBone(
final_transform,
lastTranslateName,
lastRotateName,
last_scale_name,
lastDeformName,
nextStartNode,
materialOverrides,
@@ -1955,6 +1990,7 @@ def processBone(
optionBone.name,
optionBone.name,
optionBone.name,
optionBone.name,
startNode,
materialOverrides,
namePrefix + "_" + optionBone.name,
+22 -1
View File
@@ -2,7 +2,7 @@ from pathlib import Path
import bpy, os, math, re, shutil, mathutils
from collections import defaultdict
from typing import NamedTuple
from dataclasses import dataclass
from dataclasses import dataclass, field
from bpy.utils import register_class, unregister_class
from ..panels import SM64_Panel
from ..operators import ObjectDataExporter
@@ -22,6 +22,7 @@ from .sm64_utility import (
)
from ..utility import (
yUpToZUp,
PluginError,
getDataFromFile,
saveDataToFile,
@@ -305,6 +306,7 @@ class LevelScript:
self.marioStart = None
self.persistentBlocks = PersistentBlocks.new()
self.sub_scripts: LevelScript = []
self.custom_cmds: list["SM64_CustomCmdProperties"] = []
# this is basically a smaller script jumped to from the main one
def add_subscript(self, name: str):
@@ -357,6 +359,7 @@ class LevelScript:
macrosToString(self.segmentLoads),
f"\tALLOC_LEVEL_POOL(),",
f"\t{self.mario}",
*[f"\t{cmd.to_c(1)}," for cmd in self.custom_cmds],
macrosToString(self.levelFunctions),
macrosToString(self.modelLoads),
f"{self.get_persistent_block(PersistentBlocks.levelCommands, nTabs=1)}\n",
@@ -794,6 +797,7 @@ def export_area_c(
raise PluginError(f"Error while creating area {area_root.areaIndex}: {str(exc)}") from exc
if area.mario_start is not None:
prev_level_script.marioStart = area.mario_start
prev_level_script.custom_cmds += area.custom_cmds
persistentBlockString = prev_level_script.get_persistent_block(
PersistentBlocks.areaCommands, nTabs=2, areaIndex=str(area.index)
)
@@ -893,6 +897,23 @@ def exportLevelC(obj, transformMatrix, level_name, exportDir, savePNG, customExp
if len(childAreas) == 0:
raise PluginError("The level root has no child empties with the 'Area Root' object type.")
for child in obj.children:
if child.type == "EMPTY" and child.sm64_obj_type == "Custom":
custom_props = child.fast64.sm64.custom
if custom_props.preset != "NONE" and custom_props.section == "AREA":
raise PluginError(
f"Object {obj.name} is parented to the level root but should be parented to an area root."
)
prev_level_script.custom_cmds.append(
custom_props.get_final_cmd(
obj,
bpy.context.scene.fast64.sm64.blender_to_sm64_scale,
child.matrix_world @ yUpToZUp,
child.matrix_local,
name=obj.name,
)
)
uses_env_fx = False
echoLevels = ["0x00", "0x00", "0x00"]
zoomFlags = [False, False, False, False]
+90 -52
View File
@@ -1,10 +1,11 @@
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 bpy.utils import register_class, unregister_class
from ..panels import SM64_Panel
from ..operators import ObjectDataExporter
@@ -12,6 +13,8 @@ from ..utility import (
PluginError,
CData,
Vector,
yUpToZUp,
y_up_to_z_up,
directory_ui_warnings,
filepath_ui_warnings,
toAlnum,
@@ -64,7 +67,6 @@ from .sm64_geolayout_classes import (
RotateNode,
TranslateRotateNode,
FunctionNode,
CustomNode,
BillboardNode,
ScaleNode,
)
@@ -77,6 +79,8 @@ from .animation import (
SM64_ArmatureAnimProperties,
)
from .custom_cmd.properties import SM64_CustomCmdProperties
enumTerrain = [
("Custom", "Custom", "Custom"),
("TERRAIN_GRASS", "Grass", "Grass"),
@@ -237,26 +241,28 @@ inlineGeoLayoutObjects = {
"Geo Billboard": InlineGeolayoutObjConfig("Geo Billboard", BillboardNode, can_have_dl=True, uses_location=True),
"Geo Scale": InlineGeolayoutObjConfig("Geo Scale", ScaleNode, can_have_dl=True, uses_scale=True),
"Geo Displaylist": InlineGeolayoutObjConfig("Geo Displaylist", DisplayListNode, must_have_dl=True),
"Custom Geo Command": InlineGeolayoutObjConfig("Custom Geo Command", CustomNode),
"Custom": InlineGeolayoutObjConfig("Custom", "CustomCmd"),
}
# When adding new types related to geolayout,
# Make sure to add exceptions to enumSM64EmptyWithGeolayout
enumObjectType = [
("None", "None", "None"),
("Level Root", "Level Root", "Level Root"),
("Area Root", "Area Root", "Area Root"),
("Object", "Object", "Object"),
("Macro", "Macro", "Macro"),
("Special", "Special", "Special"),
("Mario Start", "Mario Start", "Mario Start"),
("Whirlpool", "Whirlpool", "Whirlpool"),
("Water Box", "Water Box", "Water Box"),
("Camera Volume", "Camera Volume", "Camera Volume"),
("Switch", "Switch Node", "Switch Node"),
("Puppycam Volume", "Puppycam Volume", "Puppycam Volume"),
("", "Inline Geolayout Commands", ""), # This displays as a column header for the next set of options
*[(key, key, key) for key in inlineGeoLayoutObjects.keys()],
("None", "None", "None", 0),
("Level Root", "Level Root", "Level Root", 1),
("Area Root", "Area Root", "Area Root", 2),
("Object", "Object", "Object", 3),
("Macro", "Macro", "Macro", 4),
("Special", "Special", "Special", 5),
("Mario Start", "Mario Start", "Mario Start", 6),
("Whirlpool", "Whirlpool", "Whirlpool", 7),
("Water Box", "Water Box", "Water Box", 8),
("Camera Volume", "Camera Volume", "Camera Volume", 9),
("Switch", "Switch Node", "Switch Node", 10),
("Puppycam Volume", "Puppycam Volume", "Puppycam Volume", 11),
("", "Inline Geolayout Commands", "", 12), # This displays as a column header for the next set of options
*[(key, key, key, i) for i, key in enumerate(list(inlineGeoLayoutObjects.keys())[:-1], start=13)], # exclude custom
("", "", "", 12),
("Custom", "Custom", "Custom level script command", 21),
]
enumPuppycamMode = [
@@ -303,7 +309,7 @@ class SM64_Object:
self.rotation = rotation
self.name = name # to sort by when exporting
def to_c(self):
def to_c(self, _depth=0):
if self.acts == 0x1F:
return (
"OBJECT("
@@ -360,7 +366,7 @@ class SM64_Whirpool:
self.position = position
self.name = "whirlpool" # for sorting
def to_c(self):
def to_c(self, _depth=0):
return (
"WHIRPOOL("
+ str(self.index)
@@ -385,7 +391,7 @@ class SM64_Macro_Object:
self.position = position
self.rotation = rotation
def to_c(self):
def to_c(self, _depth=0):
if self.bparam is None:
return (
"MACRO_OBJECT("
@@ -437,7 +443,7 @@ class SM64_Special_Object:
data.extend(int(self.bparam).to_bytes(2, "big"))
return data
def to_c(self):
def to_c(self, _depth=0):
if self.rotation is None:
return (
"SPECIAL_OBJECT("
@@ -448,7 +454,7 @@ class SM64_Special_Object:
+ str(int(round(self.position[1])))
+ ", "
+ str(int(round(self.position[2])))
+ "),\n"
+ ")"
)
elif self.bparam is None:
return (
@@ -462,7 +468,7 @@ class SM64_Special_Object:
+ str(int(round(self.position[2])))
+ ", "
+ str(int(round(math.degrees(self.rotation[1]))))
+ "),\n"
+ ")"
)
else:
return (
@@ -478,7 +484,7 @@ class SM64_Special_Object:
+ str(int(round(math.degrees(self.rotation[1]))))
+ ", "
+ str(self.bparam)
+ "),\n"
+ ")"
)
@@ -489,7 +495,7 @@ class SM64_Mario_Start:
self.rotation = rotation
self.name = "Mario" # for sorting
def to_c(self):
def to_c(self, _depth=0):
return (
"MARIO_POS("
+ str(self.area)
@@ -526,6 +532,7 @@ class SM64_Area:
self.mario_start = None
self.splines = []
self.startDialog = startDialog
self.custom_cmds = []
def macros_name(self):
return self.name + "_macro_objs"
@@ -537,7 +544,7 @@ class SM64_Area:
data += "\t\t" + warpNode + ",\n"
# export objects in name order
for obj in sorted(self.objects, key=(lambda obj: obj.name)):
data += "\t\t" + obj.to_c() + ",\n"
data += "\t\t" + obj.to_c(2) + ",\n"
data += "\t\tTERRAIN(" + self.collision.name + "),\n"
if includeRooms:
data += "\t\tROOMS(" + self.collision.rooms_name() + "),\n"
@@ -558,7 +565,7 @@ class SM64_Area:
data.header = "extern const MacroObject " + self.macros_name() + "[];\n"
data.source += "const MacroObject " + self.macros_name() + "[] = {\n"
for macro in self.macros:
data.source += "\t" + macro.to_c() + ",\n"
data.source += "\t" + macro.to_c(1) + ",\n"
data.source += "\tMACRO_OBJECT_END(),\n};\n\n"
return data
@@ -566,13 +573,13 @@ class SM64_Area:
def to_c_camera_volumes(self):
data = ""
for camVolume in self.cameraVolumes:
data += "\t" + camVolume.to_c() + "\n"
data += "\t" + camVolume.to_c(1) + ",\n"
return data
def to_c_puppycam_volumes(self):
data = ""
for puppycamVolume in self.puppycamVolumes:
data += "\t" + puppycamVolume.to_c() + "\n"
data += "\t" + puppycamVolume.to_c(1) + ",\n"
return data
def hasCutsceneSpline(self):
@@ -609,7 +616,7 @@ class CollisionWaterBox:
data.extend(int(round(self.height)).to_bytes(2, "big", signed=True))
return data
def to_c(self):
def to_c(self, _depth=0):
data = (
"COL_WATER_BOX("
+ ("0x00" if self.waterBoxType == "Water" else "0x32")
@@ -623,7 +630,7 @@ class CollisionWaterBox:
+ str(int(round(self.high[1])))
+ ", "
+ str(int(round(self.height)))
+ "),\n"
+ ")"
)
return data
@@ -641,7 +648,7 @@ class CameraVolume:
def to_binary(self):
raise PluginError("Binary exporting not implemented for camera volumens.")
def to_c(self):
def to_c(self, _depth=0):
data = (
"{"
+ str(self.area)
@@ -661,7 +668,7 @@ class CameraVolume:
+ str(int(round(self.scale[2])))
+ ", "
+ str(convertRadiansToS16(self.rotation[1]))
+ "},"
+ "}"
)
return data
@@ -694,7 +701,7 @@ class PuppycamVolume:
def to_binary(self):
raise PluginError("Binary exporting not implemented for puppycam volumes.")
def to_c(self):
def to_c(self, _depth=0):
data = (
"{"
+ str(self.level)
@@ -730,7 +737,7 @@ class PuppycamVolume:
+ str(int(round(self.camFocus[1])))
+ ", "
+ str(int(round(self.camFocus[2])))
+ "},"
+ "}"
)
return data
@@ -805,12 +812,40 @@ def process_sm64_objects(obj, area, rootMatrix, transformMatrix, specialsOnly):
)
# Hacky solution to handle Z-up to Y-up conversion
rotation = (originalRotation @ mathutils.Quaternion((1, 0, 0), math.radians(90.0))).to_euler("ZXY")
rotation = (originalRotation @ y_up_to_z_up).to_euler("ZXY")
if obj.type == "EMPTY":
if obj.sm64_obj_type == "Area Root" and obj.areaIndex != area.index:
return
if specialsOnly:
obj_props: SM64_ObjectProperties = obj.fast64.sm64
if obj.sm64_obj_type == "Custom" and (
(obj_props.custom.cmd_type == "Collision" and specialsOnly)
or ((obj_props.custom.cmd_type == "Level") and not specialsOnly)
):
# HACK: alternatively we could just ignore transformMatrix since it only has the blender to sm64 scale
sm64_scale = bpy.context.scene.fast64.sm64.blender_to_sm64_scale
reverse = mathutils.Matrix.Diagonal((sm64_scale,) * 3).to_4x4().inverted()
local_matrix = reverse @ final_transform @ yUpToZUp
cmd = obj_props.custom.get_final_cmd(
obj,
bpy.context.scene.fast64.sm64.blender_to_sm64_scale,
reverse @ (transformMatrix @ obj.matrix_world) @ yUpToZUp,
local_matrix,
name=obj.name,
)
if specialsOnly:
area.specials.append(cmd)
else:
if obj_props.custom.preset != "NONE":
if obj_props.custom.section == "FORCE_LEVEL":
area.custom_cmds.append(cmd)
return
elif obj_props.custom.section == "LEVEL":
raise PluginError(
f"Object {obj.name} is parented to an area but should be parented to the level root instead."
)
area.objects.append(cmd)
elif specialsOnly:
if obj.sm64_obj_type == "Special":
preset = obj.sm64_special_enum if obj.sm64_special_enum != "Custom" else obj.sm64_obj_preset
area.specials.append(
@@ -1123,11 +1158,6 @@ class SM64ObjectPanel(bpy.types.Panel):
prop_split(box, obj.fast64.sm64.geo_asm, "param", "Parameter")
return
elif obj.sm64_obj_type == "Custom Geo Command":
prop_split(box, obj, "customGeoCommand", "Geo Macro")
prop_split(box, obj, "customGeoCommandArgs", "Parameters")
return
if obj_details.can_have_dl:
prop_split(box, obj, "draw_layer_static", "Draw Layer")
@@ -1152,7 +1182,7 @@ class SM64ObjectPanel(bpy.types.Panel):
info_box.label(text="Scale", icon="DOT")
if len(obj.children):
if checkIsSM64PreInlineGeoLayout(obj.sm64_obj_type):
if checkIsSM64PreInlineGeoLayout(obj):
box.box().label(text="Children of this object will just be the following geo commands.")
else:
box.box().label(text="Children of this object will be wrapped in GEO_OPEN_NODE and GEO_CLOSE_NODE.")
@@ -1181,12 +1211,14 @@ class SM64ObjectPanel(bpy.types.Panel):
parent_box.separator()
def draw(self, context):
sm64_props = context.scene.fast64.sm64
prop_split(self.layout, context.scene, "gameEditorMode", "Game")
box = self.layout.box().column()
column = self.layout.box().column() # added just for puppycam trigger importing
box.box().label(text="SM64 Object Inspector")
obj = context.object
props = obj.fast64.sm64
obj_props: SM64_ObjectProperties = obj.fast64.sm64
prop_split(box, obj, "sm64_obj_type", "Object Type")
if obj.sm64_obj_type == "Object":
@@ -1257,7 +1289,7 @@ class SM64ObjectPanel(bpy.types.Panel):
prop_split(box, levelObj, "backgroundSegment", "Custom Background Segment")
segmentExportBox = box.box()
segmentExportBox.label(
text=f"Exported Segment: _{levelObj.backgroundSegment}_{context.scene.fast64.sm64.compression_format}SegmentRomStart"
text=f"Exported Segment: _{levelObj.backgroundSegment}_{sm64_props.compression_format}SegmentRomStart"
)
box.prop(obj, "useBackgroundColor")
# box.box().label(text = 'Background IDs defined in include/geo_commands.h.')
@@ -1268,7 +1300,7 @@ class SM64ObjectPanel(bpy.types.Panel):
obj.starGetCutscenes.draw(box)
elif obj.sm64_obj_type == "Area Root":
area_props = props.area
area_props = obj_props.area
# Code that used to be in area inspector
prop_split(box, obj, "areaIndex", "Area Index")
box.prop(obj, "noMusic", text="Disable Music")
@@ -1391,12 +1423,19 @@ class SM64ObjectPanel(bpy.types.Panel):
prop_split(box, obj, "switchParam", "Parameter")
box.box().label(text="Children will ordered alphabetically.")
elif obj.sm64_obj_type == "Custom":
custom_props: SM64_CustomCmdProperties = obj_props.custom
custom_props.draw_props(box, sm64_props.binary_export, obj, blender_scale=sm64_props.blender_to_sm64_scale)
elif obj.sm64_obj_type in inlineGeoLayoutObjects:
self.draw_inline_obj(box, obj)
elif obj.sm64_obj_type == "None":
box.box().label(text="This can be used as an empty transform node in a geolayout hierarchy.")
else:
multilineLabel(box, "Unknown object type: " + obj.sm64_obj_type)
def draw_acts(self, obj, layout):
layout.label(text="Acts")
acts = layout.row()
@@ -2513,7 +2552,7 @@ class WarpNodeProperty(bpy.types.PropertyGroup):
ret.z = int(round(-difference.y * bpy.context.scene.blenderF3DScale))
return ret
def to_c(self):
def to_c(self, _depth=0):
if self.warpType == "Instant":
offset = Vector()
@@ -2970,13 +3009,14 @@ class SM64_SegmentProperties(bpy.types.PropertyGroup):
class SM64_ObjectProperties(bpy.types.PropertyGroup):
version: bpy.props.IntProperty(name="SM64_ObjectProperties Version", default=0)
cur_version = 3 # version after property migration
cur_version = 4 # version after property migration
geo_asm: bpy.props.PointerProperty(type=SM64_GeoASMProperties)
level: bpy.props.PointerProperty(type=SM64_LevelProperties)
area: bpy.props.PointerProperty(type=SM64_AreaProperties)
game_object: bpy.props.PointerProperty(type=SM64_GameObjectProperties)
segment_loads: bpy.props.PointerProperty(type=SM64_SegmentProperties)
custom: bpy.props.PointerProperty(type=SM64_CustomCmdProperties)
animation: bpy.props.PointerProperty(type=SM64_ArmatureAnimProperties)
@@ -2987,6 +3027,7 @@ class SM64_ObjectProperties(bpy.types.PropertyGroup):
SM64_GeoASMProperties.upgrade_object(obj)
if obj.fast64.sm64.version < 3:
SM64_GameObjectProperties.upgrade_object(obj)
obj.fast64.sm64.custom.upgrade_object(obj)
obj.fast64.sm64.version = SM64_ObjectProperties.cur_version
@@ -3201,9 +3242,6 @@ def sm64_obj_register():
bpy.types.Object.geoReference = bpy.props.StringProperty(name="Geolayout variable name or hex address for binary")
bpy.types.Object.customGeoCommand = bpy.props.StringProperty(name="Geolayout macro command", default="")
bpy.types.Object.customGeoCommandArgs = bpy.props.StringProperty(name="Geolayout macro arguments", default="")
bpy.types.Object.enableRoomSwitch = bpy.props.BoolProperty(name="Enable Room System")
+64 -15
View File
@@ -5,6 +5,7 @@ from mathutils import *
from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union
from bpy.types import UILayout, Scene, World
from bpy.props import FloatVectorProperty
CollectionProperty = Any # collection prop as defined by using bpy.props.CollectionProperty
@@ -32,6 +33,25 @@ class VertexWeightError(PluginError):
pass
class Matrix4x4Property(bpy.types.PropertyGroup): # blender's matrix subtype is broken :))))
row0: FloatVectorProperty(size=4, default=(1, 0, 0, 0))
row1: FloatVectorProperty(size=4, default=(0, 1, 0, 0))
row2: FloatVectorProperty(size=4, default=(0, 0, 1, 0))
row3: FloatVectorProperty(size=4, default=(0, 0, 0, 1))
def to_matrix(self):
return mathutils.Matrix((tuple(self.row0), tuple(self.row1), tuple(self.row2), tuple(self.row3)))
def from_matrix(self, matrix: mathutils.Matrix):
for i in range(4):
setattr(self, f"row{i}", tuple(matrix[i]))
def draw_props(self, layout: UILayout):
layout.label(text="Row: → | Column: ↓", icon="INFO")
for i in range(4):
layout.row().prop(self, f"row{i}", text="")
# default indentation to use when writing to decomp files
indent = " " * 4
@@ -40,7 +60,11 @@ sm64BoneUp = Vector([1, 0, 0])
transform_mtx_blender_to_n64 = lambda: Matrix(((1, 0, 0, 0), (0, 0, 1, 0), (0, -1, 0, 0), (0, 0, 0, 1)))
yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)).to_matrix().to_4x4()
y_up_to_z_up = mathutils.Quaternion((1, 0, 0), math.radians(90.0))
yUpToZUp = y_up_to_z_up.to_matrix().to_4x4()
z_up_to_y_up = mathutils.Quaternion((1, 0, 0), math.radians(-90.0))
z_up_to_y_up_matrix = z_up_to_y_up.to_matrix().to_4x4()
axis_enums = [
("X", "X", "X"),
@@ -789,6 +813,8 @@ def store_original_mtx():
# scales will be applied to the transform for each object
loc, rot, _scale = obj.matrix_local.decompose()
obj["original_mtx"] = Matrix.LocRotScale(loc, rot, None)
loc, rot, scale = obj.matrix_world.decompose()
obj["original_mtx_world"] = Matrix.LocRotScale(loc, rot, scale)
def rotate_bounds(bounds, mtx: mathutils.Matrix):
@@ -949,35 +975,35 @@ def duplicateHierarchy(obj, ignoreAttr, includeEmpties, areaIndex):
raise Exception(str(e))
enumSM64PreInlineGeoLayoutObjects = {"Geo ASM", "Geo Branch", "Geo Displaylist", "Custom Geo Command"}
enumSM64PreInlineGeoLayoutObjects = {"Geo ASM", "Geo Branch", "Geo Displaylist"}
def checkIsSM64PreInlineGeoLayout(sm64_obj_type):
return sm64_obj_type in enumSM64PreInlineGeoLayoutObjects
def checkIsSM64PreInlineGeoLayout(obj):
return obj.sm64_obj_type in enumSM64PreInlineGeoLayoutObjects
enumSM64InlineGeoLayoutObjects = {
"Geo ASM",
"Geo Branch",
"Geo Translate/Rotate",
"Geo Translate Node",
"Geo Rotation Node",
"Geo Billboard",
"Geo Scale",
"Geo Displaylist",
"Custom Geo Command",
}
def checkIsSM64InlineGeoLayout(sm64_obj_type):
return sm64_obj_type in enumSM64InlineGeoLayoutObjects
def checkIsSM64InlineGeoLayout(obj):
return (
obj.sm64_obj_type in enumSM64InlineGeoLayoutObjects
or checkIsSM64PreInlineGeoLayout(obj)
or (obj.sm64_obj_type == "Custom" and obj.fast64.sm64.custom.cmd_type == "Geo")
)
enumSM64EmptyWithGeolayout = {"None", "Level Root", "Area Root", "Switch"}
def checkSM64EmptyUsesGeoLayout(sm64_obj_type):
return sm64_obj_type in enumSM64EmptyWithGeolayout or checkIsSM64InlineGeoLayout(sm64_obj_type)
def checkSM64EmptyUsesGeoLayout(obj):
return obj.sm64_obj_type in enumSM64EmptyWithGeolayout or checkIsSM64InlineGeoLayout(obj)
def selectMeshChildrenOnly(obj, ignoreAttr, includeEmpties, areaIndex):
@@ -986,7 +1012,7 @@ def selectMeshChildrenOnly(obj, ignoreAttr, includeEmpties, areaIndex):
return
ignoreObj = ignoreAttr is not None and getattr(obj, ignoreAttr)
isMesh = obj.type == "MESH"
isEmpty = obj.type == "EMPTY" and includeEmpties and checkSM64EmptyUsesGeoLayout(obj.sm64_obj_type)
isEmpty = obj.type == "EMPTY" and includeEmpties and checkSM64EmptyUsesGeoLayout(obj)
if (isMesh or isEmpty) and not ignoreObj:
obj.select_set(True)
obj.original_name = obj.name
@@ -1020,6 +1046,8 @@ def cleanupTempMeshes():
del obj["instanced_mesh_name"]
if obj.get("original_mtx"):
del obj["original_mtx"]
if obj.get("original_mtx_world"):
del obj["original_mtx_world"]
for data in remove_data:
data_type = type(data)
@@ -1124,6 +1152,17 @@ def writeInsertableFile(filepath, dataType, address_ptrs, startPtr, data):
openfile.close()
def quantize_color(color: mathutils.Color, bit_counts: tuple[int]):
"""Quantize a color to the specified bit counts."""
assert len(color) == len(bit_counts), "Number of color channels does not match number of bit counts"
result = 0
pos = 0
for c, bit_count in zip(reversed(color), reversed(bit_counts)):
result |= round(c * (2**bit_count - 1)) << pos
pos += bit_count
return result
def colorTo16bitRGBA(color):
r = int(round(color[0] * 31))
g = int(round(color[1] * 31))
@@ -1344,8 +1383,14 @@ def exportColor(lightColor):
return [scaleToU8(value) for value in gammaCorrect(lightColor)]
def get_clean_color(srgb: list, include_alpha=False, round_color=True) -> list:
return [round(channel, 4) if round_color else channel for channel in list(srgb[: 4 if include_alpha else 3])]
def get_clean_color(color: list, include_alpha=False, round_color=True, srgb_to_linear=False) -> list:
color = list(color)
if srgb_to_linear:
color = gammaInverse(color[:3]) + color[3:]
color = color[: 4 if include_alpha else 3]
if include_alpha and len(color) < 4:
color = color + [1.0]
return tuple(round(channel, 4) if round_color else channel for channel in color)
def printBlenderMessage(msgSet, message, blenderOp):
@@ -1775,6 +1820,10 @@ binOps = {
ast.BitOr: operator.or_,
ast.BitAnd: operator.and_,
ast.BitXor: operator.xor,
ast.Pow: operator.pow,
ast.FloorDiv: operator.floordiv,
ast.USub: operator.neg,
ast.UAdd: lambda a: a,
}