Files
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

280 lines
9.8 KiB
Python

from cProfile import Profile
from pstats import SortKey, Stats
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,
get_mode_set_from_context_mode,
raisePluginError,
parentObject,
store_original_meshes,
store_original_mtx,
deselectAllObjects,
)
from .f3d.f3d_material import createF3DMat
def addMaterialByName(obj, matName, preset):
if matName in bpy.data.materials:
bpy.ops.object.material_slot_add()
obj.material_slots[0].material = bpy.data.materials[matName]
else:
material = createF3DMat(obj, preset=preset)
material.name = matName
PROFILE_ENABLED = False
class OperatorBase(Operator):
"""Base class for operators, keeps track of context mode and sets it back after running
execute_operator() and catches exceptions for raisePluginError()"""
context_mode: str = ""
icon = "NONE"
@classmethod
def is_enabled(cls, context: Context, **op_values):
return True
@classmethod
def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, **op_values):
"""Op args are passed to the operator via setattr()"""
icon = icon if icon else cls.icon
layout = layout.column()
op = layout.operator(cls.bl_idname, icon=icon, text=text)
for key, value in op_values.items():
setattr(op, key, value)
layout.enabled = cls.is_enabled(bpy.context, **op_values)
return op
def execute_operator(self, context: Context):
raise NotImplementedError()
def execute(self, context: Context):
starting_mode = context.mode
starting_mode_set = get_mode_set_from_context_mode(starting_mode)
starting_object = context.object
try:
if self.context_mode and self.context_mode != starting_mode_set:
bpy.ops.object.mode_set(mode=self.context_mode)
if PROFILE_ENABLED:
with Profile() as profile:
self.execute_operator(context)
print(Stats(profile).strip_dirs().sort_stats(SortKey.CUMULATIVE).print_stats())
else:
self.execute_operator(context)
return {"FINISHED"}
except Exception as exc:
raisePluginError(self, exc)
return {"CANCELLED"}
finally:
if starting_mode != context.mode:
if starting_mode != "OBJECT" and starting_object:
context.view_layer.objects.active = starting_object
starting_object.select_set(True)
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"
bl_property = None
bl_options = {"UNDO"}
@classmethod
def draw_props(cls, layout: UILayout, data, prop: str, name: str):
row = layout.row()
if name:
row.label(text=name)
row.prop(data, prop, text="")
row.operator(cls.bl_idname, icon="VIEWZOOM", text="")
def update_enum(self, context: Context):
raise NotImplementedError()
def execute_operator(self, context: Context):
assert self.bl_property
self.report({"INFO"}, f"Selected: {getattr(self, self.bl_property)}")
self.update_enum(context)
context.region.tag_redraw()
def invoke(self, context: Context, _):
context.window_manager.invoke_search_popup(self)
return {"RUNNING_MODAL"}
class AddWaterBox(OperatorBase):
bl_idname = "object.add_water_box"
bl_label = "Add Water Box"
bl_options = {"REGISTER", "UNDO", "PRESET"}
context_mode = "OBJECT"
icon = "CUBE"
scale: bpy.props.FloatProperty(default=10)
preset: bpy.props.StringProperty(default="Shaded Solid")
matName: bpy.props.StringProperty(default="water_mat")
def setEmptyType(self, emptyObj):
return None
def execute_operator(self, context):
deselectAllObjects()
location = mathutils.Vector(bpy.context.scene.cursor.location)
bpy.ops.mesh.primitive_plane_add(size=2 * self.scale, enter_editmode=False, align="WORLD", location=location[:])
planeObj = context.view_layer.objects.active
planeObj.ignore_collision = True
planeObj.name = "Water Box Mesh"
addMaterialByName(planeObj, self.matName, self.preset)
location += mathutils.Vector([0, 0, -self.scale])
bpy.ops.object.empty_add(type="CUBE", radius=self.scale, align="WORLD", location=location[:])
emptyObj = context.view_layer.objects.active
emptyObj.name = "Water Box"
self.setEmptyType(emptyObj)
parentObject(planeObj, emptyObj)
self.report({"INFO"}, "Water box added.")
class WarningOperator(bpy.types.Operator):
"""Extension of Operator that allows collecting and displaying warnings"""
warnings = set()
def reset_warnings(self):
self.warnings.clear()
def add_warning(self, warning: str):
self.warnings.add(warning)
def show_warnings(self):
if len(self.warnings):
self.report({"WARNING"}, "Operator completed with warnings:")
for warning in self.warnings:
self.report({"WARNING"}, warning)
self.reset_warnings()
class ObjectDataExporter(WarningOperator):
"""Operator that uses warnings and can store original matrixes and meshes for use in exporting"""
def store_object_data(self):
store_original_mtx()
store_original_meshes(self.add_warning)
def cleanup_temp_object_data(self):
cleanupTempMeshes()