mirror of
https://github.com/ApfelTeeSaft/lightspeed64.git
synced 2026-08-26 19:33:24 +00:00
* 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
407 lines
16 KiB
Python
407 lines
16 KiB
Python
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()
|