Files
lightspeed64/fast64_internal/sm64/custom_cmd/utility.py
T
Lila 17a6155f5c [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
2025-08-21 22:00:20 +01:00

127 lines
4.6 KiB
Python

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