diff --git a/__init__.py b/__init__.py index 8233d1aab..07e9be425 100644 --- a/__init__.py +++ b/__init__.py @@ -270,7 +270,7 @@ class Fast64_ObjectProperties(bpy.types.PropertyGroup): """ sm64: bpy.props.PointerProperty(type=SM64_ObjectProperties, name="SM64 Object Properties") - oot: bpy.props.PointerProperty(type=OOT_ObjectProperties, name="OOT Object Properties") + oot: bpy.props.PointerProperty(type=OOT_ObjectProperties, name="Z64 Object Properties") # TODO: rename oot to z64 class UpgradeF3DMaterialsDialog(bpy.types.Operator): diff --git a/fast64_internal/data/z64/enum_data.py b/fast64_internal/data/z64/enum_data.py index 5ecdebe2d..c3d1a5840 100644 --- a/fast64_internal/data/z64/enum_data.py +++ b/fast64_internal/data/z64/enum_data.py @@ -93,7 +93,7 @@ def __init__(self, game: str): item.attrib["ID"], item.attrib["Key"], # note: the name sets automatically after the init if None - item.attrib["Name"] if enum.attrib["Key"] == "seqId" else None, + item.attrib.get("Name"), int(item.attrib["Index"]), enum.attrib["Key"], game, diff --git a/fast64_internal/data/z64/xml/oot_enum_data.xml b/fast64_internal/data/z64/xml/oot_enum_data.xml index 39a5eb196..088725257 100644 --- a/fast64_internal/data/z64/xml/oot_enum_data.xml +++ b/fast64_internal/data/z64/xml/oot_enum_data.xml @@ -737,6 +737,9 @@ + + + diff --git a/fast64_internal/f3d/f3d_gbi.py b/fast64_internal/f3d/f3d_gbi.py index 363e995df..397ba84d4 100644 --- a/fast64_internal/f3d/f3d_gbi.py +++ b/fast64_internal/f3d/f3d_gbi.py @@ -2203,7 +2203,10 @@ def to_binary(self, f3d, segments): def to_c_static(self, name: str): data = f"Gfx {name}[] = {{\n" for command in self.commands: - data += f"\t{command.to_c(True)},\n" + if command.default_formatting: + data += f"\t{command.to_c(True)},\n" + else: + data += command.to_c(True) data += "};\n\n" return data @@ -3415,6 +3418,11 @@ class GbiMacro: This is unannotated and will not be considered when calculating the hash. """ + default_formatting = True + """ + Type: bool. Used to allow an overriden `to_c` function customize the formatting (identation, newlines, etc). + """ + def get_ptr_offsets(self, f3d): return [4] diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index ea5a730ee..0fb13f902 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -261,10 +261,11 @@ def getGroupNameFromIndex(obj, index): return None -def copyPropertyCollection(oldProp, newProp): - newProp.clear() - for item in oldProp: - newItem = newProp.add() +def copyPropertyCollection(from_prop, to_prop, do_clear: bool = True): + if do_clear: + to_prop.clear() + for item in from_prop: + newItem = to_prop.add() if isinstance(item, bpy.types.PropertyGroup): copyPropertyGroup(item, newItem) elif type(item).__name__ == "bpy_prop_collection_idprop": @@ -273,18 +274,18 @@ def copyPropertyCollection(oldProp, newProp): newItem = item -def copyPropertyGroup(oldProp, newProp): - for sub_value_attr in oldProp.bl_rna.properties.keys(): +def copyPropertyGroup(from_prop, to_prop): + for sub_value_attr in from_prop.bl_rna.properties.keys(): if sub_value_attr == "rna_type": continue - sub_value = getattr(oldProp, sub_value_attr) + sub_value = getattr(from_prop, sub_value_attr) if isinstance(sub_value, bpy.types.PropertyGroup): - copyPropertyGroup(sub_value, getattr(newProp, sub_value_attr)) + copyPropertyGroup(sub_value, getattr(to_prop, sub_value_attr)) elif type(sub_value).__name__ == "bpy_prop_collection_idprop": - newCollection = getattr(newProp, sub_value_attr) + newCollection = getattr(to_prop, sub_value_attr) copyPropertyCollection(sub_value, newCollection) else: - setattr(newProp, sub_value_attr, sub_value) + setattr(to_prop, sub_value_attr, sub_value) def get_attr_or_property(prop: dict | object, attr: str, newProp: dict | object): @@ -1331,7 +1332,7 @@ def filepath_ui_warnings( return run_and_draw_errors(layout, filepath_checks, path, empty, doesnt_exist, not_a_file, False) -def toAlnum(name, exceptions=[]): +def toAlnum(name: str, exceptions=[]): if name is None or name == "": return None for i in range(len(name)): @@ -2061,17 +2062,35 @@ def get_include_data(include: str, strip: bool = False): def get_new_object( - name: str, data: Optional[Any], selectObject: bool, parentObj: Optional[bpy.types.Object] + name: str, + data: Optional[Any], + do_select: bool, + location=[0.0, 0.0, 0.0], + rotation_euler=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0], + parent: Optional[bpy.types.Object] = None, ) -> bpy.types.Object: - newObj = bpy.data.objects.new(name=name, object_data=data) - bpy.context.view_layer.active_layer_collection.collection.objects.link(newObj) - - if selectObject: - newObj.select_set(True) - bpy.context.view_layer.objects.active = newObj - - newObj.parent = parentObj - newObj.location = [0.0, 0.0, 0.0] - newObj.rotation_euler = [0.0, 0.0, 0.0] - newObj.scale = [1.0, 1.0, 1.0] - return newObj + new_obj = bpy.data.objects.new(name=name, object_data=data) + bpy.context.view_layer.active_layer_collection.collection.objects.link(new_obj) + + if do_select: + new_obj.select_set(True) + bpy.context.view_layer.objects.active = new_obj + + new_obj.parent = parent + new_obj.location = location + new_obj.rotation_euler = rotation_euler + new_obj.scale = scale + return new_obj + + +def get_new_empty_object( + name: str, + do_select: bool = False, + location=[0.0, 0.0, 0.0], + rotation_euler=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0], + parent: Optional[bpy.types.Object] = None, +): + """Creates and returns a new empty object""" + return get_new_object(name, None, do_select, location, rotation_euler, scale, parent) diff --git a/fast64_internal/z64/README.md b/fast64_internal/z64/README.md index abc3f3386..5a0259917 100644 --- a/fast64_internal/z64/README.md +++ b/fast64_internal/z64/README.md @@ -12,6 +12,7 @@ 9. [Custom Link Process](#custom-link-process) 10. [Custom Skeleton Mesh Process](#custom-skeleton-mesh-process) 11. [Cutscenes](#cutscenes) +12. [Animated Materials](#animated-materials) ### Getting Started 1. In the 3D view properties sidebar (default hotkey to show this is `n` in the viewport), go to the ``Fast64`` tab, then ``Fast64 Global Settings`` and set ``Game`` to ``OOT``. @@ -194,3 +195,43 @@ If the camera preview in Blender isn't following where you have the bones or if 2. If you moved / rotated / etc. one of the camera shots / armatures in object mode, this transformation will be ignored. You can fix this by selecting the shot / armature in object mode and clicking Object > Apply > All Transforms. That will convert the transform to actual changed positions for each bone. If the game crashes check the transitions if you use the transition command (check both the ones from the entrance table and your cutscene script), also it will crash if you try to use the map select without having a 5th entrance (or more depending on the number of cutscenes you have) in the group for your scene. + +### Animated Materials + +This is a feature you can use for Majora's Mask and OoT backports like HackerOoT (requires enabling `Enable MM Features` for non-HackerOoT OoT decomp projects). It allows you to do some animation on any material you want, on Majora's Mask it's used to animate some actor's textures, and it's used in scenes too, this is what makes the walls in Majora's Lair animated, for instance. + +**Important**: this requires the scene to use a specific draw config called `Material Animated` (or `Material Animated (manual step)` for special cases). + +**Getting Started** + +For non-scene export you'll need to either use the `Add Animated Material` button under the `Tools` tab, or manually adding an empty object and setting the object mode to `Animated Materials`. If doing the latter make sure the object is parented to the scene object, it can be parented to a room too, or anything else as long as the scene object is in the hierarchy, but it will be exported to the scene file. + +For scenes it's integrated as a tab in the scene header properties panel. + +**Creating the animated materials list** + +For non-scene export, click on `Add Item` to add a new animated material list. + +You can pick the segment number with the `Segment Number` field (make sure to use the same number on the material you want this to be used on), for convenience the exporter will add a macro to make it more readable. `Draw Handler Type` lets you choose what kind of animated material you want, it can be one of: +- `0`: Texture Scroll +- `1`: Two-textures Scroll +- `2`: Color +- `3`: Color LERP +- `4`: Color Non-linear Interpolation +- `5`: Texture Cycle (like a GIF) + +For the LERP and non-linear interpolation color types you will also have a `Keyframe Length` field, this corresponds to the length of the animation. `Draw Color` (type 2) can use environment color but it's not mandatory unlike the other ones. + +Both texture scroll types will use the same elements: +- `Step X`: step value on the X axis +- `Step Y`: step value on the Y axis +- `Texture Width`: the width of the texture +- `Texture Height`: the height of the texture + +All 3 color types will use the same elements: +- `Frame No.`: when to execute this entry (relative to the keyframe length), not available for `Draw Color` +- `Primitive LOD Frac`: unknown purpose, feel free to complete! +- `Primitive Color`: the primitive color to apply +- `Environment Color`: the environment color to apply, optional for `Draw Color` + +The texture cycle type will show you two lists to fill, one for the texture symbols to use and another one for the indices that points to the textures list. Note that both list don't need to be the same length, also this technically uses a keyframe length too but it should always match the total number of indices that's why you can't manually choose it. diff --git a/fast64_internal/z64/__init__.py b/fast64_internal/z64/__init__.py index 3f8fac2fc..4eba6a0a9 100644 --- a/fast64_internal/z64/__init__.py +++ b/fast64_internal/z64/__init__.py @@ -57,6 +57,19 @@ from .spline.properties import spline_props_register, spline_props_unregister from .spline.panels import spline_panels_register, spline_panels_unregister +from .animated_mats.operators import animated_mats_ops_register, animated_mats_ops_unregister +from .animated_mats.panels import animated_mats_panels_register, animated_mats_panels_unregister +from .animated_mats.properties import ( + Z64_AnimatedMaterialExportSettings, + Z64_AnimatedMaterialImportSettings, + animated_mats_props_register, + animated_mats_props_unregister, +) + +from .hackeroot.operators import hackeroot_ops_register, hackeroot_ops_unregister +from .hackeroot.properties import HackerOoTSettings, hackeroot_props_register, hackeroot_props_unregister +from .hackeroot.panels import hackeroot_panels_register, hackeroot_panels_unregister + from .tools import ( oot_operator_panel_register, oot_operator_panel_unregister, @@ -110,6 +123,10 @@ class OOT_Properties(bpy.types.PropertyGroup): oot_version: bpy.props.EnumProperty(name="OoT Version", items=oot_versions_items, default="gc-eu-mq-dbg") mm_version: bpy.props.EnumProperty(name="MM Version", items=mm_versions_items, default="n64-us") oot_version_custom: bpy.props.StringProperty(name="Custom Version") + mm_features: bpy.props.BoolProperty(name="Enable MM Features", default=False) + hackeroot_settings: bpy.props.PointerProperty(type=HackerOoTSettings) + anim_mats_export_settings: bpy.props.PointerProperty(type=Z64_AnimatedMaterialExportSettings) + anim_mats_import_settings: bpy.props.PointerProperty(type=Z64_AnimatedMaterialImportSettings) def get_extracted_path(self): version = self.oot_version if game_data.z64.is_oot() else self.mm_version @@ -156,6 +173,7 @@ def is_z64sceneh_present(self): def oot_panel_register(): oot_operator_panel_register() + hackeroot_panels_register() cutscene_panels_register() scene_panels_register() f3d_panels_register() @@ -164,10 +182,12 @@ def oot_panel_register(): spline_panels_register() anim_panels_register() skeleton_panels_register() + animated_mats_panels_register() def oot_panel_unregister(): oot_operator_panel_unregister() + hackeroot_panels_unregister() cutscene_panels_unregister() collision_panels_unregister() oot_obj_panel_unregister() @@ -176,6 +196,7 @@ def oot_panel_unregister(): f3d_panels_unregister() anim_panels_unregister() skeleton_panels_unregister() + animated_mats_panels_unregister() def oot_register(registerPanels): @@ -184,13 +205,14 @@ def oot_register(registerPanels): collision_ops_register() # register first, so panel goes above mat panel collision_props_register() cutscene_props_register() + animated_mats_ops_register() + animated_mats_props_register() scene_ops_register() scene_props_register() room_ops_register() room_props_register() actor_ops_register() actor_props_register() - oot_obj_register() spline_props_register() f3d_props_register() anim_ops_register() @@ -200,6 +222,8 @@ def oot_register(registerPanels): f3d_ops_register() file_register() anim_props_register() + hackeroot_props_register() + hackeroot_ops_register() csMotion_ops_register() csMotion_props_register() @@ -207,6 +231,8 @@ def oot_register(registerPanels): csMotion_preview_register() cutscene_preview_register() + oot_obj_register() + for cls in oot_classes: register_class(cls) @@ -215,30 +241,13 @@ def oot_register(registerPanels): def oot_unregister(unregisterPanels): + if unregisterPanels: + oot_panel_unregister() + for cls in reversed(oot_classes): unregister_class(cls) - oot_operator_unregister() - collections_unregister() - collision_ops_unregister() # register first, so panel goes above mat panel - collision_props_unregister() oot_obj_unregister() - cutscene_props_unregister() - scene_ops_unregister() - scene_props_unregister() - room_ops_unregister() - room_props_unregister() - actor_ops_unregister() - actor_props_unregister() - spline_props_unregister() - f3d_props_unregister() - anim_ops_unregister() - skeleton_ops_unregister() - skeleton_props_unregister() - cutscene_ops_unregister() - f3d_ops_unregister() - file_unregister() - anim_props_unregister() cutscene_preview_unregister() csMotion_preview_unregister() @@ -246,5 +255,27 @@ def oot_unregister(unregisterPanels): csMotion_props_unregister() csMotion_ops_unregister() - if unregisterPanels: - oot_panel_unregister() + hackeroot_ops_unregister() + hackeroot_props_unregister() + anim_props_unregister() + file_unregister() + f3d_ops_unregister() + cutscene_ops_unregister() + skeleton_props_unregister() + skeleton_ops_unregister() + anim_ops_unregister() + f3d_props_unregister() + spline_props_unregister() + actor_props_unregister() + actor_ops_unregister() + room_props_unregister() + room_ops_unregister() + scene_props_unregister() + scene_ops_unregister() + animated_mats_props_unregister() + animated_mats_ops_unregister() + cutscene_props_unregister() + collision_props_unregister() + collision_ops_unregister() + collections_unregister() + oot_operator_unregister() diff --git a/fast64_internal/z64/animated_mats/operators.py b/fast64_internal/z64/animated_mats/operators.py new file mode 100644 index 000000000..45e8b3ba8 --- /dev/null +++ b/fast64_internal/z64/animated_mats/operators.py @@ -0,0 +1,54 @@ +from bpy.utils import register_class, unregister_class +from bpy.types import Operator + +from ...utility import raisePluginError + + +class Z64_ExportAnimatedMaterials(Operator): + bl_idname = "object.z64_export_animated_materials" + bl_label = "Export Animated Materials" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + from ..exporter.scene.animated_mats import SceneAnimatedMaterial + + try: + SceneAnimatedMaterial.export() + self.report({"INFO"}, "Success!") + return {"FINISHED"} + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + +class Z64_ImportAnimatedMaterials(Operator): + bl_idname = "object.z64_import_animated_materials" + bl_label = "Import Animated Materials" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + from ..exporter.scene.animated_mats import SceneAnimatedMaterial + + try: + SceneAnimatedMaterial.from_data() + self.report({"INFO"}, "Success!") + return {"FINISHED"} + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + +classes = ( + Z64_ExportAnimatedMaterials, + Z64_ImportAnimatedMaterials, +) + + +def animated_mats_ops_register(): + for cls in classes: + register_class(cls) + + +def animated_mats_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/animated_mats/panels.py b/fast64_internal/z64/animated_mats/panels.py new file mode 100644 index 000000000..3dcb5a711 --- /dev/null +++ b/fast64_internal/z64/animated_mats/panels.py @@ -0,0 +1,29 @@ +from bpy.utils import register_class, unregister_class + +from ...panels import OOT_Panel +from ..utility import is_oot_features, is_hackeroot + + +class Z64_AnimatedMaterialsPanel(OOT_Panel): + bl_idname = "Z64_PT_animated_materials" + bl_label = "Animated Materials Exporter" + + def draw(self, context): + if not is_oot_features() or is_hackeroot(): + context.scene.fast64.oot.anim_mats_export_settings.draw_props(self.layout.box()) + context.scene.fast64.oot.anim_mats_import_settings.draw_props(self.layout.box()) + else: + self.layout.label(text="MM features are disabled.", icon="QUESTION") + + +panel_classes = (Z64_AnimatedMaterialsPanel,) + + +def animated_mats_panels_register(): + for cls in panel_classes: + register_class(cls) + + +def animated_mats_panels_unregister(): + for cls in reversed(panel_classes): + unregister_class(cls) diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py new file mode 100644 index 000000000..d2248cc77 --- /dev/null +++ b/fast64_internal/z64/animated_mats/properties.py @@ -0,0 +1,516 @@ +import bpy + +from bpy.utils import register_class, unregister_class +from bpy.types import PropertyGroup, UILayout, Object +from bpy.props import ( + IntProperty, + PointerProperty, + BoolProperty, + EnumProperty, + StringProperty, + CollectionProperty, + FloatVectorProperty, +) + +from typing import Optional + +from ...utility import prop_split +from ..collection_utility import drawCollectionOps, draw_utility_ops +from ..utility import get_list_tab_text, getEnumIndex, is_hackeroot +from .operators import Z64_ExportAnimatedMaterials, Z64_ImportAnimatedMaterials + + +# no custom since we only need to know where to export the data +enum_mode = [ + ("Scene", "Scene", "Scene"), + ("Actor", "Actor", "Actor"), +] + +# see `sMatAnimDrawHandlers` in `z_scene_proc.c` +enum_anim_mat_type = [ + ("Custom", "Custom", "Custom"), + ("tex_scroll", "Draw Texture Scroll", "Draw Texture Scroll"), + ("two_tex_scroll", "Draw Two Texture Scroll", "Draw Two Texture Scroll"), + ("color", "Draw Color", "Draw Color"), + ("color_lerp", "Draw Color Lerp", "Draw Color Lerp"), + ("color_nonlinear_interp", "Draw Color Non-Linear Interp", "Draw Color Non-Linear Interp"), + ("tex_cycle", "Draw Texture Cycle", "Draw Texture Cycle"), +] + + +class Z64_AnimatedMatColorKeyFrame(PropertyGroup): + frame_num: IntProperty( + name="Frame No.", + min=0, + set=lambda self, value: self.on_frame_num_set(value), + get=lambda self: self.on_frame_num_get(), + ) + internal_frame_num: IntProperty(min=0) + internal_length: IntProperty(min=0) + + def validate_frame_num(self): + if self.internal_frame_num >= self.internal_length: + # TODO: figure out if having the same value is fine + self.internal_frame_num = self.internal_length - 1 + + def on_frame_num_set(self, value): + self.internal_frame_num = value + self.validate_frame_num() + + def on_frame_num_get(self): + self.validate_frame_num() + return self.internal_frame_num + + prim_lod_frac: IntProperty(name="Primitive LOD Frac", min=0, max=255, default=128) + prim_color: FloatVectorProperty( + name="Primitive Color", + subtype="COLOR", + size=4, + min=0, + max=1, + default=(1, 1, 1, 1), + ) + + env_color: FloatVectorProperty( + name="Environment Color", + subtype="COLOR", + size=4, + min=0, + max=1, + default=(1, 1, 1, 1), + ) + + def draw_props( + self, + layout: UILayout, + owner: Object, + header_index: int, + parent_index: int, + index: int, + is_draw_color: bool, + use_env_color: bool, + ): + drawCollectionOps( + layout, + index, + "Animated Mat. Color", + header_index, + owner.name, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + + # "draw color" type don't need this + if not is_draw_color: + prop_split(layout, self, "frame_num", "Frame No.") + + prop_split(layout, self, "prim_lod_frac", "Primitive LOD Frac") + prop_split(layout, self, "prim_color", "Primitive Color") + + if not is_draw_color or use_env_color: + prop_split(layout, self, "env_color", "Environment Color") + + +class Z64_AnimatedMatColorParams(PropertyGroup): + keyframe_length: IntProperty( + name="Keyframe Length", + min=0, + set=lambda self, value: self.on_length_set(value), + get=lambda self: self.on_length_get(), + ) + internal_keyframe_length: IntProperty(min=0) + + keyframes: CollectionProperty(type=Z64_AnimatedMatColorKeyFrame) + use_env_color: BoolProperty() + + # ui only props + show_entries: BoolProperty(default=False) + + internal_color_type: StringProperty() + + def update_keyframes(self): + for keyframe in self.keyframes: + keyframe.internal_length = self.internal_keyframe_length + + def on_length_set(self, value): + self.internal_keyframe_length = value + self.update_keyframes() + + def on_length_get(self): + self.update_keyframes() + return self.internal_keyframe_length + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int): + is_draw_color = self.internal_color_type == "color" + + if not is_draw_color: + prop_split(layout, self, "keyframe_length", "Keyframe Length") + + if is_draw_color: + layout.prop(self, "use_env_color", text="Use Environment Color") + + prop_text = get_list_tab_text("Keyframes", len(self.keyframes)) + layout.prop(self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + + if self.show_entries: + for i, keyframe in enumerate(self.keyframes): + keyframe.draw_props( + layout, owner, header_index, parent_index, i, is_draw_color, not is_draw_color or self.use_env_color + ) + + draw_utility_ops( + layout.row(), + len(self.keyframes), + "Animated Mat. Color", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + + +class Z64_AnimatedMatTexScrollItem(PropertyGroup): + step_x: IntProperty(default=0) + step_y: IntProperty(default=0) + width: IntProperty(min=0) + height: IntProperty(min=0) + + def set_from_data(self, raw_data: list[str]): + self.step_x = int(raw_data[0], base=0) + self.step_y = int(raw_data[1], base=0) + self.width = int(raw_data[2], base=0) + self.height = int(raw_data[3], base=0) + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "step_x", "Step X") + prop_split(layout, self, "step_y", "Step Y") + prop_split(layout, self, "width", "Texture Width") + prop_split(layout, self, "height", "Texture Height") + + +class Z64_AnimatedMatTexScrollParams(PropertyGroup): + texture_1: PointerProperty(type=Z64_AnimatedMatTexScrollItem) + texture_2: PointerProperty(type=Z64_AnimatedMatTexScrollItem) + + # ui only props + show_entries: BoolProperty(default=False) + + internal_scroll_type: StringProperty(default="two_tex_scroll") + + def draw_props(self, layout: UILayout): + tab_text = "Two-Texture Scroll" if self.internal_scroll_type == "two_tex_scroll" else "Texture Scroll" + layout.prop(self, "show_entries", text=tab_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + + if self.show_entries: + if self.internal_scroll_type == "two_tex_scroll": + tex1_box = layout.box().column() + tex1_box.label(text="Texture 1") + self.texture_1.draw_props(tex1_box) + + tex2_box = layout.box().column() + tex2_box.label(text="Texture 2") + self.texture_2.draw_props(tex2_box) + else: + self.texture_1.draw_props(layout) + + +class Z64_AnimatedMatTexCycleTexture(PropertyGroup): + symbol: StringProperty(name="Texture Symbol") + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int, index: int): + drawCollectionOps( + layout, + index, + "Animated Mat. Cycle (Texture)", + header_index, + owner.name, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + prop_split(layout, self, "symbol", "Texture Symbol") + + +class Z64_AnimatedMatTexCycleKeyFrame(PropertyGroup): + texture_index: IntProperty(min=0) + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int, index: int): + drawCollectionOps( + layout, + index, + "Animated Mat. Cycle (Index)", + header_index, + owner.name, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + prop_split(layout, self, "texture_index", "Texture Symbol") + + +class Z64_AnimatedMatTexCycleParams(PropertyGroup): + keyframes: CollectionProperty(type=Z64_AnimatedMatTexCycleKeyFrame) + textures: CollectionProperty(type=Z64_AnimatedMatTexCycleTexture) + + # ui only props + show_entries: BoolProperty(default=False) + show_textures: BoolProperty(default=False) + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int): + texture_box = layout.box() + prop_text = get_list_tab_text("Textures", len(self.textures)) + texture_box.prop( + self, "show_textures", text=prop_text, icon="TRIA_DOWN" if self.show_textures else "TRIA_RIGHT" + ) + if self.show_textures: + for i, texture in enumerate(self.textures): + texture.draw_props(texture_box, owner, header_index, parent_index, i) + draw_utility_ops( + texture_box.row(), + len(self.textures), + "Animated Mat. Cycle (Texture)", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + + index_box = layout.box() + prop_text = get_list_tab_text("Keyframes", len(self.keyframes)) + index_box.prop(self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + if self.show_entries: + for i, keyframe in enumerate(self.keyframes): + keyframe.draw_props(index_box, owner, header_index, parent_index, i) + draw_utility_ops( + index_box.row(), + len(self.keyframes), + "Animated Mat. Cycle (Index)", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + + +class Z64_AnimatedMaterialItem(PropertyGroup): + """see the `AnimatedMaterial` struct from `z64scene.h`""" + + segment_num: IntProperty(name="Segment Number", min=8, max=13, default=8) + + user_type: EnumProperty( + name="Draw Handler Type", + items=enum_anim_mat_type, + default=2, + description="Index to `sMatAnimDrawHandlers`", + get=lambda self: getEnumIndex(enum_anim_mat_type, self.type), + set=lambda self, value: self.on_type_set(value), + ) + type: StringProperty(default=enum_anim_mat_type[2][0]) + type_custom: StringProperty(name="Custom Draw Handler Index", default="2") + + color_params: PointerProperty(type=Z64_AnimatedMatColorParams) + tex_scroll_params: PointerProperty(type=Z64_AnimatedMatTexScrollParams) + tex_cycle_params: PointerProperty(type=Z64_AnimatedMatTexCycleParams) + + # ui only props + show_item: BoolProperty(default=False) + + def on_type_set(self, value: int): + self.type = enum_anim_mat_type[value][0] + + if "tex_scroll" in self.type: + self.tex_scroll_params.internal_scroll_type = self.type + elif "color" in self.type: + self.color_params.internal_color_type = self.type + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, index: int): + layout.prop( + self, "show_item", text=f"Item No.{index + 1}", icon="TRIA_DOWN" if self.show_item else "TRIA_RIGHT" + ) + + if self.show_item: + drawCollectionOps(layout, index, "Animated Mat.", header_index, owner.name, ask_for_amount=True) + + prop_split(layout, self, "segment_num", "Segment Number") + + layout_type = layout.column() + prop_split(layout_type, self, "user_type", "Draw Handler Type") + + if self.type == "Custom": + layout_type.label( + text="This only allows you to choose a custom index for the function handler.", icon="ERROR" + ) + prop_split(layout_type, self, "type_custom", "Custom Draw Handler Index") + elif self.type in {"tex_scroll", "two_tex_scroll"}: + self.tex_scroll_params.draw_props(layout_type) + elif self.type in {"color", "color_lerp", "color_nonlinear_interp"}: + self.color_params.draw_props(layout_type, owner, header_index, index) + elif self.type == "tex_cycle": + self.tex_cycle_params.draw_props(layout_type, owner, header_index, index) + + +class Z64_AnimatedMaterial(PropertyGroup): + """Defines an Animated Material array""" + + entries: CollectionProperty(type=Z64_AnimatedMaterialItem) + + # ui only props + show_list: BoolProperty(default=True) + show_entries: BoolProperty(default=True) + + def draw_props( + self, + layout: UILayout, + owner: Object, + index: Optional[int], + sub_index: Optional[int] = None, + is_scene: bool = True, + ): + if index is not None: + layout.prop( + self, "show_list", text=f"List No.{index + 1}", icon="TRIA_DOWN" if self.show_list else "TRIA_RIGHT" + ) + + if self.show_list: + if index is not None: + drawCollectionOps( + layout, + index, + "Animated Mat. List", + sub_index, + owner.name, + ask_for_copy=False, + ask_for_amount=False, + ) + + prop_text = get_list_tab_text("Animated Materials", len(self.entries)) + layout_entries = layout.column() + layout_entries.prop( + self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT" + ) + + if self.show_entries: + for i, item in enumerate(self.entries): + item.draw_props(layout_entries.box().column(), owner, sub_index, i) + + draw_utility_ops( + layout_entries.row(), + len(self.entries), + "Animated Mat.", + sub_index, + owner.name, + do_copy=is_scene, + ask_for_amount=True, + ) + + +class Z64_AnimatedMaterialProperty(PropertyGroup): + """List of Animated Material arrays""" + + # this is probably useless since usually you wouldn't use different animated materials + # on different headers but it's better to give users the choice + items: CollectionProperty(type=Z64_AnimatedMaterial) + + # ui only props + show_entries: BoolProperty(default=True) + + def draw_props(self, layout: UILayout, owner: Object): + layout = layout.column() + + prop_text = get_list_tab_text("Animated Materials List", len(self.items)) + layout_entries = layout.column() + layout_entries.prop( + self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT" + ) + + if self.show_entries: + for i, item in enumerate(self.items): + item.draw_props(layout_entries.box().column(), owner, i, i, is_scene=False) + + draw_utility_ops( + layout_entries.row(), len(self.items), "Animated Mat. List", None, owner.name, do_copy=False + ) + + +class Z64_AnimatedMaterialExportSettings(PropertyGroup): + object_name: StringProperty(default="gameplay_keep") + + include_name: StringProperty(default="animated_materials.h") + is_custom_inc: BoolProperty(default=False) + + export_path: StringProperty(name="File", subtype="DIR_PATH") + export_obj: PointerProperty(type=Object, poll=lambda self, obj: self.filter(obj)) + is_custom_path: BoolProperty(default=False) + + def filter(self, obj): + return obj.type == "EMPTY" and obj.ootEmptyType == "Animated Materials" + + def get_include_name(self): + if is_hackeroot(): + return "animated_materials.h" + + if self.is_custom_inc: + return self.include_name if self.include_name.endswith(".h") else f"{self.include_name}.h" + + if bpy.context.scene.fast64.oot.is_z64sceneh_present(): + return "z64scene.h" + + return "scene.h" + + def draw_props(self, layout: UILayout): + layout = layout.column() + layout.label(text="Animated Materials Exporter") + prop_split(layout, self, "export_obj", "Export Object") + + if not is_hackeroot(): + inc_box = layout.box() + inc_box.prop(self, "is_custom_inc", text="Custom Include") + if self.is_custom_inc: + prop_split(inc_box, self, "include_name", "Include") + + path_box = layout.box() + path_box.prop(self, "is_custom_path", text="Custom Path") + if self.is_custom_path: + path_box.label(text="The object name will be the file name", icon="QUESTION") + prop_split(path_box, self, "export_path", "Export To") + else: + prop_split(path_box, self, "object_name", "Object Name") + + layout.operator(Z64_ExportAnimatedMaterials.bl_idname) + + +class Z64_AnimatedMaterialImportSettings(PropertyGroup): + import_path: StringProperty(name="File", subtype="FILE_PATH") + + def draw_props(self, layout: UILayout): + layout = layout.column() + layout.label(text="Animated Materials Importer") + prop_split(layout, self, "import_path", "Import From") + layout.operator(Z64_ImportAnimatedMaterials.bl_idname) + + +classes = ( + Z64_AnimatedMatColorKeyFrame, + Z64_AnimatedMatColorParams, + Z64_AnimatedMatTexScrollItem, + Z64_AnimatedMatTexScrollParams, + Z64_AnimatedMatTexCycleTexture, + Z64_AnimatedMatTexCycleKeyFrame, + Z64_AnimatedMatTexCycleParams, + Z64_AnimatedMaterialItem, + Z64_AnimatedMaterial, + Z64_AnimatedMaterialProperty, + Z64_AnimatedMaterialExportSettings, + Z64_AnimatedMaterialImportSettings, +) + + +def animated_mats_props_register(): + for cls in classes: + register_class(cls) + + +def animated_mats_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py index 8b5c77af8..36e77182d 100644 --- a/fast64_internal/z64/collection_utility.py +++ b/fast64_internal/z64/collection_utility.py @@ -1,9 +1,12 @@ import bpy -from bpy.types import Operator +from bpy.types import Operator, UILayout from bpy.utils import register_class, unregister_class -from bpy.props import IntProperty, StringProperty -from ..utility import PluginError, ootGetSceneOrRoomHeader +from bpy.props import IntProperty, StringProperty, BoolProperty, EnumProperty +from typing import Optional + +from ..game_data import game_data +from ..utility import PluginError, ootGetSceneOrRoomHeader, copyPropertyCollection, copyPropertyGroup class OOTCollectionAdd(Operator): @@ -14,15 +17,52 @@ class OOTCollectionAdd(Operator): option: IntProperty() collectionType: StringProperty(default="Actor") subIndex: IntProperty(default=0) + collection_index: IntProperty(default=0) objName: StringProperty() + ask_for_copy: BoolProperty(default=False) + do_copy_previous: BoolProperty(default=True) + + ask_for_amount: BoolProperty(default=False) + amount: IntProperty(min=1, default=1) + def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) + + for i in range(self.amount): + new_entry = collection.add() + collection.move(len(collection) - 1, self.option + i) + + if self.ask_for_copy and self.do_copy_previous: + copyPropertyGroup(collection[self.option - 1 + i], new_entry) + + if not self.ask_for_amount: + # should always default to 1 but just in case force a break + break + + owner = bpy.data.objects[self.objName] + if self.collectionType == "Actor CS" and owner.ootEmptyType == "Actor Cutscene": + context.scene.fast64.oot.global_actor_cs_count = len(collection) - collection.add() - collection.move(len(collection) - 1, self.option) + if self.collectionType == "Scene": + new_entry.internal_header_index = 4 + + context.region.tag_redraw() return {"FINISHED"} + def invoke(self, context, _): + if self.ask_for_copy or self.ask_for_amount: + return context.window_manager.invoke_props_dialog(self, width=300) + return self.execute(context) + + def draw(self, _): + layout = self.layout + if self.ask_for_copy: + layout.prop(self, "do_copy_previous", text="Copy Previous Entry") + + if self.ask_for_amount: + layout.prop(self, "amount", text="Number of items to add") + class OOTCollectionRemove(Operator): bl_idname = "object.oot_collection_remove" @@ -32,13 +72,73 @@ class OOTCollectionRemove(Operator): option: IntProperty() collectionType: StringProperty(default="Actor") subIndex: IntProperty(default=0) + collection_index: IntProperty(default=0) objName: StringProperty() + ask_for_amount: BoolProperty(default=False) + amount: IntProperty( + min=0, + default=0, + set=lambda self, value: OOTCollectionRemove.on_amount_set(self, value), + get=lambda self: OOTCollectionRemove.on_amount_get(self), + ) + internal_amount: IntProperty(min=0, default=0) + + # static methods because it doesn't work otherwise + @staticmethod + def on_amount_set(owner, value): + owner.internal_amount = value + + @staticmethod + def on_amount_get(owner): + collection = getCollection(owner.objName, owner.collectionType, owner.subIndex, owner.collection_index) + maximum = len(collection) - owner.option + + if owner.internal_amount >= maximum: + owner.internal_amount = maximum + + return owner.internal_amount + def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) - collection.remove(self.option) + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) + + if self.amount > 0: + for _ in range(self.amount): + if not self.ask_for_amount or self.option >= len(collection): + break + + collection.remove(self.option) + else: + collection.remove(self.option) + + owner = bpy.data.objects[self.objName] + if self.collectionType == "Actor CS" and owner.ootEmptyType == "Actor Cutscene": + context.scene.fast64.oot.global_actor_cs_count = len(collection) + + context.region.tag_redraw() return {"FINISHED"} + def invoke(self, context, _): + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) + if self.ask_for_amount and self.option + 1 < len(collection): + return context.window_manager.invoke_props_dialog(self, width=300) + return self.execute(context) + + def draw(self, _): + layout = self.layout + + if self.ask_for_amount: + layout.prop(self, "amount", text="Number of following items to remove") + + if self.amount == 0: + text = f"Will remove Item No. {self.option + 1}." + elif self.amount == 1: + text = f"Will remove Item No. {self.option + 1} and the next one." + else: + text = f"Will remove Item No. {self.option + 1} and the next {self.amount}." + + layout.label(text=text) + class OOTCollectionMove(Operator): bl_idname = "object.oot_collection_move" @@ -48,15 +148,104 @@ class OOTCollectionMove(Operator): option: IntProperty() offset: IntProperty() subIndex: IntProperty(default=0) + collection_index: IntProperty(default=0) objName: StringProperty() collectionType: StringProperty(default="Actor") def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) collection.move(self.option, self.option + self.offset) + context.region.tag_redraw() + return {"FINISHED"} + + +class OOTCollectionClear(Operator): + bl_idname = "object.oot_collection_clear" + bl_label = "Clear All Items" + bl_options = {"REGISTER", "UNDO"} + + collection_type: StringProperty(default="Actor") + sub_index: IntProperty(default=0) + collection_index: IntProperty(default=0) + obj_name: StringProperty() + + def execute(self, context): + collection = getCollection(self.obj_name, self.collection_type, self.sub_index, self.collection_index) + collection.clear() + context.region.tag_redraw() + return {"FINISHED"} + + def invoke(self, context, _): + return context.window_manager.invoke_props_dialog(self, width=300) + + def draw(self, _): + layout = self.layout + layout.label(text="Are you sure you want to clear this collection?") + + +class OOTCollectionCopy(Operator): + bl_idname = "object.oot_collection_copy" + bl_label = "Copy Items" + bl_options = {"REGISTER", "UNDO"} + + collection_type: StringProperty(default="Actor") + sub_index: IntProperty(default=0) + collection_index: IntProperty(default=0) + obj_name: StringProperty() + + from_cs_index: IntProperty(min=game_data.z64.cs_index_start, default=game_data.z64.cs_index_start) + from_header_index: EnumProperty(items=lambda self, _: OOTCollectionCopy.get_items(self)) + do_clear: BoolProperty(default=True) + + @staticmethod + def get_items(owner: "OOTCollectionCopy"): + enum = [ + ("0", "Child Day", "Child Day"), + ("1", "Child Night", "Child Night"), + ("2", "Adult Day", "Adult Day"), + ("3", "Adult Night", "Adult Night"), + ("4", "Cutscene", "Cutscene"), + ] + enum.pop(owner.sub_index if owner.sub_index < 4 else 4) + return enum + + def execute(self, context): + from_header_index = int(self.from_header_index) if self.from_header_index != "4" else self.from_cs_index + + def try_get_collection(obj_name, collection_type, sub_index: int, collection_index: int): + try: + return getCollection(obj_name, collection_type, sub_index, collection_index) + except AttributeError: + return None + + col_from = try_get_collection(self.obj_name, self.collection_type, from_header_index, self.collection_index) + col_to = try_get_collection(self.obj_name, self.collection_type, self.sub_index, self.collection_index) + + if col_from is None: + self.report({"ERROR"}, "The selected header cannot be used because it's using the previous header.") + return {"CANCELLED"} + + if col_to is None: + self.report({"ERROR"}, "Unexpected error occurred.") + return {"CANCELLED"} + + copyPropertyCollection(col_from, col_to, do_clear=self.do_clear) + context.region.tag_redraw() return {"FINISHED"} + def invoke(self, context, _): + return context.window_manager.invoke_props_dialog(self, width=300) + + def draw(self, _): + layout = self.layout + layout.prop(self, "from_header_index", text="Copy From") + + if self.from_header_index == "4": + layout.prop(self, "from_cs_index", text="Cutscene Index") + + layout.prop(self, "do_clear", text="Clear the destination collection before copying") + def getCollectionFromIndex(obj, prop, subIndex, isRoom): header = ootGetSceneOrRoomHeader(obj, subIndex, isRoom) @@ -66,7 +255,7 @@ def getCollectionFromIndex(obj, prop, subIndex, isRoom): # Operators cannot store mutable references (?), so to reuse PropertyCollection modification code we do this. # Save a string identifier in the operator, then choose the member variable based on that. # subIndex is for a collection within a collection element -def getCollection(objName, collectionType, subIndex): +def getCollection(objName, collectionType, subIndex: int, collection_index: int = 0): obj = bpy.data.objects[objName] if collectionType == "Actor": collection = obj.ootActorProperty.headerSettings.cutsceneHeaders @@ -86,6 +275,23 @@ def getCollection(objName, collectionType, subIndex): collection = getCollectionFromIndex(obj, "exitList", subIndex, False) elif collectionType == "Object": collection = getCollectionFromIndex(obj, "objectList", subIndex, True) + elif collectionType == "Animated Mat. List": + collection = obj.fast64.oot.animated_materials.items + elif collectionType.startswith("Animated Mat."): + if obj.ootEmptyType == "Scene": + header = ootGetSceneOrRoomHeader(obj, subIndex, False) + props = header.animated_material + else: + props = obj.fast64.oot.animated_materials.items[subIndex] + + if collectionType == "Animated Mat.": + collection = props.entries + elif collectionType == "Animated Mat. Color": + collection = props.entries[collection_index].color_params.keyframes + elif collectionType == "Animated Mat. Cycle (Index)": + collection = props.entries[collection_index].tex_cycle_params.keyframes + elif collectionType == "Animated Mat. Cycle (Texture)": + collection = props.entries[collection_index].tex_cycle_params.textures elif collectionType == "Curve": collection = obj.ootSplineProperty.headerSettings.cutsceneHeaders elif collectionType.startswith("CSHdr."): @@ -115,7 +321,16 @@ def getCollection(objName, collectionType, subIndex): return collection -def drawAddButton(layout, index, collectionType, subIndex, objName): +def drawAddButton( + layout, + index, + collectionType, + subIndex, + objName, + collection_index: int = 0, + ask_for_copy: bool = False, + ask_for_amount: bool = False, +): if subIndex is None: subIndex = 0 addOp = layout.operator(OOTCollectionAdd.bl_idname) @@ -123,9 +338,67 @@ def drawAddButton(layout, index, collectionType, subIndex, objName): addOp.collectionType = collectionType addOp.subIndex = subIndex addOp.objName = objName - - -def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd=True, compact=False): + addOp.collection_index = collection_index + addOp.ask_for_copy = ask_for_copy + addOp.ask_for_amount = ask_for_amount + + +def draw_clear_button( + layout: UILayout, collection_type: str, sub_index: Optional[int], obj_name: str, collection_index: int = 0 +): + if sub_index is None: + sub_index = 0 + copy_op: OOTCollectionClear = layout.operator(OOTCollectionClear.bl_idname) + copy_op.collection_type = collection_type + copy_op.sub_index = sub_index + copy_op.obj_name = obj_name + copy_op.collection_index = collection_index + + +def draw_copy_button( + layout: UILayout, collection_type: str, sub_index: Optional[int], obj_name: str, collection_index: int = 0 +): + if sub_index is None: + sub_index = 0 + copy_op: OOTCollectionCopy = layout.operator(OOTCollectionCopy.bl_idname) + copy_op.collection_type = collection_type + copy_op.sub_index = sub_index + copy_op.obj_name = obj_name + copy_op.collection_index = collection_index + + +def draw_utility_ops( + layout: bpy.types.UILayout, + index: int, + collection_type: str, + header_index: Optional[int], + obj_name: str, + collection_index: int = 0, + do_copy: bool = True, + ask_for_copy: bool = False, + ask_for_amount: bool = False, +): + drawAddButton( + layout, index, collection_type, header_index, obj_name, collection_index, ask_for_copy, ask_for_amount + ) + draw_clear_button(layout, collection_type, header_index, obj_name, collection_index) + + if do_copy: + draw_copy_button(layout, collection_type, header_index, obj_name, collection_index) + + +def drawCollectionOps( + layout, + index, + collectionType, + subIndex, + objName, + allowAdd=True, + compact=False, + collection_index: int = 0, + ask_for_copy: bool = False, + ask_for_amount: bool = False, +): if subIndex is None: subIndex = 0 @@ -140,12 +413,17 @@ def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd addOp.collectionType = collectionType addOp.subIndex = subIndex addOp.objName = objName + addOp.collection_index = collection_index + addOp.ask_for_copy = ask_for_copy + addOp.ask_for_amount = ask_for_amount removeOp = buttons.operator(OOTCollectionRemove.bl_idname, text="Delete" if not compact else "", icon="REMOVE") removeOp.option = index removeOp.collectionType = collectionType removeOp.subIndex = subIndex removeOp.objName = objName + removeOp.collection_index = collection_index + removeOp.ask_for_amount = ask_for_amount moveUp = buttons.operator(OOTCollectionMove.bl_idname, text="Up" if not compact else "", icon="TRIA_UP") moveUp.option = index @@ -153,6 +431,7 @@ def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd moveUp.collectionType = collectionType moveUp.subIndex = subIndex moveUp.objName = objName + moveUp.collection_index = collection_index moveDown = buttons.operator(OOTCollectionMove.bl_idname, text="Down" if not compact else "", icon="TRIA_DOWN") moveDown.option = index @@ -160,12 +439,15 @@ def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd moveDown.collectionType = collectionType moveDown.subIndex = subIndex moveDown.objName = objName + moveDown.collection_index = collection_index collections_classes = ( OOTCollectionAdd, OOTCollectionRemove, OOTCollectionMove, + OOTCollectionClear, + OOTCollectionCopy, ) diff --git a/fast64_internal/z64/constants.py b/fast64_internal/z64/constants.py index 6ed83eb2e..d12ef01ac 100644 --- a/fast64_internal/z64/constants.py +++ b/fast64_internal/z64/constants.py @@ -443,67 +443,6 @@ # ("0xFF", "0xFF", "0xFF"), ] -ootEnumDrawConfig = [ - ("Custom", "Custom", "Custom"), - ("SDC_DEFAULT", "Default", "Default"), - ("SDC_HYRULE_FIELD", "Hyrule Field (Spot00)", "Spot00"), - ("SDC_KAKARIKO_VILLAGE", "Kakariko Village (Spot01)", "Spot01"), - ("SDC_ZORAS_RIVER", "Zora's River (Spot03)", "Spot03"), - ("SDC_KOKIRI_FOREST", "Kokiri Forest (Spot04)", "Spot04"), - ("SDC_LAKE_HYLIA", "Lake Hylia (Spot06)", "Spot06"), - ("SDC_ZORAS_DOMAIN", "Zora's Domain (Spot07)", "Spot07"), - ("SDC_ZORAS_FOUNTAIN", "Zora's Fountain (Spot08)", "Spot08"), - ("SDC_GERUDO_VALLEY", "Gerudo Valley (Spot09)", "Spot09"), - ("SDC_LOST_WOODS", "Lost Woods (Spot10)", "Spot10"), - ("SDC_DESERT_COLOSSUS", "Desert Colossus (Spot11)", "Spot11"), - ("SDC_GERUDOS_FORTRESS", "Gerudo's Fortress (Spot12)", "Spot12"), - ("SDC_HAUNTED_WASTELAND", "Haunted Wasteland (Spot13)", "Spot13"), - ("SDC_HYRULE_CASTLE", "Hyrule Castle (Spot15)", "Spot15"), - ("SDC_DEATH_MOUNTAIN_TRAIL", "Death Mountain Trail (Spot16)", "Spot16"), - ("SDC_DEATH_MOUNTAIN_CRATER", "Death Mountain Crater (Spot17)", "Spot17"), - ("SDC_GORON_CITY", "Goron City (Spot18)", "Spot18"), - ("SDC_LON_LON_RANCH", "Lon Lon Ranch (Spot20)", "Spot20"), - ("SDC_FIRE_TEMPLE", "Fire Temple (Hidan)", "Hidan"), - ("SDC_DEKU_TREE", "Inside the Deku Tree (Ydan)", "Ydan"), - ("SDC_DODONGOS_CAVERN", "Dodongo's Cavern (Ddan)", "Ddan"), - ("SDC_JABU_JABU", "Inside Jabu Jabu's Belly (Bdan)", "Bdan"), - ("SDC_FOREST_TEMPLE", "Forest Temple (Bmori1)", "Bmori1"), - ("SDC_WATER_TEMPLE", "Water Temple (Mizusin)", "Mizusin"), - ("SDC_SHADOW_TEMPLE_AND_WELL", "Shadow Temple (Hakadan)", "Hakadan"), - ("SDC_SPIRIT_TEMPLE", "Spirit Temple (Jyasinzou)", "Jyasinzou"), - ("SDC_INSIDE_GANONS_CASTLE", "Inside Ganon's Castle (Ganontika)", "Ganontika"), - ("SDC_GERUDO_TRAINING_GROUND", "Gerudo Training Ground (Men)", "Men"), - ("SDC_DEKU_TREE_BOSS", "Gohma's Lair (Ydan Boss)", "Ydan Boss"), - ("SDC_WATER_TEMPLE_BOSS", "Morpha's Lair (Mizusin Bs)", "Mizusin Bs"), - ("SDC_TEMPLE_OF_TIME", "Temple of Time (Tokinoma)", "Tokinoma"), - ("SDC_GROTTOS", "Grottos (Kakusiana)", "Kakusiana"), - ("SDC_CHAMBER_OF_THE_SAGES", "Chamber of the Sages (Kenjyanoma)", "Kenjyanoma"), - ("SDC_GREAT_FAIRYS_FOUNTAIN", "Great Fairy Fountain", "Great Fairy Fountain"), - ("SDC_SHOOTING_GALLERY", "Shooting Gallery (Syatekijyou)", "Syatekijyou"), - ("SDC_CASTLE_COURTYARD_GUARDS", "Castle Hedge Maze (Day) (Hairal Niwa)", "Hairal Niwa"), - ("SDC_OUTSIDE_GANONS_CASTLE", "Ganon's Castle Exterior (Ganon Tou)", "Ganon Tou"), - ("SDC_ICE_CAVERN", "Ice Cavern (Ice Doukuto)", "Ice Doukuto"), - ( - "SDC_GANONS_TOWER_COLLAPSE_EXTERIOR", - "Ganondorf's Death Scene (Tower Escape Exterior) (Ganon Final)", - "Ganon Final", - ), - ("SDC_FAIRYS_FOUNTAIN", "Fairy Fountain", "Fairy Fountain"), - ("SDC_THIEVES_HIDEOUT", "Thieves' Hideout (Gerudoway)", "Gerudoway"), - ("SDC_BOMBCHU_BOWLING_ALLEY", "Bombchu Bowling Alley (Bowling)", "Bowling"), - ("SDC_ROYAL_FAMILYS_TOMB", "Royal Family's Tomb (Hakaana Ouke)", "Hakaana Ouke"), - ("SDC_LAKESIDE_LABORATORY", "Lakeside Laboratory (Hylia Labo)", "Hylia Labo"), - ("SDC_LON_LON_BUILDINGS", "Lon Lon Ranch House & Tower (Souko)", "Souko"), - ("SDC_MARKET_GUARD_HOUSE", "Guard House (Miharigoya)", "Miharigoya"), - ("SDC_POTION_SHOP_GRANNY", "Granny's Potion Shop (Mahouya)", "Mahouya"), - ("SDC_CALM_WATER", "Calm Water", "Calm Water"), - ("SDC_GRAVE_EXIT_LIGHT_SHINING", "Grave Exit Light Shining", "Grave Exit Light Shining"), - ("SDC_BESITU", "Ganondorf Test Room (Besitu)", "Besitu"), - ("SDC_FISHING_POND", "Fishing Pond (Turibori)", "Turibori"), - ("SDC_GANONS_TOWER_COLLAPSE_INTERIOR", "Ganon's Tower (Collapsing) (Ganon Sonogo)", "Ganon Sonogo"), - ("SDC_INSIDE_GANONS_CASTLE_COLLAPSE", "Inside Ganon's Castle (Collapsing) (Ganontika Sonogo)", "Ganontika Sonogo"), -] - oot_world_defaults = { "geometryMode": { "zBuffer": True, diff --git a/fast64_internal/z64/cutscene/classes.py b/fast64_internal/z64/cutscene/classes.py index ffb6cbc90..9f2329a85 100644 --- a/fast64_internal/z64/cutscene/classes.py +++ b/fast64_internal/z64/cutscene/classes.py @@ -501,13 +501,13 @@ class CutsceneObjectFactory: """This class contains functions to create new Blender objects""" def getNewEmptyObject(self, name: str, selectObject: bool, parentObj: Object): - return get_new_object(name, None, selectObject, parentObj) + return get_new_object(name, None, selectObject, parent=parentObj) def getNewArmatureObject(self, name: str, selectObject: bool, parentObj: Object): newArmatureData = bpy.data.armatures.new(name) newArmatureData.display_type = "STICK" newArmatureData.show_names = True - newArmatureObject = get_new_object(name, newArmatureData, selectObject, parentObj) + newArmatureObject = get_new_object(name, newArmatureData, selectObject, parent=parentObj) return newArmatureObject def getNewCutsceneObject(self, name: str, frameCount: int, parentObj: Object): @@ -585,7 +585,7 @@ def getNewCameraObject( self, name: str, displaySize: float, clipStart: float, clipEnd: float, alpha: float, parentObj: Object ): newCamera = bpy.data.cameras.new(name) - newCameraObj = get_new_object(name, newCamera, False, parentObj) + newCameraObj = get_new_object(name, newCamera, False, parent=parentObj) newCameraObj.data.display_size = displaySize newCameraObj.data.clip_start = clipStart newCameraObj.data.clip_end = clipEnd diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index dc9c1362c..9c6afcf6f 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -4,18 +4,86 @@ from mathutils import Matrix from bpy.types import Object from typing import Optional + +from ....game_data import game_data from ....utility import PluginError, CData, indent from ....f3d.f3d_gbi import TextureExportSettings, ScrollMethod from ...scene.properties import OOTSceneHeaderProperty from ...model_classes import OOTModel, OOTGfxFormatter -from ...utility import ExportInfo +from ...utility import ExportInfo, is_hackeroot from ..file import SceneFile from ..utility import Utility, altHeaderList from ..collision import CollisionHeader +from .animated_mats import SceneAnimatedMaterial from .header import SceneAlternateHeader, SceneHeader from .rooms import RoomEntries +def get_anm_mat_target_name(scene_obj: Object, alt_prop: OOTSceneHeaderProperty, name: str, header_index: int): + """ + This function tries to find the name of the animated material array we want to use. + It can return either a string, if using another header's data, or None, in which case we will export for the current header. + """ + + animated_materials = None + + # start by checking if we should try to find the right header's name + if alt_prop.reuse_anim_mat: + # little map for convenience + header_map = { + "Child Night": ("childNightHeader", 1), + "Adult Day": ("adultDayHeader", 2), + "Adult Night": ("adultNightHeader", 3), + } + + # initial values + target_prop = alt_prop + index = None + + # search as long as we don't find an entry + while target_prop.reuse_anim_mat: + # if it's not none it means this at least the second iteration + if index is not None: + # infinite loops can happen if you set header A to reuse header B and header B to reuse header A + assert ( + target_prop.internal_anim_mat_header != alt_prop.internal_anim_mat_header + ), f"infinite loop in {repr(scene_obj.name)}'s Animated Materials" + + if target_prop.internal_anim_mat_header == "Child Day": + target_prop = scene_obj.ootSceneHeader + index = 0 + elif target_prop.internal_anim_mat_header == "Cutscene": + index = cs_index = alt_prop.reuse_anim_mat_cs_index + assert ( + cs_index >= game_data.z64.cs_index_start and cs_index != header_index + ), "invalid cutscene index (Animated Material)" + + cs_index -= game_data.z64.cs_index_start + assert cs_index < len( + scene_obj.ootAlternateSceneHeaders.cutsceneHeaders + ), f"CS Header No. {index} don't exist (Animated Material)" + + target_prop = scene_obj.ootAlternateSceneHeaders.cutsceneHeaders[cs_index] + else: + prop_name, index = header_map[target_prop.internal_anim_mat_header] + target_prop = getattr(scene_obj.ootAlternateSceneHeaders, prop_name) + + if target_prop.usePreviousHeader: + index -= 1 + + if prop_name == "childNightHeader": + target_prop = scene_obj.ootSceneHeader + elif prop_name == "adultDayHeader": + target_prop = scene_obj.ootAlternateSceneHeaders.childNightHeader + elif prop_name == "adultNightHeader": + target_prop = scene_obj.ootAlternateSceneHeaders.adultDayHeader + + assert index is not None + animated_materials = f"{name}_header{index:02}_AnimatedMaterial" + + return animated_materials + + @dataclass class Scene: """This class defines a scene""" @@ -38,58 +106,98 @@ def new( model: OOTModel, ): i = 0 - rooms = RoomEntries.new( - f"{name}_roomList", - name.removesuffix("_scene"), - model, - original_scene_obj, - sceneObj, - transform, - exportInfo, - ) - - colHeader = CollisionHeader.new( - f"{name}_collisionHeader", - name, - sceneObj, - transform, - exportInfo.useMacros, - True, + use_mat_anim = ( + "mat_anim" in sceneObj.ootSceneHeader.sceneTableEntry.drawConfig + or bpy.context.scene.ootSceneExportSettings.customExport ) try: mainHeader = SceneHeader.new( - f"{name}_header{i:02}", sceneObj.ootSceneHeader, sceneObj, transform, i, exportInfo.useMacros + f"{name}_header{i:02}", + sceneObj.ootSceneHeader, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + None, ) + + if mainHeader.infos is not None: + model.draw_config = mainHeader.infos.drawConfig except Exception as exc: raise PluginError(f"In main scene header: {exc}") from exc + hasAlternateHeaders = False altHeader = SceneAlternateHeader(f"{name}_alternateHeaders") altProp = sceneObj.ootAlternateSceneHeaders for i, header in enumerate(altHeaderList, 1): altP: OOTSceneHeaderProperty = getattr(altProp, f"{header}Header") + if altP.usePreviousHeader: continue + try: + target_name = get_anm_mat_target_name(sceneObj, altP, name, i) + setattr( altHeader, header, - SceneHeader.new(f"{name}_header{i:02}", altP, sceneObj, transform, i, exportInfo.useMacros), + SceneHeader.new( + f"{name}_header{i:02}", + altP, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + target_name, + ), ) hasAlternateHeaders = True except Exception as exc: raise PluginError(f"In alternate scene header {header}: {exc}") from exc altHeader.cutscenes = [] - for i, csHeader in enumerate(altProp.cutsceneHeaders, 4): + for i, csHeader in enumerate(altProp.cutsceneHeaders, game_data.z64.cs_index_start): try: + target_name = get_anm_mat_target_name(sceneObj, csHeader, name, i) + altHeader.cutscenes.append( - SceneHeader.new(f"{name}_header{i:02}", csHeader, sceneObj, transform, i, exportInfo.useMacros) + SceneHeader.new( + f"{name}_header{i:02}", + csHeader, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + target_name, + ) ) except Exception as exc: raise PluginError(f"In alternate, cutscene header {i}: {exc}") from exc + rooms = RoomEntries.new( + f"{name}_roomList", + name.removesuffix("_scene"), + model, + original_scene_obj, + sceneObj, + transform, + exportInfo, + ) + + colHeader = CollisionHeader.new( + f"{name}_collisionHeader", + name, + sceneObj, + transform, + exportInfo.useMacros, + True, + ) + hasAlternateHeaders = True if len(altHeader.cutscenes) > 0 else hasAlternateHeaders altHeader = altHeader if hasAlternateHeaders else None return Scene(name, model, mainHeader, altHeader, rooms, colHeader, hasAlternateHeaders) @@ -121,7 +229,7 @@ def getSceneHeaderFromIndex(self, headerIndex: int) -> SceneHeader | None: if headerIndex == i: return getattr(self.altHeader, header) - for i, csHeader in enumerate(self.altHeader.cutscenes, 4): + for i, csHeader in enumerate(self.altHeader.cutscenes, game_data.z64.cs_index_start): if headerIndex == i: return csHeader @@ -150,6 +258,7 @@ def getCmdList(self, curHeader: SceneHeader, hasAltHeaders: bool): + curHeader.entranceActors.getCmd() + (curHeader.exits.getCmd() if len(curHeader.exits.exitList) > 0 else "") + (curHeader.cutscene.getCmd() if len(curHeader.cutscene.entries) > 0 else "") + + (curHeader.anim_mat.get_cmd() if curHeader.anim_mat is not None else "") + Utility.getEndCmd() + "};\n\n" ) @@ -174,7 +283,11 @@ def getSceneMainC(self): headers.append((csHeader, f"Cutscene No. {i + 1}")) altHeaderPtrs = "\n".join( - indent + curHeader.name + "," if curHeader is not None else indent + "NULL," if i < 4 else "" + indent + curHeader.name + "," + if curHeader is not None + else indent + "NULL," + if i < game_data.z64.cs_index_start + else "" for i, (curHeader, _) in enumerate(headers, 1) ) @@ -285,6 +398,9 @@ def getNewSceneFile(self, path: str, isSingleFile: bool, textureExportSettings: '#include "scene.h"', ] + if is_hackeroot(): + includes.append('#include "animated_materials.h"') + backwards_compatibility = [ "// For older decomp versions", "#ifndef SCENE_CMD_PLAYER_ENTRY_LIST", @@ -316,6 +432,7 @@ def getNewSceneFile(self, path: str, isSingleFile: bool, textureExportSettings: + f"#define {self.name.upper()}_H\n\n" + ("\n".join(includes) + "\n\n") + "\n".join(backwards_compatibility) + + (SceneAnimatedMaterial.mat_seg_num_macro if "AnimatedMaterial" in sceneMainData.header else "") + sceneMainData.header + "".join(cs.header for cs in sceneCutsceneData) + sceneCollisionData.header diff --git a/fast64_internal/z64/exporter/scene/animated_mats.py b/fast64_internal/z64/exporter/scene/animated_mats.py new file mode 100644 index 000000000..452ab1952 --- /dev/null +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -0,0 +1,486 @@ +import bpy +import re + +from dataclasses import dataclass +from bpy.types import Object +from typing import Optional +from pathlib import Path + +from ....utility import CData, PluginError, exportColor, scaleToU8, toAlnum, get_new_empty_object, indent +from ...utility import getObjectList, is_hackeroot +from ...scene.properties import OOTSceneHeaderProperty +from ...animated_mats.properties import ( + Z64_AnimatedMatColorParams, + Z64_AnimatedMatTexScrollParams, + Z64_AnimatedMatTexCycleParams, + Z64_AnimatedMaterial, + Z64_AnimatedMaterialExportSettings, + Z64_AnimatedMaterialImportSettings, +) + +from ...importer.scene_header import parse_animated_material + + +class AnimatedMatColorParams: + def __init__( + self, + props: Z64_AnimatedMatColorParams, + segment_num: int, + type_num: int, + base_name: str, + index: int, + type: str, + suffix: str = "", + ): + is_draw_color = type == "color" + self.segment_num = segment_num + self.type_num = type_num + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}ColorParams{self.header_suffix}" + self.frame_length = len(props.keyframes) if is_draw_color else props.keyframe_length + self.prim_colors: list[tuple[int, int, int, int, int]] = [] + self.env_colors: list[tuple[int, int, int, int]] = [] + self.frames: list[int] = [] + + for keyframe in props.keyframes: + prim = exportColor(keyframe.prim_color[0:3]) + [scaleToU8(keyframe.prim_color[3])] + self.prim_colors.append((prim[0], prim[1], prim[2], prim[3], keyframe.prim_lod_frac)) + + if not is_draw_color or props.use_env_color: + self.env_colors.append(tuple(exportColor(keyframe.env_color[0:3]) + [scaleToU8(keyframe.env_color[3])])) + + if not is_draw_color: + self.frames.append(keyframe.frame_num) + + if not is_draw_color and keyframe.frame_num > self.frame_length: + raise PluginError("ERROR: the frame number cannot be higher than the total frame count!") + + self.frame_count = len(self.frames) + + if not is_draw_color: + assert len(self.frames) == len(self.prim_colors) == len(self.env_colors) + + if is_draw_color and props.use_env_color: + assert len(self.prim_colors) == len(self.env_colors) + + def to_c(self, all_externs: bool = True): + data = CData() + prim_array_name = f"{self.base_name}ColorPrimColor{self.header_suffix}" + env_array_name = f"{self.base_name}ColorEnvColor{self.header_suffix}" + frames_array_name = f"{self.base_name}ColorKeyFrames{self.header_suffix}" + params_name = f"AnimatedMatColorParams {self.name}" + + if len(self.env_colors) == 0: + env_array_name = "NULL" + + if len(self.frames) == 0: + frames_array_name = "NULL" + + # .h + if all_externs: + data.header = ( + f"extern F3DPrimColor {prim_array_name}[];\n" + + (f"extern F3DEnvColor {env_array_name}[];\n" if len(self.env_colors) > 0 else "") + + (f"extern u16 {frames_array_name}[];\n" if len(self.frames) > 0 else "") + + f"extern {params_name};\n" + ) + + # .c + data.source = ( + ( + (f"F3DPrimColor {prim_array_name}[]" + " = {\n" + indent) + + f",\n{indent}".join( + "{ " + f"{entry[0]}, {entry[1]}, {entry[2]}, {entry[3]}, {entry[4]}" + " }" + for entry in self.prim_colors + ) + + "\n};\n\n" + ) + + ( + ( + (f"F3DEnvColor {env_array_name}[]" + " = {\n" + indent) + + f",\n{indent}".join( + "{ " + f"{entry[0]}, {entry[1]}, {entry[2]}, {entry[3]}" + " }" for entry in self.env_colors + ) + + "\n};\n\n" + ) + if len(self.env_colors) > 0 + else "" + ) + + ( + ( + (f"u16 {frames_array_name}[]" + " = {\n" + indent) + + f",\n{indent}".join(f"{entry}" for entry in self.frames) + + "\n};\n\n" + ) + if len(self.frames) > 0 + else "" + ) + + ( + (params_name + " = {\n") + + (indent + f"{self.frame_length},\n") + + (indent + f"{self.frame_count},\n") + + (indent + f"{prim_array_name},\n") + + (indent + f"{env_array_name},\n") + + (indent + f"{frames_array_name},\n") + + "};\n\n" + ) + ) + + return data + + +class AnimatedMatTexScrollParams: + def __init__( + self, + props: Z64_AnimatedMatTexScrollParams, + segment_num: int, + type_num: int, + base_name: str, + index: int, + type: str, + suffix: str = "", + ): + self.segment_num = segment_num + self.type_num = type_num + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.texture_1 = ( + "{ " + + f"{props.texture_1.step_x}, {props.texture_1.step_y}, {props.texture_1.width}, {props.texture_1.height}" + + " }," + ) + self.texture_2: Optional[str] = None + + if type == "two_tex_scroll": + self.name = f"{self.base_name}{suffix}TwoTexScrollParams{self.header_suffix}" + self.texture_2 = ( + "{ " + + f"{props.texture_2.step_x}, {props.texture_2.step_y}, {props.texture_2.width}, {props.texture_2.height}" + + " }," + ) + else: + self.name = f"{self.base_name}{suffix}TexScrollParams{self.header_suffix}" + + def to_c(self, all_externs: bool = True): + data = CData() + params_name = f"AnimatedMatTexScrollParams {self.name}[]" + + # .h + if all_externs: + data.header = f"extern {params_name};\n" + + # .c + data.source = f"{params_name}" + " = {\n" + indent + self.texture_1 + + if self.texture_2 is not None: + data.source += "\n" + indent + self.texture_2 + + data.source += "\n};\n\n" + + return data + + +class AnimatedMatTexCycleParams: + def __init__( + self, + props: Z64_AnimatedMatTexCycleParams, + segment_num: int, + type_num: int, + base_name: str, + index: int, + type: str, + suffix: str = "", + ): + self.segment_num = segment_num + self.type_num = type_num + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}TexCycleParams{self.header_suffix}" + self.textures: list[str] = [] + self.texture_indices: list[int] = [] + + for texture in props.textures: + self.textures.append(texture.symbol) + + for keyframe in props.keyframes: + assert keyframe.texture_index < len(self.textures), "ERROR: invalid AnimatedMatTexCycle texture index" + self.texture_indices.append(keyframe.texture_index) + + self.frame_length = len(self.texture_indices) + assert len(self.textures) > 0, "you need at least one texture symbol (Animated Material)" + assert len(self.texture_indices) > 0, "you need at least one texture index (Animated Material)" + + def to_c(self, all_externs: bool = True): + data = CData() + texture_array_name = f"{self.base_name}CycleTextures{self.header_suffix}" + texture_indices_array_name = f"{self.base_name}CycleTextureIndices{self.header_suffix}" + params_name = f"AnimatedMatTexCycleParams {self.name}" + + # .h + if all_externs: + data.header = ( + f"extern TexturePtr {texture_array_name}[];\n" + + f"extern u8 {texture_indices_array_name}[];\n" + + f"extern {params_name};\n" + ) + + # .c + data.source = ( + ( + (f"TexturePtr {texture_array_name}[]" + " = {\n") + + indent + + f",\n{indent}".join(texture for texture in self.textures) + + "\n};\n\n" + ) + + ( + (f"u8 {texture_indices_array_name}[]" + " = {\n") + + indent + + ", ".join(f"{index}" for index in self.texture_indices) + + "\n};\n\n" + ) + + ( + (params_name + " = {\n") + + indent + + f"{self.frame_length}, {texture_array_name}, {texture_indices_array_name}," + + "\n};\n\n" + ) + ) + + return data + + +class AnimatedMaterial: + def __init__(self, props: Z64_AnimatedMaterial, base_name: str, suffix: str = ""): + self.name = base_name + self.entries: list[AnimatedMatColorParams | AnimatedMatTexScrollParams | AnimatedMatTexCycleParams] = [] + + if len(props.entries) == 0: + return + + type_list_map: dict[ + str, tuple[AnimatedMatColorParams | AnimatedMatTexScrollParams | AnimatedMatTexCycleParams, str, int] + ] = { + "tex_scroll": (AnimatedMatTexScrollParams, "tex_scroll_params", 0), + "two_tex_scroll": (AnimatedMatTexScrollParams, "tex_scroll_params", 1), + "color": (AnimatedMatColorParams, "color_params", 2), + "color_lerp": (AnimatedMatColorParams, "color_params", 3), + "color_nonlinear_interp": (AnimatedMatColorParams, "color_params", 4), + "tex_cycle": (AnimatedMatTexCycleParams, "tex_cycle_params", 5), + } + + for i, item in enumerate(props.entries): + type = item.type if item.type != "Custom" else item.typeCustom + if type != "Custom": + class_def, prop_name, type_num = type_list_map[type] + self.entries.append( + class_def(getattr(item, prop_name), item.segment_num, type_num, base_name, i, type, suffix) + ) + + def to_c(self, all_externs: bool = True): + data = CData() + + for entry in self.entries: + data.append(entry.to_c(all_externs)) + + array_name = f"AnimatedMaterial {self.name}[]" + + # .h + data.header += f"extern {array_name};\n" + + # .c + data.source += array_name + " = {\n" + indent + + if len(self.entries) > 0: + entries = [ + f"MATERIAL_SEGMENT_NUM({entry.segment_num}), " + + f"{entry.type_num}, " + + f"{'&' if entry.type_num in {2, 3, 4, 5} else ''}{entry.name}" + for entry in self.entries + ] + + # the last entry's segment need to be negative + if len(self.entries) > 0 and self.entries[-1].segment_num > 0: + entries[-1] = f"LAST_{entries[-1]}" + + data.source += f",\n{indent}".join("{ " + entry + " }" for entry in entries) + else: + data.source += "{ 0, 6, NULL }" + + data.source += "\n};\n" + return data + + +@dataclass +class SceneAnimatedMaterial: + """This class hosts Animated Materials data for scenes""" + + name: str + animated_material: Optional[AnimatedMaterial] + + # add a macro for the segment number for convenience (only if using animated materials) + mat_seg_num_macro = "\n".join( + [ + "// Animated Materials requires the segment number to be offset by 7", + "#ifndef MATERIAL_SEGMENT_NUM", + "#define MATERIAL_SEGMENT_NUM(n) ((n) - 7)", + "#endif\n", + "// The last entry also requires to be a negative number", + "#ifndef LAST_MATERIAL_SEGMENT_NUM", + "#define LAST_MATERIAL_SEGMENT_NUM(n) -MATERIAL_SEGMENT_NUM(n)", + "#endif\n\n", + ] + ) + + @staticmethod + def new(name: str, props: OOTSceneHeaderProperty, is_reuse: bool): + return SceneAnimatedMaterial(name, AnimatedMaterial(props.animated_material, name) if not is_reuse else None) + + @staticmethod + def export(): + """Exports animated materials data as C files, this should be called to do a separate export from the scene.""" + + settings: Z64_AnimatedMaterialExportSettings = bpy.context.scene.fast64.oot.anim_mats_export_settings + export_obj: Object = settings.export_obj + name = toAlnum(export_obj.name) + assert name is not None + + # convert props + entries: list[AnimatedMaterial] = [ + AnimatedMaterial(item, f"{name}_AnimatedMaterial_{i:02}", "_") + for i, item in enumerate(export_obj.fast64.oot.animated_materials.items) + ] + assert len(entries) > 0, "The Animated Material list is empty!" + + filename = f"{name.lower()}_anim_mats" + + # create C data + data = CData() + data.header += f'#include "{settings.get_include_name()}"\n' + + if is_hackeroot(): + data.header += '#include "config.h"\n\n' + + if bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + data.header += "#if ENABLE_ANIMATED_MATERIALS\n\n" + else: + data.header += "\n" + + if not settings.is_custom_path: + data.source += f'#include "assets/objects/{settings.object_name}/{filename}.h"\n\n' + + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + data.source += "#if ENABLE_ANIMATED_MATERIALS\n\n" + + data.header += SceneAnimatedMaterial.mat_seg_num_macro + + for entry in entries: + c_data = entry.to_c(False) + c_data.source += "\n" + data.append(c_data) + + if is_hackeroot(): + if not settings.is_custom_path: + data.header += "\n" + else: + data.source = data.source[:-1] + + extra = "" + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + extra = "#endif\n" + + data.source += extra + + if not settings.is_custom_path: + data.header += extra + + # write C data + if settings.is_custom_path: + export_path = Path(settings.export_path) + export_path.mkdir(exist_ok=True) + else: + export_path = Path(bpy.context.scene.ootDecompPath) / "assets" / "objects" / settings.object_name + + export_path = export_path.resolve() + assert export_path.exists(), f"This path doesn't exist: {repr(export_path)}" + + if settings.is_custom_path: + c_path = export_path / f"{filename}.inc.c" + c_path.write_text(data.header + "\n" + data.source) + else: + h_path = export_path / f"{filename}.h" + h_path.write_text(data.header) + + c_path = export_path / f"{filename}.c" + c_path.write_text(data.source) + + @staticmethod + def from_data(): + """Imports animated materials data from C files, this should be called to do a separate import from the scene.""" + + settings: Z64_AnimatedMaterialImportSettings = bpy.context.scene.fast64.oot.anim_mats_import_settings + import_path = Path(settings.import_path).resolve() + + file_data = import_path.read_text() + array_names = [ + match.group(1) + for match in re.finditer(r"AnimatedMaterial\s([a-zA-Z0-9_]*)\[\]\s=\s\{", file_data, re.DOTALL) + ] + + new_obj = get_new_empty_object("Actor Animated Materials") + new_obj.ootEmptyType = "Animated Materials" + + for array_name in array_names: + parse_animated_material(new_obj.fast64.oot.animated_materials.items.add(), file_data, array_name) + + def get_cmd(self): + """Returns the animated material scene command""" + + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + return ( + "#if ENABLE_ANIMATED_MATERIALS\n" + + indent + + f"SCENE_CMD_ANIMATED_MATERIAL_LIST({self.name}),\n" + + "#endif\n" + ) + else: + return indent + f"SCENE_CMD_ANIMATED_MATERIAL_LIST({self.name}),\n" + + def to_c(self, is_scene: bool = True): + data = CData() + + if self.animated_material is not None: + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + data.source += "#if ENABLE_ANIMATED_MATERIALS\n" + data.header += "#if ENABLE_ANIMATED_MATERIALS\n" + + data.append(self.animated_material.to_c()) + + extra = "" + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + extra = "#endif\n" + + data.source += extra + "\n" + data.header += ("\n" if not is_scene else "") + extra + + return data + + +@dataclass +class ActorAnimatedMaterial: + """This class hosts Animated Materials data for actors""" + + name: str + entries: list[AnimatedMaterial] + + @staticmethod + def new(name: str, scene_obj: Object, header_index: int): + obj_list = getObjectList(scene_obj.children_recursive, "EMPTY", "Animated Materials") + entries: list[AnimatedMaterial] = [] + + for obj in obj_list: + entries.extend( + [AnimatedMaterial(item, name, header_index) for item in obj.fast64.oot.animated_materials.items] + ) + + return ActorAnimatedMaterial(name, entries) diff --git a/fast64_internal/z64/exporter/scene/general.py b/fast64_internal/z64/exporter/scene/general.py index a01a83aff..768d1ef41 100644 --- a/fast64_internal/z64/exporter/scene/general.py +++ b/fast64_internal/z64/exporter/scene/general.py @@ -47,7 +47,7 @@ def from_data(raw_data: str, not_zapd_assets: bool): blend_rate *= 4 else: blend_and_fogs = entry.split("},")[-1].split(",") - blend_split = blend_and_fogs[0].split("|") + blend_split = blend_and_fogs[0].removeprefix("(").removesuffix(")").split("|") blend_raw = blend_split[0] fog_near = hexOrDecInt(blend_split[1]) z_far = hexOrDecInt(blend_and_fogs[1]) diff --git a/fast64_internal/z64/exporter/scene/header.py b/fast64_internal/z64/exporter/scene/header.py index 72bc8d32f..21b03e4d5 100644 --- a/fast64_internal/z64/exporter/scene/header.py +++ b/fast64_internal/z64/exporter/scene/header.py @@ -2,12 +2,15 @@ from typing import Optional from mathutils import Matrix from bpy.types import Object + from ....utility import CData +from ...utility import is_oot_features from ...scene.properties import OOTSceneHeaderProperty from ..cutscene import SceneCutscene from .general import SceneLighting, SceneInfos, SceneExits from .actors import SceneTransitionActors, SceneEntranceActors, SceneSpawns from .pathways import ScenePathways +from .animated_mats import SceneAnimatedMaterial @dataclass @@ -24,11 +27,27 @@ class SceneHeader: spawns: Optional[SceneSpawns] path: Optional[ScenePathways] + # MM (or modded OoT) + anim_mat: Optional[SceneAnimatedMaterial] + @staticmethod def new( - name: str, props: OOTSceneHeaderProperty, sceneObj: Object, transform: Matrix, headerIndex: int, useMacros: bool + name: str, + props: OOTSceneHeaderProperty, + sceneObj: Object, + transform: Matrix, + headerIndex: int, + useMacros: bool, + use_mat_anim: bool, + target_name: Optional[str], ): entranceActors = SceneEntranceActors.new(f"{name}_playerEntryList", sceneObj, transform, headerIndex) + + animated_materials = None + if use_mat_anim and not is_oot_features(): + final_name = target_name if target_name is not None else f"{name}_AnimatedMaterial" + animated_materials = SceneAnimatedMaterial.new(final_name, props, target_name is not None) + return SceneHeader( name, SceneInfos.new(props, sceneObj), @@ -39,6 +58,7 @@ def new( entranceActors, SceneSpawns(f"{name}_entranceList", entranceActors.entries), ScenePathways.new(f"{name}_pathway", sceneObj, transform, headerIndex), + animated_materials, ) def getC(self): @@ -67,6 +87,10 @@ def getC(self): if len(self.path.pathList) > 0: headerData.append(self.path.getC()) + # Write the animated material list, if used + if self.anim_mat is not None: + headerData.append(self.anim_mat.to_c()) + return headerData diff --git a/fast64_internal/z64/exporter/scene/rooms.py b/fast64_internal/z64/exporter/scene/rooms.py index e6d1e3c0d..02a75d7f4 100644 --- a/fast64_internal/z64/exporter/scene/rooms.py +++ b/fast64_internal/z64/exporter/scene/rooms.py @@ -55,6 +55,7 @@ def new( f"{roomName}_dl", model.DLFormat, None, + model.draw_config, ) ), roomIndex, diff --git a/fast64_internal/z64/f3d/properties.py b/fast64_internal/z64/f3d/properties.py index 4f29b0e8b..e57fa50e3 100644 --- a/fast64_internal/z64/f3d/properties.py +++ b/fast64_internal/z64/f3d/properties.py @@ -5,6 +5,7 @@ from ...f3d.f3d_material import update_world_default_rendermode from ...f3d.f3d_parser import ootEnumDrawLayers from ...utility import prop_split +from ..utility import is_hackeroot class OOTDLExportSettings(PropertyGroup): diff --git a/fast64_internal/z64/file_settings.py b/fast64_internal/z64/file_settings.py index ef7bdd95c..26fa2e6f2 100644 --- a/fast64_internal/z64/file_settings.py +++ b/fast64_internal/z64/file_settings.py @@ -27,12 +27,19 @@ def draw(self, context): prop_split(col, context.scene.fast64.oot, "oot_version_custom", "Custom Version") col.prop(context.scene.fast64.oot, "headerTabAffectsVisibility") - col.prop(context.scene.fast64.oot, "hackerFeaturesEnabled") - if not context.scene.fast64.oot.hackerFeaturesEnabled: + if game_data.z64.is_oot(): + col.prop(context.scene.fast64.oot, "hackerFeaturesEnabled") + + if not context.scene.fast64.oot.hackerFeaturesEnabled: + col.prop(context.scene.fast64.oot, "mm_features") + + if game_data.z64.is_mm() or not context.scene.fast64.oot.hackerFeaturesEnabled: col.prop(context.scene.fast64.oot, "useDecompFeatures") col.prop(context.scene.fast64.oot, "exportMotionOnly") - col.prop(context.scene.fast64.oot, "use_new_actor_panel") + + if game_data.z64.is_oot(): + col.prop(context.scene.fast64.oot, "use_new_actor_panel") oot_classes = (OOT_FileSettingsPanel,) diff --git a/fast64_internal/z64/hackeroot/operators.py b/fast64_internal/z64/hackeroot/operators.py new file mode 100644 index 000000000..d7a584e03 --- /dev/null +++ b/fast64_internal/z64/hackeroot/operators.py @@ -0,0 +1,31 @@ +import os + +from bpy.path import abspath +from bpy.types import Operator +from bpy.utils import register_class, unregister_class + +from ..exporter.decomp_edit.config import Config + + +class HackerOoT_ClearBootupScene(Operator): + bl_idname = "object.hackeroot_clear_bootup_scene" + bl_label = "Undo Boot To Scene" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + Config.clearBootupScene(os.path.join(abspath(context.scene.ootDecompPath), "include/config/config_debug.h")) + self.report({"INFO"}, "Success!") + return {"FINISHED"} + + +classes = (HackerOoT_ClearBootupScene,) + + +def hackeroot_ops_register(): + for cls in classes: + register_class(cls) + + +def hackeroot_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/hackeroot/panels.py b/fast64_internal/z64/hackeroot/panels.py new file mode 100644 index 000000000..267c11dd6 --- /dev/null +++ b/fast64_internal/z64/hackeroot/panels.py @@ -0,0 +1,27 @@ +from bpy.utils import register_class, unregister_class + +from ...panels import OOT_Panel + + +class HackerOoTSettingsPanel(OOT_Panel): + bl_idname = "Z64_PT_hackeroot_settings" + bl_label = "HackerOoT Settings" + + def draw(self, context): + if context.scene.fast64.oot.hackerFeaturesEnabled: + context.scene.fast64.oot.hackeroot_settings.draw_props(context, self.layout) + else: + self.layout.label(text="HackerOoT features are disabled.", icon="QUESTION") + + +panel_classes = (HackerOoTSettingsPanel,) + + +def hackeroot_panels_register(): + for cls in panel_classes: + register_class(cls) + + +def hackeroot_panels_unregister(): + for cls in reversed(panel_classes): + unregister_class(cls) diff --git a/fast64_internal/z64/hackeroot/properties.py b/fast64_internal/z64/hackeroot/properties.py new file mode 100644 index 000000000..d89997b6d --- /dev/null +++ b/fast64_internal/z64/hackeroot/properties.py @@ -0,0 +1,36 @@ +import bpy + +from bpy.utils import register_class, unregister_class + +from ..scene.properties import OOTBootupSceneOptions +from .operators import HackerOoT_ClearBootupScene + + +class HackerOoTSettings(bpy.types.PropertyGroup): + export_ifdefs: bpy.props.BoolProperty(default=True) + + def draw_props(self, context: bpy.types.Context, layout: bpy.types.UILayout): + export_box = layout.box() + export_box.label(text="Export Settings") + export_box.prop(self, "export_ifdefs", text="Export ifdefs") + + boot_box = export_box.box().column() + + bootOptions: OOTBootupSceneOptions = context.scene.fast64.oot.bootupSceneOptions + bootOptions.draw_props(boot_box) + + boot_box.label(text="Note: Scene boot config changes aren't detected by the make process.", icon="ERROR") + boot_box.operator(HackerOoT_ClearBootupScene.bl_idname, text="Undo Boot To Scene (HackerOOT Repo)") + + +classes = (HackerOoTSettings,) + + +def hackeroot_props_register(): + for cls in classes: + register_class(cls) + + +def hackeroot_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/importer/classes.py b/fast64_internal/z64/importer/classes.py index 37927dc28..1c32e6cde 100644 --- a/fast64_internal/z64/importer/classes.py +++ b/fast64_internal/z64/importer/classes.py @@ -17,6 +17,7 @@ def __init__( includePaths: bool, includeWaterBoxes: bool, includeCutscenes: bool, + includeAnimatedMats: bool, is_single_file: bool, is_fast64_data: bool, not_zapd_assets: bool, @@ -37,6 +38,7 @@ def __init__( self.includePaths = includePaths self.includeWaterBoxes = includeWaterBoxes self.includeCutscenes = includeCutscenes + self.includeAnimatedMats = includeAnimatedMats self.is_single_file = is_single_file self.is_fast64_data = is_fast64_data self.not_zapd_assets = not_zapd_assets diff --git a/fast64_internal/z64/importer/scene.py b/fast64_internal/z64/importer/scene.py index e5fd46345..621696846 100644 --- a/fast64_internal/z64/importer/scene.py +++ b/fast64_internal/z64/importer/scene.py @@ -5,6 +5,7 @@ from pathlib import Path +from ...game_data import game_data from ...utility import PluginError, readFile, hexOrDecInt from ...f3d.f3d_parser import parseMatrices from ...f3d.f3d_gbi import get_F3D_GBI @@ -12,7 +13,6 @@ from ..model_classes import OOTF3DContext from ..exporter.decomp_edit.scene_table import SceneTableUtility from ..scene.properties import OOTImportSceneSettingsProperty -from ..constants import ootEnumDrawConfig from ..cutscene.importer import importCutsceneData from .scene_header import parseSceneCommands from .classes import SharedSceneData @@ -108,7 +108,10 @@ def parseScene( True, ) - file_path = Path(sceneFolderPath).resolve() / f"{sceneName}_scene.c" + if game_data.z64.is_oot(): + file_path = Path(sceneFolderPath).resolve() / f"{sceneName}_scene.c" + else: + file_path = Path(sceneFolderPath).resolve() / f"{sceneName}.c" is_single_file = True if not file_path.exists(): @@ -129,7 +132,11 @@ def parseScene( if bpy.context.mode != "OBJECT": bpy.context.mode = "OBJECT" - sceneCommandsName = f"{sceneName}_sceneCommands" + if game_data.z64.is_oot(): + sceneCommandsName = f"{sceneName}_sceneCommands" + else: + sceneCommandsName = f"{sceneName}Commands" + not_zapd_assets = False # fast64 naming @@ -138,13 +145,13 @@ def parseScene( sceneCommandsName = f"{sceneName}_scene_header00" # newer assets system naming - if sceneCommandsName not in sceneData: + if game_data.z64.is_oot() and sceneCommandsName not in sceneData: not_zapd_assets = True sceneCommandsName = f"{sceneName}_scene" sharedSceneData = SharedSceneData( sceneFolderPath, - f"{sceneName}_scene", + f"{sceneName}_scene" if game_data.z64.is_oot() else sceneName, settings.includeMesh, settings.includeCollision, settings.includeActors, @@ -154,6 +161,7 @@ def parseScene( settings.includePaths, settings.includeWaterBoxes, settings.includeCutscenes, + settings.includeAnimatedMats, is_single_file, f"{sceneName}_scene_header00" in sceneData, not_zapd_assets, @@ -173,7 +181,8 @@ def parseScene( if not settings.isCustomDest: drawConfigName = SceneTableUtility.get_draw_config(sceneName) - drawConfigData = readFile(os.path.join(importPath, "src/code/z_scene_table.c")) + filename = "z_scene_table" if game_data.z64.is_oot() else "z_scene_proc" + drawConfigData = readFile(os.path.join(importPath, f"src/code/{filename}.c")) parseDrawConfig(drawConfigName, sceneData, drawConfigData, f3dContext) bpy.context.space_data.overlay.show_relationship_lines = False @@ -191,7 +200,7 @@ def parseScene( sceneObj.ootSceneHeader.sceneTableEntry, "drawConfig", SceneTableUtility.get_draw_config(sceneName), - ootEnumDrawConfig, + game_data.z64.get_enum("drawConfig"), ) if bpy.context.scene.fast64.oot.headerTabAffectsVisibility: diff --git a/fast64_internal/z64/importer/scene_header.py b/fast64_internal/z64/importer/scene_header.py index 5674cfcae..048ebc69a 100644 --- a/fast64_internal/z64/importer/scene_header.py +++ b/fast64_internal/z64/importer/scene_header.py @@ -7,27 +7,26 @@ from typing import Optional from ...game_data import game_data -from ...utility import PluginError, get_new_object, parentObject, hexOrDecInt, gammaInverse +from ...utility import PluginError, get_new_empty_object, parentObject, hexOrDecInt, gammaInverse from ...f3d.f3d_parser import parseMatrices from ..exporter.scene.general import EnvLightSettings from ..model_classes import OOTF3DContext from ..scene.properties import OOTSceneHeaderProperty, OOTLightProperty -from ..utility import setCustomProperty +from ..utility import setCustomProperty, is_hackeroot, getEnumIndex from .constants import headerNames -from .utility import getDataMatch, stripName, parse_commands_data +from .utility import getDataMatch, stripName from .classes import SharedSceneData from .room_header import parseRoomCommands from .actor import parseTransActorList, parseSpawnList, parseEntranceList from .scene_collision import parseCollisionHeader from .scene_pathways import parsePathList +from ..animated_mats.properties import Z64_AnimatedMaterial, enum_anim_mat_type from ..constants import ( ootEnumAudioSessionPreset, - ootEnumMusicSeq, ootEnumCameraMode, ootEnumMapLocation, ootEnumNaviHints, - ootEnumGlobalObject, ootEnumSkyboxLighting, ) @@ -125,7 +124,9 @@ def parseLightList( lights_empty = None if len(lightList) > 0: - lights_empty = get_new_object(f"{sceneObj.name} Lights (header {headerIndex})", None, False, sceneObj) + lights_empty = get_new_empty_object( + f"{sceneObj.name} Lights (header {headerIndex})", do_select=False, parent=sceneObj + ) lights_empty.ootEmptyType = "None" parent_obj = lights_empty if lights_empty is not None else sceneObj @@ -147,7 +148,9 @@ def parseLightList( new_tod_light = sceneHeader.tod_lights.add() if i > 0 else None settings_name = "Default Settings" if i == 0 else f"Light Settings {i}" - sub_lights_empty = get_new_object(f"(Header {headerIndex}) {settings_name}", None, False, parent_obj) + sub_lights_empty = get_new_empty_object( + f"(Header {headerIndex}) {settings_name}", do_select=False, parent=parent_obj + ) sub_lights_empty.ootEmptyType = "None" for tod_type in ["Dawn", "Day", "Dusk", "Night"]: @@ -287,6 +290,141 @@ def parseAlternateSceneHeaders( ) +def parse_animated_material(anim_mat: Z64_AnimatedMaterial, scene_data: str, list_name: str): + anim_mat_type_to_struct = { + 0: "AnimatedMatTexScrollParams", + 1: "AnimatedMatTexScrollParams", + 2: "AnimatedMatColorParams", + 3: "AnimatedMatColorParams", + 4: "AnimatedMatColorParams", + 5: "AnimatedMatTexCycleParams", + } + + struct_to_regex = { + "AnimatedMatTexScrollParams": r"\{\s?(0?x?\-?\d+),\s?(0?x?\-?\d+),\s?(0?x?\-?\d+),\s?(0?x?\-?\d+)\s?\}", + "AnimatedMatColorParams": r"(\d+)(\,\n?\s*)?(\d+)(\,\n?\s*)?([a-zA-Z0-9_]*)(\,\n?\s*)?([a-zA-Z0-9_]*)(\,\n?\s*)?([a-zA-Z0-9_]*)", + "AnimatedMatTexCycleParams": r"(\d+)(\,\n?\s*)?([a-zA-Z0-9_]*)(\,\n?\s*)?([a-zA-Z0-9_]*)", + } + + data_match = getDataMatch(scene_data, list_name, "AnimatedMaterial", "animated material") + anim_mat_data = data_match.strip().split("\n") + + for data in anim_mat_data: + data = data.replace("{", "").replace("}", "").removesuffix(",").strip() + + split = data.split(", ") + + type_num = int(split[1], base=0) + + if type_num == 6: + continue + + raw_segment = split[0] + + if "MATERIAL_SEGMENT_NUM" in raw_segment: + raw_segment = raw_segment.removesuffix(")").split("(")[1] + + segment = int(raw_segment, base=0) + data_ptr = split[2].removeprefix("&") + + is_array = type_num in {0, 1} + struct_name = anim_mat_type_to_struct[type_num] + regex = struct_to_regex[struct_name] + data_match = getDataMatch(scene_data, data_ptr, struct_name, "animated params", is_array, False) + params_data: list[list[str]] | list[str] = [] + + if is_array: + params_data = [ + [match.group(1), match.group(2), match.group(3), match.group(4)] + for match in re.finditer(regex, data_match, re.DOTALL) + ] + else: + match = re.search(regex, data_match, re.DOTALL) + assert match is not None + + params_data = [match.group(1), match.group(3), match.group(5)] + if struct_name == "AnimatedMatColorParams": + params_data.extend([match.group(7), match.group(9)]) + + entry = anim_mat.entries.add() + entry.segment_num = segment + enum_type = entry.user_type = enum_anim_mat_type[type_num + 1][0] + entry.on_type_set(getEnumIndex(enum_anim_mat_type, enum_type)) + + if struct_name == "AnimatedMatTexScrollParams": + entry.tex_scroll_params.texture_1.set_from_data(params_data[0]) + + if len(params_data) > 1: + entry.tex_scroll_params.texture_2.set_from_data(params_data[1]) + elif struct_name == "AnimatedMatColorParams": + entry.color_params.keyframe_length = int(params_data[0], base=0) + + prim_match = getDataMatch(scene_data, params_data[2], "F3DPrimColor", "animated material prim color", True) + prim_data = prim_match.strip().replace(" ", "").replace("}", "").replace("{", "").split("\n") + + use_env_color = params_data[3] != "NULL" + use_frame_indices = params_data[4] != "NULL" + + env_data = [None] * len(prim_data) + if use_env_color: + env_match = getDataMatch(scene_data, params_data[3], "F3DEnvColor", "animated material env color", True) + env_data = env_match.strip().replace(" ", "").replace("}", "").replace("{", "").split("\n") + + frame_data = [None] * len(prim_data) + if use_frame_indices: + frame_match = getDataMatch( + scene_data, params_data[4], "u16", "animated material color frame data", True + ) + frame_data = ( + frame_match.strip() + .replace(" ", "") + .replace(",\n", ",") + .replace(",", "\n") + .removesuffix("\n") + .split("\n") + ) + + assert len(prim_data) == len(env_data) == len(frame_data) + + for prim_color_raw, env_color_raw, frame in zip(prim_data, env_data, frame_data): + prim_color = [hexOrDecInt(elem) for elem in prim_color_raw.split(",") if len(elem) > 0] + + color_entry = entry.color_params.keyframes.add() + + if use_frame_indices: + assert frame is not None + color_entry.frame_num = int(frame, base=0) + + color_entry.prim_lod_frac = prim_color[4] + color_entry.prim_color = parseColor(prim_color[0:3]) + (1,) + + if use_env_color: + assert env_color_raw is not None + env_color = [hexOrDecInt(elem) for elem in env_color_raw.split(",") if len(elem) > 0] + color_entry.env_color = parseColor(env_color[0:3]) + (1,) + elif struct_name == "AnimatedMatTexCycleParams": + entry.tex_cycle_params.keyframe_length = int(params_data[0], base=0) + textures: list[str] = [] + frames: list[int] = [] + + data_match = getDataMatch(scene_data, params_data[1], "TexturePtr", "animated material texture ptr", True) + for texture_ptr in data_match.replace(" ", "").replace("\n", "").split(","): + if len(texture_ptr) > 0: + textures.append(texture_ptr.strip()) + + data_match = getDataMatch(scene_data, params_data[2], "u8", "animated material frame data", True) + for frame_num in data_match.replace(",", "\n").strip().split("\n"): + frames.append(int(frame_num.strip(), base=0)) + + for symbol in textures: + cycle_entry = entry.tex_cycle_params.textures.add() + cycle_entry.symbol = symbol + + for frame_num in frames: + cycle_entry = entry.tex_cycle_params.keyframes.add() + cycle_entry.texture_index = frame_num + + def parseSceneCommands( sceneName: Optional[str], sceneObj: Optional[bpy.types.Object], @@ -306,93 +444,162 @@ def parseSceneCommands( if headerIndex == 0: sceneHeader = sceneObj.ootSceneHeader - elif headerIndex < 4: + elif game_data.z64.is_oot() and headerIndex < game_data.z64.cs_index_start: sceneHeader = getattr(sceneObj.ootAlternateSceneHeaders, headerNames[headerIndex]) sceneHeader.usePreviousHeader = False else: cutsceneHeaders = sceneObj.ootAlternateSceneHeaders.cutsceneHeaders - while len(cutsceneHeaders) < headerIndex - 3: + while len(cutsceneHeaders) < headerIndex - (game_data.z64.cs_index_start - 1): cutsceneHeaders.add() - sceneHeader = cutsceneHeaders[headerIndex - 4] + sceneHeader = cutsceneHeaders[headerIndex - game_data.z64.cs_index_start] commands = getDataMatch(sceneData, sceneCommandsName, ["SceneCmd", "SCmdBase"], "scene commands") - cmd_map = parse_commands_data(commands) entranceList = None - altHeadersListName = None - for command, args in cmd_map.items(): + # command to delay: command args + delayed_commands: dict[str, list[str]] = {} + command_map: dict[str, list[str]] = {} + + # store the commands to process with the corresponding args + for commandMatch in re.finditer(rf"(SCENE\_CMD\_[a-zA-Z0-9\_]*)\s*\((.*?)\)\s*,", commands, flags=re.DOTALL): + command = commandMatch.group(1) + args = [arg.strip() for arg in commandMatch.group(2).split(",")] + command_map[command] = args + + command_list = list(command_map.keys()) + + for command, args in command_map.items(): if command == "SCENE_CMD_SOUND_SETTINGS": setCustomProperty(sceneHeader, "audioSessionPreset", args[0], ootEnumAudioSessionPreset) setCustomProperty(sceneHeader, "nightSeq", args[1], game_data.z64.get_enum("nature_id")) - setCustomProperty(sceneHeader, "musicSeq", args[2], ootEnumMusicSeq) - elif command == "SCENE_CMD_ROOM_LIST": - # Assumption that all scenes use the same room list. - if headerIndex == 0: - if roomObjs is not None: - raise PluginError("Attempting to parse a room list while room objs already loaded.") - roomListName = stripName(args[1]) - roomObjs = parseRoomList(sceneObj, sceneData, roomListName, f3dContext, sharedSceneData, headerIndex) - # This must be handled after rooms, so that room objs can be referenced - elif command == "SCENE_CMD_TRANSITION_ACTOR_LIST" and sharedSceneData.includeActors: - transActorListName = stripName(args[1]) - parseTransActorList(roomObjs, sceneData, transActorListName, sharedSceneData, headerIndex) + if args[2].startswith("NA_BGM_"): + enum_id = args[2] + else: + enum_id = game_data.z64.enums.enumByKey["seq_id"].item_by_index[int(args[2])].id - elif command == "SCENE_CMD_MISC_SETTINGS": + setCustomProperty(sceneHeader, "musicSeq", enum_id, game_data.z64.get_enum("musicSeq")) + command_list.remove(command) + elif command == "SCENE_CMD_ROOM_LIST": + # Delay until actor cutscenes are processed + delayed_commands[command] = args + command_list.remove(command) + elif command == "SCENE_CMD_TRANSITION_ACTOR_LIST": + if sharedSceneData.includeActors: + # This must be handled after rooms, so that room objs can be referenced + delayed_commands[command] = args + command_list.remove(command) + elif game_data.z64.is_oot() and command == "SCENE_CMD_MISC_SETTINGS": setCustomProperty(sceneHeader, "cameraMode", args[0], ootEnumCameraMode) setCustomProperty(sceneHeader, "mapLocation", args[1], ootEnumMapLocation) + command_list.remove(command) + elif command == "SCENE_CMD_COL_HEADER": + # Delay until after rooms are processed + delayed_commands[command] = args + command_list.remove(command) + elif command in {"SCENE_CMD_ENTRANCE_LIST", "SCENE_CMD_SPAWN_LIST"}: + if sharedSceneData.includeActors: + # Delay until after rooms are processed + delayed_commands["SCENE_CMD_SPAWN_LIST"] = args + command_list.remove(command) + elif command == "SCENE_CMD_SPECIAL_FILES": + if game_data.z64.is_oot(): + setCustomProperty(sceneHeader, "naviCup", args[0], ootEnumNaviHints) + setCustomProperty(sceneHeader, "globalObject", args[1], game_data.z64.get_enum("globalObject")) + command_list.remove(command) + elif command == "SCENE_CMD_PATH_LIST": + if sharedSceneData.includePaths: + pathListName = stripName(args[0]) + parsePathList(sceneObj, sceneData, pathListName, headerIndex, sharedSceneData) + command_list.remove(command) + elif command in {"SCENE_CMD_SPAWN_LIST", "SCENE_CMD_PLAYER_ENTRY_LIST"}: + if sharedSceneData.includeActors: + # This must be handled after entrance list, so that entrance list and room list can be referenced + delayed_commands["SCENE_CMD_PLAYER_ENTRY_LIST"] = args + command_list.remove(command) + elif command == "SCENE_CMD_SKYBOX_SETTINGS": + args_index = 0 + if game_data.z64.is_mm(): + sceneHeader.skybox_texture_id = args[args_index] + args_index += 1 + setCustomProperty(sceneHeader, "skyboxID", args[args_index], game_data.z64.get_enum("skybox")) + setCustomProperty( + sceneHeader, "skyboxCloudiness", args[args_index + 1], game_data.z64.get_enum("skybox_config") + ) + setCustomProperty(sceneHeader, "skyboxLighting", args[args_index + 2], ootEnumSkyboxLighting) + command_list.remove(command) + elif command == "SCENE_CMD_EXIT_LIST": + exitListName = stripName(args[0]) + parseExitList(sceneHeader, sceneData, exitListName) + command_list.remove(command) + elif command == "SCENE_CMD_ENV_LIGHT_SETTINGS": + if sharedSceneData.includeLights: + if not (args[1] == "NULL" or args[1] == "0" or args[1] == "0x00"): + lightsListName = stripName(args[1]) + parseLightList(sceneObj, sceneHeader, sceneData, lightsListName, headerIndex, sharedSceneData) + command_list.remove(command) + elif command == "SCENE_CMD_CUTSCENE_DATA": + if sharedSceneData.includeCutscenes: + sceneHeader.writeCutscene = True + sceneHeader.csWriteType = "Object" + csObjName = f"Cutscene.{args[0]}" + try: + sceneHeader.csWriteObject = bpy.data.objects[csObjName] + except: + print(f"ERROR: Cutscene ``{csObjName}`` do not exist!") + command_list.remove(command) + elif command == "SCENE_CMD_ALTERNATE_HEADER_LIST": + # Delay until after rooms are processed + delayed_commands[command] = args + command_list.remove(command) + elif command == "SCENE_CMD_END": + command_list.remove(command) + + # handle Majora's Mask (or modded OoT) exclusive commands + elif game_data.z64.is_mm() or is_hackeroot(): + if command == "SCENE_CMD_ANIMATED_MATERIAL_LIST": + if sharedSceneData.includeAnimatedMats: + parse_animated_material(sceneHeader.animated_material, sceneData, stripName(args[0])) + command_list.remove(command) + + if "SCENE_CMD_ROOM_LIST" in delayed_commands: + args = delayed_commands["SCENE_CMD_ROOM_LIST"] + # Assumption that all scenes use the same room list. + if headerIndex == 0: + if roomObjs is not None: + raise PluginError("Attempting to parse a room list while room objs already loaded.") + roomListName = stripName(args[1]) + roomObjs = parseRoomList(sceneObj, sceneData, roomListName, f3dContext, sharedSceneData, headerIndex) + delayed_commands.pop("SCENE_CMD_ROOM_LIST") + else: + raise PluginError("ERROR: no room command found for this scene!") + + # any other delayed command requires rooms to be processed + for command, args in delayed_commands.items(): + if command == "SCENE_CMD_TRANSITION_ACTOR_LIST" and sharedSceneData.includeActors: + transActorListName = stripName(args[1]) + parseTransActorList(roomObjs, sceneData, transActorListName, sharedSceneData, headerIndex) elif command == "SCENE_CMD_COL_HEADER": # Assumption that all scenes use the same collision. if headerIndex == 0: collisionHeaderName = args[0][1:] # remove '&' parseCollisionHeader(sceneObj, roomObjs, sceneData, collisionHeaderName, sharedSceneData) - elif ( - command in {"SCENE_CMD_ENTRANCE_LIST", "SCENE_CMD_SPAWN_LIST"} - and sharedSceneData.includeActors - and len(args) == 1 - ): + elif command == "SCENE_CMD_SPAWN_LIST" and sharedSceneData.includeActors and len(args) == 1: if not (args[0] == "NULL" or args[0] == "0" or args[0] == "0x00"): entranceListName = stripName(args[0]) entranceList = parseEntranceList(sceneHeader, roomObjs, sceneData, entranceListName) - elif command == "SCENE_CMD_SPECIAL_FILES": - setCustomProperty(sceneHeader, "naviCup", args[0], ootEnumNaviHints) - setCustomProperty(sceneHeader, "globalObject", args[1], ootEnumGlobalObject) - elif command == "SCENE_CMD_PATH_LIST" and sharedSceneData.includePaths: - pathListName = stripName(args[0]) - parsePathList(sceneObj, sceneData, pathListName, headerIndex, sharedSceneData) - - # This must be handled after entrance list, so that entrance list can be referenced - elif command in {"SCENE_CMD_SPAWN_LIST", "SCENE_CMD_PLAYER_ENTRY_LIST"} and sharedSceneData.includeActors: + elif command == "SCENE_CMD_PLAYER_ENTRY_LIST" and sharedSceneData.includeActors: if not (args[1] == "NULL" or args[1] == "0" or args[1] == "0x00"): spawnListName = stripName(args[1]) parseSpawnList(roomObjs, sceneData, spawnListName, entranceList, sharedSceneData, headerIndex) # Clear entrance list entranceList = None - - elif command == "SCENE_CMD_SKYBOX_SETTINGS": - setCustomProperty(sceneHeader, "skyboxID", args[0], game_data.z64.get_enum("skybox")) - setCustomProperty(sceneHeader, "skyboxCloudiness", args[1], game_data.z64.get_enum("skybox_config")) - setCustomProperty(sceneHeader, "skyboxLighting", args[2], ootEnumSkyboxLighting) - elif command == "SCENE_CMD_EXIT_LIST": - exitListName = stripName(args[0]) - parseExitList(sceneHeader, sceneData, exitListName) - elif command == "SCENE_CMD_ENV_LIGHT_SETTINGS" and sharedSceneData.includeLights: - if not (args[1] == "NULL" or args[1] == "0" or args[1] == "0x00"): - lightsListName = stripName(args[1]) - parseLightList(sceneObj, sceneHeader, sceneData, lightsListName, headerIndex, sharedSceneData) - elif command == "SCENE_CMD_CUTSCENE_DATA" and sharedSceneData.includeCutscenes: - sceneHeader.writeCutscene = True - sceneHeader.csWriteType = "Object" - csObjName = f"Cutscene.{args[0]}" - try: - sceneHeader.csWriteObject = bpy.data.objects[csObjName] - except: - print(f"ERROR: Cutscene ``{csObjName}`` do not exist!") elif command == "SCENE_CMD_ALTERNATE_HEADER_LIST": - # Delay until after rooms are parsed - altHeadersListName = stripName(args[0]) + parseAlternateSceneHeaders(sceneObj, roomObjs, sceneData, stripName(args[0]), f3dContext, sharedSceneData) - if altHeadersListName is not None: - parseAlternateSceneHeaders(sceneObj, roomObjs, sceneData, altHeadersListName, f3dContext, sharedSceneData) + if len(command_list) > 0: + print(f"INFO: The following scene commands weren't processed for header {headerIndex}:") + for command in command_list: + print(f"- {repr(command)}") return sceneObj diff --git a/fast64_internal/z64/model_classes.py b/fast64_internal/z64/model_classes.py index 457ee3413..fef534f6c 100644 --- a/fast64_internal/z64/model_classes.py +++ b/fast64_internal/z64/model_classes.py @@ -1,12 +1,18 @@ -import bpy, os, re, mathutils -from typing import Union +import bpy +import os +import re +import mathutils + +from typing import Union, Optional +from dataclasses import dataclass from ..f3d.f3d_parser import F3DContext, F3DTextureReference, getImportData from ..f3d.f3d_material import TextureProperty, createF3DMat, texFormatOf, texBitSizeF3D -from ..utility import PluginError, hexOrDecInt, create_or_get_world -from ..f3d.flipbook import TextureFlipbook, FlipbookProperty, usesFlipbook, ootFlipbookReferenceIsValid +from ..utility import PluginError, hexOrDecInt, create_or_get_world, indent +from ..f3d.flipbook import TextureFlipbook, usesFlipbook, ootFlipbookReferenceIsValid from ..f3d.f3d_writer import VertexGroupInfo, TriangleConverterInfo + from ..f3d.f3d_texture_writer import ( getColorsUsedInImage, mergePalettes, @@ -14,12 +20,12 @@ writeNonCITextureData, getTextureNamesFromImage, ) + from ..f3d.f3d_gbi import ( FModel, FMaterial, FImage, FImageKey, - FPaletteKey, GfxMatWriteMethod, SPDisplayList, GfxList, @@ -27,10 +33,12 @@ DLFormat, SPMatrix, GfxFormatter, - MTX_SIZE, DPSetTile, + MTX_SIZE, ) +from .utility import is_hackeroot + # read included asset data def ootGetIncludedAssetData(basePath: str, currentPaths: list[str], data: str) -> str: @@ -95,10 +103,33 @@ def ootGetLinkData(basePath: str) -> str: return actorData +# custom `SPDisplayList` so we can customize the C output +@dataclass(unsafe_hash=True) +class DynamicMaterialDL(SPDisplayList): + is_animated_material_sdc: bool + + def __post_init__(self): + self.default_formatting = False + + def to_c(self, static=True): + assert static + if ( + is_hackeroot() + and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs + and self.is_animated_material_sdc + ): + return ( + "#if ENABLE_ANIMATED_MATERIALS\n" + indent + f"gsSPDisplayList({self.displayList.name}),\n" + "#endif\n" + ) + else: + return indent + f"gsSPDisplayList({self.displayList.name}),\n" + + class OOTModel(FModel): - def __init__(self, name, DLFormat, drawLayerOverride): + def __init__(self, name, DLFormat, drawLayerOverride, draw_config: Optional[str] = None): self.drawLayerOverride = drawLayerOverride self.flipbooks: list[TextureFlipbook] = [] + self.draw_config = draw_config FModel.__init__(self, name, DLFormat, GfxMatWriteMethod.WriteAll) @@ -283,16 +314,20 @@ def onMaterialCommandsBuilt(self, fMaterial, material, drawLayer): # handle dynamic material calls gfxList = fMaterial.material matDrawLayer = getattr(material.ootMaterial, drawLayer.lower()) + for i in range(8, 14): - if getattr(matDrawLayer, "segment" + format(i, "X")): + if getattr(matDrawLayer, f"segment{i:X}"): gfxList.commands.append( - SPDisplayList(GfxList("0x" + format(i, "X") + "000000", GfxListTag.Material, DLFormat.Static)) + DynamicMaterialDL( + GfxList(f"0x0{i:X}000000", GfxListTag.Material, DLFormat.Static), "mat_anim" in self.draw_config + ) ) + for i in range(0, 2): - p = "customCall" + str(i) + p = f"customCall{i}" if getattr(matDrawLayer, p): gfxList.commands.append( - SPDisplayList(GfxList(getattr(matDrawLayer, p + "_seg"), GfxListTag.Material, DLFormat.Static)) + SPDisplayList(GfxList(getattr(matDrawLayer, f"{p}_seg"), GfxListTag.Material, DLFormat.Static)) ) def onAddMesh(self, fMesh, contextObj): diff --git a/fast64_internal/z64/props_panel_main.py b/fast64_internal/z64/props_panel_main.py index 5a3663d1f..efd472cb8 100644 --- a/fast64_internal/z64/props_panel_main.py +++ b/fast64_internal/z64/props_panel_main.py @@ -1,11 +1,12 @@ import bpy from bpy.utils import register_class, unregister_class from ..utility import prop_split, gammaInverse -from .utility import getSceneObj, getRoomObj +from .utility import getSceneObj, getRoomObj, is_oot_features from .scene.properties import OOTSceneProperties from .room.properties import OOTObjectProperty, OOTRoomHeaderProperty, OOTAlternateRoomHeaderProperty from .collision.properties import OOTWaterBoxProperty from .cutscene.properties import OOTCutsceneProperty +from .animated_mats.properties import Z64_AnimatedMaterialProperty from .cutscene.motion.properties import ( OOTCutsceneMotionProperty, CutsceneCmdActorCueListProperty, @@ -37,15 +38,16 @@ ("CS Actor Cue Preview", "CS Actor Cue Preview", "CS Actor Cue Preview"), ("CS Player Cue Preview", "CS Player Cue Preview", "CS Player Cue Preview"), ("CS Dummy Cue", "CS Dummy Cue", "CS Dummy Cue"), + ("Animated Materials", "Animated Materials", "Animated Materials"), # ('Camera Volume', 'Camera Volume', 'Camera Volume'), ] def drawSceneHeader(box: bpy.types.UILayout, obj: bpy.types.Object): objName = obj.name - obj.ootSceneHeader.draw_props(box, None, None, objName) + obj.ootSceneHeader.draw_props(box.box(), None, None, obj) if obj.ootSceneHeader.menuTab == "Alternate": - obj.ootAlternateSceneHeaders.draw_props(box, objName) + obj.ootAlternateSceneHeaders.draw_props(box.box(), obj) box.prop(obj.fast64.oot.scene, "write_dummy_room_list") @@ -172,6 +174,13 @@ def draw(self, context): csProp: OOTCutsceneProperty = obj.ootCutsceneProperty csProp.draw_props(box, obj) + elif obj.ootEmptyType == "Animated Materials": + if is_oot_features() and not context.scene.fast64.oot.hackerFeaturesEnabled: + box.label(text="This required MM or HackerOoT features to be enabled.") + else: + anim_props: Z64_AnimatedMaterialProperty = obj.fast64.oot.animated_materials + anim_props.draw_props(box, obj) + elif obj.ootEmptyType in [ "CS Actor Cue List", "CS Player Cue List", @@ -192,7 +201,9 @@ def draw(self, context): class OOT_ObjectProperties(bpy.types.PropertyGroup): + # bpy.data.objects["XXXX"].fast64.oot. scene: bpy.props.PointerProperty(type=OOTSceneProperties) + animated_materials: bpy.props.PointerProperty(type=Z64_AnimatedMaterialProperty) @staticmethod def upgrade_changed_props(): diff --git a/fast64_internal/z64/scene/operators.py b/fast64_internal/z64/scene/operators.py index 648d2a704..3e4b302bf 100644 --- a/fast64_internal/z64/scene/operators.py +++ b/fast64_internal/z64/scene/operators.py @@ -1,5 +1,4 @@ import bpy -import os from bpy.path import abspath from bpy.types import Operator @@ -7,12 +6,11 @@ from bpy.utils import register_class, unregister_class from bpy.ops import object from mathutils import Matrix, Vector -from ...f3d.f3d_gbi import TextureExportSettings, DLFormat from ...utility import PluginError, raisePluginError, ootGetSceneOrRoomHeader from ..utility import ExportInfo, RemoveInfo, sceneNameFromID from ..constants import ootEnumMusicSeq, ootEnumSceneID from ..importer import parseScene -from ..exporter.decomp_edit.config import Config + from ..exporter import SceneExport, Files @@ -87,17 +85,6 @@ def invoke(self, context, event): return {"RUNNING_MODAL"} -class OOT_ClearBootupScene(Operator): - bl_idname = "object.oot_clear_bootup_scene" - bl_label = "Undo Boot To Scene" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - def execute(self, context): - Config.clearBootupScene(os.path.join(abspath(context.scene.ootDecompPath), "include/config/config_debug.h")) - self.report({"INFO"}, "Success!") - return {"FINISHED"} - - class OOT_ImportScene(Operator): """Import an OOT scene from C.""" @@ -253,7 +240,6 @@ def draw(self, context): classes = ( OOT_SearchMusicSeqEnumOperator, OOT_SearchSceneEnumOperator, - OOT_ClearBootupScene, OOT_ImportScene, OOT_ExportScene, OOT_RemoveScene, diff --git a/fast64_internal/z64/scene/panels.py b/fast64_internal/z64/scene/panels.py index ef32b2923..3801381d7 100644 --- a/fast64_internal/z64/scene/panels.py +++ b/fast64_internal/z64/scene/panels.py @@ -1,6 +1,3 @@ -import bpy -import os - from bpy.types import UILayout from bpy.utils import register_class, unregister_class from ...panels import OOT_Panel @@ -10,14 +7,12 @@ OOTExportSceneSettingsProperty, OOTImportSceneSettingsProperty, OOTRemoveSceneSettingsProperty, - OOTBootupSceneOptions, ) from .operators import ( OOT_ImportScene, OOT_ExportScene, OOT_RemoveScene, - OOT_ClearBootupScene, OOT_SearchSceneEnumOperator, ) @@ -43,18 +38,6 @@ def draw(self, context): self.drawSceneSearchOp(exportBox, settings.option, "Export") settings.draw_props(exportBox) - if context.scene.fast64.oot.hackerFeaturesEnabled: - hackerOoTBox = exportBox.box().column() - hackerOoTBox.label(text="HackerOoT Options") - - bootOptions: OOTBootupSceneOptions = context.scene.fast64.oot.bootupSceneOptions - bootOptions.draw_props(hackerOoTBox) - - hackerOoTBox.label( - text="Note: Scene boot config changes aren't detected by the make process.", icon="ERROR" - ) - hackerOoTBox.operator(OOT_ClearBootupScene.bl_idname, text="Undo Boot To Scene (HackerOOT Repo)") - exportBox.operator(OOT_ExportScene.bl_idname) # Scene Importer diff --git a/fast64_internal/z64/scene/properties.py b/fast64_internal/z64/scene/properties.py index 34dca46f0..5338de3cb 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -12,13 +12,15 @@ FloatVectorProperty, ) from bpy.utils import register_class, unregister_class +from typing import Optional from ...game_data import game_data from ...render_settings import on_update_oot_render_settings from ...utility import prop_split, customExportWarning from ..cutscene.constants import ootEnumCSWriteType from ..collection_utility import drawCollectionOps, drawAddButton -from ..utility import onMenuTabChange, onHeaderMenuTabChange, drawEnumWithCustom +from ..utility import onMenuTabChange, onHeaderMenuTabChange, drawEnumWithCustom, is_oot_features, getEnumIndex +from ..animated_mats.properties import Z64_AnimatedMaterial from ..constants import ( ootEnumMusicSeq, @@ -30,7 +32,6 @@ ootEnumCameraMode, ootEnumAudioSessionPreset, ootEnumHeaderMenu, - ootEnumDrawConfig, ootEnumHeaderMenuComplete, ) @@ -39,6 +40,7 @@ ("Lighting", "Lighting", "Lighting"), ("Cutscene", "Cutscene", "Cutscene"), ("Exits", "Exits", "Exits"), + ("AnimMats", "Material Anim.", "Material Anim."), ] ootEnumSceneMenu = ootEnumSceneMenuAlternate + [ ("Alternate", "Alternate", "Alternate"), @@ -245,13 +247,17 @@ def draw_props(self, layout: UILayout, index: Optional[int], header_index: int, class OOTSceneTableEntryProperty(PropertyGroup): - drawConfig: EnumProperty(items=ootEnumDrawConfig, name="Scene Draw Config", default="SDC_DEFAULT") + drawConfig: EnumProperty( + items=lambda self, context: game_data.z64.get_enum("drawConfig"), name="Scene Draw Config", default=1 + ) drawConfigCustom: StringProperty(name="Scene Draw Config Custom") - hasTitle: BoolProperty(default=True) def draw_props(self, layout: UILayout): drawEnumWithCustom(layout, self, "drawConfig", "Draw Config", "") + if "mat_anim" in self.drawConfig and is_oot_features(): + layout.label(text="This draw config requires MM features to be enabled.", icon="ERROR") + class OOTExtraCutsceneProperty(PropertyGroup): csObject: PointerProperty( @@ -332,7 +338,57 @@ class OOTSceneHeaderProperty(PropertyGroup): name="Title Card", default="none", description="Segment name of the title card to use" ) - def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, objName: str): + reuse_anim_mat: BoolProperty(default=False) + reuse_anim_mat_cs_index: IntProperty(min=game_data.z64.cs_index_start, default=game_data.z64.cs_index_start) + animated_material: PointerProperty(type=Z64_AnimatedMaterial) + + internal_anim_mat_header: StringProperty(default="Child Day") # used for the export + internal_header_index: IntProperty(min=1, default=1) # used for the UI + reuse_anim_mat_header: EnumProperty( + items=lambda self, context: self.get_anim_mat_header_list(), + set=lambda self, value: self.on_am_header_set(value), + get=lambda self: self.on_am_header_get(), + ) + + def get_anim_mat_header_list(self): + # all but child night + enum_am_headers_1 = ootEnumHeaderMenuComplete.copy() + enum_am_headers_1.pop(1) + + # all but adult day + enum_am_headers_2 = ootEnumHeaderMenuComplete.copy() + enum_am_headers_2.pop(2) + + # all but adult night + enum_am_headers_3 = ootEnumHeaderMenuComplete.copy() + enum_am_headers_3.pop(3) + + enum_am_headers_4 = ootEnumHeaderMenuComplete.copy() + + am_enum_map = { + 1: enum_am_headers_1, + 2: enum_am_headers_2, + 3: enum_am_headers_3, + 4: enum_am_headers_4, + } + + return am_enum_map[self.internal_header_index] + + def on_am_header_set(self, value): + enum = self.get_anim_mat_header_list() + self.internal_anim_mat_header = enum[value][0] + + def on_am_header_get(self): + index = getEnumIndex(self.get_anim_mat_header_list(), self.internal_anim_mat_header) + return index if index is not None else 0 + + def draw_props( + self, + layout: UILayout, + dropdownLabel: str, + headerIndex: int, + obj: Object, + ): from .operators import OOT_SearchMusicSeqEnumOperator # temp circular import fix if dropdownLabel is not None: @@ -340,18 +396,19 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj if not self.expandTab: return if headerIndex is not None and headerIndex > 3: - drawCollectionOps(layout, headerIndex - game_data.z64.cs_index_start, "Scene", None, objName) + drawCollectionOps(layout, headerIndex - game_data.z64.cs_index_start, "Scene", None, obj.name) if headerIndex is not None and headerIndex > 0 and headerIndex < game_data.z64.cs_index_start: layout.prop(self, "usePreviousHeader", text="Use Previous Header") if self.usePreviousHeader: return + menu_box = layout.grid_flow(row_major=True, align=True, columns=3) if headerIndex is None or headerIndex == 0: - layout.row().prop(self, "menuTab", expand=True) + menu_box.prop(self, "menuTab", expand=True) menuTab = self.menuTab else: - layout.row().prop(self, "altMenuTab", expand=True) + menu_box.prop(self, "altMenuTab", expand=True) menuTab = self.altMenuTab if menuTab == "General": @@ -372,7 +429,7 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj drawEnumWithCustom(skyboxAndSound, self, "skyboxCloudiness", "Cloudiness", "") drawEnumWithCustom(skyboxAndSound, self, "musicSeq", "Music Sequence", "") musicSearch = skyboxAndSound.operator(OOT_SearchMusicSeqEnumOperator.bl_idname, icon="VIEWZOOM") - musicSearch.objName = objName + musicSearch.objName = obj.name musicSearch.headerIndex = headerIndex if headerIndex is not None else 0 drawEnumWithCustom(skyboxAndSound, self, "nightSeq", "Nighttime SFX", "") drawEnumWithCustom(skyboxAndSound, self, "audioSessionPreset", "Audio Session Preset", "") @@ -388,15 +445,15 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj drawEnumWithCustom(lighting, self, "skyboxLighting", "Lighting Mode", "") if self.skyboxLighting == "LIGHT_MODE_TIME": # Time of Day - self.timeOfDayLights.draw_props(lighting.box(), None, headerIndex, objName) + self.timeOfDayLights.draw_props(lighting.box(), None, headerIndex, obj.name) for i, tod_light in enumerate(self.tod_lights): - tod_light.draw_props(lighting.box(), i, headerIndex, objName) - drawAddButton(lighting, len(self.tod_lights), "ToD Light", headerIndex, objName) + tod_light.draw_props(lighting.box(), i, headerIndex, obj.name) + drawAddButton(lighting, len(self.tod_lights), "ToD Light", headerIndex, obj.name) else: for i in range(len(self.lightList)): - self.lightList[i].draw_props(lighting, f"Lighting {i}", True, i, headerIndex, objName, "Light") - drawAddButton(lighting, len(self.lightList), "Light", headerIndex, objName) + self.lightList[i].draw_props(lighting, f"Lighting {i}", True, i, headerIndex, obj.name, "Light") + drawAddButton(lighting, len(self.lightList), "Light", headerIndex, obj.name) elif menuTab == "Cutscene": cutscene = layout.column() @@ -413,18 +470,43 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj cutscene.label(text="Extra cutscenes (not in any header):") for i in range(len(self.extraCutscenes)): box = cutscene.box().column() - drawCollectionOps(box, i, "extraCutscenes", None, objName, True) + drawCollectionOps(box, i, "extraCutscenes", None, obj.name, True) box.prop(self.extraCutscenes[i], "csObject", text="CS obj") if len(self.extraCutscenes) == 0: - drawAddButton(cutscene, 0, "extraCutscenes", 0, objName) + drawAddButton(cutscene, 0, "extraCutscenes", 0, obj.name) elif menuTab == "Exits": exitBox = layout.column() exitBox.box().label(text="Exit List") for i in range(len(self.exitList)): - self.exitList[i].draw_props(exitBox, i, headerIndex, objName) + self.exitList[i].draw_props(exitBox, i, headerIndex, obj.name) + + drawAddButton(exitBox, len(self.exitList), "Exit", headerIndex, obj.name) + + elif menuTab == "AnimMats": + if headerIndex is not None: + layout.prop(self, "reuse_anim_mat", text="Use Existing Material Anim.") - drawAddButton(exitBox, len(self.exitList), "Exit", headerIndex, objName) + if "mat_anim" not in obj.ootSceneHeader.sceneTableEntry.drawConfig: + wrong_box = layout.box().column() + wrong_box.label(text="Wrong Draw Config", icon="ERROR") + + if bpy.context.scene.ootSceneExportSettings.customExport: + wrong_box.label(text="Make sure the `scene_table.h` entry is using") + wrong_box.label(text="the right draw config.") + else: + wrong_box.label(text="Make sure one of the 'Material Animated'") + wrong_box.label(text="draw configs is selected otherwise") + wrong_box.label(text="animated materials won't be exported.") + + if headerIndex is not None and headerIndex > 0 and self.reuse_anim_mat: + pass + prop_split(layout, self, "reuse_anim_mat_header", "Use Material Anim. from") + + if self.internal_anim_mat_header == "Cutscene": + prop_split(layout, self, "reuse_anim_mat_cs_index", "Cutscene Index") + else: + self.animated_material.draw_props(layout, obj, None, headerIndex) def update_cutscene_index(self: "OOTAlternateSceneHeaderProperty", context: Context): @@ -441,30 +523,35 @@ class OOTAlternateSceneHeaderProperty(PropertyGroup): cutsceneHeaders: CollectionProperty(type=OOTSceneHeaderProperty) headerMenuTab: EnumProperty(name="Header Menu", items=ootEnumHeaderMenu, update=onHeaderMenuTabChange) - currentCutsceneIndex: IntProperty(default=1, update=update_cutscene_index) + currentCutsceneIndex: IntProperty(default=game_data.z64.cs_index_start, update=update_cutscene_index) - def draw_props(self, layout: UILayout, objName: str): + def draw_props(self, layout: UILayout, obj: Object): headerSetup = layout.column() - # headerSetup.box().label(text = "Alternate Headers") headerSetupBox = headerSetup.column() + menu_tab_map = { + "Child Night": ("childNightHeader", 1), + "Adult Day": ("adultDayHeader", 2), + "Adult Night": ("adultNightHeader", 3), + "Cutscene": ("cutsceneHeaders", game_data.z64.cs_index_start), + } + headerSetupBox.row().prop(self, "headerMenuTab", expand=True) - if self.headerMenuTab == "Child Night": - self.childNightHeader.draw_props(headerSetupBox, None, 1, objName) - elif self.headerMenuTab == "Adult Day": - self.adultDayHeader.draw_props(headerSetupBox, None, 2, objName) - elif self.headerMenuTab == "Adult Night": - self.adultNightHeader.draw_props(headerSetupBox, None, 3, objName) - elif self.headerMenuTab == "Cutscene": + attr_name, header_index = menu_tab_map[self.headerMenuTab] + + if header_index < 4: + getattr(self, attr_name).draw_props(headerSetupBox, None, header_index, obj) + else: prop_split(headerSetup, self, "currentCutsceneIndex", "Cutscene Index") - drawAddButton(headerSetup, len(self.cutsceneHeaders), "Scene", None, objName) + drawAddButton(headerSetup, len(self.cutsceneHeaders), "Scene", None, obj.name) index = self.currentCutsceneIndex if index - game_data.z64.cs_index_start < len(self.cutsceneHeaders): - self.cutsceneHeaders[index - game_data.z64.cs_index_start].draw_props(headerSetup, None, index, objName) + self.cutsceneHeaders[index - game_data.z64.cs_index_start].draw_props(headerSetup, None, index, obj) else: headerSetup.label(text="No cutscene header for this index.", icon="QUESTION") +# TODO: move to HackerOoT properties.py class OOTBootupSceneOptions(PropertyGroup): bootToScene: BoolProperty(default=False, name="Boot To Scene") overrideHeader: BoolProperty(default=False, name="Override Header") @@ -557,6 +644,7 @@ class OOTImportSceneSettingsProperty(PropertyGroup): includePaths: BoolProperty(name="Paths", default=True) includeWaterBoxes: BoolProperty(name="Water Boxes", default=True) includeCutscenes: BoolProperty(name="Cutscenes", default=False) + includeAnimatedMats: BoolProperty(name="Animated Materials", default=False) option: EnumProperty(items=ootEnumSceneID, default="SCENE_DEKU_TREE") def draw_props(self, layout: UILayout, sceneOption: str): @@ -575,6 +663,11 @@ def draw_props(self, layout: UILayout, sceneOption: str): includeButtons3.prop(self, "includePaths", toggle=1) includeButtons3.prop(self, "includeWaterBoxes", toggle=1) includeButtons3.prop(self, "includeCutscenes", toggle=1) + + includeButtons4 = col.row(align=True) + if not is_oot_features(): + includeButtons4.prop(self, "includeAnimatedMats", toggle=1) + col.prop(self, "isCustomDest") if self.isCustomDest: diff --git a/fast64_internal/z64/tools/operators.py b/fast64_internal/z64/tools/operators.py index 09bed65b5..0d8e26a8c 100644 --- a/fast64_internal/z64/tools/operators.py +++ b/fast64_internal/z64/tools/operators.py @@ -1,11 +1,12 @@ import bpy from mathutils import Vector -from bpy.ops import mesh, object, curve +from bpy.ops import mesh, object from bpy.types import Operator, Object, Context from bpy.props import FloatProperty, StringProperty, EnumProperty, BoolProperty + from ...operators import AddWaterBox, addMaterialByName -from ...utility import parentObject, setOrigin +from ...utility import parentObject, setOrigin, get_new_empty_object from ..cutscene.motion.utility import setupCutscene, createNewCameraShot from ..utility import getNewPath from .quick_import import QuickImportAborted, quick_import_exec @@ -301,3 +302,54 @@ def execute(self, context: Context): self.report({"ERROR"}, e.message) return {"CANCELLED"} return {"FINISHED"} + + +class Z64_AddAnimatedMaterial(Operator): + bl_idname = "object.z64_add_animated_material" + bl_label = "Add Animated Materials" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Create a new Animated Material empty object." + + add_test_color: BoolProperty(default=False) + obj_name: StringProperty(default="Actor Animated Materials") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "obj_name", text="Name") + self.layout.prop(self, "add_test_color", text="Add Color Non-linear Interpolation Example") + + def execute(self, context: Context): + new_obj = get_new_empty_object(self.obj_name) + new_obj.ootEmptyType = "Animated Materials" + + if self.add_test_color: + am_props = new_obj.fast64.oot.animated_materials + new_am = am_props.items.add() + new_am_item = new_am.entries.add() + new_am_item.type = "color_nonlinear_interp" + new_am_item.color_params.keyframe_length = 60 + + keyframe_1 = new_am_item.color_params.keyframes.add() + keyframe_1.frame_num = 0 + keyframe_1.prim_lod_frac = 128 + + keyframe_2 = new_am_item.color_params.keyframes.add() + keyframe_2.frame_num = 5 + keyframe_2.prim_lod_frac = 128 + + keyframe_3 = new_am_item.color_params.keyframes.add() + keyframe_3.frame_num = 30 + keyframe_3.prim_lod_frac = 128 + keyframe_3.prim_color = (1.0, 0.18, 0.0, 1.0) # FF7600 + + keyframe_4 = new_am_item.color_params.keyframes.add() + keyframe_4.frame_num = 55 + keyframe_4.prim_lod_frac = 128 + + keyframe_5 = new_am_item.color_params.keyframes.add() + keyframe_5.frame_num = 59 + keyframe_5.prim_lod_frac = 128 + + return {"FINISHED"} diff --git a/fast64_internal/z64/tools/panel.py b/fast64_internal/z64/tools/panel.py index ee3331466..a710142be 100644 --- a/fast64_internal/z64/tools/panel.py +++ b/fast64_internal/z64/tools/panel.py @@ -9,6 +9,7 @@ OOT_AddPath, OOTClearTransformAndLock, OOTQuickImport, + Z64_AddAnimatedMaterial, ) @@ -26,6 +27,7 @@ def draw(self, context): col.operator(OOT_AddPath.bl_idname) col.operator(OOTClearTransformAndLock.bl_idname) col.operator(OOTQuickImport.bl_idname) + col.operator(Z64_AddAnimatedMaterial.bl_idname) oot_operator_panel_classes = [ @@ -41,6 +43,7 @@ def draw(self, context): OOT_AddPath, OOTClearTransformAndLock, OOTQuickImport, + Z64_AddAnimatedMaterial, ] diff --git a/fast64_internal/z64/utility.py b/fast64_internal/z64/utility.py index 7e84b233d..e2a222525 100644 --- a/fast64_internal/z64/utility.py +++ b/fast64_internal/z64/utility.py @@ -8,9 +8,12 @@ from mathutils import Vector from bpy.types import Object from typing import Callable, Optional, TYPE_CHECKING, List -from .constants import ootSceneIDToName from dataclasses import dataclass +from ..game_data import game_data +from .constants import ootSceneIDToName + + from ..utility import ( PluginError, prop_split, @@ -603,7 +606,9 @@ def __init__(self, position, scale, emptyScale): self.cullDepth = abs(int(round(scale[0] * emptyScale))) -def setCustomProperty(data: any, prop: str, value: str, enumList: list[tuple[str, str, str]] | None): +def setCustomProperty( + data: any, prop: str, value: str, enumList: list[tuple[str, str, str]] | None, custom_name: Optional[str] = None +): if enumList is not None: if value in [enumItem[0] for enumItem in enumList]: setattr(data, prop, value) @@ -619,7 +624,7 @@ def setCustomProperty(data: any, prop: str, value: str, enumList: list[tuple[str pass setattr(data, prop, "Custom") - setattr(data, prop + str("Custom"), value) + setattr(data, custom_name if custom_name is not None else f"{prop}Custom", value) def getCustomProperty(data, prop): @@ -779,6 +784,17 @@ def callback(thisHeader, otherObj: bpy.types.Object): onHeaderPropertyChange(self, context, callback) +def on_alt_menu_tab_change(self, context: bpy.types.Context): + if self.headerMenuTab == "Child Night": + self.childNightHeader.internal_header_index = 1 + elif self.headerMenuTab == "Adult Day": + self.adultDayHeader.internal_header_index = 2 + elif self.headerMenuTab == "Adult Night": + self.adultNightHeader.internal_header_index = 3 + elif self.headerMenuTab == "Cutscene" and (self.currentCutsceneIndex - 4) < len(self.cutsceneHeaders): + self.cutsceneHeaders[self.currentCutsceneIndex - 4].internal_header_index = 4 + + def onHeaderMenuTabChange(self, context: bpy.types.Context): def callback(thisHeader, otherObj: bpy.types.Object): if otherObj.ootEmptyType == "Scene": @@ -791,6 +807,11 @@ def callback(thisHeader, otherObj: bpy.types.Object): onHeaderPropertyChange(self, context, callback) + active_obj = context.view_layer.objects.active + if active_obj is not None and active_obj.ootEmptyType == "Scene": + # not using `self` is intended + on_alt_menu_tab_change(context.view_layer.objects.active.ootAlternateSceneHeaders, context) + def onHeaderPropertyChange(self, context: bpy.types.Context, callback: Callable[[any, bpy.types.Object], None]): if not bpy.context.scene.fast64.oot.headerTabAffectsVisibility or bpy.context.scene.ootActiveHeaderLock: @@ -1018,3 +1039,24 @@ def get_actor_prop_from_obj(actor_obj: Object) -> "OOTActorProperty": raise PluginError(f"ERROR: Empty type not supported: {actor_obj.ootEmptyType}") return actor_prop + + +def get_list_tab_text(base_text: str, list_length: int): + if list_length > 0: + items_amount = f"{list_length} Item{'s' if list_length > 1 else ''}" + else: + items_amount = "Empty" + + return f"{base_text} ({items_amount})" + + +def is_oot_features(): + return ( + game_data.z64.is_oot() + and not bpy.context.scene.fast64.oot.mm_features + and not bpy.context.scene.fast64.oot.hackerFeaturesEnabled + ) + + +def is_hackeroot(): + return game_data.z64.is_oot() and bpy.context.scene.fast64.oot.hackerFeaturesEnabled