mirror of
https://github.com/ApfelTeeSaft/lightspeed64.git
synced 2026-08-26 19:33:24 +00:00
[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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)})'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user