From 5ddeec52b0692d640120c40fdf49bc771f617855 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:44:11 +0100 Subject: [PATCH 01/20] meta stuff --- __init__.py | 2 +- fast64_internal/utility.py | 11 ++++++ fast64_internal/z64/__init__.py | 53 +++++++++++++++------------ fast64_internal/z64/file_settings.py | 13 +++++-- fast64_internal/z64/importer/scene.py | 20 +++++++--- fast64_internal/z64/utility.py | 32 ++++++++++++++-- 6 files changed, 96 insertions(+), 35 deletions(-) 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/utility.py b/fast64_internal/utility.py index 61b503dcd..1e93b1b82 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -2058,3 +2058,14 @@ def get_include_data(include: str, strip: bool = False): # return the data as a string return data + + +def get_new_empty_object(name: str, location=[0.0, 0.0, 0.0], rotation_euler=[0.0, 0.0, 0.0], scale=[1.0, 1.0, 1.0]): + """Creates and returns a new empty object""" + + new_obj = bpy.data.objects.new(name, None) + bpy.context.scene.collection.objects.link(new_obj) + new_obj.location = location + new_obj.rotation_euler = rotation_euler + new_obj.scale = scale + return new_obj diff --git a/fast64_internal/z64/__init__.py b/fast64_internal/z64/__init__.py index 3f8fac2fc..6f396a02a 100644 --- a/fast64_internal/z64/__init__.py +++ b/fast64_internal/z64/__init__.py @@ -57,6 +57,8 @@ from .spline.properties import spline_props_register, spline_props_unregister from .spline.panels import spline_panels_register, spline_panels_unregister +from .animated_mats.properties import animated_mats_register, animated_mats_unregister + from .tools import ( oot_operator_panel_register, oot_operator_panel_unregister, @@ -110,6 +112,7 @@ 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) def get_extracted_path(self): version = self.oot_version if game_data.z64.is_oot() else self.mm_version @@ -190,7 +193,6 @@ def oot_register(registerPanels): room_props_register() actor_ops_register() actor_props_register() - oot_obj_register() spline_props_register() f3d_props_register() anim_ops_register() @@ -200,6 +202,7 @@ def oot_register(registerPanels): f3d_ops_register() file_register() anim_props_register() + animated_mats_register() csMotion_ops_register() csMotion_props_register() @@ -207,6 +210,8 @@ def oot_register(registerPanels): csMotion_preview_register() cutscene_preview_register() + oot_obj_register() + for cls in oot_classes: register_class(cls) @@ -215,30 +220,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 +234,24 @@ def oot_unregister(unregisterPanels): csMotion_props_unregister() csMotion_ops_unregister() - if unregisterPanels: - oot_panel_unregister() + animated_mats_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() + cutscene_props_unregister() + collision_props_unregister() + collision_ops_unregister() + collections_unregister() + oot_operator_unregister() 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/importer/scene.py b/fast64_internal/z64/importer/scene.py index d4270c5a8..2d02922d2 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 @@ -107,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(): @@ -128,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 @@ -137,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, @@ -153,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, @@ -172,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 diff --git a/fast64_internal/z64/utility.py b/fast64_internal/z64/utility.py index 0fbb28e71..ee9509231 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, @@ -601,7 +604,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) @@ -617,7 +622,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): @@ -1016,3 +1021,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 From d19bcc634a137ffa6a4dae50222f4ff92e9d2366 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:44:44 +0100 Subject: [PATCH 02/20] implement animated materials (import/export) --- .../z64/animated_mats/properties.py | 290 ++++++++++++++++ fast64_internal/z64/collection_utility.py | 50 ++- .../z64/exporter/scene/__init__.py | 1 + .../z64/exporter/scene/animated_mats.py | 327 ++++++++++++++++++ fast64_internal/z64/exporter/scene/general.py | 2 +- fast64_internal/z64/exporter/scene/header.py | 13 + fast64_internal/z64/importer/classes.py | 2 + fast64_internal/z64/importer/scene_header.py | 304 ++++++++++++---- fast64_internal/z64/props_panel_main.py | 13 +- fast64_internal/z64/scene/properties.py | 8 +- 10 files changed, 942 insertions(+), 68 deletions(-) create mode 100644 fast64_internal/z64/animated_mats/properties.py create mode 100644 fast64_internal/z64/exporter/scene/animated_mats.py diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py new file mode 100644 index 000000000..78018f6d0 --- /dev/null +++ b/fast64_internal/z64/animated_mats/properties.py @@ -0,0 +1,290 @@ +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 ...utility import prop_split +from ..collection_utility import drawAddButton, drawCollectionOps, getCollection +from ..utility import get_list_tab_text + + +# 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) + + prim_lod_frac: IntProperty(name="Primitive LOD Frac", min=0, max=255) + 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, parent_index: int, index: int): + drawCollectionOps(layout, index, "Animated Mat. Color", None, owner.name, collection_index=parent_index) + 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") + prop_split(layout, self, "env_color", "Environment Color") + + +class Z64_AnimatedMatColorParams(PropertyGroup): + keyframe_length: IntProperty(name="Keyframe Length", min=0) + keyframes: CollectionProperty(type=Z64_AnimatedMatColorKeyFrame) + + # ui only props + show_entries: BoolProperty(default=False) + + def draw_props(self, layout: UILayout, owner: Object, parent_index: int): + prop_split(layout, self, "keyframe_length", "Keyframe Length") + + 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, parent_index, i) + + drawAddButton(layout, len(self.keyframes), "Animated Mat. Color", None, owner.name, parent_index) + + +class Z64_AnimatedMatTexScrollItem(PropertyGroup): + step_x: IntProperty(default=0) + step_y: IntProperty(default=0) + width: IntProperty(min=0) + height: IntProperty(min=0) + + def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: int): + drawCollectionOps(layout, index, "Animated Mat. Scroll", None, owner.name, collection_index=parent_index) + 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): + entries: CollectionProperty(type=Z64_AnimatedMatTexScrollItem) + + # ui only props + show_entries: BoolProperty(default=False) + + def draw_props(self, layout: UILayout, owner: Object, parent_index: int): + prop_text = get_list_tab_text("Tex. Scroll", len(self.entries)) + layout.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, owner, parent_index, i) + + drawAddButton(layout, len(self.entries), "Animated Mat. Scroll", None, owner.name, parent_index) + + +class Z64_AnimatedMatTexCycleTexture(PropertyGroup): + symbol: StringProperty(name="Texture Symbol") + + def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: int): + drawCollectionOps( + layout, index, "Animated Mat. Cycle (Texture)", None, owner.name, collection_index=parent_index + ) + prop_split(layout, self, "symbol", "Texture Symbol") + + +class Z64_AnimatedMatTexCycleKeyFrame(PropertyGroup): + texture_index: IntProperty(min=0) + + def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: int): + drawCollectionOps(layout, index, "Animated Mat. Cycle (Index)", None, owner.name, collection_index=parent_index) + 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, 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, parent_index, i) + drawAddButton( + texture_box, len(self.textures), "Animated Mat. Cycle (Texture)", None, owner.name, parent_index + ) + + 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, parent_index, i) + drawAddButton(index_box, len(self.keyframes), "Animated Mat. Cycle (Index)", None, owner.name, parent_index) + + +class Z64_AnimatedMaterialItem(PropertyGroup): + """see the `AnimatedMaterial` struct from `z64scene.h`""" + + segment_num: IntProperty(name="Segment Number", min=8, max=13, default=8) + type: EnumProperty( + name="Draw Handler Type", items=enum_anim_mat_type, default=2, description="Index to `sMatAnimDrawHandlers`" + ) + 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 draw_props(self, layout: UILayout, owner: Object, 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.", None, owner.name) + + prop_split(layout, self, "segment_num", "Segment Number") + + layout_type = layout.column() + prop_split(layout_type, self, "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, owner, index) + elif self.type in {"color", "color_lerp", "color_nonlinear_interp"}: + self.color_params.draw_props(layout_type, owner, index) + elif self.type == "tex_cycle": + self.tex_cycle_params.draw_props(layout_type, owner, index) + + +class Z64_AnimatedMaterial(PropertyGroup): + """Defines an Animated Material array""" + + header_index: IntProperty(name="Header Index", min=-1, default=-1, description="Header Index, -1 means all headers") + 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: int): + 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: + drawCollectionOps(layout, index, "Animated Mat. List", None, owner.name) + prop_split(layout, self, "header_index", "Header Index") + + 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, i) + + drawAddButton(layout_entries, len(self.entries), "Animated Mat.", None, owner.name) + + +class Z64_AnimatedMaterialProperty(PropertyGroup): + """List of Animated Material arrays""" + + mode: EnumProperty(name="Export To", items=enum_mode) + + # 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_split(layout, self, "mode", "Export To") + + prop_text = get_list_tab_text("Animated Materials List", len(self.items)) + layout_entries = layout.box().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) + + drawAddButton(layout_entries, len(self.items), "Animated Mat. List", None, owner.name) + + +classes = ( + Z64_AnimatedMatColorKeyFrame, + Z64_AnimatedMatColorParams, + Z64_AnimatedMatTexScrollItem, + Z64_AnimatedMatTexScrollParams, + Z64_AnimatedMatTexCycleTexture, + Z64_AnimatedMatTexCycleKeyFrame, + Z64_AnimatedMatTexCycleParams, + Z64_AnimatedMaterialItem, + Z64_AnimatedMaterial, + Z64_AnimatedMaterialProperty, +) + + +def animated_mats_register(): + for cls in classes: + register_class(cls) + + +def animated_mats_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 98da2cc66..e06f58d27 100644 --- a/fast64_internal/z64/collection_utility.py +++ b/fast64_internal/z64/collection_utility.py @@ -14,13 +14,19 @@ class OOTCollectionAdd(Operator): option: IntProperty() collectionType: StringProperty(default="Actor") subIndex: IntProperty(default=0) + collection_index: IntProperty(default=0) objName: StringProperty() def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) collection.add() collection.move(len(collection) - 1, 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) + return {"FINISHED"} @@ -32,11 +38,17 @@ class OOTCollectionRemove(Operator): option: IntProperty() collectionType: StringProperty(default="Actor") subIndex: IntProperty(default=0) + collection_index: IntProperty(default=0) objName: StringProperty() def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) 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) + return {"FINISHED"} @@ -48,12 +60,13 @@ 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) return {"FINISHED"} @@ -66,7 +79,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 @@ -84,6 +97,24 @@ 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 == "Animated Mat.": + collection = obj.fast64.oot.animated_materials.items[subIndex].entries + elif collectionType == "Animated Mat. Color": + collection = obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].color_params.keyframes + elif collectionType == "Animated Mat. Scroll": + collection = ( + obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].tex_scroll_params.entries + ) + elif collectionType == "Animated Mat. Cycle (Index)": + collection = ( + obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].tex_cycle_params.keyframes + ) + elif collectionType == "Animated Mat. Cycle (Texture)": + collection = ( + obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].tex_cycle_params.textures + ) elif collectionType == "Curve": collection = obj.ootSplineProperty.headerSettings.cutsceneHeaders elif collectionType.startswith("CSHdr."): @@ -113,7 +144,7 @@ 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): if subIndex is None: subIndex = 0 addOp = layout.operator(OOTCollectionAdd.bl_idname) @@ -121,9 +152,12 @@ def drawAddButton(layout, index, collectionType, subIndex, objName): addOp.collectionType = collectionType addOp.subIndex = subIndex addOp.objName = objName + addOp.collection_index = collection_index -def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd=True, compact=False): +def drawCollectionOps( + layout, index, collectionType, subIndex, objName, allowAdd=True, compact=False, collection_index: int = 0 +): if subIndex is None: subIndex = 0 @@ -138,12 +172,14 @@ def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd addOp.collectionType = collectionType addOp.subIndex = subIndex addOp.objName = objName + addOp.collection_index = collection_index 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 moveUp = buttons.operator(OOTCollectionMove.bl_idname, text="Up" if not compact else "", icon="TRIA_UP") moveUp.option = index @@ -151,6 +187,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 @@ -158,6 +195,7 @@ def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd moveDown.collectionType = collectionType moveDown.subIndex = subIndex moveDown.objName = objName + moveDown.collection_index = collection_index collections_classes = ( diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index 1959fcdfb..2e993b4fb 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -150,6 +150,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 and curHeader.anim_mat.is_used() else "") + Utility.getEndCmd() + "};\n\n" ) 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..19b720cea --- /dev/null +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -0,0 +1,327 @@ +from dataclasses import dataclass +from bpy.types import Object + +from ....utility import CData, PluginError, exportColor, scaleToU8, indent +from ...utility import getObjectList, is_oot_features, is_hackeroot +from ...animated_mats.properties import ( + Z64_AnimatedMatColorParams, + Z64_AnimatedMatTexScrollParams, + Z64_AnimatedMatTexCycleParams, + Z64_AnimatedMaterial, +) + + +class AnimatedMatColorParams: + def __init__( + self, + props: Z64_AnimatedMatColorParams, + segment_num: int, + type_num: int, + base_name: str, + header_index: int, + index: int, + ): + # the code adds back 7 when processing animated materials + self.segment_num = segment_num - 7 + self.type_num = type_num + self.base_name = base_name + self.header_index = header_index + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}ColorParams{self.header_suffix}" + self.frame_length = 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)) + self.env_colors.append(tuple(exportColor(keyframe.env_color[0:3]) + [scaleToU8(keyframe.env_color[3])])) + self.frames.append(keyframe.frame_num) + + if 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) + assert len(self.frames) == len(self.prim_colors) == len(self.env_colors) + + def to_c(self): + 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}" + + # .h + data.header = ( + f"extern F3DPrimColor {prim_array_name}[];\n" + + f"extern F3DEnvColor {env_array_name}[];\n" + + f"extern u16 {frames_array_name}[];\n" + + 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" + ) + + ( + (f"u16 {frames_array_name}[]" + " = {\n" + indent) + + f",\n{indent}".join(f"{entry}" for entry in self.frames) + + "\n};\n\n" + ) + + ( + (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, + header_index: int, + index: int, + ): + # the code adds back 7 when processing animated materials + self.segment_num = segment_num - 7 + self.type_num = type_num + self.base_name = base_name + self.header_index = header_index + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}TexScrollParams{self.header_suffix}" + self.entries: list[str] = [] + + for item in props.entries: + self.entries.append("{ " + f"{item.step_x}, {item.step_y}, {item.width}, {item.height}" + " }") + + def to_c(self): + data = CData() + params_name = f"AnimatedMatTexScrollParams {self.name}[]" + + # .h + data.header = f"extern {params_name};\n" + + # .c + data.source = ( + f"{params_name}" + " = {\n" + indent + f",\n{indent}".join(entry for entry in self.entries) + "\n};\n\n" + ) + + return data + + +class AnimatedMatTexCycleParams: + def __init__( + self, + props: Z64_AnimatedMatTexCycleParams, + segment_num: int, + type_num: int, + base_name: str, + header_index: int, + index: int, + ): + # the code adds back 7 when processing animated materials + self.segment_num = segment_num - 7 + self.type_num = type_num + self.base_name = base_name + self.header_index = header_index + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}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) + + def to_c(self): + 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 + 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, scene_header_index: int): + self.name = base_name + self.scene_header_index = scene_header_index + self.header_index = props.header_index + self.entries: list[AnimatedMatColorParams | AnimatedMatTexScrollParams | AnimatedMatTexCycleParams] = [] + + 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): + if item.type != "Custom": + class_def, prop_name, type_num = type_list_map[item.type] + # example: `self.tex_scroll_entries.append(AnimatedMatTexScrollParams(item.tex_scroll_params, base_name, header_index))` + self.entries.append( + class_def(getattr(item, prop_name), item.segment_num, type_num, base_name, self.header_index, i) + ) + + # the last entry's segment need to be negative + if len(self.entries) > 0 and self.entries[-1].segment_num > 0: + self.entries[-1].segment_num = -self.entries[-1].segment_num + + def to_c(self): + data = CData() + + for entry in self.entries: + if entry.header_index == -1 or entry.header_index == self.scene_header_index: + data.append(entry.to_c()) + + if len(self.entries) > 0: + array_name = f"AnimatedMaterial {self.name}[]" + + # .h + data.header += f"extern {array_name};" + + # .c + data.source += ( + (array_name + " = {\n" + indent) + + f",\n{indent}".join( + "{ " + + f"{entry.segment_num} /* {abs(entry.segment_num) + 7} */, " + + f"{entry.type_num}, " + + f"{'&' if entry.type_num in {2, 3, 4, 5} else ''}{entry.name}" + + " }" + for entry in self.entries + ) + + "\n};\n" + ) + else: + raise PluginError("ERROR: Trying to export animated materials with empty entries!") + + return data + + +@dataclass +class SceneAnimatedMaterial: + """This class hosts exit data""" + + name: str + header_index: int + 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: + if obj.fast64.oot.animated_materials.mode == "Scene": + entries.extend( + [AnimatedMaterial(item, name, header_index) for item in obj.fast64.oot.animated_materials.items] + ) + + last_index = -1 + for entry in entries: + if entry.header_index >= 0: + if entry.header_index > last_index: + last_index = entry.header_index + else: + raise PluginError("ERROR: Animated Materials header indices are not consecutives!") + + return SceneAnimatedMaterial(name, header_index, entries) + + def is_used(self): + return not is_oot_features() and len(self.entries) > 0 + + def get_cmd(self): + """Returns the sound settings, misc settings, special files and skybox settings scene commands""" + + if is_hackeroot(): + 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): + data = CData() + + if is_hackeroot(): + data.source += "#if ENABLE_ANIMATED_MATERIALS\n" + data.header += "#if ENABLE_ANIMATED_MATERIALS\n" + + for entry in self.entries: + data.append(entry.to_c()) + + if is_hackeroot(): + data.source += "#endif\n\n" + data.header += "#endif\n\n" + else: + data.source += "\n" + data.header += "\n" + + return data diff --git a/fast64_internal/z64/exporter/scene/general.py b/fast64_internal/z64/exporter/scene/general.py index 572574590..3f6d5f062 100644 --- a/fast64_internal/z64/exporter/scene/general.py +++ b/fast64_internal/z64/exporter/scene/general.py @@ -37,7 +37,7 @@ def from_data(raw_data: str, not_zapd_assets: bool): colors_and_dirs.append([hexOrDecInt(value) for value in match.group(2).split(",")]) 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..f0f1afd2e 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,6 +27,9 @@ class SceneHeader: spawns: Optional[SceneSpawns] path: Optional[ScenePathways] + # MM (or HackerOoT) + anim_mat: Optional[SceneAnimatedMaterial] + @staticmethod def new( name: str, props: OOTSceneHeaderProperty, sceneObj: Object, transform: Matrix, headerIndex: int, useMacros: bool @@ -39,6 +45,9 @@ def new( entranceActors, SceneSpawns(f"{name}_entranceList", entranceActors.entries), ScenePathways.new(f"{name}_pathway", sceneObj, transform, headerIndex), + SceneAnimatedMaterial.new(f"{name}_AnimatedMaterial", sceneObj, headerIndex) + if not is_oot_features() + else None, ) def getC(self): @@ -67,6 +76,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 and self.anim_mat.is_used(): + headerData.append(self.anim_mat.to_c()) + return headerData 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_header.py b/fast64_internal/z64/importer/scene_header.py index 5afed4355..373b821e3 100644 --- a/fast64_internal/z64/importer/scene_header.py +++ b/fast64_internal/z64/importer/scene_header.py @@ -8,12 +8,12 @@ from typing import Optional from ...game_data import game_data -from ...utility import PluginError, readFile, parentObject, hexOrDecInt, gammaInverse +from ...utility import PluginError, readFile, parentObject, hexOrDecInt, gammaInverse, get_new_empty_object 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 getEvalParams, setCustomProperty +from ..utility import getEvalParams, setCustomProperty, getObjectList, is_hackeroot from .constants import headerNames from .utility import getDataMatch, stripName, parse_commands_data from .classes import SharedSceneData @@ -21,6 +21,7 @@ from .actor import parseTransActorList, parseSpawnList, parseEntranceList from .scene_collision import parseCollisionHeader from .scene_pathways import parsePathList +from ..animated_mats.properties import enum_anim_mat_type from ..constants import ( ootEnumAudioSessionPreset, @@ -235,6 +236,122 @@ def parseAlternateSceneHeaders( ) +animated_material_first_list_name = "" +anim_mat_type_to_struct = { + 0: "AnimatedMatTexScrollParams", + 1: "AnimatedMatTexScrollParams", + 2: "AnimatedMatColorParams", + 3: "AnimatedMatColorParams", + 4: "AnimatedMatColorParams", + 5: "AnimatedMatTexCycleParams", +} + + +def parse_animated_material(scene_obj: bpy.types.Object, header_index: int, scene_data: str, list_name: str): + global animated_material_first_list_name + + data_match = getDataMatch(scene_data, list_name, "AnimatedMaterial", "animated material") + anim_mat_data = data_match.strip().split("\n") + + if header_index == 0: + animated_material_first_list_name = list_name + anim_mat_obj = get_new_empty_object("Animated Material") + anim_mat_obj.ootEmptyType = "Animated Materials" + parentObject(scene_obj, anim_mat_obj) + else: + obj_list = getObjectList(scene_obj.children_recursive, "EMPTY", "Animated Materials") + anim_mat_obj = obj_list[0] + + # if the alternate header is using the first header's data then don't do anything + if header_index > 0 and list_name == animated_material_first_list_name: + return + + anim_mat_props = anim_mat_obj.fast64.oot.animated_materials + anim_mat_item = anim_mat_props.items.add() + anim_mat_item.header_index = header_index + + for data in anim_mat_data: + data = data.replace("{", "").replace("}", "").removesuffix(",").strip() + + split = data.split(", ") + segment = int(split[0], base=0) + type_num = int(split[1], base=0) + data_ptr = split[2].removeprefix("&") + + is_array = type_num in {0, 1} + struct_name = anim_mat_type_to_struct[type_num] + data_match = getDataMatch(scene_data, data_ptr, struct_name, "animated params", is_array, False) + + if is_array: + params_data = data_match.replace("{", "").replace("}", "").replace(" ", "").split("\n") + else: + params_data = data_match.replace("\n", "").replace(" ", "").split(",") + + entry = anim_mat_item.entries.add() + entry.segment_num = abs(segment) + 7 + entry.type = enum_anim_mat_type[type_num + 1][0] + + if struct_name == "AnimatedMatTexScrollParams": + for params in params_data: + if len(params) > 0: + split = params.split(",") + scroll_entry = entry.tex_scroll_params.entries.add() + scroll_entry.step_x = int(split[0], base=0) + scroll_entry.step_y = int(split[1], base=0) + scroll_entry.width = int(split[2], base=0) + scroll_entry.height = int(split[3], base=0) + 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") + + 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_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] + env_color = [hexOrDecInt(elem) for elem in env_color_raw.split(",") if len(elem) > 0] + + color_entry = entry.color_params.keyframes.add() + 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,) + 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(",", "\n").strip().split("\n"): + 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], @@ -254,93 +371,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 HackerOoT) exclusive commands + elif game_data.z64.is_mm() or is_hackeroot(): + if command == "SCENE_CMD_ANIMATED_MATERIAL_LIST": + if sharedSceneData.includeAnimatedMats: + parse_animated_material(sceneObj, headerIndex, 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/props_panel_main.py b/fast64_internal/z64/props_panel_main.py index 5a3663d1f..7a7e97fd9 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,6 +38,7 @@ ("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'), ] @@ -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/properties.py b/fast64_internal/z64/scene/properties.py index 78ba22db7..2605feb92 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -17,7 +17,7 @@ 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 from ..constants import ( ootEnumMusicSeq, @@ -537,6 +537,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): @@ -555,6 +556,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: From 193318760874865b9497226a2d4f8293e48b5ab0 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:08:01 +0100 Subject: [PATCH 03/20] fixed endif in headers for hackeroot --- fast64_internal/z64/exporter/scene/animated_mats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast64_internal/z64/exporter/scene/animated_mats.py b/fast64_internal/z64/exporter/scene/animated_mats.py index 19b720cea..5352170ba 100644 --- a/fast64_internal/z64/exporter/scene/animated_mats.py +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -319,7 +319,7 @@ def to_c(self): if is_hackeroot(): data.source += "#endif\n\n" - data.header += "#endif\n\n" + data.header += "\n#endif\n" else: data.source += "\n" data.header += "\n" From 73d03a61811cecb5766e8842c2b277c2ccdd659d Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:11:54 +0100 Subject: [PATCH 04/20] add missing include for hackeroot --- fast64_internal/z64/exporter/scene/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index 2e993b4fb..6c77ae9c4 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -8,7 +8,7 @@ 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 @@ -286,6 +286,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", From 328dcd0790e71811cd98f5ac26c7b2f09721997e Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:10:44 +0100 Subject: [PATCH 05/20] write a guide --- fast64_internal/z64/README.md | 61 ++++++++++++++++++++ images/z64/animated_materials/am_part_1.png | Bin 0 -> 13698 bytes images/z64/animated_materials/am_part_2.png | Bin 0 -> 24848 bytes images/z64/animated_materials/am_part_3.png | Bin 0 -> 38323 bytes 4 files changed, 61 insertions(+) create mode 100644 images/z64/animated_materials/am_part_1.png create mode 100644 images/z64/animated_materials/am_part_2.png create mode 100644 images/z64/animated_materials/am_part_3.png diff --git a/fast64_internal/z64/README.md b/fast64_internal/z64/README.md index abc3f3386..6584c76c8 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) +11. [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,63 @@ 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. + +**Getting Started** + +To get started 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. + +
+This is how the UI should look like at this point: + +![alt-text](/images/z64/animated_materials/am_part_1.png) +
+ +Note: the `Export To` option doesn't do anything, leave it to `Scene`. In the future you will be able to export to an actor. + +**Creating the animated materials list** + +Click on `Add Item` to add a new animated material list. + +
+This is how the UI should look like at this point: + +![alt-text](/images/z64/animated_materials/am_part_2.png) +
+ +`Header Index` lets you choose which header this list belongs to, a value of `-1` means "every headers". Below you should have the list of the materials you can setup. Click on `Add Item` to add a new item to that list. + +
+This is how the UI should look like at this point: + +![alt-text](/images/z64/animated_materials/am_part_3.png) +
+ +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 it shows the real number then when it exports it corrects that number. It's just how the in-game implementation works. `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 color types you will also have a `Keyframe Length` field, this corresponds to the length of the animation. + +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 + +Note: for the two-textures scroll type you will need to add 2 items per texture since it targets multi-textures (it can be used to animate water for instance). + +All 3 color types will use the same elements: +- `Frame No.`: when to execute this entry (relative to the keyframe length) +- `Primitive LOD Frac`: unknown purpose, feel free to complete! +- `Primitive Color`: the primitive color to apply +- `Environment Color`: the environment color to apply + +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/images/z64/animated_materials/am_part_1.png b/images/z64/animated_materials/am_part_1.png new file mode 100644 index 0000000000000000000000000000000000000000..da4635c81d44f73c093d072a1ba2212a0c8a140b GIT binary patch literal 13698 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ð#&+zlbwr;O+;k!oYfb86doR4{C@fA95vY6#(no*qRYPT9^VBHb?i5#x;Wg-V%VYpkKa zp}c!^I8;Wdc;Df7h7y~DeE)ab9dpj#0pErK>-8pM^Tt6KNGPcIDTjLG`jhmt^M9NP znA>9L5*XMw_){GdKY`IgHA|o=FiAW`2bYyOtja-)EtFopS_D|7w)_q@7CuwyAG+1y zA~&OM)X_u(qyi->nMJBHTKdDXW^(W1YC>?e(L8D8b;=3t$Expad9$0gsVVYH@RVfk)u>X zvn0s|vi%R7T^LrcRbffgrRLqy;@783w8Bmwj!;Mg)S$79(+3jC(56jRBi(_M%jFZ3 zQGO0-+(O-fmG7nXg!7Xm)y z?frukDosw~NWSDe^dDw(Ulm@T<#rRMj~`oH%{Txb;^n=Y-0xOi@<{2(xzX^Yyu7Bq{=fM; zXU1CBcAkLgyEHVf-@RLIERSXx??oNOB_|_`NUDfdM{txhC?h_9mi3=zRa%QLfKdR} z!>Q~1Gj1%dC+QhrXIMR^od3^{Z{grVP$<}c%YNQ@=(M@r!tdX|4;j$l0eSZSg9ULq zet(>C(qhbIVPWx7K|!dY4D*QH4)y)goL9&_@7AbSczyrJ^YDA#;s+RRyhJB#V8D5~ zha5JL^1jQG^sWpykQVFgCS_Drqupo}JGn7breVK}CUBElXh2UG9O}5k)xzkQ&5{vk za=G{bBWpAma=IE>g8lQ%piWuU;Za8YCh_X3t1aLu1`dtC^c{x&xd#5>`4m>Q1%VbCC^O4Lsc0#Rq6BjgSc+criELFHFaRwhy{j0Yqt z1OQRSU{k=eh!`Ykq0lFsoU6r!nTw#hVk!hRA52|KD;SB&&(8j!njyNso*Pr`d*-ZN zs!PtRx79oP14oKp#Q9y6AJAs{G8479n6QChvfiZX;J^t`$koXlMv}F42Zj1!=nFug z)Fh55LGHs~V4wx-**gF|52&#&80iQIF?Sz40CJxA{!a=Z>G9c&6@A$r9UT?H@Qz%~ z8vdsb%y3o1Is-cwemK+v353D#kbKVn;ytAUQ+8H9AWKlx+;5 z1O=;%hoV75g4Xn?2MPrgBY(oLM+rZ#OW1Rd5eSOQ7^pn-RRjp@M2NYV;k8`MO{n=o zF=93VzbbL>` z+I0++lsNlCtiuTd$3KXGn=cl3?mwH)^x3i26p7kQ70XZsb;SCJO7iOPO{Ui>HKp7F zD93A!YndP=1NrrBJ`5ywzyH52geISE;F~LCS)tjOsSIJ}J>I1N0$V zlp!lM)f&fIPQyld$dG~QW$VS4M%Q9Vn2*mw$OGM4n|mMeNLK+xSOU+pa)P*#@H;NEva;=dtj$yZRLgUK+;9iWQ2)(=HQL|iw=!i`(K$N#qFpYRc?AU{05)ksEdXWp1&TM53D?Jubx71>PR?>Aub*98vT-&bHbF1unPI~hdI|4& z=SEN)@j%hfFDm-9Jyj2+eVJrVBXwwE@tZjjE0R%{O~8pJP@!pK&1qPVVK6Tr9JEV5R3xzNPuN!!)>BwYXF;?ZiofV zhtW1aAJ9%n)c<(i+j@R}%%pTgE>r1-(#9}-*l=dli~}1PKH!hI03wAB!pt#*-kU{A zDyx3h8v{dU{V(hRq6_<IAXg1ai81o+_fv3&h|(ig6b#+a%UZJTZ(m( zWr1GydAV78zbZ)&>8&PK>G`npsd(%D%eILsVZhSOm}aGJXz^4}(v?_AJ4X>|MvliJ;4CfEhf zl&_P`(3aEKc-?@@{IZ4)?1E(HJGNB$w)mI40voNg{5EMS@#E>~RjHdZQVLe(A~c>j zTZ~>sNlR#*gcm}^FOy|FzwP)9vZ?EPV|;zAy_Tqvs*jF9LpLW4+*R`6%(ua?+{K7k z$%Ybx0aD<@)C8-rUD*PGJhXPP&q!e4ghIL@`r+9p&M*JUHb(RXz? zz4FIMs$U1FAL6gBK3fIUH@CW4)1NL8$gY?k*tUE8moL(wf_%KyI%`T#LD31q6_h~X zvcv>TcbY9YySn;rQRaSGLqo&tHcEx+-?n=RH;`F3wn_j^)klC7hW5W^-E9P*#}$`fs;+O2_dl|9p+rhaL=b#ppUvm6&?t{Wdl*4k z;p_;3P)=dI0D=3+E20?_=$d=eoGikj>T>1eY9A`-D>fL4FS}NcG*WKHCN|?gcpF9b zJ-TYYiy84lNAK?Uzl?o2sH}$_du;?%x72?7V_ojrKY8ZwB;9)Cpz8M!okn$wA=%^8 z@-IAp)gNyrKFO9!CXJ&Ve=xX4M)_UQh?2;Xa-WoQY>fX=2_77l%A)OvlOpD<9l92I9kZOrwUaV z!tLrmiF!mB(ce(eE!Wa^o$g&f*LZ?=Kp2fxczu3Hw$Q|eHx&+;5+b?!vtnJIPmmZygzh3fxNQA59(ulomcb011~ z;t&Rs??G>TH2rAM=SUCQj^7*am56xAN$R%xKws!#1#)=Lnk(CiMEXUI$a}diA;V#L zt1a8}T`m&dg|2c}gm8qTKT%Kd?ojL~{eX_c5bXR@zxw!ZAuWhwKthNxao|fhjOv#E z?2_d(lhzP11(M}zck(TXt(CqZf@!`SJgNH$WDn?~;ZHBU_Ssmvy7^WnI2v4rY&g}z zzt<^6#4Lve@g97RcCo&|*8dfP9+|u89CCc6t`${W@4B+-P(~igc+$f|Z&Dq#k?TEL zWlvi(f?N4^$#{11YriAzcT20A*Xqy^^ZZ0{biI*pz}WC#6XU)pKQUAM_VU&+$crBa z1Z)nEjjwX?+p9(h4M`HmJ~L0A$R3qhJ8t^$=*_LfQqjElp%c!12t!)_p_x)t{$%m5 zhvNySh-zYC1T-CC@o&8?@5G*T&l>eqWv(2i&3(8(ul zFKU|0%*omGy?*Ef#=k^ey=r6$e1h0e`j<5kNM58d^x4)O{loDhn6|r5txZ9r#Bz)9 z^eMHkoKV}p8;PbJJh>MTIINR874jj7uw(v!gcyOCLU7K*9eOW!go>9RB#~2WbeSPN zw`XRyrUMc#9Gd&!{Tu;vs?M5N1UwG(CdVRagTH2E#VEG5Q*X|-+pdrCaF6Ud+jeVy zZw=LW+*M2(&4f)>E&-{ee{Ff_B%n>CFO+T$smm!4Pi8nR8Z@y_-hz;bw>Rhu99$#r-dmkdb9n#E*PaiLWOe^{hme=w5n|(# z?*X&_pVZ0W*6yB*>%Qn25oKCu>Y0CXPA)gEPv;7?`4(rBE6Iy;&GjW@#|xgDa4)ZA zzUpoj`5P6kfjzM0%h$RH13!&6194p+;S{iu?kIQOZI(B*BvL26?(XH zmAUj?E&37KlwPozLhg?`B#iHCWzWlwM-*VEe6xy{Z$lV^S!kZ%DLtFNGXg{ORtIdE zZWfjNxk_Ym)2nh2+jSV4KX@JQRD>&YGFoC_D>;4k91dHXl9e+&s1e4J^ty;~Ss|Jp zHe6^-q;_p%4-H=5v~GMAeW7?W2vtwMb%%ix%2vx~zjp6b_dTuykD>jx2u?3t(>t=}aEcf+d|#Jz{3*a}F3TP|h1 z-6L6<(cF_^gztmp&^w6Z-yBDaqA{pl8jpRLw^Ytolabb?VW1LUi6}sDf?pV$$xeKOnPmFLa|73^P0G`zE?ePrZ{@xhxDXJ2$3NEsR1) zc!@>m`#s$~Jy*!|L)FN)BCg@i!De4fbo-Kq`QFR39<)b4E4Xqe+rHZ=thHXhx#%S4 zT$CL*%|O^#XL%RL0t=^o!YH*NM_r@Ii5gl2=w(f*u+sU6EMJOmhC|4|qM?xBMitr! z__wD|-7T>NEvX`~ZXuG}CE`+@Uz5&WAn7G%yuz|aC+lY`zHT$P5}5i7Q?!9`7(~J=Lsp2;^x)LYoTWQY_VWXfy z3B*dVkn_{l?lZ57vCSp-5OdPca^vuiZ;dQyLZi9iCcxqI7UwveK`S7I5jze3Efas1z9NgsgX<^#{m(f?e`(Fe zcRiSAO(fK3P)PoJ%^b8k?;e4%rNJLXN+03<3~v%b z+52z%U4V(=+Gm4Yb-K%zHb|9**@t;_tCW)$(mk)h^%ouW@bZ*@`*Ibs%68XGQ>;G! z%>}mBJn~8mNpoly%{@C+on>-_l1V<^FTG;FJ-8$K{3uoOTE|MTTdlk7a_kbtA(o9Q zf(PXk9je&(f1j$bTsk(qzC<2<$c9D^zWQ}}*q2E)>9)vqh<;sMLmO;Hr(nFW(xn2obMG=irBuZYn2sD;%GMWM3Yp z!CxKW^%rC`8jc7CmNeCw%^;4mCp)E~N|6M)hn_RJ#d&N=>&W4MU~?T1&3@j@$CN;;XCLxY9io2imji@Va8&14Ktt5S)g>R|_n zrHg};!{#Ryhp{@k5oB0$8k67Mq~aB>zUG~>&w5J5-@CbtJmU*xRDbfMVzC!_9DlX+ z{`&fwb5*64-bA``yWfte6Ft0f=CD%Fr3NxXtJ+9fD?49s` z)hD5>SJ|eq>nIR1-5EmEkVCf=`dx-m(d3*7J{X=eX*(vTGrct={!%$Oo*`PB?EVxk z(Z^`}rJnP0#^u|ZqRs!i@GGm$`uOpCU>*5l0qExAQshl$>%&(9o*dCkk&xMlyI&Wo z8Xi$d!w+)DH)1|v!(X9) z-!`7sC?g9EEWBig4+esReR0g|{-kMx0NQ=~N86Aa&wXhuz7k@kDpdXn`!dtH|_@#{74)R{2qOP)=x8hf-YaKmj?7`Ka)Z}Wvsu=ck63Oq! zM-Eb}LiOCTo#a^gn@SuGmy6ax*ha8)Us)Prtizf-@9&*|Ne43P7JM36&nQM`s3gP?+ zd6A({!Y$pO_Y)qxbqQIDe$}yBME~TCo^x=&*kkVPI@<=zc3iumo-bDrD?v7p)Y4|g z=Pyc%o+1tZggmJ?Znx(%`LP;-g`+hd&B1%CS?sRbQB0HLRE3DXROrBk4Of0TL zq|sj^@^#4=XhEk=h%`uUY0Lwg66xJ{>5=zmbdE;LxL&#u_U=R$4cSl4G~^l}*%ijO zz><|NsekmjC|5kDefF&(FGzC60BuoT(dAD`sg+&>;(z|d`>O zb-=;(vpClz0@-eEEUm=*TEnE&8I;a}-;1%@o}$vjK_H1Wxn)c}^oGcuES0f$?LLv( zw`U_p3+jfbFNs1_*`KBZsO z?mRAVyQD#10%mJQMJwr>f;}wd)F>!mTL_%bn(Q&%vVdIIWX)~e$;QFK8O$tE1>Y0q zTR(*VyByQ2`3MXKh2PCFJo(UCn;Xd=b$WUVQ*#8Imn8&92bA~j35YHISvD=VehAvp zFXiMG+?&&D-+;qU61tawmf{2&|1g|G@bsWhP2B_R)vA2SCFk@n0qqV73SyN)^eQ

F%5S6W6}k`6aGPai@(@ZTT#Xc=8ysyh!jm%C^jOVNvJz)%u!xx`?5)#zf6 z0u3IaX{XzuQ?>;3PII8X9`4MfgT^w(#R^2OQkod0kpMB^c5+e0e*x4~AZiyV#L!c9 zsZmI2UAh9RtZ>ZO08MVZ8rWb?b#)w2y7xef7$|YzOyGO@x^EtzS0guzr6amV z)Hx4~P-~d8ndKufQXQ?REB*(JYa2NJfQbg&L)tCI0AFKGH2$np5FHek!w!fOFwIy` z)jju^^An;Gw42Y3k$el>9iZC_0Ydxp=N3Sk1kwahx!Hk}=u=EgArRSfavp(xI%dGY zUi}VS*eb21F~N1GH?z@osMdNk6f}x~h}M;C;;#hE5CEBy(1~(8%}ZnaCP2=%zFO1; z&(UPMI-EOb>HsV%lXlOHni7C<0r+2jrU}?Ktmk}g0EP@tpzM0i2jcbg^q2yZCh!^n zf4S$yc6};vTY<$r^_2vz(_x?-Qwckn_SEAmi6(-%}c@{8ChTrqHMvrQKpSRaio!D^x24r1r;vh^cNRoeXN)Q0(_@= zd3mN(xc_OP6WqB|<3C4+kG*M#BvTnx9iWB-WiBEIG&L~lYhj^5t0!V^I8n!#KIXz%LXMxLK)H0Y1DDD|x-u+LS=RgM;bV-0!Byns^OY&li50`{i5nSlW zvuAk$H`fz5!>v9i6OEj@dvYW5Hy6H=KuS`l3<2hF9qaLHC{) zsGg9%{(i5kBP4i7f#eCFXQ&UliY0?9O#shIQ*&gp6xE#1o<)B8w5jI^_FYr?8z@u`3cZ(BKu%8| z-M`X-Is$JEgbc!%pFJb0Kz{CCBG?E4-3ee<294VKDe#sEPE$Dr7%Ev%U*9WQ5U<$Nr$aqW`XKE29a7TzKHlU>JHajYW@z-(c3m4aBseUr-{<`G8u5Q-n`oXb zW>lN<@iI)-Eoy0kzigIg)-BPI_qMN5OyC#qlou^^ZgH;V0*wX>`<_6x7o~6mZ^1NW zgEvIRy_+Mttel5i8XEpK?UuLPE5}HC8esT(dep!+GU)C9Ka(tY6@{Y+sC!C* zv=ZDX{4vGg5?H?->eGP|`>-BLpj4BPlmr~{@>*JBdS%&QfMdeIF)=bS^00XKi)lIh qX<}lcv#~7Z6`uqzgVp$}09guhPJBHfY4AD}L`hEdWx0%L(Ek8fCyBHG literal 0 HcmV?d00001 diff --git a/images/z64/animated_materials/am_part_2.png b/images/z64/animated_materials/am_part_2.png new file mode 100644 index 0000000000000000000000000000000000000000..c3cf7a7236e23667a990df8bc8289a98fe6f7d80 GIT binary patch literal 24848 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 Hdh>q(fInJQ literal 0 HcmV?d00001 diff --git a/images/z64/animated_materials/am_part_3.png b/images/z64/animated_materials/am_part_3.png new file mode 100644 index 0000000000000000000000000000000000000000..310c1d3dbb827fa4d9c858a21417317f2bc6c3e0 GIT binary patch literal 38323 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 z zK1}VAqEpT(@itLONuaPax6~LGwuXmYxPtl@)nowUvd|891c0Yc52ZMsP;+&MZ k@-SKTC%X=A1?$GPn{pv5@nxmSWcWuz^`uIk^4XyO2f1cbTmS$7 literal 0 HcmV?d00001 From 4b81511162b4c380b20422d8b28bb745d700997c Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:40:20 +0100 Subject: [PATCH 06/20] add draw config stuff --- fast64_internal/data/z64/enum_data.py | 2 +- .../data/z64/xml/oot_enum_data.xml | 3 + fast64_internal/z64/README.md | 2 + .../z64/animated_mats/properties.py | 3 +- fast64_internal/z64/constants.py | 61 ------------------- fast64_internal/z64/importer/scene.py | 3 +- fast64_internal/z64/scene/properties.py | 9 ++- 7 files changed, 15 insertions(+), 68 deletions(-) diff --git a/fast64_internal/data/z64/enum_data.py b/fast64_internal/data/z64/enum_data.py index 5ecdebe2d..bb5538f68 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.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..3c60e1680 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/z64/README.md b/fast64_internal/z64/README.md index 6584c76c8..5f700f60d 100644 --- a/fast64_internal/z64/README.md +++ b/fast64_internal/z64/README.md @@ -200,6 +200,8 @@ If the game crashes check the transitions if you use the transition command (che 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** To get started 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. diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index 78018f6d0..c230d2f59 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -252,9 +252,10 @@ def draw_props(self, layout: UILayout, owner: Object): layout = layout.column() prop_split(layout, self, "mode", "Export To") + layout.label(text="Make sure one of the 'Material Animated' draw configs is selected.", icon="QUESTION") prop_text = get_list_tab_text("Animated Materials List", len(self.items)) - layout_entries = layout.box().column() + layout_entries = layout.column() layout_entries.prop( self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT" ) 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/importer/scene.py b/fast64_internal/z64/importer/scene.py index 2d02922d2..848113460 100644 --- a/fast64_internal/z64/importer/scene.py +++ b/fast64_internal/z64/importer/scene.py @@ -13,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 @@ -200,7 +199,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/scene/properties.py b/fast64_internal/z64/scene/properties.py index 2605feb92..edfb87e25 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -29,7 +29,6 @@ ootEnumCameraMode, ootEnumAudioSessionPreset, ootEnumHeaderMenu, - ootEnumDrawConfig, ootEnumHeaderMenuComplete, ) @@ -234,13 +233,17 @@ def draw_props(self, layout: UILayout): 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( From 59fac0cc6a0b5e333ea7089397471ae52daeea09 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:44:46 +0100 Subject: [PATCH 07/20] add ifdef around segment call for hackeroot --- fast64_internal/f3d/f3d_gbi.py | 10 ++++- fast64_internal/z64/README.md | 2 + fast64_internal/z64/f3d/properties.py | 8 +++- fast64_internal/z64/model_classes.py | 53 +++++++++++++++++++++------ 4 files changed, 59 insertions(+), 14 deletions(-) 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/z64/README.md b/fast64_internal/z64/README.md index 5f700f60d..90cb8868a 100644 --- a/fast64_internal/z64/README.md +++ b/fast64_internal/z64/README.md @@ -240,6 +240,8 @@ You can pick the segment number with the `Segment Number` field (make sure to us - `4`: Color Non-linear Interpolation - `5`: Texture Cycle (like a GIF) +Note: for HackerOoT users, you can choose to toggle exporting the `ENABLE_ANIMATED_MATERIALS` ifdef around the segment call from the display list with the `Use Segment for Animated Materials` checkbox in the material's `Dynamic Material Properties` panel. This is not mandatory and only there for convenience. + For the color types you will also have a `Keyframe Length` field, this corresponds to the length of the animation. Both texture scroll types will use the same elements: diff --git a/fast64_internal/z64/f3d/properties.py b/fast64_internal/z64/f3d/properties.py index 4f29b0e8b..f54a7a332 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): @@ -94,6 +95,7 @@ class OOTDynamicMaterialDrawLayerProperty(PropertyGroup): customCall0_seg: StringProperty(description="Segment address of a display list to call, e.g. 0x08000010") customCall1: BoolProperty() customCall1_seg: StringProperty(description="Segment address of a display list to call, e.g. 0x08000010") + is_anim_mat: BoolProperty(default=False) def key(self): return ( @@ -105,10 +107,14 @@ def key(self): self.segmentD, self.customCall0_seg if self.customCall0 else None, self.customCall1_seg if self.customCall1 else None, + self.is_anim_mat, ) def draw_props(self, layout: UILayout, suffix: str): - row = layout.row() + if is_hackeroot(): + layout.prop(self, "is_anim_mat", text="Use Segment for Animated Materials") + + row = layout.box().row() for colIndex in range(2): col = row.column() for rowIndex in range(3): diff --git a/fast64_internal/z64/model_classes.py b/fast64_internal/z64/model_classes.py index 457ee3413..b60fb32c3 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,6 +103,25 @@ def ootGetLinkData(basePath: str) -> str: return actorData +# custom `SPDisplayList` so we can customize the C output +@dataclass(unsafe_hash=True) +class DynamicMaterialDL(SPDisplayList): + displayList: GfxList + + def __post_init__(self): + self.add_ifdef = False + self.default_formatting = False + + def to_c(self, static=True): + assert static + if self.add_ifdef: + 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): self.drawLayerOverride = drawLayerOverride @@ -283,16 +310,18 @@ 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")): - gfxList.commands.append( - SPDisplayList(GfxList("0x" + format(i, "X") + "000000", GfxListTag.Material, DLFormat.Static)) - ) + if getattr(matDrawLayer, f"segment{i:X}"): + command = DynamicMaterialDL(GfxList(f"0x0{i:X}000000", GfxListTag.Material, DLFormat.Static)) + command.add_ifdef = is_hackeroot() and matDrawLayer.is_anim_mat + gfxList.commands.append(command) + 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): From 5cc17aba4ec99f48c9ee5b011b19312dd25bdf2e Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:27:08 +0100 Subject: [PATCH 08/20] create hackeroot settings and add an "export ifdefs" option --- fast64_internal/z64/__init__.py | 17 ++++++-- .../z64/animated_mats/properties.py | 4 +- .../z64/exporter/scene/__init__.py | 41 +++++++++++-------- fast64_internal/z64/exporter/scene/rooms.py | 1 + fast64_internal/z64/f3d/properties.py | 7 +--- fast64_internal/z64/hackeroot/operators.py | 31 ++++++++++++++ fast64_internal/z64/hackeroot/panels.py | 27 ++++++++++++ fast64_internal/z64/hackeroot/properties.py | 36 ++++++++++++++++ fast64_internal/z64/model_classes.py | 20 +++++---- fast64_internal/z64/scene/operators.py | 16 +------- fast64_internal/z64/scene/panels.py | 17 -------- fast64_internal/z64/scene/properties.py | 1 + 12 files changed, 150 insertions(+), 68 deletions(-) create mode 100644 fast64_internal/z64/hackeroot/operators.py create mode 100644 fast64_internal/z64/hackeroot/panels.py create mode 100644 fast64_internal/z64/hackeroot/properties.py diff --git a/fast64_internal/z64/__init__.py b/fast64_internal/z64/__init__.py index 6f396a02a..7b8349ba5 100644 --- a/fast64_internal/z64/__init__.py +++ b/fast64_internal/z64/__init__.py @@ -57,7 +57,11 @@ from .spline.properties import spline_props_register, spline_props_unregister from .spline.panels import spline_panels_register, spline_panels_unregister -from .animated_mats.properties import animated_mats_register, animated_mats_unregister +from .animated_mats.properties import 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, @@ -113,6 +117,7 @@ class OOT_Properties(bpy.types.PropertyGroup): 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) def get_extracted_path(self): version = self.oot_version if game_data.z64.is_oot() else self.mm_version @@ -159,6 +164,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() @@ -171,6 +177,7 @@ def oot_panel_register(): def oot_panel_unregister(): oot_operator_panel_unregister() + hackeroot_panels_unregister() cutscene_panels_unregister() collision_panels_unregister() oot_obj_panel_unregister() @@ -202,7 +209,9 @@ def oot_register(registerPanels): f3d_ops_register() file_register() anim_props_register() - animated_mats_register() + hackeroot_props_register() + hackeroot_ops_register() + animated_mats_props_register() csMotion_ops_register() csMotion_props_register() @@ -234,7 +243,9 @@ def oot_unregister(unregisterPanels): csMotion_props_unregister() csMotion_ops_unregister() - animated_mats_unregister() + animated_mats_props_unregister() + hackeroot_ops_unregister() + hackeroot_props_unregister() anim_props_unregister() file_unregister() f3d_ops_unregister() diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index c230d2f59..25ea668cb 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -281,11 +281,11 @@ def draw_props(self, layout: UILayout, owner: Object): ) -def animated_mats_register(): +def animated_mats_props_register(): for cls in classes: register_class(cls) -def animated_mats_unregister(): +def animated_mats_props_unregister(): for cls in reversed(classes): unregister_class(cls) diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index 6c77ae9c4..40596e506 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -38,31 +38,17 @@ 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, - ) try: mainHeader = SceneHeader.new( f"{name}_header{i:02}", sceneObj.ootSceneHeader, sceneObj, transform, i, exportInfo.useMacros ) + + 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 @@ -90,6 +76,25 @@ def new( 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) 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 f54a7a332..e57fa50e3 100644 --- a/fast64_internal/z64/f3d/properties.py +++ b/fast64_internal/z64/f3d/properties.py @@ -95,7 +95,6 @@ class OOTDynamicMaterialDrawLayerProperty(PropertyGroup): customCall0_seg: StringProperty(description="Segment address of a display list to call, e.g. 0x08000010") customCall1: BoolProperty() customCall1_seg: StringProperty(description="Segment address of a display list to call, e.g. 0x08000010") - is_anim_mat: BoolProperty(default=False) def key(self): return ( @@ -107,14 +106,10 @@ def key(self): self.segmentD, self.customCall0_seg if self.customCall0 else None, self.customCall1_seg if self.customCall1 else None, - self.is_anim_mat, ) def draw_props(self, layout: UILayout, suffix: str): - if is_hackeroot(): - layout.prop(self, "is_anim_mat", text="Use Segment for Animated Materials") - - row = layout.box().row() + row = layout.row() for colIndex in range(2): col = row.column() for rowIndex in range(3): 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/model_classes.py b/fast64_internal/z64/model_classes.py index b60fb32c3..fef534f6c 100644 --- a/fast64_internal/z64/model_classes.py +++ b/fast64_internal/z64/model_classes.py @@ -106,15 +106,18 @@ def ootGetLinkData(basePath: str) -> str: # custom `SPDisplayList` so we can customize the C output @dataclass(unsafe_hash=True) class DynamicMaterialDL(SPDisplayList): - displayList: GfxList + is_animated_material_sdc: bool def __post_init__(self): - self.add_ifdef = False self.default_formatting = False def to_c(self, static=True): assert static - if self.add_ifdef: + 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" ) @@ -123,9 +126,10 @@ def to_c(self, static=True): 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) @@ -313,9 +317,11 @@ def onMaterialCommandsBuilt(self, fMaterial, material, drawLayer): for i in range(8, 14): if getattr(matDrawLayer, f"segment{i:X}"): - command = DynamicMaterialDL(GfxList(f"0x0{i:X}000000", GfxListTag.Material, DLFormat.Static)) - command.add_ifdef = is_hackeroot() and matDrawLayer.is_anim_mat - gfxList.commands.append(command) + gfxList.commands.append( + DynamicMaterialDL( + GfxList(f"0x0{i:X}000000", GfxListTag.Material, DLFormat.Static), "mat_anim" in self.draw_config + ) + ) for i in range(0, 2): p = f"customCall{i}" 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 edfb87e25..56e9fb203 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -448,6 +448,7 @@ def draw_props(self, layout: UILayout, objName: str): 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") From 524349b0f5fff8b2bd2d2782250930496d5d0714 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:56:51 +0100 Subject: [PATCH 09/20] add operator --- fast64_internal/utility.py | 9 +++- fast64_internal/z64/tools/operators.py | 57 +++++++++++++++++++++++++- fast64_internal/z64/tools/panel.py | 3 ++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 1e93b1b82..01b31174d 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -2060,7 +2060,13 @@ def get_include_data(include: str, strip: bool = False): return data -def get_new_empty_object(name: str, location=[0.0, 0.0, 0.0], rotation_euler=[0.0, 0.0, 0.0], scale=[1.0, 1.0, 1.0]): +def get_new_empty_object( + name: str, + 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""" new_obj = bpy.data.objects.new(name, None) @@ -2068,4 +2074,5 @@ def get_new_empty_object(name: str, location=[0.0, 0.0, 0.0], rotation_euler=[0. new_obj.location = location new_obj.rotation_euler = rotation_euler new_obj.scale = scale + new_obj.parent = parent return new_obj diff --git a/fast64_internal/z64/tools/operators.py b/fast64_internal/z64/tools/operators.py index 09bed65b5..5a8ba6a43 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,55 @@ 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="Scene 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): + scene_obj: Object = context.scene.ootSceneExportObj + new_obj = get_new_empty_object(self.obj_name, parent=scene_obj) + 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, ] From c2d441c4fe207433762c822c313d131f7852fd64 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Wed, 12 Nov 2025 05:01:05 +0100 Subject: [PATCH 10/20] fix draw color, allow exporting empty lists and add initial scene embed --- fast64_internal/z64/__init__.py | 4 +- .../z64/animated_mats/properties.py | 70 +++++-- fast64_internal/z64/collection_utility.py | 37 ++-- fast64_internal/z64/constants.py | 17 ++ .../z64/exporter/scene/__init__.py | 55 ++++- .../z64/exporter/scene/animated_mats.py | 190 +++++++++++------- fast64_internal/z64/exporter/scene/header.py | 20 +- fast64_internal/z64/props_panel_main.py | 4 +- fast64_internal/z64/scene/properties.py | 97 ++++++--- fast64_internal/z64/utility.py | 14 ++ 10 files changed, 362 insertions(+), 146 deletions(-) diff --git a/fast64_internal/z64/__init__.py b/fast64_internal/z64/__init__.py index 7b8349ba5..4767da579 100644 --- a/fast64_internal/z64/__init__.py +++ b/fast64_internal/z64/__init__.py @@ -194,6 +194,7 @@ def oot_register(registerPanels): collision_ops_register() # register first, so panel goes above mat panel collision_props_register() cutscene_props_register() + animated_mats_props_register() scene_ops_register() scene_props_register() room_ops_register() @@ -211,7 +212,6 @@ def oot_register(registerPanels): anim_props_register() hackeroot_props_register() hackeroot_ops_register() - animated_mats_props_register() csMotion_ops_register() csMotion_props_register() @@ -243,7 +243,6 @@ def oot_unregister(unregisterPanels): csMotion_props_unregister() csMotion_ops_unregister() - animated_mats_props_unregister() hackeroot_ops_unregister() hackeroot_props_unregister() anim_props_unregister() @@ -261,6 +260,7 @@ def oot_unregister(unregisterPanels): room_ops_unregister() scene_props_unregister() scene_ops_unregister() + animated_mats_props_unregister() cutscene_props_unregister() collision_props_unregister() collision_ops_unregister() diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index 25ea668cb..d0ac4650e 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -10,9 +10,11 @@ FloatVectorProperty, ) +from typing import Optional + from ...utility import prop_split -from ..collection_utility import drawAddButton, drawCollectionOps, getCollection -from ..utility import get_list_tab_text +from ..collection_utility import drawAddButton, drawCollectionOps +from ..utility import get_list_tab_text, getEnumIndex # no custom since we only need to know where to export the data @@ -55,30 +57,49 @@ class Z64_AnimatedMatColorKeyFrame(PropertyGroup): default=(1, 1, 1, 1), ) - def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: int): + def draw_props( + self, layout: UILayout, owner: Object, parent_index: int, index: int, is_draw_color: bool, use_env_color: bool + ): drawCollectionOps(layout, index, "Animated Mat. Color", None, owner.name, collection_index=parent_index) - prop_split(layout, self, "frame_num", "Frame No.") + + # "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") - prop_split(layout, self, "env_color", "Environment 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) keyframes: CollectionProperty(type=Z64_AnimatedMatColorKeyFrame) + use_env_color: BoolProperty() # ui only props show_entries: BoolProperty(default=False) + internal_color_type: StringProperty() + def draw_props(self, layout: UILayout, owner: Object, parent_index: int): - prop_split(layout, self, "keyframe_length", "Keyframe Length") + 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, parent_index, i) + keyframe.draw_props( + layout, owner, parent_index, i, is_draw_color, not is_draw_color or self.use_env_color + ) drawAddButton(layout, len(self.keyframes), "Animated Mat. Color", None, owner.name, parent_index) @@ -166,9 +187,16 @@ class Z64_AnimatedMaterialItem(PropertyGroup): """see the `AnimatedMaterial` struct from `z64scene.h`""" segment_num: IntProperty(name="Segment Number", min=8, max=13, default=8) - type: EnumProperty( - name="Draw Handler Type", items=enum_anim_mat_type, default=2, description="Index to `sMatAnimDrawHandlers`" + + 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) @@ -178,6 +206,12 @@ class Z64_AnimatedMaterialItem(PropertyGroup): # ui only props show_item: BoolProperty(default=False) + def on_type_set(self, value: str): + self.type = enum_anim_mat_type[value][0] + + if self.type in {"color", "color_lerp", "color_nonlinear_interp"}: + self.color_params.internal_color_type = self.type + def draw_props(self, layout: UILayout, owner: Object, index: int): layout.prop( self, "show_item", text=f"Item No.{index + 1}", icon="TRIA_DOWN" if self.show_item else "TRIA_RIGHT" @@ -189,7 +223,7 @@ def draw_props(self, layout: UILayout, owner: Object, index: int): prop_split(layout, self, "segment_num", "Segment Number") layout_type = layout.column() - prop_split(layout_type, self, "type", "Draw Handler Type") + prop_split(layout_type, self, "user_type", "Draw Handler Type") if self.type == "Custom": layout_type.label( @@ -207,21 +241,21 @@ def draw_props(self, layout: UILayout, owner: Object, index: int): class Z64_AnimatedMaterial(PropertyGroup): """Defines an Animated Material array""" - header_index: IntProperty(name="Header Index", min=-1, default=-1, description="Header Index, -1 means all headers") 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: int): - layout.prop( - self, "show_list", text=f"List No.{index + 1}", icon="TRIA_DOWN" if self.show_list else "TRIA_RIGHT" - ) + def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], header_index: Optional[int] = None): + 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: - drawCollectionOps(layout, index, "Animated Mat. List", None, owner.name) - prop_split(layout, self, "header_index", "Header Index") + if index is not None: + drawCollectionOps(layout, index, "Animated Mat. List", None, owner.name) prop_text = get_list_tab_text("Animated Materials", len(self.entries)) layout_entries = layout.column() @@ -233,7 +267,7 @@ def draw_props(self, layout: UILayout, owner: Object, index: int): for i, item in enumerate(self.entries): item.draw_props(layout_entries.box().column(), owner, i) - drawAddButton(layout_entries, len(self.entries), "Animated Mat.", None, owner.name) + drawAddButton(layout_entries, len(self.entries), "Animated Mat.", header_index, owner.name) class Z64_AnimatedMaterialProperty(PropertyGroup): diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py index e06f58d27..719db54b9 100644 --- a/fast64_internal/z64/collection_utility.py +++ b/fast64_internal/z64/collection_utility.py @@ -20,13 +20,16 @@ class OOTCollectionAdd(Operator): def execute(self, context): collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) - collection.add() + new_entry = collection.add() collection.move(len(collection) - 1, 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) + if self.collectionType == "Scene": + new_entry.internal_header_index = 4 + return {"FINISHED"} @@ -99,22 +102,22 @@ def getCollection(objName, collectionType, subIndex: int, collection_index: int collection = getCollectionFromIndex(obj, "objectList", subIndex, True) elif collectionType == "Animated Mat. List": collection = obj.fast64.oot.animated_materials.items - elif collectionType == "Animated Mat.": - collection = obj.fast64.oot.animated_materials.items[subIndex].entries - elif collectionType == "Animated Mat. Color": - collection = obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].color_params.keyframes - elif collectionType == "Animated Mat. Scroll": - collection = ( - obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].tex_scroll_params.entries - ) - elif collectionType == "Animated Mat. Cycle (Index)": - collection = ( - obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].tex_cycle_params.keyframes - ) - elif collectionType == "Animated Mat. Cycle (Texture)": - collection = ( - obj.fast64.oot.animated_materials.items[subIndex].entries[collection_index].tex_cycle_params.textures - ) + elif collectionType.startswith("Animated Mat."): + if obj.ootEmptyType == "Scene": + props = getCollectionFromIndex(obj, "animated_material", subIndex, False) + 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. Scroll": + collection = props.entries[collection_index].tex_scroll_params.entries + 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."): diff --git a/fast64_internal/z64/constants.py b/fast64_internal/z64/constants.py index d12ef01ac..0512c79d4 100644 --- a/fast64_internal/z64/constants.py +++ b/fast64_internal/z64/constants.py @@ -15,6 +15,23 @@ ("Child Day", "Child Day", "Child Day"), ] + ootEnumHeaderMenu +enum_am_headers_1 = ootEnumHeaderMenuComplete.copy() +enum_am_headers_1.pop(1) +enum_am_headers_1 = ootEnumHeaderMenuComplete.copy() +enum_am_headers_1.pop(1) +enum_am_headers_2 = ootEnumHeaderMenuComplete.copy() +enum_am_headers_2.pop(2) +enum_am_headers_3 = ootEnumHeaderMenuComplete.copy() +enum_am_headers_3.pop(3) +enum_am_headers_4 = ootEnumHeaderMenuComplete.copy() +enum_am_headers_4.pop(4) +am_enum_map = { + 1: enum_am_headers_1, + 2: enum_am_headers_2, + 3: enum_am_headers_3, + 4: enum_am_headers_4, +} + ootEnumCameraMode = [ ("Custom", "Custom", "Custom"), ("0x00", "Default", "Default"), diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index 40596e506..a29277232 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -4,6 +4,8 @@ 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 @@ -16,6 +18,31 @@ from .rooms import RoomEntries +def get_anm_mat_target_name(alt_prop: OOTSceneHeaderProperty, name: str, header_index: int): + animated_materials = None + + if alt_prop.reuse_anim_mat: + header_map = { + "Child Night": 1, + "Adult Day": 2, + "Adult Night": 3, + } + + anim_mat_header = alt_prop.internal_anim_mat_header + + if anim_mat_header == "Child Day": + index = 0 + elif anim_mat_header == "Cutscene": + assert header_index >= game_data.z64.cs_index_start + index = header_index + else: + index = header_map[anim_mat_header] + + animated_materials = f"{name}_header{index:02}_AnimatedMaterial" + + return animated_materials + + @dataclass class Scene: """This class defines a scene""" @@ -41,7 +68,7 @@ def new( 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, None ) if mainHeader.infos is not None: @@ -55,23 +82,33 @@ def new( 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(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, 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(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, target_name + ) ) except Exception as exc: raise PluginError(f"In alternate, cutscene header {i}: {exc}") from exc @@ -126,7 +163,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 @@ -155,7 +192,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 and curHeader.anim_mat.is_used() else "") + + (curHeader.anim_mat.get_cmd() if curHeader.anim_mat is not None else "") + Utility.getEndCmd() + "};\n\n" ) @@ -180,7 +217,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) ) diff --git a/fast64_internal/z64/exporter/scene/animated_mats.py b/fast64_internal/z64/exporter/scene/animated_mats.py index 5352170ba..f8edec34a 100644 --- a/fast64_internal/z64/exporter/scene/animated_mats.py +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -1,8 +1,12 @@ +import bpy + from dataclasses import dataclass from bpy.types import Object +from typing import Optional from ....utility import CData, PluginError, exportColor, scaleToU8, indent from ...utility import getObjectList, is_oot_features, is_hackeroot +from ...scene.properties import OOTSceneHeaderProperty from ...animated_mats.properties import ( Z64_AnimatedMatColorParams, Z64_AnimatedMatTexScrollParams, @@ -18,17 +22,17 @@ def __init__( segment_num: int, type_num: int, base_name: str, - header_index: int, index: int, + type: str, ): + is_draw_color = type == "color" # the code adds back 7 when processing animated materials self.segment_num = segment_num - 7 self.type_num = type_num self.base_name = base_name - self.header_index = header_index self.header_suffix = f"_{index:02}" self.name = f"{self.base_name}ColorParams{self.header_suffix}" - self.frame_length = props.keyframe_length + 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] = [] @@ -36,14 +40,23 @@ def __init__( 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)) - self.env_colors.append(tuple(exportColor(keyframe.env_color[0:3]) + [scaleToU8(keyframe.env_color[3])])) - self.frames.append(keyframe.frame_num) - if keyframe.frame_num > self.frame_length: + 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) - assert len(self.frames) == len(self.prim_colors) == len(self.env_colors) + + 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): data = CData() @@ -52,11 +65,17 @@ def to_c(self): 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 data.header = ( f"extern F3DPrimColor {prim_array_name}[];\n" - + f"extern F3DEnvColor {env_array_name}[];\n" - + f"extern u16 {frames_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" ) @@ -71,16 +90,24 @@ def to_c(self): + "\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 + ( + (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" ) - + "\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" + ( + (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") @@ -103,14 +130,13 @@ def __init__( segment_num: int, type_num: int, base_name: str, - header_index: int, index: int, + type: str, ): # the code adds back 7 when processing animated materials self.segment_num = segment_num - 7 self.type_num = type_num self.base_name = base_name - self.header_index = header_index self.header_suffix = f"_{index:02}" self.name = f"{self.base_name}TexScrollParams{self.header_suffix}" self.entries: list[str] = [] @@ -140,14 +166,13 @@ def __init__( segment_num: int, type_num: int, base_name: str, - header_index: int, index: int, + type: str, ): # the code adds back 7 when processing animated materials self.segment_num = segment_num - 7 self.type_num = type_num self.base_name = base_name - self.header_index = header_index self.header_suffix = f"_{index:02}" self.name = f"{self.base_name}TexCycleParams{self.header_suffix}" self.textures: list[str] = [] @@ -201,12 +226,13 @@ def to_c(self): class AnimatedMaterial: - def __init__(self, props: Z64_AnimatedMaterial, base_name: str, scene_header_index: int): + def __init__(self, props: Z64_AnimatedMaterial, base_name: str): self.name = base_name - self.scene_header_index = scene_header_index - self.header_index = props.header_index self.entries: list[AnimatedMatColorParams | AnimatedMatTexScrollParams | AnimatedMatTexCycleParams] = [] + if len(props.entries) == 0: + return + type_list_map: dict[ str, tuple[AnimatedMatColorParams | AnimatedMatTexScrollParams | AnimatedMatTexCycleParams, str, int] ] = { @@ -219,12 +245,12 @@ def __init__(self, props: Z64_AnimatedMaterial, base_name: str, scene_header_ind } for i, item in enumerate(props.entries): - if item.type != "Custom": - class_def, prop_name, type_num = type_list_map[item.type] + type = item.type if item.type != "Custom" else item.typeCustom + if type != "Custom": + class_def, prop_name, type_num = type_list_map[type] + # example: `self.tex_scroll_entries.append(AnimatedMatTexScrollParams(item.tex_scroll_params, base_name, header_index))` - self.entries.append( - class_def(getattr(item, prop_name), item.segment_num, type_num, base_name, self.header_index, i) - ) + self.entries.append(class_def(getattr(item, prop_name), item.segment_num, type_num, base_name, i, type)) # the last entry's segment need to be negative if len(self.entries) > 0 and self.entries[-1].segment_num > 0: @@ -234,31 +260,29 @@ def to_c(self): data = CData() for entry in self.entries: - if entry.header_index == -1 or entry.header_index == self.scene_header_index: - data.append(entry.to_c()) + data.append(entry.to_c()) - if len(self.entries) > 0: - array_name = f"AnimatedMaterial {self.name}[]" + array_name = f"AnimatedMaterial {self.name}[]" - # .h - data.header += f"extern {array_name};" + # .h + data.header += f"extern {array_name};" - # .c - data.source += ( - (array_name + " = {\n" + indent) - + f",\n{indent}".join( - "{ " - + f"{entry.segment_num} /* {abs(entry.segment_num) + 7} */, " - + f"{entry.type_num}, " - + f"{'&' if entry.type_num in {2, 3, 4, 5} else ''}{entry.name}" - + " }" - for entry in self.entries - ) - + "\n};\n" + # .c + data.source += array_name + " = {\n" + indent + + if len(self.entries) > 0: + data.source += f",\n{indent}".join( + "{ " + + f"{entry.segment_num} /* {abs(entry.segment_num) + 7} */, " + + f"{entry.type_num}, " + + f"{'&' if entry.type_num in {2, 3, 4, 5} else ''}{entry.name}" + + " }" + for entry in self.entries ) else: - raise PluginError("ERROR: Trying to export animated materials with empty entries!") + data.source += "{ 0, 6, NULL }" + data.source += "\n};\n" return data @@ -267,7 +291,50 @@ class SceneAnimatedMaterial: """This class hosts exit data""" name: str - header_index: int + animated_material: Optional[AnimatedMaterial] + + @staticmethod + def new(name: str, props: OOTSceneHeaderProperty, is_reuse: bool): + return SceneAnimatedMaterial(name, AnimatedMaterial(props.animated_material, name) if not is_reuse else None) + + 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): + 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()) + + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + data.source += "#endif\n\n" + data.header += "\n#endif\n" + else: + data.source += "\n" + data.header += "\n" + + return data + + +@dataclass +class ActorAnimatedMaterial: + """This class hosts exit data""" + + name: str entries: list[AnimatedMaterial] @staticmethod @@ -281,43 +348,22 @@ def new(name: str, scene_obj: Object, header_index: int): [AnimatedMaterial(item, name, header_index) for item in obj.fast64.oot.animated_materials.items] ) - last_index = -1 - for entry in entries: - if entry.header_index >= 0: - if entry.header_index > last_index: - last_index = entry.header_index - else: - raise PluginError("ERROR: Animated Materials header indices are not consecutives!") - - return SceneAnimatedMaterial(name, header_index, entries) + return ActorAnimatedMaterial(name, entries) def is_used(self): return not is_oot_features() and len(self.entries) > 0 - def get_cmd(self): - """Returns the sound settings, misc settings, special files and skybox settings scene commands""" - - if is_hackeroot(): - 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): data = CData() - if is_hackeroot(): + 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" for entry in self.entries: data.append(entry.to_c()) - if is_hackeroot(): + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: data.source += "#endif\n\n" data.header += "\n#endif\n" else: diff --git a/fast64_internal/z64/exporter/scene/header.py b/fast64_internal/z64/exporter/scene/header.py index f0f1afd2e..4543e12da 100644 --- a/fast64_internal/z64/exporter/scene/header.py +++ b/fast64_internal/z64/exporter/scene/header.py @@ -32,9 +32,21 @@ class SceneHeader: @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, + target_name: Optional[str], ): entranceActors = SceneEntranceActors.new(f"{name}_playerEntryList", sceneObj, transform, headerIndex) + + animated_materials = None + if 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), @@ -45,9 +57,7 @@ def new( entranceActors, SceneSpawns(f"{name}_entranceList", entranceActors.entries), ScenePathways.new(f"{name}_pathway", sceneObj, transform, headerIndex), - SceneAnimatedMaterial.new(f"{name}_AnimatedMaterial", sceneObj, headerIndex) - if not is_oot_features() - else None, + animated_materials, ) def getC(self): @@ -77,7 +87,7 @@ def getC(self): headerData.append(self.path.getC()) # Write the animated material list, if used - if self.anim_mat is not None and self.anim_mat.is_used(): + if self.anim_mat is not None: headerData.append(self.anim_mat.to_c()) return headerData diff --git a/fast64_internal/z64/props_panel_main.py b/fast64_internal/z64/props_panel_main.py index 7a7e97fd9..efd472cb8 100644 --- a/fast64_internal/z64/props_panel_main.py +++ b/fast64_internal/z64/props_panel_main.py @@ -45,9 +45,9 @@ 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") diff --git a/fast64_internal/z64/scene/properties.py b/fast64_internal/z64/scene/properties.py index 56e9fb203..fc8fada9c 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -11,13 +11,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, is_oot_features +from ..utility import onMenuTabChange, onHeaderMenuTabChange, drawEnumWithCustom, is_oot_features, getEnumIndex +from ..animated_mats.properties import Z64_AnimatedMaterial from ..constants import ( ootEnumMusicSeq, @@ -30,6 +32,7 @@ ootEnumAudioSessionPreset, ootEnumHeaderMenu, ootEnumHeaderMenuComplete, + am_enum_map, ) ootEnumSceneMenuAlternate = [ @@ -37,6 +40,7 @@ ("Lighting", "Lighting", "Lighting"), ("Cutscene", "Cutscene", "Cutscene"), ("Exits", "Exits", "Exits"), + ("AnimMats", "Material Anim.", "Material Anim."), ] ootEnumSceneMenu = ootEnumSceneMenuAlternate + [ ("Alternate", "Alternate", "Alternate"), @@ -320,7 +324,36 @@ 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): + 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: @@ -328,18 +361,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": @@ -360,7 +394,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", "") @@ -378,8 +412,8 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj self.timeOfDayLights.draw_props(lighting) else: for i in range(len(self.lightList)): - self.lightList[i].draw_props(lighting, "Lighting " + str(i), True, i, headerIndex, objName) - drawAddButton(lighting, len(self.lightList), "Light", headerIndex, objName) + self.lightList[i].draw_props(lighting, "Lighting " + str(i), True, i, headerIndex, obj.name) + drawAddButton(lighting, len(self.lightList), "Light", headerIndex, obj.name) elif menuTab == "Cutscene": cutscene = layout.column() @@ -396,18 +430,31 @@ 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, objName) + 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.") + + 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): @@ -426,24 +473,28 @@ class OOTAlternateSceneHeaderProperty(PropertyGroup): headerMenuTab: EnumProperty(name="Header Menu", items=ootEnumHeaderMenu, update=onHeaderMenuTabChange) currentCutsceneIndex: IntProperty(default=1, 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") diff --git a/fast64_internal/z64/utility.py b/fast64_internal/z64/utility.py index ee9509231..de793bb83 100644 --- a/fast64_internal/z64/utility.py +++ b/fast64_internal/z64/utility.py @@ -782,6 +782,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": @@ -794,6 +805,9 @@ def callback(thisHeader, otherObj: bpy.types.Object): onHeaderPropertyChange(self, context, callback) + if context.object.ootEmptyType == "Scene": + on_alt_menu_tab_change(self, 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: From 4d4e90e9d6f56bef8e88dd388f0d4b4853651594 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:58:53 +0100 Subject: [PATCH 11/20] fixed export, removed texscroll lists as it's useless and export tweaks --- .../z64/animated_mats/properties.py | 88 ++++++++------ fast64_internal/z64/collection_utility.py | 2 - fast64_internal/z64/constants.py | 1 - .../z64/exporter/scene/__init__.py | 113 +++++++++++++++--- .../z64/exporter/scene/animated_mats.py | 66 +++++----- fast64_internal/z64/exporter/scene/header.py | 3 +- fast64_internal/z64/scene/properties.py | 9 +- 7 files changed, 194 insertions(+), 88 deletions(-) diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index d0ac4650e..3d30a7dab 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -58,9 +58,16 @@ class Z64_AnimatedMatColorKeyFrame(PropertyGroup): ) def draw_props( - self, layout: UILayout, owner: Object, parent_index: int, index: int, is_draw_color: bool, use_env_color: bool + 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", None, owner.name, collection_index=parent_index) + drawCollectionOps(layout, index, "Animated Mat. Color", header_index, owner.name, collection_index=parent_index) # "draw color" type don't need this if not is_draw_color: @@ -83,7 +90,7 @@ class Z64_AnimatedMatColorParams(PropertyGroup): internal_color_type: StringProperty() - def draw_props(self, layout: UILayout, owner: Object, parent_index: int): + 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: @@ -98,10 +105,10 @@ def draw_props(self, layout: UILayout, owner: Object, parent_index: int): if self.show_entries: for i, keyframe in enumerate(self.keyframes): keyframe.draw_props( - layout, owner, parent_index, i, is_draw_color, not is_draw_color or self.use_env_color + layout, owner, header_index, parent_index, i, is_draw_color, not is_draw_color or self.use_env_color ) - drawAddButton(layout, len(self.keyframes), "Animated Mat. Color", None, owner.name, parent_index) + drawAddButton(layout, len(self.keyframes), "Animated Mat. Color", header_index, owner.name, parent_index) class Z64_AnimatedMatTexScrollItem(PropertyGroup): @@ -110,8 +117,7 @@ class Z64_AnimatedMatTexScrollItem(PropertyGroup): width: IntProperty(min=0) height: IntProperty(min=0) - def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: int): - drawCollectionOps(layout, index, "Animated Mat. Scroll", None, owner.name, collection_index=parent_index) + 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") @@ -119,28 +125,37 @@ def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: class Z64_AnimatedMatTexScrollParams(PropertyGroup): - entries: CollectionProperty(type=Z64_AnimatedMatTexScrollItem) + texture_1: PointerProperty(type=Z64_AnimatedMatTexScrollItem) + texture_2: PointerProperty(type=Z64_AnimatedMatTexScrollItem) # ui only props show_entries: BoolProperty(default=False) - def draw_props(self, layout: UILayout, owner: Object, parent_index: int): - prop_text = get_list_tab_text("Tex. Scroll", len(self.entries)) - layout.prop(self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + 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: - for i, item in enumerate(self.entries): - item.draw_props(layout, owner, parent_index, i) + 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) - drawAddButton(layout, len(self.entries), "Animated Mat. Scroll", None, owner.name, parent_index) + 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, parent_index: int, index: int): + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int, index: int): drawCollectionOps( - layout, index, "Animated Mat. Cycle (Texture)", None, owner.name, collection_index=parent_index + layout, index, "Animated Mat. Cycle (Texture)", header_index, owner.name, collection_index=parent_index ) prop_split(layout, self, "symbol", "Texture Symbol") @@ -148,8 +163,10 @@ def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: class Z64_AnimatedMatTexCycleKeyFrame(PropertyGroup): texture_index: IntProperty(min=0) - def draw_props(self, layout: UILayout, owner: Object, parent_index: int, index: int): - drawCollectionOps(layout, index, "Animated Mat. Cycle (Index)", None, owner.name, collection_index=parent_index) + 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 + ) prop_split(layout, self, "texture_index", "Texture Symbol") @@ -161,7 +178,7 @@ class Z64_AnimatedMatTexCycleParams(PropertyGroup): show_entries: BoolProperty(default=False) show_textures: BoolProperty(default=False) - def draw_props(self, layout: UILayout, owner: Object, parent_index: int): + 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( @@ -169,9 +186,9 @@ def draw_props(self, layout: UILayout, owner: Object, parent_index: int): ) if self.show_textures: for i, texture in enumerate(self.textures): - texture.draw_props(texture_box, owner, parent_index, i) + texture.draw_props(texture_box, owner, header_index, parent_index, i) drawAddButton( - texture_box, len(self.textures), "Animated Mat. Cycle (Texture)", None, owner.name, parent_index + texture_box, len(self.textures), "Animated Mat. Cycle (Texture)", header_index, owner.name, parent_index ) index_box = layout.box() @@ -179,8 +196,10 @@ def draw_props(self, layout: UILayout, owner: Object, parent_index: int): 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, parent_index, i) - drawAddButton(index_box, len(self.keyframes), "Animated Mat. Cycle (Index)", None, owner.name, parent_index) + keyframe.draw_props(index_box, owner, header_index, parent_index, i) + drawAddButton( + index_box, len(self.keyframes), "Animated Mat. Cycle (Index)", header_index, owner.name, parent_index + ) class Z64_AnimatedMaterialItem(PropertyGroup): @@ -209,16 +228,18 @@ class Z64_AnimatedMaterialItem(PropertyGroup): def on_type_set(self, value: str): self.type = enum_anim_mat_type[value][0] - if self.type in {"color", "color_lerp", "color_nonlinear_interp"}: + 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, index: int): + 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.", None, owner.name) + drawCollectionOps(layout, index, "Animated Mat.", header_index, owner.name) prop_split(layout, self, "segment_num", "Segment Number") @@ -231,11 +252,11 @@ def draw_props(self, layout: UILayout, owner: Object, index: int): ) 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, owner, index) + 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, index) + 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, index) + self.tex_cycle_params.draw_props(layout_type, owner, header_index, index) class Z64_AnimatedMaterial(PropertyGroup): @@ -255,7 +276,7 @@ def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], head if self.show_list: if index is not None: - drawCollectionOps(layout, index, "Animated Mat. List", None, owner.name) + drawCollectionOps(layout, index, "Animated Mat. List", header_index, owner.name) prop_text = get_list_tab_text("Animated Materials", len(self.entries)) layout_entries = layout.column() @@ -265,7 +286,7 @@ def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], head if self.show_entries: for i, item in enumerate(self.entries): - item.draw_props(layout_entries.box().column(), owner, i) + item.draw_props(layout_entries.box().column(), owner, header_index, i) drawAddButton(layout_entries, len(self.entries), "Animated Mat.", header_index, owner.name) @@ -273,8 +294,6 @@ def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], head class Z64_AnimatedMaterialProperty(PropertyGroup): """List of Animated Material arrays""" - mode: EnumProperty(name="Export To", items=enum_mode) - # 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) @@ -285,9 +304,6 @@ class Z64_AnimatedMaterialProperty(PropertyGroup): def draw_props(self, layout: UILayout, owner: Object): layout = layout.column() - prop_split(layout, self, "mode", "Export To") - layout.label(text="Make sure one of the 'Material Animated' draw configs is selected.", icon="QUESTION") - prop_text = get_list_tab_text("Animated Materials List", len(self.items)) layout_entries = layout.column() layout_entries.prop( diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py index 719db54b9..bf222edac 100644 --- a/fast64_internal/z64/collection_utility.py +++ b/fast64_internal/z64/collection_utility.py @@ -112,8 +112,6 @@ def getCollection(objName, collectionType, subIndex: int, collection_index: int collection = props.entries elif collectionType == "Animated Mat. Color": collection = props.entries[collection_index].color_params.keyframes - elif collectionType == "Animated Mat. Scroll": - collection = props.entries[collection_index].tex_scroll_params.entries elif collectionType == "Animated Mat. Cycle (Index)": collection = props.entries[collection_index].tex_cycle_params.keyframes elif collectionType == "Animated Mat. Cycle (Texture)": diff --git a/fast64_internal/z64/constants.py b/fast64_internal/z64/constants.py index 0512c79d4..9c191e42a 100644 --- a/fast64_internal/z64/constants.py +++ b/fast64_internal/z64/constants.py @@ -24,7 +24,6 @@ enum_am_headers_3 = ootEnumHeaderMenuComplete.copy() enum_am_headers_3.pop(3) enum_am_headers_4 = ootEnumHeaderMenuComplete.copy() -enum_am_headers_4.pop(4) am_enum_map = { 1: enum_am_headers_1, 2: enum_am_headers_2, diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index a29277232..143929173 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -18,26 +18,66 @@ from .rooms import RoomEntries -def get_anm_mat_target_name(alt_prop: OOTSceneHeaderProperty, name: str, header_index: int): +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": 1, - "Adult Day": 2, - "Adult Night": 3, + "Child Night": ("childNightHeader", 1), + "Adult Day": ("adultDayHeader", 2), + "Adult Night": ("adultNightHeader", 3), } - anim_mat_header = alt_prop.internal_anim_mat_header - - if anim_mat_header == "Child Day": - index = 0 - elif anim_mat_header == "Cutscene": - assert header_index >= game_data.z64.cs_index_start - index = header_index - else: - index = header_map[anim_mat_header] - + # 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 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 @@ -65,10 +105,18 @@ def new( model: OOTModel, ): i = 0 + use_mat_anim = "mat_anim" in sceneObj.ootSceneHeader.sceneTableEntry.drawConfig try: mainHeader = SceneHeader.new( - f"{name}_header{i:02}", sceneObj.ootSceneHeader, sceneObj, transform, i, exportInfo.useMacros, None + f"{name}_header{i:02}", + sceneObj.ootSceneHeader, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + None, ) if mainHeader.infos is not None: @@ -87,13 +135,20 @@ def new( continue try: - target_name = get_anm_mat_target_name(altP, name, i) + 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, target_name + f"{name}_header{i:02}", + altP, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + target_name, ), ) hasAlternateHeaders = True @@ -103,11 +158,18 @@ def new( altHeader.cutscenes = [] for i, csHeader in enumerate(altProp.cutsceneHeaders, game_data.z64.cs_index_start): try: - target_name = get_anm_mat_target_name(csHeader, name, i) + 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, target_name + f"{name}_header{i:02}", + csHeader, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + target_name, ) ) except Exception as exc: @@ -346,6 +408,18 @@ def getNewSceneFile(self, path: str, isSingleFile: bool, textureExportSettings: "#endif\n\n", ] + # add a macro for the segment number for convenience (only if using animated materials) + mat_seg_num_macro = [ + "// 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", + ] + return SceneFile( self.name, sceneMainData.source, @@ -363,6 +437,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) + + ("\n".join(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 index f8edec34a..46e17cf48 100644 --- a/fast64_internal/z64/exporter/scene/animated_mats.py +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -26,8 +26,7 @@ def __init__( type: str, ): is_draw_color = type == "color" - # the code adds back 7 when processing animated materials - self.segment_num = segment_num - 7 + self.segment_num = segment_num self.type_num = type_num self.base_name = base_name self.header_suffix = f"_{index:02}" @@ -133,16 +132,26 @@ def __init__( index: int, type: str, ): - # the code adds back 7 when processing animated materials - self.segment_num = segment_num - 7 + 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}TexScrollParams{self.header_suffix}" - self.entries: list[str] = [] + 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 - for item in props.entries: - self.entries.append("{ " + f"{item.step_x}, {item.step_y}, {item.width}, {item.height}" + " }") + if type == "two_tex_scroll": + self.name = f"{self.base_name}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}TexScrollParams{self.header_suffix}" def to_c(self): data = CData() @@ -152,9 +161,12 @@ def to_c(self): data.header = f"extern {params_name};\n" # .c - data.source = ( - f"{params_name}" + " = {\n" + indent + f",\n{indent}".join(entry for entry in self.entries) + "\n};\n\n" - ) + 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 @@ -169,8 +181,7 @@ def __init__( index: int, type: str, ): - # the code adds back 7 when processing animated materials - self.segment_num = segment_num - 7 + self.segment_num = segment_num self.type_num = type_num self.base_name = base_name self.header_suffix = f"_{index:02}" @@ -186,6 +197,8 @@ def __init__( 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): data = CData() @@ -248,14 +261,8 @@ def __init__(self, props: Z64_AnimatedMaterial, base_name: str): type = item.type if item.type != "Custom" else item.typeCustom if type != "Custom": class_def, prop_name, type_num = type_list_map[type] - - # example: `self.tex_scroll_entries.append(AnimatedMatTexScrollParams(item.tex_scroll_params, base_name, header_index))` self.entries.append(class_def(getattr(item, prop_name), item.segment_num, type_num, base_name, i, type)) - # the last entry's segment need to be negative - if len(self.entries) > 0 and self.entries[-1].segment_num > 0: - self.entries[-1].segment_num = -self.entries[-1].segment_num - def to_c(self): data = CData() @@ -271,14 +278,18 @@ def to_c(self): data.source += array_name + " = {\n" + indent if len(self.entries) > 0: - data.source += f",\n{indent}".join( - "{ " - + f"{entry.segment_num} /* {abs(entry.segment_num) + 7} */, " + 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 }" @@ -343,10 +354,9 @@ def new(name: str, scene_obj: Object, header_index: int): entries: list[AnimatedMaterial] = [] for obj in obj_list: - if obj.fast64.oot.animated_materials.mode == "Scene": - entries.extend( - [AnimatedMaterial(item, name, header_index) for item in obj.fast64.oot.animated_materials.items] - ) + 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/header.py b/fast64_internal/z64/exporter/scene/header.py index 4543e12da..0e39d1f5b 100644 --- a/fast64_internal/z64/exporter/scene/header.py +++ b/fast64_internal/z64/exporter/scene/header.py @@ -38,12 +38,13 @@ def new( 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 not is_oot_features(): + 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) diff --git a/fast64_internal/z64/scene/properties.py b/fast64_internal/z64/scene/properties.py index fc8fada9c..13c5be51e 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -447,6 +447,13 @@ def draw_props( if headerIndex is not None: layout.prop(self, "reuse_anim_mat", text="Use Existing Material Anim.") + if "mat_anim" not in obj.ootSceneHeader.sceneTableEntry.drawConfig: + wrong_box = layout.box().column() + wrong_box.label(text="Wrong Draw Config", icon="ERROR") + 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") @@ -471,7 +478,7 @@ 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, obj: Object): headerSetup = layout.column() From 25712fbe2ad6d5c15d574c7adc0a78816c13c998 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:26:05 +0100 Subject: [PATCH 12/20] add clear and copy, improve add and delete --- fast64_internal/utility.py | 21 +- .../z64/animated_mats/properties.py | 82 +++++- fast64_internal/z64/collection_utility.py | 257 +++++++++++++++++- fast64_internal/z64/utility.py | 5 +- 4 files changed, 332 insertions(+), 33 deletions(-) diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 01b31174d..41d65520a 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): diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index 3d30a7dab..689a51e6a 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -13,7 +13,7 @@ from typing import Optional from ...utility import prop_split -from ..collection_utility import drawAddButton, drawCollectionOps +from ..collection_utility import drawAddButton, drawCollectionOps, draw_utility_ops from ..utility import get_list_tab_text, getEnumIndex @@ -67,7 +67,16 @@ def draw_props( is_draw_color: bool, use_env_color: bool, ): - drawCollectionOps(layout, index, "Animated Mat. Color", header_index, owner.name, collection_index=parent_index) + 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: @@ -108,7 +117,15 @@ def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_ layout, owner, header_index, parent_index, i, is_draw_color, not is_draw_color or self.use_env_color ) - drawAddButton(layout, len(self.keyframes), "Animated Mat. Color", header_index, owner.name, parent_index) + 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): @@ -155,7 +172,14 @@ class Z64_AnimatedMatTexCycleTexture(PropertyGroup): 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 + 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") @@ -165,7 +189,14 @@ class Z64_AnimatedMatTexCycleKeyFrame(PropertyGroup): 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 + 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") @@ -187,8 +218,14 @@ def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_ if self.show_textures: for i, texture in enumerate(self.textures): texture.draw_props(texture_box, owner, header_index, parent_index, i) - drawAddButton( - texture_box, len(self.textures), "Animated Mat. Cycle (Texture)", header_index, owner.name, parent_index + 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() @@ -197,8 +234,14 @@ def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_ if self.show_entries: for i, keyframe in enumerate(self.keyframes): keyframe.draw_props(index_box, owner, header_index, parent_index, i) - drawAddButton( - index_box, len(self.keyframes), "Animated Mat. Cycle (Index)", header_index, owner.name, parent_index + draw_utility_ops( + index_box.row(), + len(self.keyframes), + "Animated Mat. Cycle (Index)", + header_index, + owner.name, + parent_index, + ask_for_amount=True, ) @@ -239,7 +282,7 @@ def draw_props(self, layout: UILayout, owner: Object, header_index: int, index: ) if self.show_item: - drawCollectionOps(layout, index, "Animated Mat.", header_index, owner.name) + drawCollectionOps(layout, index, "Animated Mat.", header_index, owner.name, ask_for_amount=True) prop_split(layout, self, "segment_num", "Segment Number") @@ -276,7 +319,15 @@ def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], head if self.show_list: if index is not None: - drawCollectionOps(layout, index, "Animated Mat. List", header_index, owner.name) + drawCollectionOps( + layout, + index, + "Animated Mat. List", + header_index, + owner.name, + ask_for_copy=True, + ask_for_amount=True, + ) prop_text = get_list_tab_text("Animated Materials", len(self.entries)) layout_entries = layout.column() @@ -288,7 +339,14 @@ def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], head for i, item in enumerate(self.entries): item.draw_props(layout_entries.box().column(), owner, header_index, i) - drawAddButton(layout_entries, len(self.entries), "Animated Mat.", header_index, owner.name) + draw_utility_ops( + layout_entries.row(), + len(self.entries), + "Animated Mat.", + header_index, + owner.name, + ask_for_amount=True, + ) class Z64_AnimatedMaterialProperty(PropertyGroup): diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py index bf222edac..2ad378baf 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): @@ -17,11 +20,25 @@ class OOTCollectionAdd(Operator): 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, self.collection_index) - new_entry = collection.add() - collection.move(len(collection) - 1, self.option) + 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": @@ -30,8 +47,22 @@ def execute(self, context): 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" @@ -44,16 +75,69 @@ class OOTCollectionRemove(Operator): 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) + + @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, self.collection_index) - collection.remove(self.option) + + 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 - 1}." + + layout.label(text=text) + class OOTCollectionMove(Operator): bl_idname = "object.oot_collection_move" @@ -71,9 +155,97 @@ class OOTCollectionMove(Operator): def execute(self, context): 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) return getattr(header, prop) @@ -104,7 +276,8 @@ def getCollection(objName, collectionType, subIndex: int, collection_index: int collection = obj.fast64.oot.animated_materials.items elif collectionType.startswith("Animated Mat."): if obj.ootEmptyType == "Scene": - props = getCollectionFromIndex(obj, "animated_material", subIndex, False) + header = ootGetSceneOrRoomHeader(obj, subIndex, False) + props = header.animated_material else: props = obj.fast64.oot.animated_materials.items[subIndex] @@ -145,7 +318,16 @@ def getCollection(objName, collectionType, subIndex: int, collection_index: int return collection -def drawAddButton(layout, index, collectionType, subIndex, objName, collection_index: int = 0): +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) @@ -154,10 +336,62 @@ def drawAddButton(layout, index, collectionType, subIndex, objName, collection_i 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 + + +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, + 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) + 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 + 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 @@ -174,6 +408,8 @@ def drawCollectionOps( 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 @@ -181,6 +417,7 @@ def drawCollectionOps( 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 @@ -203,6 +440,8 @@ def drawCollectionOps( OOTCollectionAdd, OOTCollectionRemove, OOTCollectionMove, + OOTCollectionClear, + OOTCollectionCopy, ) diff --git a/fast64_internal/z64/utility.py b/fast64_internal/z64/utility.py index de793bb83..a48bc79b1 100644 --- a/fast64_internal/z64/utility.py +++ b/fast64_internal/z64/utility.py @@ -805,8 +805,9 @@ def callback(thisHeader, otherObj: bpy.types.Object): onHeaderPropertyChange(self, context, callback) - if context.object.ootEmptyType == "Scene": - on_alt_menu_tab_change(self, context) + if context.view_layer.objects.active.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]): From 17b0e9ac5080df1fc1e00409967768e39882c963 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:23:57 +0100 Subject: [PATCH 13/20] fixed importer --- .../z64/animated_mats/properties.py | 8 +- fast64_internal/z64/importer/scene_header.py | 143 ++++++++++-------- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index 689a51e6a..5bcc21232 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -134,6 +134,12 @@ class Z64_AnimatedMatTexScrollItem(PropertyGroup): 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") @@ -268,7 +274,7 @@ class Z64_AnimatedMaterialItem(PropertyGroup): # ui only props show_item: BoolProperty(default=False) - def on_type_set(self, value: str): + def on_type_set(self, value: int): self.type = enum_anim_mat_type[value][0] if "tex_scroll" in self.type: diff --git a/fast64_internal/z64/importer/scene_header.py b/fast64_internal/z64/importer/scene_header.py index 373b821e3..0565f61a6 100644 --- a/fast64_internal/z64/importer/scene_header.py +++ b/fast64_internal/z64/importer/scene_header.py @@ -1,5 +1,4 @@ import math -import os import re import bpy import mathutils @@ -8,14 +7,14 @@ from typing import Optional from ...game_data import game_data -from ...utility import PluginError, readFile, parentObject, hexOrDecInt, gammaInverse, get_new_empty_object +from ...utility import PluginError, 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 getEvalParams, setCustomProperty, getObjectList, is_hackeroot +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 @@ -25,11 +24,9 @@ from ..constants import ( ootEnumAudioSessionPreset, - ootEnumMusicSeq, ootEnumCameraMode, ootEnumMapLocation, ootEnumNaviHints, - ootEnumGlobalObject, ootEnumSkyboxLighting, ) @@ -236,108 +233,124 @@ def parseAlternateSceneHeaders( ) -animated_material_first_list_name = "" -anim_mat_type_to_struct = { - 0: "AnimatedMatTexScrollParams", - 1: "AnimatedMatTexScrollParams", - 2: "AnimatedMatColorParams", - 3: "AnimatedMatColorParams", - 4: "AnimatedMatColorParams", - 5: "AnimatedMatTexCycleParams", -} +def parse_animated_material(scene_header: OOTSceneHeaderProperty, header_index: int, scene_data: str, list_name: str): + anim_mat_type_to_struct = { + 0: "AnimatedMatTexScrollParams", + 1: "AnimatedMatTexScrollParams", + 2: "AnimatedMatColorParams", + 3: "AnimatedMatColorParams", + 4: "AnimatedMatColorParams", + 5: "AnimatedMatTexCycleParams", + } - -def parse_animated_material(scene_obj: bpy.types.Object, header_index: int, scene_data: str, list_name: str): - global animated_material_first_list_name + 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") - if header_index == 0: - animated_material_first_list_name = list_name - anim_mat_obj = get_new_empty_object("Animated Material") - anim_mat_obj.ootEmptyType = "Animated Materials" - parentObject(scene_obj, anim_mat_obj) - else: - obj_list = getObjectList(scene_obj.children_recursive, "EMPTY", "Animated Materials") - anim_mat_obj = obj_list[0] - - # if the alternate header is using the first header's data then don't do anything - if header_index > 0 and list_name == animated_material_first_list_name: - return - - anim_mat_props = anim_mat_obj.fast64.oot.animated_materials - anim_mat_item = anim_mat_props.items.add() - anim_mat_item.header_index = header_index + anim_mat_item = scene_header.animated_material for data in anim_mat_data: data = data.replace("{", "").replace("}", "").removesuffix(",").strip() split = data.split(", ") - segment = int(split[0], base=0) + raw_segment = split[0] + + if "MATERIAL_SEGMENT_NUM" in raw_segment: + raw_segment = raw_segment.removesuffix(")").split("(")[1] + + segment = int(raw_segment, base=0) type_num = int(split[1], 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 = data_match.replace("{", "").replace("}", "").replace(" ", "").split("\n") + 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: - params_data = data_match.replace("\n", "").replace(" ", "").split(",") + 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_item.entries.add() - entry.segment_num = abs(segment) + 7 - entry.type = enum_anim_mat_type[type_num + 1][0] + 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": - for params in params_data: - if len(params) > 0: - split = params.split(",") - scroll_entry = entry.tex_scroll_params.entries.add() - scroll_entry.step_x = int(split[0], base=0) - scroll_entry.step_y = int(split[1], base=0) - scroll_entry.width = int(split[2], base=0) - scroll_entry.height = int(split[3], base=0) + 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") - 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_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") - ) + 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] - env_color = [hexOrDecInt(elem) for elem in env_color_raw.split(",") if len(elem) > 0] color_entry = entry.color_params.keyframes.add() - color_entry.frame_num = int(frame, base=0) + + 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,) - color_entry.env_color = parseColor(env_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(",", "\n").strip().split("\n"): - textures.append(texture_ptr.strip()) + 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"): @@ -485,7 +498,7 @@ def parseSceneCommands( elif game_data.z64.is_mm() or is_hackeroot(): if command == "SCENE_CMD_ANIMATED_MATERIAL_LIST": if sharedSceneData.includeAnimatedMats: - parse_animated_material(sceneObj, headerIndex, sceneData, stripName(args[0])) + parse_animated_material(sceneHeader, headerIndex, sceneData, stripName(args[0])) command_list.remove(command) if "SCENE_CMD_ROOM_LIST" in delayed_commands: From e01e8655d64ba399b41f43130f8a75d96185375f Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:33:47 +0100 Subject: [PATCH 14/20] add export panel for actors --- fast64_internal/utility.py | 2 +- fast64_internal/z64/__init__.py | 15 +- .../z64/animated_mats/operators.py | 54 +++++ fast64_internal/z64/animated_mats/panels.py | 29 +++ .../z64/animated_mats/properties.py | 92 +++++++- fast64_internal/z64/collection_utility.py | 5 +- .../z64/exporter/scene/__init__.py | 15 +- .../z64/exporter/scene/animated_mats.py | 213 +++++++++++++----- fast64_internal/z64/importer/scene_header.py | 17 +- fast64_internal/z64/tools/operators.py | 5 +- 10 files changed, 356 insertions(+), 91 deletions(-) create mode 100644 fast64_internal/z64/animated_mats/operators.py create mode 100644 fast64_internal/z64/animated_mats/panels.py diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 41d65520a..4610d1b34 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -1332,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)): diff --git a/fast64_internal/z64/__init__.py b/fast64_internal/z64/__init__.py index 4767da579..4eba6a0a9 100644 --- a/fast64_internal/z64/__init__.py +++ b/fast64_internal/z64/__init__.py @@ -57,7 +57,14 @@ from .spline.properties import spline_props_register, spline_props_unregister from .spline.panels import spline_panels_register, spline_panels_unregister -from .animated_mats.properties import animated_mats_props_register, animated_mats_props_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 @@ -118,6 +125,8 @@ class OOT_Properties(bpy.types.PropertyGroup): 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 @@ -173,6 +182,7 @@ def oot_panel_register(): spline_panels_register() anim_panels_register() skeleton_panels_register() + animated_mats_panels_register() def oot_panel_unregister(): @@ -186,6 +196,7 @@ def oot_panel_unregister(): f3d_panels_unregister() anim_panels_unregister() skeleton_panels_unregister() + animated_mats_panels_unregister() def oot_register(registerPanels): @@ -194,6 +205,7 @@ 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() @@ -261,6 +273,7 @@ def oot_unregister(unregisterPanels): scene_props_unregister() scene_ops_unregister() animated_mats_props_unregister() + animated_mats_ops_unregister() cutscene_props_unregister() collision_props_unregister() collision_ops_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 index 5bcc21232..7d4e23168 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -1,3 +1,5 @@ +import bpy + from bpy.utils import register_class, unregister_class from bpy.types import PropertyGroup, UILayout, Object from bpy.props import ( @@ -13,8 +15,9 @@ from typing import Optional from ...utility import prop_split -from ..collection_utility import drawAddButton, drawCollectionOps, draw_utility_ops -from ..utility import get_list_tab_text, getEnumIndex +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 @@ -317,7 +320,14 @@ class Z64_AnimatedMaterial(PropertyGroup): show_list: BoolProperty(default=True) show_entries: BoolProperty(default=True) - def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], header_index: Optional[int] = None): + 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" @@ -329,10 +339,10 @@ def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], head layout, index, "Animated Mat. List", - header_index, + sub_index, owner.name, - ask_for_copy=True, - ask_for_amount=True, + ask_for_copy=False, + ask_for_amount=False, ) prop_text = get_list_tab_text("Animated Materials", len(self.entries)) @@ -343,14 +353,15 @@ def draw_props(self, layout: UILayout, owner: Object, index: Optional[int], head if self.show_entries: for i, item in enumerate(self.entries): - item.draw_props(layout_entries.box().column(), owner, header_index, i) + item.draw_props(layout_entries.box().column(), owner, sub_index, i) draw_utility_ops( layout_entries.row(), len(self.entries), "Animated Mat.", - header_index, + sub_index, owner.name, + do_copy=is_scene, ask_for_amount=True, ) @@ -376,9 +387,68 @@ def draw_props(self, layout: UILayout, owner: Object): if self.show_entries: for i, item in enumerate(self.items): - item.draw_props(layout_entries.box().column(), owner, i) + 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" - drawAddButton(layout_entries, len(self.items), "Animated Mat. List", None, owner.name) + 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 = ( @@ -392,6 +462,8 @@ def draw_props(self, layout: UILayout, owner: Object): Z64_AnimatedMaterialItem, Z64_AnimatedMaterial, Z64_AnimatedMaterialProperty, + Z64_AnimatedMaterialExportSettings, + Z64_AnimatedMaterialImportSettings, ) diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py index 2ad378baf..ca481cc0d 100644 --- a/fast64_internal/z64/collection_utility.py +++ b/fast64_internal/z64/collection_utility.py @@ -371,6 +371,7 @@ def draw_utility_ops( 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, ): @@ -378,7 +379,9 @@ def draw_utility_ops( 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) - draw_copy_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( diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index 143929173..2f2274696 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -14,6 +14,7 @@ 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 @@ -408,18 +409,6 @@ def getNewSceneFile(self, path: str, isSingleFile: bool, textureExportSettings: "#endif\n\n", ] - # add a macro for the segment number for convenience (only if using animated materials) - mat_seg_num_macro = [ - "// 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", - ] - return SceneFile( self.name, sceneMainData.source, @@ -437,7 +426,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) - + ("\n".join(mat_seg_num_macro) if "AnimatedMaterial" in sceneMainData.header else "") + + (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 index 46e17cf48..6b4b566e0 100644 --- a/fast64_internal/z64/exporter/scene/animated_mats.py +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -1,19 +1,25 @@ 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, indent -from ...utility import getObjectList, is_oot_features, is_hackeroot +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__( @@ -24,13 +30,14 @@ def __init__( 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}ColorParams{self.header_suffix}" + 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]] = [] @@ -57,7 +64,7 @@ def __init__( if is_draw_color and props.use_env_color: assert len(self.prim_colors) == len(self.env_colors) - def to_c(self): + 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}" @@ -71,12 +78,13 @@ def to_c(self): frames_array_name = "NULL" # .h - 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" - ) + 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 = ( @@ -131,6 +139,7 @@ def __init__( base_name: str, index: int, type: str, + suffix: str = "", ): self.segment_num = segment_num self.type_num = type_num @@ -144,21 +153,22 @@ def __init__( self.texture_2: Optional[str] = None if type == "two_tex_scroll": - self.name = f"{self.base_name}TwoTexScrollParams{self.header_suffix}" + 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}TexScrollParams{self.header_suffix}" + self.name = f"{self.base_name}{suffix}TexScrollParams{self.header_suffix}" - def to_c(self): + def to_c(self, all_externs: bool = True): data = CData() params_name = f"AnimatedMatTexScrollParams {self.name}[]" # .h - data.header = f"extern {params_name};\n" + if all_externs: + data.header = f"extern {params_name};\n" # .c data.source = f"{params_name}" + " = {\n" + indent + self.texture_1 @@ -180,12 +190,13 @@ def __init__( 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}TexCycleParams{self.header_suffix}" + self.name = f"{self.base_name}{suffix}TexCycleParams{self.header_suffix}" self.textures: list[str] = [] self.texture_indices: list[int] = [] @@ -200,18 +211,19 @@ def __init__( 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): + 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 - data.header = ( - f"extern TexturePtr {texture_array_name}[];\n" - + f"extern u8 {texture_indices_array_name}[];\n" - + f"extern {params_name};\n" - ) + 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 = ( @@ -239,7 +251,7 @@ def to_c(self): class AnimatedMaterial: - def __init__(self, props: Z64_AnimatedMaterial, base_name: str): + def __init__(self, props: Z64_AnimatedMaterial, base_name: str, suffix: str = ""): self.name = base_name self.entries: list[AnimatedMatColorParams | AnimatedMatTexScrollParams | AnimatedMatTexCycleParams] = [] @@ -261,18 +273,20 @@ def __init__(self, props: Z64_AnimatedMaterial, base_name: str): 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)) + self.entries.append( + class_def(getattr(item, prop_name), item.segment_num, type_num, base_name, i, type, suffix) + ) - def to_c(self): + def to_c(self, all_externs: bool = True): data = CData() for entry in self.entries: - data.append(entry.to_c()) + data.append(entry.to_c(all_externs)) array_name = f"AnimatedMaterial {self.name}[]" # .h - data.header += f"extern {array_name};" + data.header += f"extern {array_name};\n" # .c data.source += array_name + " = {\n" + indent @@ -299,15 +313,126 @@ def to_c(self): @dataclass class SceneAnimatedMaterial: - """This class hosts exit data""" + """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""" @@ -331,19 +456,19 @@ def to_c(self): data.append(self.animated_material.to_c()) + extra = "" if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: - data.source += "#endif\n\n" - data.header += "\n#endif\n" - else: - data.source += "\n" - data.header += "\n" + extra = "#endif\n" + + data.source += extra + "\n" + data.header += "\n" + extra return data @dataclass class ActorAnimatedMaterial: - """This class hosts exit data""" + """This class hosts Animated Materials data for actors""" name: str entries: list[AnimatedMaterial] @@ -359,25 +484,3 @@ def new(name: str, scene_obj: Object, header_index: int): ) return ActorAnimatedMaterial(name, entries) - - def is_used(self): - return not is_oot_features() and len(self.entries) > 0 - - def to_c(self): - data = CData() - - 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" - - for entry in self.entries: - data.append(entry.to_c()) - - if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: - data.source += "#endif\n\n" - data.header += "\n#endif\n" - else: - data.source += "\n" - data.header += "\n" - - return data diff --git a/fast64_internal/z64/importer/scene_header.py b/fast64_internal/z64/importer/scene_header.py index 0565f61a6..cf413deeb 100644 --- a/fast64_internal/z64/importer/scene_header.py +++ b/fast64_internal/z64/importer/scene_header.py @@ -20,7 +20,7 @@ from .actor import parseTransActorList, parseSpawnList, parseEntranceList from .scene_collision import parseCollisionHeader from .scene_pathways import parsePathList -from ..animated_mats.properties import enum_anim_mat_type +from ..animated_mats.properties import Z64_AnimatedMaterial, enum_anim_mat_type from ..constants import ( ootEnumAudioSessionPreset, @@ -233,7 +233,7 @@ def parseAlternateSceneHeaders( ) -def parse_animated_material(scene_header: OOTSceneHeaderProperty, header_index: int, scene_data: str, list_name: str): +def parse_animated_material(anim_mat: Z64_AnimatedMaterial, scene_data: str, list_name: str): anim_mat_type_to_struct = { 0: "AnimatedMatTexScrollParams", 1: "AnimatedMatTexScrollParams", @@ -252,19 +252,22 @@ def parse_animated_material(scene_header: OOTSceneHeaderProperty, header_index: data_match = getDataMatch(scene_data, list_name, "AnimatedMaterial", "animated material") anim_mat_data = data_match.strip().split("\n") - anim_mat_item = scene_header.animated_material - 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) - type_num = int(split[1], base=0) data_ptr = split[2].removeprefix("&") is_array = type_num in {0, 1} @@ -286,7 +289,7 @@ def parse_animated_material(scene_header: OOTSceneHeaderProperty, header_index: if struct_name == "AnimatedMatColorParams": params_data.extend([match.group(7), match.group(9)]) - entry = anim_mat_item.entries.add() + 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)) @@ -498,7 +501,7 @@ def parseSceneCommands( elif game_data.z64.is_mm() or is_hackeroot(): if command == "SCENE_CMD_ANIMATED_MATERIAL_LIST": if sharedSceneData.includeAnimatedMats: - parse_animated_material(sceneHeader, headerIndex, sceneData, stripName(args[0])) + parse_animated_material(sceneHeader.animated_material, sceneData, stripName(args[0])) command_list.remove(command) if "SCENE_CMD_ROOM_LIST" in delayed_commands: diff --git a/fast64_internal/z64/tools/operators.py b/fast64_internal/z64/tools/operators.py index 5a8ba6a43..0d8e26a8c 100644 --- a/fast64_internal/z64/tools/operators.py +++ b/fast64_internal/z64/tools/operators.py @@ -311,7 +311,7 @@ class Z64_AddAnimatedMaterial(Operator): bl_description = "Create a new Animated Material empty object." add_test_color: BoolProperty(default=False) - obj_name: StringProperty(default="Scene Animated Materials") + obj_name: StringProperty(default="Actor Animated Materials") def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -321,8 +321,7 @@ def draw(self, context): self.layout.prop(self, "add_test_color", text="Add Color Non-linear Interpolation Example") def execute(self, context: Context): - scene_obj: Object = context.scene.ootSceneExportObj - new_obj = get_new_empty_object(self.obj_name, parent=scene_obj) + new_obj = get_new_empty_object(self.obj_name) new_obj.ootEmptyType = "Animated Materials" if self.add_test_color: From c06d9a49c367da0098920e6e987f482edd7d9cd3 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:18:42 +0100 Subject: [PATCH 15/20] docs update --- fast64_internal/z64/README.md | 38 ++++---------------- images/z64/animated_materials/am_part_1.png | Bin 13698 -> 0 bytes images/z64/animated_materials/am_part_2.png | Bin 24848 -> 0 bytes images/z64/animated_materials/am_part_3.png | Bin 38323 -> 0 bytes 4 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 images/z64/animated_materials/am_part_1.png delete mode 100644 images/z64/animated_materials/am_part_2.png delete mode 100644 images/z64/animated_materials/am_part_3.png diff --git a/fast64_internal/z64/README.md b/fast64_internal/z64/README.md index 90cb8868a..1cc2cdaa3 100644 --- a/fast64_internal/z64/README.md +++ b/fast64_internal/z64/README.md @@ -204,35 +204,15 @@ This is a feature you can use for Majora's Mask and OoT backports like HackerOoT **Getting Started** -To get started 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 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. -

-This is how the UI should look like at this point: - -![alt-text](/images/z64/animated_materials/am_part_1.png) -
- -Note: the `Export To` option doesn't do anything, leave it to `Scene`. In the future you will be able to export to an actor. +For scenes it's integrated as a tab in the scene header properties panel. **Creating the animated materials list** -Click on `Add Item` to add a new animated material list. - -
-This is how the UI should look like at this point: - -![alt-text](/images/z64/animated_materials/am_part_2.png) -
- -`Header Index` lets you choose which header this list belongs to, a value of `-1` means "every headers". Below you should have the list of the materials you can setup. Click on `Add Item` to add a new item to that list. - -
-This is how the UI should look like at this point: - -![alt-text](/images/z64/animated_materials/am_part_3.png) -
+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 it shows the real number then when it exports it corrects that number. It's just how the in-game implementation works. `Draw Handler Type` lets you choose what kind of animated material you want, it can be one of: +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 @@ -240,9 +220,7 @@ You can pick the segment number with the `Segment Number` field (make sure to us - `4`: Color Non-linear Interpolation - `5`: Texture Cycle (like a GIF) -Note: for HackerOoT users, you can choose to toggle exporting the `ENABLE_ANIMATED_MATERIALS` ifdef around the segment call from the display list with the `Use Segment for Animated Materials` checkbox in the material's `Dynamic Material Properties` panel. This is not mandatory and only there for convenience. - -For the color types you will also have a `Keyframe Length` field, this corresponds to the length of the animation. +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 @@ -250,12 +228,10 @@ Both texture scroll types will use the same elements: - `Texture Width`: the width of the texture - `Texture Height`: the height of the texture -Note: for the two-textures scroll type you will need to add 2 items per texture since it targets multi-textures (it can be used to animate water for instance). - All 3 color types will use the same elements: -- `Frame No.`: when to execute this entry (relative to the keyframe length) +- `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 +- `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/images/z64/animated_materials/am_part_1.png b/images/z64/animated_materials/am_part_1.png deleted file mode 100644 index da4635c81d44f73c093d072a1ba2212a0c8a140b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13698 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ð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 diff --git a/images/z64/animated_materials/am_part_2.png b/images/z64/animated_materials/am_part_2.png deleted file mode 100644 index c3cf7a7236e23667a990df8bc8289a98fe6f7d80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24848 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 Hdh>q(fInJQ diff --git a/images/z64/animated_materials/am_part_3.png b/images/z64/animated_materials/am_part_3.png deleted file mode 100644 index 310c1d3dbb827fa4d9c858a21417317f2bc6c3e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38323 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 z zK1}VAqEpT(@itLONuaPax6~LGwuXmYxPtl@)nowUvd|891c0Yc52ZMsP;+&MZ k@-SKTC%X=A1?$GPn{pv5@nxmSWcWuz^`uIk^4XyO2f1cbTmS$7 From 71e1871eb52435c8f5fcbfd5833329979ffcb68e Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:43:00 +0100 Subject: [PATCH 16/20] self-review --- .../data/z64/xml/oot_enum_data.xml | 2 +- fast64_internal/z64/README.md | 2 +- fast64_internal/z64/collection_utility.py | 3 +- fast64_internal/z64/constants.py | 16 --------- .../z64/exporter/scene/__init__.py | 2 +- .../z64/exporter/scene/animated_mats.py | 4 +-- fast64_internal/z64/exporter/scene/header.py | 2 +- fast64_internal/z64/importer/scene_header.py | 2 +- fast64_internal/z64/scene/properties.py | 33 ++++++++++++++++--- fast64_internal/z64/utility.py | 3 +- 10 files changed, 40 insertions(+), 29 deletions(-) diff --git a/fast64_internal/data/z64/xml/oot_enum_data.xml b/fast64_internal/data/z64/xml/oot_enum_data.xml index 3c60e1680..088725257 100644 --- a/fast64_internal/data/z64/xml/oot_enum_data.xml +++ b/fast64_internal/data/z64/xml/oot_enum_data.xml @@ -737,7 +737,7 @@ - + diff --git a/fast64_internal/z64/README.md b/fast64_internal/z64/README.md index 1cc2cdaa3..5a0259917 100644 --- a/fast64_internal/z64/README.md +++ b/fast64_internal/z64/README.md @@ -12,7 +12,7 @@ 9. [Custom Link Process](#custom-link-process) 10. [Custom Skeleton Mesh Process](#custom-skeleton-mesh-process) 11. [Cutscenes](#cutscenes) -11. [Animated Materials](#animated-materials) +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``. diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py index ca481cc0d..6e605e138 100644 --- a/fast64_internal/z64/collection_utility.py +++ b/fast64_internal/z64/collection_utility.py @@ -84,6 +84,7 @@ class OOTCollectionRemove(Operator): ) 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 @@ -134,7 +135,7 @@ def draw(self, _): 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 - 1}." + text = f"Will remove Item No. {self.option + 1} and the next {self.amount}." layout.label(text=text) diff --git a/fast64_internal/z64/constants.py b/fast64_internal/z64/constants.py index 9c191e42a..d12ef01ac 100644 --- a/fast64_internal/z64/constants.py +++ b/fast64_internal/z64/constants.py @@ -15,22 +15,6 @@ ("Child Day", "Child Day", "Child Day"), ] + ootEnumHeaderMenu -enum_am_headers_1 = ootEnumHeaderMenuComplete.copy() -enum_am_headers_1.pop(1) -enum_am_headers_1 = ootEnumHeaderMenuComplete.copy() -enum_am_headers_1.pop(1) -enum_am_headers_2 = ootEnumHeaderMenuComplete.copy() -enum_am_headers_2.pop(2) -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, -} - ootEnumCameraMode = [ ("Custom", "Custom", "Custom"), ("0x00", "Default", "Default"), diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index 2f2274696..327d9504f 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -44,7 +44,7 @@ def get_anm_mat_target_name(scene_obj: Object, alt_prop: OOTSceneHeaderProperty, 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 set header A to reuse header B and header B to reuse header A + # 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" diff --git a/fast64_internal/z64/exporter/scene/animated_mats.py b/fast64_internal/z64/exporter/scene/animated_mats.py index 6b4b566e0..452ab1952 100644 --- a/fast64_internal/z64/exporter/scene/animated_mats.py +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -446,7 +446,7 @@ def get_cmd(self): else: return indent + f"SCENE_CMD_ANIMATED_MATERIAL_LIST({self.name}),\n" - def to_c(self): + def to_c(self, is_scene: bool = True): data = CData() if self.animated_material is not None: @@ -461,7 +461,7 @@ def to_c(self): extra = "#endif\n" data.source += extra + "\n" - data.header += "\n" + extra + data.header += ("\n" if not is_scene else "") + extra return data diff --git a/fast64_internal/z64/exporter/scene/header.py b/fast64_internal/z64/exporter/scene/header.py index 0e39d1f5b..21b03e4d5 100644 --- a/fast64_internal/z64/exporter/scene/header.py +++ b/fast64_internal/z64/exporter/scene/header.py @@ -27,7 +27,7 @@ class SceneHeader: spawns: Optional[SceneSpawns] path: Optional[ScenePathways] - # MM (or HackerOoT) + # MM (or modded OoT) anim_mat: Optional[SceneAnimatedMaterial] @staticmethod diff --git a/fast64_internal/z64/importer/scene_header.py b/fast64_internal/z64/importer/scene_header.py index cf413deeb..6489f7461 100644 --- a/fast64_internal/z64/importer/scene_header.py +++ b/fast64_internal/z64/importer/scene_header.py @@ -497,7 +497,7 @@ def parseSceneCommands( elif command == "SCENE_CMD_END": command_list.remove(command) - # handle Majora's Mask (or HackerOoT) exclusive commands + # 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: diff --git a/fast64_internal/z64/scene/properties.py b/fast64_internal/z64/scene/properties.py index 13c5be51e..3d5626b4e 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -32,7 +32,6 @@ ootEnumAudioSessionPreset, ootEnumHeaderMenu, ootEnumHeaderMenuComplete, - am_enum_map, ) ootEnumSceneMenuAlternate = [ @@ -337,6 +336,27 @@ class OOTSceneHeaderProperty(PropertyGroup): ) 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): @@ -450,9 +470,14 @@ def draw_props( if "mat_anim" not in obj.ootSceneHeader.sceneTableEntry.drawConfig: wrong_box = layout.box().column() wrong_box.label(text="Wrong Draw Config", icon="ERROR") - 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 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 diff --git a/fast64_internal/z64/utility.py b/fast64_internal/z64/utility.py index a48bc79b1..385c823ef 100644 --- a/fast64_internal/z64/utility.py +++ b/fast64_internal/z64/utility.py @@ -805,7 +805,8 @@ def callback(thisHeader, otherObj: bpy.types.Object): onHeaderPropertyChange(self, context, callback) - if context.view_layer.objects.active.ootEmptyType == "Scene": + 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) From b58762cc44b6a793551c1e71e81eaf95cc5c677b Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:07:59 +0100 Subject: [PATCH 17/20] force frame_num to be under keyframe_length (+ small export bugfix) --- .../z64/animated_mats/properties.py | 38 +++++++++++++++++-- .../z64/exporter/scene/__init__.py | 5 ++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index 7d4e23168..a355410b9 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -39,9 +39,25 @@ class Z64_AnimatedMatColorKeyFrame(PropertyGroup): - frame_num: IntProperty(name="Frame No.", min=0) + 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 on_frame_num_set(self, value): + self.internal_frame_num = value - prim_lod_frac: IntProperty(name="Primitive LOD Frac", min=0, max=255) + def on_frame_num_get(self): + if self.internal_frame_num >= self.internal_length: + self.internal_frame_num = self.internal_length - 1 + + 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", @@ -93,7 +109,14 @@ def draw_props( class Z64_AnimatedMatColorParams(PropertyGroup): - keyframe_length: IntProperty(name="Keyframe Length", min=0) + 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() @@ -102,6 +125,15 @@ class Z64_AnimatedMatColorParams(PropertyGroup): internal_color_type: StringProperty() + def on_length_set(self, value): + self.internal_keyframe_length = value + + for keyframe in self.keyframes: + keyframe.internal_length = value + + def on_length_get(self): + 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" diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py index 327d9504f..013532291 100644 --- a/fast64_internal/z64/exporter/scene/__init__.py +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -106,7 +106,10 @@ def new( model: OOTModel, ): i = 0 - use_mat_anim = "mat_anim" in sceneObj.ootSceneHeader.sceneTableEntry.drawConfig + use_mat_anim = ( + "mat_anim" in sceneObj.ootSceneHeader.sceneTableEntry.drawConfig + or bpy.context.scene.ootSceneExportSettings.customExport + ) try: mainHeader = SceneHeader.new( From 78d2d1dbaa5954d38ecc311ab4e7717d6bb095e1 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:31:03 +0100 Subject: [PATCH 18/20] fixed tiny issue --- fast64_internal/data/z64/enum_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast64_internal/data/z64/enum_data.py b/fast64_internal/data/z64/enum_data.py index bb5538f68..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.get("Name"), + item.attrib.get("Name"), int(item.attrib["Index"]), enum.attrib["Key"], game, From fd8156c3ce864f1d05da46e4e8f01f21356a6d11 Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:50:35 +0100 Subject: [PATCH 19/20] fixed tiny issue --- .../z64/animated_mats/properties.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index a355410b9..9ec518ab0 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -48,13 +48,16 @@ class Z64_AnimatedMatColorKeyFrame(PropertyGroup): internal_frame_num: IntProperty(min=0) internal_length: IntProperty(min=0) + def validate_frame_num(self): + if self.internal_frame_num >= self.internal_length: + 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): - if self.internal_frame_num >= self.internal_length: - self.internal_frame_num = self.internal_length - 1 - + self.validate_frame_num() return self.internal_frame_num prim_lod_frac: IntProperty(name="Primitive LOD Frac", min=0, max=255, default=128) @@ -125,13 +128,16 @@ class Z64_AnimatedMatColorParams(PropertyGroup): 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 - - for keyframe in self.keyframes: - keyframe.internal_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): From a74b2612b0d5183b395db193b2967ae187d1a92e Mon Sep 17 00:00:00 2001 From: Yanis002 <35189056+Yanis002@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:55:07 +0100 Subject: [PATCH 20/20] add comment about length - 1 --- fast64_internal/z64/animated_mats/properties.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py index 9ec518ab0..d2248cc77 100644 --- a/fast64_internal/z64/animated_mats/properties.py +++ b/fast64_internal/z64/animated_mats/properties.py @@ -50,6 +50,7 @@ class Z64_AnimatedMatColorKeyFrame(PropertyGroup): 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):