diff --git a/README.md b/README.md index 99c71d58f..6e7636e82 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,11 @@ Forked from [kurethedead/fast64 on BitBucket](https://bitbucket.org/kurethedead/ This is a Blender plugin that allows one to export F3D display lists. It also has the ability to export assets for Super Mario 64 and Ocarina of Time decompilation projects. It supports custom color combiners / geometry modes / etc. It is also possible to use exported C code in homebrew applications. -Make sure to save often, as this plugin is prone to crashing when creating materials / undoing material creation. This is a Blender issue. - - +Make sure to save often, as this plugin is prone to crashing when creating materials / undoing material creation. [This is a Blender issue](https://developer.blender.org/T70574). ### Example models can be found [here](https://github.com/Fast-64/fast64-models) -![alt-text](/images/mat_inspector.png) +Fast64 features an updater (which can also be used to try out in testing features and bugfixes), [follow these instructions to use it](https://fast64.readthedocs.io/en/latest/common/updater/updater.html) ### Credits Thanks to anonymous_moose, Cheezepin, Rovert, and especially InTheBeef for testing. @@ -27,7 +25,7 @@ We have a Discord server for support as well as development [here](https://disco ### Links to Docs / Guides for Each Game 1. [ Super Mario 64 ](/fast64_internal/sm64/README.md) -2. [ Ocarina Of Time ](/fast64_internal/oot/README.md) +2. [ Ocarina Of Time ](/fast64_internal/z64/README.md) ### Installation Download the repository as a zip file. In Blender, go to Edit -> Preferences -> Add-Ons and click the "Install" button to install the plugin from the zip file. Find the Fast64 addon in the addon list and enable it. If it does not show up, go to Edit -> Preferences -> Save&Load and make sure 'Auto Run Python Scripts' is enabled. @@ -48,6 +46,12 @@ In F3D material properties, you can enable "Large Texture Mode". This will let y ### Decomp vs Homebrew Compatibility There may occur cases where code is formatted differently based on the code use case. In the tools panel under the Fast64 File Settings subheader, you can toggle homebrew compatibility. +### glTF 2.0 Support +Fast64 supports several extensions for glTF 2.0 that can be exported and imported via the glTF 2.0 io addon integrated in blender via hooks. It also implements hacks for broken versions of the addon. + +Currently only basic n64 material properties and F3D properties are supported, but extensions for game modes like SM64 and OOT can be added in the future. +See the [glTF README](/fast64_internal/f3d/glTF/README.md) for details and schemas. + ### Converting To F3D v5 Materials A new optimized shader graph was introduced to decrease processing times for material creation and exporting. If you have a project that still uses old materials, you may want to convert them to v5. To convert an old project, click the "Recreate F3D Materials As V5" operator near the top of the Fast64 tab in 3D view. This may take a while depending on the number of materials in the project. Then go to the outliner, change the display mode to "Orphan Data" (broken heart icon), then click "Purge" in the top right corner. Purge multiple times until all of the old node groups are gone. @@ -66,27 +70,7 @@ Selecting F3DEX3 as your microcode unlocks a large number of additional presets For cel shading, it is recommended to start with one of the cel shading presets, then modify the settings under the `Use Cel Shading` panel. Hover over each UI control for additional information about how that setting works. -### Updater - -Fast64 features an updater ([CGCookie/blender-addon-updater](https://github.com/CGCookie/blender-addon-updater)). - -It can be found in the addon preferences: - -![How the updater in the addon preferences looks, right after addon install](/images/updater_initially.png) - -Click the "Check now for fast64 update" button to check for updates. - -![Updater preferences after clicking the "check for updates" button](/images/updater_after_check.png) - -Click "Install main / old version" and choose "Main" if it isn't already selected: - -![Updater: install main](/images/updater_install_main.png) - -Click OK, there should be a message "Addon successfully installed" and prompting you to restart Blender: - -![Updater: successful install, must restart](/images/updater_success_restart.png) - -Clicking the red button will close Blender. After restarting, fast64 will be up-to-date with the latest main revision. +### [Repo Settings](https://fast64.readthedocs.io/en/latest/common/repo_settings/repo_settings.html) ### Fast64 Development If you'd like to develop in VSCode, follow this tutorial to get proper autocomplete. Skip the linter for now, we'll need to make sure the entire project gets linted before enabling autosave linting because the changes will be massive. diff --git a/__init__.py b/__init__.py index 4cf580ee1..0b6a15556 100644 --- a/__init__.py +++ b/__init__.py @@ -1,10 +1,12 @@ import bpy + from bpy.utils import register_class, unregister_class from bpy.path import abspath from . import addon_updater_ops -from .fast64_internal.utility import prop_split, multilineLabel, draw_and_check_tab +from .fast64_internal.game_data import game_data +from .fast64_internal.utility import prop_split, multilineLabel, set_prop_if_in_data, Matrix4x4Property from .fast64_internal.repo_settings import ( draw_repo_settings, @@ -13,15 +15,16 @@ repo_settings_operators_unregister, ) -from .fast64_internal.sm64 import sm64_register, sm64_unregister +from .fast64_internal.sm64 import sm64_register, sm64_unregister, SM64_ActionProperty from .fast64_internal.sm64.sm64_constants import sm64_world_defaults from .fast64_internal.sm64.settings.properties import SM64_Properties from .fast64_internal.sm64.sm64_geolayout_bone import SM64_BoneProperties from .fast64_internal.sm64.sm64_objects import SM64_ObjectProperties -from .fast64_internal.oot import OOT_Properties, oot_register, oot_unregister -from .fast64_internal.oot.oot_constants import oot_world_defaults -from .fast64_internal.oot.props_panel_main import OOT_ObjectProperties +from .fast64_internal.z64 import OOT_Properties, oot_register, oot_unregister +from .fast64_internal.z64.constants import oot_world_defaults +from .fast64_internal.z64.props_panel_main import OOT_ObjectProperties +from .fast64_internal.z64.actor.properties import initOOTActorProperties from .fast64_internal.utility_anim import utility_anim_register, utility_anim_unregister, ArmatureApplyWithMeshOperator from .fast64_internal.mk64.mk64_constants import mk64_world_defaults @@ -34,7 +37,6 @@ mat_unregister, check_or_ask_color_management, ) -from .fast64_internal.f3d.f3d_render_engine import render_engine_register, render_engine_unregister from .fast64_internal.f3d.f3d_writer import f3d_writer_register, f3d_writer_unregister from .fast64_internal.f3d.f3d_parser import f3d_parser_register, f3d_parser_unregister from .fast64_internal.f3d.flipbook import flipbook_register, flipbook_unregister @@ -56,10 +58,19 @@ on_update_render_settings, ) +from .fast64_internal.gltf_extension import ( + glTF2ExportUserExtension, # Import these so they are visible to the glTF add-on + glTF2ImportUserExtension, + glTF2_pre_export_callback, + Fast64GlTFSettings, + gltf_extension_register, + gltf_extension_unregister, +) + # info about add on bl_info = { "name": "Fast64 (HM64)", - "version": (2, 3, 0), + "version": (2, 4, 0), "author": "kurethedead", "location": "3DView", "description": "Plugin for exporting F3D display lists and other game data related to Nintendo 64 games.", @@ -70,6 +81,7 @@ gameEditorEnum = ( ("SM64", "SM64", "Super Mario 64", 0), ("OOT", "OOT", "Ocarina Of Time", 1), + # ("MM", "MM", "Majora's Mask", 4), ("MK64", "MK64", "Mario Kart 64", 3), ("Homebrew", "Homebrew", "Homebrew", 2), ) @@ -90,7 +102,9 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() col.scale_y = 1.1 # extra padding - prop_split(col, context.scene, "f3d_type", "F3D Microcode") + prop_split(col, context.scene, "f3d_type", "Microcode") + if context.scene.f3d_type in {"F3DEX3", "T3D"}: + prop_split(col, context.scene, "packed_normals_algorithm", "Packed normals alg") col.prop(context.scene, "saveTextures") col.prop(context.scene, "f3d_simple", text="Simple Material UI") col.prop(context.scene, "exportInlineF3D", text="Bleed and Inline Material Exports") @@ -166,6 +180,8 @@ class Fast64Settings_Properties(bpy.types.PropertyGroup): version: bpy.props.IntProperty(name="Fast64Settings_Properties Version", default=0) + glTF: bpy.props.PointerProperty(type=Fast64GlTFSettings, name="glTF Properties") + anim_range_choice: bpy.props.EnumProperty( name="Anim Range", description="What to use to determine what frames of the animation to export", @@ -215,6 +231,19 @@ class Fast64Settings_Properties(bpy.types.PropertyGroup): internal_game_update_ver: bpy.props.IntProperty(default=0) + def to_repo_settings(self): + data = {} + data["autoLoad"] = self.auto_repo_load_settings + data["autoPickTextureFormat"] = self.auto_pick_texture_format + if self.auto_pick_texture_format: + data["preferRGBAOverCI"] = self.prefer_rgba_over_ci + return data + + def from_repo_settings(self, data: dict): + set_prop_if_in_data(self, "auto_repo_load_settings", data, "autoLoad") + set_prop_if_in_data(self, "auto_pick_texture_format", data, "autoPickTextureFormat") + set_prop_if_in_data(self, "prefer_rgba_over_ci", data, "preferRGBAOverCI") + class Fast64_Properties(bpy.types.PropertyGroup): """ @@ -229,6 +258,14 @@ class Fast64_Properties(bpy.types.PropertyGroup): renderSettings: bpy.props.PointerProperty(type=Fast64RenderSettings_Properties, name="Fast64 Render Settings") +class Fast64_ActionProperties(bpy.types.PropertyGroup): + """ + Properties in Action.fast64. + """ + + sm64: bpy.props.PointerProperty(type=SM64_ActionProperty, name="SM64 Properties") + + class Fast64_BoneProperties(bpy.types.PropertyGroup): """ Properties in bone.fast64 (bpy.types.Bone) @@ -296,24 +333,6 @@ def execute(self, context: "bpy.types.Context"): return {"FINISHED"} -# def updateGameEditor(scene, context): -# if scene.currentGameEditorMode == 'SM64': -# sm64_panel_unregister() -# elif scene.currentGameEditorMode == 'Z64': -# oot_panel_unregister() -# else: -# raise PluginError("Unhandled game editor mode " + str(scene.currentGameEditorMode)) -# -# if scene.gameEditorMode == 'SM64': -# sm64_panel_register() -# elif scene.gameEditorMode == 'Z64': -# oot_panel_register() -# else: -# raise PluginError("Unhandled game editor mode " + str(scene.gameEditorMode)) -# -# scene.currentGameEditorMode = scene.gameEditorMode - - class ExampleAddonPreferences(bpy.types.AddonPreferences, addon_updater_ops.AddonUpdaterPreferences): bl_idname = __package__ @@ -326,6 +345,7 @@ def draw(self, context): Fast64RenderSettings_Properties, ManualUpdatePreviewOperator, Fast64_Properties, + Fast64_ActionProperties, Fast64_BoneProperties, Fast64_ObjectProperties, Fast64_CurveProperties, @@ -339,8 +359,10 @@ def draw(self, context): def upgrade_changed_props(): """Set scene properties after a scene loads, used for migrating old properties""" SM64_Properties.upgrade_changed_props() + OOT_Properties.upgrade_changed_props() MK64_Properties.upgrade_changed_props() SM64_ObjectProperties.upgrade_changed_props() + SM64_BoneProperties.upgrade_changed_props() OOT_ObjectProperties.upgrade_changed_props() for scene in bpy.data.scenes: settings: Fast64Settings_Properties = scene.fast64.settings @@ -369,6 +391,8 @@ def upgrade_scene_props_node(): @bpy.app.handlers.persistent def after_load(_a, _b): + game_data.update(bpy.context.scene.gameEditorMode) + settings = bpy.context.scene.fast64.settings if any(mat.is_f3d for mat in bpy.data.materials): check_or_ask_color_management(bpy.context) @@ -391,11 +415,12 @@ def set_game_defaults(scene: bpy.types.Scene, set_ucode=True): if scene.gameEditorMode == "SM64": f3d_type = "F3D" world_defaults = sm64_world_defaults - elif scene.gameEditorMode == "OOT": + elif scene.gameEditorMode in {"OOT", "MM"}: f3d_type = "F3DEX2/LX2" world_defaults = oot_world_defaults elif scene.gameEditorMode == "MK64": f3d_type = "F3DEX/LX" + world_defaults = mk64_world_defaults elif scene.gameEditorMode == "Homebrew": f3d_type = "F3D" world_defaults = {} # This will set some pretty bad defaults, but trust the user @@ -406,6 +431,7 @@ def set_game_defaults(scene: bpy.types.Scene, set_ucode=True): def gameEditorUpdate(scene: bpy.types.Scene, _context): + game_data.update(scene.gameEditorMode) set_game_defaults(scene) @@ -430,14 +456,17 @@ def register(): register_class(ExampleAddonPreferences) addon_updater_ops.register(bl_info) + register_class(Matrix4x4Property) + initOOTActorProperties() utility_anim_register() mat_register() - render_engine_register() bsdf_conv_register() sm64_register(True) oot_register(True) mk64_register(True) + gltf_extension_register() + repo_settings_operators_register() for cls in classes: @@ -471,6 +500,7 @@ def register(): bpy.types.Bone.fast64 = bpy.props.PointerProperty(type=Fast64_BoneProperties, name="Fast64 Bone Properties") bpy.types.Object.fast64 = bpy.props.PointerProperty(type=Fast64_ObjectProperties, name="Fast64 Object Properties") bpy.types.Curve.fast64 = bpy.props.PointerProperty(type=Fast64_CurveProperties, name="Fast64 Curve Properties") + bpy.types.Action.fast64 = bpy.props.PointerProperty(type=Fast64_ActionProperties, name="Fast64 Action Properties") bpy.app.handlers.load_post.append(after_load) @@ -486,9 +516,10 @@ def unregister(): oot_unregister(True) mk64_unregister(True) mat_unregister() + gltf_extension_unregister() bsdf_conv_unregister() bsdf_conv_panel_unregsiter() - render_engine_unregister() + unregister_class(Matrix4x4Property) del bpy.types.Scene.fullTraceback del bpy.types.Scene.ignoreTextureRestrictions @@ -500,6 +531,7 @@ def unregister(): del bpy.types.Scene.fast64 del bpy.types.Bone.fast64 del bpy.types.Object.fast64 + del bpy.types.Action.fast64 repo_settings_operators_unregister() diff --git a/addon_updater.py b/addon_updater.py index 54149ab3a..32386f7c4 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -68,6 +68,8 @@ def __init__(self): self._latest_release = None self._use_releases = False self._include_branches = False + self._include_merge_requests = False + self._merge_requests = list() self._include_branch_list = ['master'] self._include_branch_auto_check = False self._manual_only = False @@ -304,6 +306,17 @@ def include_branches(self, value): except: raise ValueError("include_branches must be a boolean value") + @property + def include_merge_requests(self): + return self._include_merge_requests + + @include_merge_requests.setter + def include_merge_requests(self, value): + try: + self._include_merge_requests = bool(value) + except: + raise ValueError("include_branches must be a boolean value") + @property def json(self): if len(self._json) == 0: @@ -415,6 +428,13 @@ def subfolder_path(self, value): self._subfolder_path = value @property + def merge_requests(self): + if len(self._merge_requests) == 0: + return {} + return { + mr["id"]: mr["title"] for mr in self._merge_requests + } + @property def tags(self): if len(self._tags) == 0: return list() @@ -590,9 +610,25 @@ def form_repo_url(self): def form_tags_url(self): return self._engine.form_tags_url(self) + + def form_mrs_url(self): + return self._engine.form_mrs_url(self) def form_branch_url(self, branch): return self._engine.form_branch_url(branch, self) + + def form_mr_url(self, mr): + return self._engine.form_mr_url(mr, self) + + def get_mrs(self): + if self._include_merge_requests is False: + return + request = self.form_mrs_url() + self.print_verbose("Getting merge requests from server") + + # get all merge requests, internet call + all_mrs = self._engine.parse_mrs(self.get_api(request), self) or [] + self._merge_requests = [mr for mr in all_mrs if mr["state"] == "open"] def get_tags(self): request = self.form_tags_url() @@ -1274,8 +1310,9 @@ def check_for_update(self, now=False): self._update_version, self._update_link) - # Primary internet call, sets self._tags and self._tag_latest. + # Primary internet call, sets self._tags, self._merge_requests and self._tag_latest. self.get_tags() + self.get_mrs() self._json["last_check"] = str(datetime.now()) self.save_updater_json() @@ -1357,8 +1394,21 @@ def set_tag(self, name): self._update_link = link if not tg: raise ValueError("Version tag not found: " + name) + + def set_mr(self, id: str): + """Assign the merge request name and url to update to""" + mr = None + for merge_request in self._merge_requests: + if id == str(merge_request["id"]): + mr = merge_request + break + if mr: + self._update_link = self.form_mr_url(mr) + self._update_version = mr["id"] + else: + raise ValueError("Merge request not found: " + id) - def run_update(self, force=False, revert_tag=None, clean=False, callback=None): + def run_update(self, force=False, revert_tag=None, merge_request=False, clean=False, callback=None): """Runs an install, update, or reversion of an addon from online source Arguments: @@ -1372,7 +1422,10 @@ def run_update(self, force=False, revert_tag=None, clean=False, callback=None): self._json["version_text"] = dict() if revert_tag is not None: - self.set_tag(revert_tag) + if merge_request: + self.set_mr(revert_tag) + else: + self.set_tag(revert_tag) self._update_ready = True # clear the errors if any @@ -1643,9 +1696,15 @@ def form_repo_url(self, updater): def form_tags_url(self, updater): return self.form_repo_url(updater) + "/refs/tags?sort=-name" + + def form_mrs_url(self, updater): + raise NotImplementedError def form_branch_url(self, branch, updater): return self.get_zip_url(branch, updater) + + def form_mr_url(self, mr, updater): + raise NotImplementedError def get_zip_url(self, name, updater): return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format( @@ -1661,6 +1720,9 @@ def parse_tags(self, response, updater): "name": tag["name"], "zipball_url": self.get_zip_url(tag["name"], updater) } for tag in response["values"]] + + def parse_mrs(self, response, updater): + raise NotImplementedError class GithubEngine: @@ -1680,17 +1742,31 @@ def form_tags_url(self, updater): return "{}/releases".format(self.form_repo_url(updater)) else: return "{}/tags".format(self.form_repo_url(updater)) + + def form_mrs_url(self, updater): + return "{}/pulls".format(self.form_repo_url(updater)) def form_branch_list_url(self, updater): return "{}/branches".format(self.form_repo_url(updater)) def form_branch_url(self, branch, updater): return "{}/zipball/{}".format(self.form_repo_url(updater), branch) + + def form_mr_url(self, mr, updater): + head = mr["head"] + branch_name = head["ref"] + repo = head["repo"]["url"] + return "{}/zipball/{}".format(repo, branch_name) def parse_tags(self, response, updater): if response is None: return list() return response + + def parse_mrs(self, response, updater): + if response is None: + return list() + return response class GitlabEngine: @@ -1706,6 +1782,9 @@ def form_repo_url(self, updater): def form_tags_url(self, updater): return "{}/repository/tags".format(self.form_repo_url(updater)) + + def form_mrs_url(self, updater): + raise NotImplementedError def form_branch_list_url(self, updater): # does not validate branch name. @@ -1717,6 +1796,9 @@ def form_branch_url(self, branch, updater): # instead of branch zip to get direct path, would need. return "{}/repository/archive.zip?sha={}".format( self.form_repo_url(updater), branch) + + def form_mr_url(self, mr, updater): + raise NotImplementedError def get_zip_url(self, sha, updater): return "{base}/repository/archive.zip?sha={sha}".format( @@ -1734,6 +1816,9 @@ def parse_tags(self, response, updater): "name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater) } for tag in response] + + def parse_mrs(self, response, updater): + raise NotImplementedError # ----------------------------------------------------------------------------- diff --git a/addon_updater_ops.py b/addon_updater_ops.py index 381b02dcc..6112df95e 100644 --- a/addon_updater_ops.py +++ b/addon_updater_ops.py @@ -58,7 +58,7 @@ def clear_state(self): self.error_msg = None self.async_checking = None - def run_update(self, force, callback, clean): + def run_update(self, force, callback, merge_request, clean): pass def check_for_update(self, now): @@ -89,9 +89,9 @@ def make_annotations(cls): bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, bpy.props._PropertyDeferred)} if bl_props: - if '__annotations__' not in cls.__dict__: - setattr(cls, '__annotations__', {}) - annotations = cls.__dict__['__annotations__'] + if not hasattr(cls, '__annotations__'): + cls.__annotations__ = {} + annotations = cls.__annotations__ for k, v in bl_props.items(): annotations[k] = v delattr(cls, k) @@ -340,14 +340,8 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator): def target_version(self, context): # In case of error importing updater. if updater.invalid_updater: - ret = [] - - ret = [] - i = 0 - for tag in updater.tags: - ret.append((tag, tag, "Select to install " + tag)) - i += 1 - return ret + return [] + return [(tag, tag, "Select to install " + tag) for tag in updater.tags] target = bpy.props.EnumProperty( name="Target version to install", @@ -407,6 +401,78 @@ def execute(self, context): return {'FINISHED'} +class AddonUpdaterTryMR(bpy.types.Operator): + bl_label = updater.addon + " merge requests" + bl_idname = updater.addon + ".updater_try_merge_request" + bl_description = "Install a merge request of the {x} addon".format( + x=updater.addon) + bl_options = {'REGISTER', 'INTERNAL'} + + def target_version(self, context): + # In case of error importing updater. + if updater.invalid_updater: + return [] + + return [(str(id), title, "Select to install " + title) for id, title in updater.merge_requests.items()] + + target = bpy.props.EnumProperty( + name="Target version to install", + description="Select the version to install", + items=target_version + ) + + # If true, run clean install - ie remove all files before adding new + # equivalent to deleting the addon and reinstalling, except the + # updater folder/backup folder remains. + clean_install = bpy.props.BoolProperty( + name="Clean install", + description=("If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install"), + default=False, + options={'HIDDEN'} + ) + + @classmethod + def poll(cls, context): + if updater.invalid_updater: + return False + return updater.update_ready is not None and len(updater.merge_requests) > 0 + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + if updater.invalid_updater: + layout.label(text="Updater error") + return + split = layout_split(layout, factor=0.5) + sub_col = split.column() + sub_col.label(text="Select install version") + sub_col = split.column() + sub_col.prop(self, "target", text="") + + def execute(self, context): + # In case of error importing updater. + if updater.invalid_updater: + return {'CANCELLED'} + + res = updater.run_update( + force=False, + revert_tag=self.target, + merge_request=True, + callback=post_update_callback, + clean=self.clean_install) + + # Should return 0, if not something happened. + if res == 0: + updater.print_verbose("Updater returned successful") + else: + updater.print_verbose( + "Updater returned {}, , error occurred".format(res)) + return {'CANCELLED'} + + return {'FINISHED'} class AddonUpdaterInstallManually(bpy.types.Operator): """As a fallback, direct the user to download the addon manually""" @@ -950,21 +1016,20 @@ def update_settings_ui(self, context, element=None): # Element is a UI element, such as layout, a row, column, or box. if element is None: element = self.layout - box = element.box() # In case of error importing updater. if updater.invalid_updater: - box.label(text="Error initializing updater code:") - box.label(text=updater.error_msg) + element.label(text="Error initializing updater code:") + element.label(text=updater.error_msg) return settings = get_user_preferences(context) if not settings: - box.label(text="Error getting updater preferences", icon='ERROR') + element.label(text="Error getting updater preferences", icon='ERROR') return # auto-update settings - box.label(text="Updater Settings") - row = box.row() + element.label(text="Updater Settings") + row = element.row() # special case to tell user to restart blender, if set that way if not updater.auto_reload_post_update: @@ -976,7 +1041,7 @@ def update_settings_ui(self, context, element=None): icon="ERROR") return - split = layout_split(row, factor=0.4) + split = layout_split(row, factor=0.5) sub_col = split.column() sub_col.prop(settings, "auto_check_update") sub_col = split.column() @@ -998,7 +1063,7 @@ def update_settings_ui(self, context, element=None): # check_col.prop(settings,"updater_interval_minutes") # Checking / managing updates. - row = box.row() + row = element.row() col = row.column() if updater.error is not None: sub_col = col.row(align=True) @@ -1018,7 +1083,7 @@ def update_settings_ui(self, context, element=None): split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") - elif updater.update_ready is None and not updater.async_checking: + elif not updater.async_checking: col.scale_y = 2 col.operator(AddonUpdaterCheckNow.bl_idname) elif updater.update_ready is None: # async is running @@ -1048,25 +1113,13 @@ def update_settings_ui(self, context, element=None): split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") - elif updater.update_ready and not updater.manual_only: - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version)) - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - elif updater.update_ready and updater.manual_only: col.scale_y = 2 dl_now_txt = "Download " + str(updater.update_version) col.operator("wm.url_open", text=dl_now_txt).url = updater.website - else: # i.e. that updater.update_ready == False. - sub_col = col.row(align=True) + else: # i.e. that updater.update_ready == False. + sub_col = element.row(align=True) sub_col.scale_y = 1 split = sub_col.split(align=True) split.enabled = False @@ -1080,13 +1133,24 @@ def update_settings_ui(self, context, element=None): if not updater.manual_only: col = row.column(align=True) + if updater.update_ready: + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) + split.operator(AddonUpdaterUpdateNow.bl_idname, + text=f"Update now to {updater.update_version} (Stable)") + if updater.include_branches and len(updater.include_branch_list) > 0: - branch = updater.include_branch_list[0] col.operator(AddonUpdaterUpdateTarget.bl_idname, - text="Install {} / old version".format(branch)) + text="Install specific version (Latest or Older)") else: col.operator(AddonUpdaterUpdateTarget.bl_idname, text="(Re)install addon version") + + if updater.include_merge_requests and len(updater.merge_requests) > 0: + col.operator(AddonUpdaterTryMR.bl_idname, + text="Try a pull request") + last_date = "none found" backup_path = os.path.join(updater.stage_path, "backup") if "backup_date" in updater.json and os.path.isdir(backup_path): @@ -1095,9 +1159,9 @@ def update_settings_ui(self, context, element=None): else: last_date = updater.json["backup_date"] backup_text = "Restore addon backup ({})".format(last_date) - col.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text) + element.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text) - row = box.row() + row = element.row() row.scale_y = 0.7 last_check = updater.json["last_check"] if updater.error is not None and updater.error_msg is not None: @@ -1195,7 +1259,7 @@ def update_settings_ui_condensed(self, context, element=None): split = sub_col.split(align=True) split.scale_y = 2 split.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version)) + text=f"Update now to {updater.update_version} (Stable)") split = sub_col.split(align=True) split.scale_y = 2 split.operator(AddonUpdaterCheckNow.bl_idname, @@ -1358,6 +1422,7 @@ class AddonUpdaterPreferences: AddonUpdaterCheckNow, AddonUpdaterUpdateNow, AddonUpdaterUpdateTarget, + AddonUpdaterTryMR, AddonUpdaterInstallManually, AddonUpdaterUpdatedSuccessful, AddonUpdaterRestoreBackup, @@ -1440,6 +1505,9 @@ def register(bl_info): # the "install {branch}/older version" operator. updater.include_branches = True + # Allows the user to try merge requests as an option to "update" to. + updater.include_merge_requests = True + # (GitHub only) This options allows using "releases" instead of "tags", # which enables pulling down release logs/notes, as well as installs update # from release-attached zips (instead of the auto-packaged code generated diff --git a/fast64_internal/__init__.py b/fast64_internal/__init__.py deleted file mode 100644 index 6bb605c2a..000000000 --- a/fast64_internal/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .f3d_material_converter import * -from .f3d import * -from .sm64 import * -from .oot import * # is this really needed? -from .panels import * diff --git a/fast64_internal/data/__init__.py b/fast64_internal/data/__init__.py new file mode 100644 index 000000000..514ce8cf3 --- /dev/null +++ b/fast64_internal/data/__init__.py @@ -0,0 +1,2 @@ +from .z64.data import Z64_Data +from .z64.object_data import Z64_ObjectData diff --git a/fast64_internal/data/z64/actor_data.py b/fast64_internal/data/z64/actor_data.py new file mode 100644 index 000000000..ca6f71183 --- /dev/null +++ b/fast64_internal/data/z64/actor_data.py @@ -0,0 +1,166 @@ +from os import path +from dataclasses import dataclass +from pathlib import Path +from .common import Z64_BaseElement, get_xml_root + + +@dataclass +class Z64_ParameterElement: + type: str # bool, enum, type, property, etc... + index: int + mask: int + name: str + subType: str # used for and + target: str + tiedTypes: list[int] + items: list[tuple[int, str]] # for and , int is "Value"/"Params" and str is the name + valueRange: list[int] + + +@dataclass +class Z64_ListElement: + key: str + name: str + value: int + + +@dataclass +class Z64_ActorElement(Z64_BaseElement): + category: str + tiedObjects: list[str] + params: list[Z64_ParameterElement] + + +class Z64_ActorData: + """Everything related to OoT Actors""" + + def __init__(self, game: str): + # Path to the ``ActorList.xml`` file + xml_path = Path(f"{path.dirname(path.abspath(__file__))}/xml/{game.lower()}_actor_list.xml") + actor_root = get_xml_root(xml_path.resolve()) + + # general actor list + self.actorList: list[Z64_ActorElement] = [] + + # list elements + self.chestItems: list[Z64_ListElement] = [] + self.collectibleItems: list[Z64_ListElement] = [] + self.messageItems: list[Z64_ListElement] = [] + + listNameToList = { + "Chest Content": self.chestItems, + "Collectibles": self.collectibleItems, + "Elf_Msg Message ID": self.messageItems, + } + + for elem in actor_root.iterfind("List"): + listName = elem.get("Name") + if listName is not None: + for item in elem: + listNameToList[listName].append( + Z64_ListElement(item.get("Key"), item.get("Name"), int(item.get("Value"), base=16)) + ) + + for actor in actor_root.iterfind("Actor"): + tiedObjects = [] + objKey = actor.get("ObjectKey") + actorName = f"{actor.attrib['Name']} - {actor.attrib['ID'].removeprefix('ACTOR_')}" + + if objKey is not None: # actors don't always use an object + tiedObjects = objKey.split(",") + + # parameters + params: list[Z64_ParameterElement] = [] + for elem in actor: + elemType = elem.tag + if elemType != "Notes": + items: list[tuple[int, str]] = [] + if elemType == "Type" or elemType == "Enum": + for item in elem: + key = "Params" if elemType == "Type" else "Value" + name = item.text.strip() if elemType == "Type" else item.get("Name") + if key is not None and name is not None: + items.append((int(item.get(key), base=16), name)) + + # not every actor have parameters tied to a specific actor type + tiedTypes = elem.get("TiedActorTypes") + tiedTypeList = [] + if tiedTypes is not None: + tiedTypeList = [int(val, base=16) for val in tiedTypes.split(",")] + + defaultName = f"{elem.get('Type')} {elemType}" + valueRange = elem.get("ValueRange") + params.append( + Z64_ParameterElement( + elemType, + int(elem.get("Index", "1")), + int(elem.get("Mask", "0xFFFF"), base=16), + elem.get("Name", defaultName if not "None" in defaultName else elemType), + elem.get("Type"), + elem.get("Target", "Params"), + tiedTypeList, + items, + [int(val) for val in valueRange.split("-")] if valueRange is not None else [None, None], + ) + ) + + self.actorList.append( + Z64_ActorElement( + actor.attrib["ID"], + actor.attrib["Key"], + actorName, + int(actor.attrib["Index"]), + actor.attrib["Category"], + tiedObjects, + params, + ) + ) + + self.actorsByKey = {actor.key: actor for actor in self.actorList} + self.actorsByID = {actor.id: actor for actor in self.actorList} + + self.chestItemByKey = {elem.key: elem for elem in self.chestItems} + self.collectibleItemsByKey = {elem.key: elem for elem in self.collectibleItems} + self.messageItemsByKey = {elem.key: elem for elem in self.messageItems} + + self.chestItemByValue = {elem.value: elem for elem in self.chestItems} + self.collectibleItemsByValue = {elem.value: elem for elem in self.collectibleItems} + self.messageItemsByValue = {elem.value: elem for elem in self.messageItems} + + # list of tuples used by Blender's enum properties + + lastIndex = max(1, *(actor.index for actor in self.actorList)) + self.ootEnumActorID = [("None", f"{i} (Deleted from the XML)", "None") for i in range(lastIndex)] + self.ootEnumActorID.insert(0, ("Custom", "Custom Actor", "Custom")) + + doorTotal = 0 + for actor in self.actorList: + if actor.category == "ACTORCAT_DOOR": + doorTotal += 1 + self.ootEnumTransitionActorID = [("None", f"{i} (Deleted from the XML)", "None") for i in range(doorTotal)] + self.ootEnumTransitionActorID.insert(0, ("Custom", "Custom Actor", "Custom")) + + i = 1 + for actor in self.actorList: + self.ootEnumActorID[actor.index] = (actor.id, actor.name, actor.id) + if actor.category == "ACTORCAT_DOOR": + self.ootEnumTransitionActorID[i] = (actor.id, actor.name, actor.id) + i += 1 + + self.ootEnumChestContent = [(elem.key, elem.name, elem.key) for elem in self.chestItems] + self.ootEnumCollectibleItems = [(elem.key, elem.name, elem.key) for elem in self.collectibleItems] + self.ootEnumNaviMessageData = [(elem.key, elem.name, elem.key) for elem in self.messageItems] + + self.ootEnumChestContent.insert(0, ("Custom", "Custom Value", "Custom")) + self.ootEnumCollectibleItems.insert(0, ("Custom", "Custom Value", "Custom")) + self.ootEnumNaviMessageData.insert(0, ("Custom", "Custom Value", "Custom")) + + def getItems(self, actorUser: str): + if actorUser == "Actor": + return self.ootEnumActorID + elif actorUser == "Transition Actor": + return self.ootEnumTransitionActorID + elif actorUser == "Entrance": + return [(self.actorsByKey["player"].id, self.actorsByKey["player"].name, self.actorsByKey["player"].id)] + else: + raise ValueError("ERROR: The Actor User is unknown!") diff --git a/fast64_internal/oot/data/oot_getters.py b/fast64_internal/data/z64/common.py similarity index 64% rename from fast64_internal/oot/data/oot_getters.py rename to fast64_internal/data/z64/common.py index fb4fbfe09..7cd3b5159 100644 --- a/fast64_internal/oot/data/oot_getters.py +++ b/fast64_internal/data/z64/common.py @@ -1,7 +1,16 @@ from xml.etree.ElementTree import parse as parseXML, Element +from dataclasses import dataclass -def getXMLRoot(xmlPath: str) -> Element: +@dataclass +class Z64_BaseElement: + id: str + key: str + name: str + index: int + + +def get_xml_root(xmlPath: str) -> Element: """Parse an XML file and return its root element""" try: return parseXML(xmlPath).getroot() diff --git a/fast64_internal/data/z64/data.py b/fast64_internal/data/z64/data.py new file mode 100644 index 000000000..58732c610 --- /dev/null +++ b/fast64_internal/data/z64/data.py @@ -0,0 +1,964 @@ +import bpy + +from collections import OrderedDict +from dataclasses import dataclass +from typing import Optional +from bpy.types import Context +from .enum_data import Z64_EnumData +from .object_data import Z64_ObjectData +from .actor_data import Z64_ActorData + +# --- + +# TODO: get this from XML + +oot_enum_nature_id = [ + ("Custom", "Custom", "Custom"), + ("0x00", "General Night", "NATURE_ID_GENERAL_NIGHT"), + ("0x01", "Market Entrance", "NATURE_ID_MARKET_ENTRANCE"), + ("0x02", "Kakariko Region", "NATURE_ID_KAKARIKO_REGION"), + ("0x03", "Market Ruins", "NATURE_ID_MARKET_RUINS"), + ("0x04", "Kokiri Region", "NATURE_ID_KOKIRI_REGION"), + ("0x05", "Market Night", "NATURE_ID_MARKET_NIGHT"), + ("0x06", "NATURE_ID_06", "NATURE_ID_06"), + ("0x07", "Ganon's Lair", "NATURE_ID_GANONS_LAIR"), + ("0x08", "NATURE_ID_08", "NATURE_ID_08"), + ("0x09", "NATURE_ID_09", "NATURE_ID_09"), + ("0x0A", "Wasteland", "NATURE_ID_WASTELAND"), + ("0x0B", "Colossus", "NATURE_ID_COLOSSUS"), + ("0x0C", "Nature DMT", "NATURE_ID_DEATH_MOUNTAIN_TRAIL"), + ("0x0D", "NATURE_ID_0D", "NATURE_ID_0D"), + ("0x0E", "NATURE_ID_0E", "NATURE_ID_0E"), + ("0x0F", "NATURE_ID_0F", "NATURE_ID_0F"), + ("0x10", "NATURE_ID_10", "NATURE_ID_10"), + ("0x11", "NATURE_ID_11", "NATURE_ID_11"), + ("0x12", "NATURE_ID_12", "NATURE_ID_12"), + ("0x13", "None", "NATURE_ID_NONE"), + ("0xFF", "Disabled", "NATURE_ID_DISABLED"), +] + +enum_ambiance_id = [ + ("Custom", "Custom", "Custom"), + ("0x00", "AMBIENCE_ID_00", "AMBIENCE_ID_00"), + ("0x01", "AMBIENCE_ID_01", "AMBIENCE_ID_01"), + ("0x02", "AMBIENCE_ID_02", "AMBIENCE_ID_02"), + ("0x03", "AMBIENCE_ID_03", "AMBIENCE_ID_03"), + ("0x04", "AMBIENCE_ID_04", "AMBIENCE_ID_04"), + ("0x05", "AMBIENCE_ID_05", "AMBIENCE_ID_05"), + ("0x06", "AMBIENCE_ID_06", "AMBIENCE_ID_06"), + ("0x07", "AMBIENCE_ID_07", "AMBIENCE_ID_07"), + ("0x08", "AMBIENCE_ID_08", "AMBIENCE_ID_08"), + ("0x09", "AMBIENCE_ID_09", "AMBIENCE_ID_09"), + ("0x0A", "AMBIENCE_ID_0A", "AMBIENCE_ID_0A"), + ("0x0B", "AMBIENCE_ID_0B", "AMBIENCE_ID_0B"), + ("0x0C", "AMBIENCE_ID_0C", "AMBIENCE_ID_0C"), + ("0x0D", "AMBIENCE_ID_0D", "AMBIENCE_ID_0D"), + ("0x0E", "AMBIENCE_ID_0E", "AMBIENCE_ID_0E"), + ("0x0F", "AMBIENCE_ID_0F", "AMBIENCE_ID_0F"), + ("0x10", "AMBIENCE_ID_10", "AMBIENCE_ID_10"), + ("0x11", "AMBIENCE_ID_11", "AMBIENCE_ID_11"), + ("0x12", "AMBIENCE_ID_12", "AMBIENCE_ID_12"), + ("0x13", "AMBIENCE_ID_13", "AMBIENCE_ID_13"), + ("0xFF", "AMBIENCE_ID_DISABLED", "AMBIENCE_ID_DISABLED"), +] + +# --- + +oot_enum_skybox = [ + ("Custom", "Custom", "Custom"), + ("0x00", "None", "None"), + ("0x01", "Standard Sky", "Standard Sky"), + ("0x02", "Hylian Bazaar", "Hylian Bazaar"), + ("0x03", "Brown Cloudy Sky", "Brown Cloudy Sky"), + ("0x04", "Market Ruins", "Market Ruins"), + ("0x05", "Black Cloudy Night", "Black Cloudy Night"), + ("0x07", "Link's House", "Link's House"), + ("0x09", "Market (Main Square, Day)", "Market (Main Square, Day)"), + ("0x0A", "Market (Main Square, Night)", "Market (Main Square, Night)"), + ("0x0B", "Happy Mask Shop", "Happy Mask Shop"), + ("0x0C", "Know-It-All Brothers' House", "Know-It-All Brothers' House"), + ("0x0E", "Kokiri Twins' House", "Kokiri Twins' House"), + ("0x0F", "Stable", "Stable"), + ("0x10", "Stew Lady's House", "Stew Lady's House"), + ("0x11", "Kokiri Shop", "Kokiri Shop"), + ("0x13", "Goron Shop", "Goron Shop"), + ("0x14", "Zora Shop", "Zora Shop"), + ("0x16", "Kakariko Potions Shop", "Kakariko Potions Shop"), + ("0x17", "Hylian Potions Shop", "Hylian Potions Shop"), + ("0x18", "Bomb Shop", "Bomb Shop"), + ("0x1A", "Dog Lady's House", "Dog Lady's House"), + ("0x1B", "Impa's House", "Impa's House"), + ("0x1C", "Gerudo Tent", "Gerudo Tent"), + ("0x1D", "Environment Color", "Environment Color"), + ("0x20", "Mido's House", "Mido's House"), + ("0x21", "Saria's House", "Saria's House"), + ("0x22", "Dog Guy's House", "Dog Guy's House"), +] + +mm_enum_skybox = [ + ("Custom", "Custom", "Custom"), + ("SKYBOX_NONE", "None", "0x00"), + ("SKYBOX_NORMAL_SKY", "Standard Sky", "0x01"), + ("SKYBOX_2", "SKYBOX_2", "0x02"), + ("SKYBOX_3", "SKYBOX_3", "0x03"), + ("SKYBOX_CUTSCENE_MAP", "Cutscene Map", "0x05"), +] + +oot_enum_skybox_config = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Sunny", "Sunny"), + ("0x01", "Cloudy", "Cloudy"), +] + +mm_enum_skybox_config = [ + ("Custom", "Custom", "Custom"), + ("SKYBOX_CONFIG_0", "SKYBOX_CONFIG_0", "0x00"), + ("SKYBOX_CONFIG_1", "SKYBOX_CONFIG_1", "0x01"), + ("SKYBOX_CONFIG_2", "SKYBOX_CONFIG_2", "0x02"), + ("SKYBOX_CONFIG_3", "SKYBOX_CONFIG_3", "0x03"), + ("SKYBOX_CONFIG_4", "SKYBOX_CONFIG_4", "0x04"), + ("SKYBOX_CONFIG_5", "SKYBOX_CONFIG_5", "0x05"), + ("SKYBOX_CONFIG_6", "SKYBOX_CONFIG_6", "0x06"), + ("SKYBOX_CONFIG_7", "SKYBOX_CONFIG_7", "0x07"), + ("SKYBOX_CONFIG_8", "SKYBOX_CONFIG_8", "0x08"), + ("SKYBOX_CONFIG_9", "SKYBOX_CONFIG_9", "0x09"), + ("SKYBOX_CONFIG_10", "SKYBOX_CONFIG_10", "0x0A"), + ("SKYBOX_CONFIG_11", "SKYBOX_CONFIG_11", "0x0B"), + ("SKYBOX_CONFIG_12", "SKYBOX_CONFIG_12", "0x0C"), + ("SKYBOX_CONFIG_13", "SKYBOX_CONFIG_13", "0x0D"), + ("SKYBOX_CONFIG_14", "SKYBOX_CONFIG_14", "0x0E"), + ("SKYBOX_CONFIG_15", "SKYBOX_CONFIG_15", "0x0F"), + ("SKYBOX_CONFIG_16", "SKYBOX_CONFIG_16", "0x10"), + ("SKYBOX_CONFIG_17", "SKYBOX_CONFIG_17", "0x11"), + ("SKYBOX_CONFIG_18", "SKYBOX_CONFIG_18", "0x12"), + ("SKYBOX_CONFIG_19", "SKYBOX_CONFIG_19", "0x13"), + ("SKYBOX_CONFIG_20", "SKYBOX_CONFIG_20", "0x14"), + ("SKYBOX_CONFIG_21", "SKYBOX_CONFIG_21", "0x15"), + ("SKYBOX_CONFIG_22", "SKYBOX_CONFIG_22", "0x16"), + ("SKYBOX_CONFIG_23", "SKYBOX_CONFIG_23", "0x17"), + ("SKYBOX_CONFIG_24", "SKYBOX_CONFIG_24", "0x18"), + ("SKYBOX_CONFIG_25", "SKYBOX_CONFIG_25", "0x19"), + ("SKYBOX_CONFIG_26", "SKYBOX_CONFIG_26", "0x1A"), + ("SKYBOX_CONFIG_27", "SKYBOX_CONFIG_27", "0x1B"), +] + +oot_enum_environment_type = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Default", "Default"), + ("0x01", "Sneezing", "Sneezing"), + ("0x02", "Wiping Forehead", "Wiping Forehead"), + ("0x04", "Yawning", "Yawning"), + ("0x07", "Gasping For Breath", "Gasping For Breath"), + ("0x09", "Brandish Sword", "Brandish Sword"), + ("0x0A", "Adjust Tunic", "Adjust Tunic"), + ("0xFF", "Hops On Epona", "Hops On Epona"), +] + +mm_enum_environment_type = [ + ("Custom", "Custom", "Custom"), + ("ROOM_ENV_DEFAULT", "Default", "0x00"), + ("ROOM_ENV_COLD", "Cold", "0x01"), + ("ROOM_ENV_WARM", "Warm", "0x02"), + ("ROOM_ENV_HOT", "Hot", "0x03"), + ("ROOM_ENV_UNK_STRETCH_1", "Unknown Stretch 1", "0x04"), + ("ROOM_ENV_UNK_STRETCH_2", "Unknown Stretch 2", "0x05"), + ("ROOM_ENV_UNK_STRETCH_3", "Unknown Stretch 3", "0x06"), +] + +oot_enum_room_type = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Default", "Default"), + ("0x01", "Dungeon Behavior (Z-Target, Sun's Song)", "Dungeon Behavior (Z-Target, Sun's Song)"), + ("0x02", "Disable Backflips/Sidehops", "Disable Backflips/Sidehops"), + ("0x03", "Disable Color Dither", "Disable Color Dither"), + ("0x04", "(?) Horse Camera Related", "(?) Horse Camera Related"), + ("0x05", "Disable Darker Screen Effect (NL/Spins)", "Disable Darker Screen Effect (NL/Spins)"), +] + +mm_enum_room_type = [ + ("Custom", "Custom", "Custom"), + ("ROOM_TYPE_NORMAL", "Normal", "0x00"), + ("ROOM_TYPE_DUNGEON", "Dungeon", "0x01"), + ("ROOM_TYPE_INDOORS", "Indoors", "0x02"), + ("ROOM_TYPE_3", "Type 3", "0x03"), + ("ROOM_TYPE_4", "Type 4 (Horse related)", "0x04"), + ("ROOM_TYPE_BOSS", "Boss", "0x05"), +] + +oot_enum_floor_property = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Default", "Default"), + ("0x05", "Trigger Respawn", "Trigger Respawn"), + ("0x06", "Grab Wall", "Grab Wall"), + ("0x08", "Stop Air Momentum", "Stop Air Momentum"), + ("0x09", "Fall Instead Of Jumping", "Fall Instead Of Jumping"), + ("0x0B", "Dive Animation", "Dive Animation"), + ("0x0C", "Trigger Void", "Trigger Void"), +] + +mm_enum_floor_property = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Default", "FLOOR_PROPERTY_0"), + ("0x01", "Frontflip Jump Animation", "FLOOR_PROPERTY_1"), + ("0x02", "Sideflip Jump Animation", "FLOOR_PROPERTY_2"), + ("0x05", "Trigger Respawn (sets human no mask)", "FLOOR_PROPERTY_5"), + ("0x06", "Grab Wall", "FLOOR_PROPERTY_6"), + ("0x07", "Unknown (sets speed to 0)", "FLOOR_PROPERTY_7"), + ("0x08", "Stop Air Momentum", "FLOOR_PROPERTY_8"), + ("0x09", "Fall Instead Of Jumping", "FLOOR_PROPERTY_9"), + ("0x0B", "Dive Animation", "FLOOR_PROPERTY_11"), + ("0x0C", "Trigger Void", "FLOOR_PROPERTY_12"), + ("0x0D", "Trigger Void (runs `Player_Action_1`)", "FLOOR_PROPERTY_13"), +] + +oot_enum_floor_type = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Default", "Default"), + ("0x01", "Haunted Wasteland Camera", "Haunted Wasteland Camera"), + ("0x02", "Fire (damages every 6s)", "Fire (damages every 6s)"), + ("0x03", "Fire (damages every 3s)", "Fire (damages every 3s)"), + ("0x04", "Shallow Sand", "Shallow Sand"), + ("0x05", "Slippery", "Slippery"), + ("0x06", "Ignore Fall Damage", "Ignore Fall Damage"), + ("0x07", "Quicksand Crossing (Blocks Epona)", "Quicksand Crossing (Epona Uncrossable)"), + ("0x08", "Jabu Jabu's Belly Floor", "Jabu Jabu's Belly Floor"), + ("0x09", "Trigger Void", "Trigger Void"), + ("0x0A", "Stops Air Momentum", "Stops Air Momentum"), + ("0x0B", "Grotto Exit Animation", "Link Looks Up"), + ("0x0C", "Quicksand Crossing (Epona Crossable)", "Quicksand Crossing (Epona Crossable)"), +] + +mm_enum_floor_type = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Default", "FLOOR_TYPE_0"), + ("0x01", "Unused (?)", "FLOOR_TYPE_1"), + ("0x02", "Fire Damages (burns Player every second)", "FLOOR_TYPE_2"), + ("0x03", "Fire Damages 2 (burns Player every second)", "FLOOR_TYPE_3"), + ("0x04", "Shallow Sand", "FLOOR_TYPE_4"), + ("0x05", "Ice (Slippery)", "FLOOR_TYPE_5"), + ("0x06", "Ignore Fall Damages", "FLOOR_TYPE_6"), + ("0x07", "Quicksand (blocks Epona)", "FLOOR_TYPE_7"), + ("0x08", "Jabu Jabu's Belly Floor (Unused)", "FLOOR_TYPE_8"), + ("0x09", "Triggers Void", "FLOOR_TYPE_9"), + ("0x0A", "Stops Air Momentum", "FLOOR_TYPE_10"), + ("0x0B", "Grotto Exit Animation", "FLOOR_TYPE_11"), + ("0x0C", "Quicksand (doesn't block Epona)", "FLOOR_TYPE_12"), + ("0x0D", "Deeper Shallow Sand", "FLOOR_TYPE_13"), + ("0x0E", "Shallow Snow", "FLOOR_TYPE_14"), + ("0x0F", "Deeper Shallow Snow", "FLOOR_TYPE_15"), +] + +enum_floor_effect = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Default", "FLOOR_EFFECT_0"), + ("0x01", "Steep/Slippery Slope", "FLOOR_EFFECT_1"), + ("0x02", "Walkable (Preserves Exit Flags)", "FLOOR_EFFECT_2"), +] + +oot_enum_camera_setting_type = [ + ("Custom", "Custom", "Custom"), + ("CAM_SET_NONE", "None", "None"), + ("CAM_SET_NORMAL0", "Normal0", "Normal0"), + ("CAM_SET_NORMAL1", "Normal1", "Normal1"), + ("CAM_SET_DUNGEON0", "Dungeon0", "Dungeon0"), + ("CAM_SET_DUNGEON1", "Dungeon1", "Dungeon1"), + ("CAM_SET_NORMAL3", "Normal3", "Normal3"), + ("CAM_SET_HORSE0", "Horse", "Horse"), + ("CAM_SET_BOSS_GOMA", "Boss_gohma", "Boss_gohma"), + ("CAM_SET_BOSS_DODO", "Boss_dodongo", "Boss_dodongo"), + ("CAM_SET_BOSS_BARI", "Boss_barinade", "Boss_barinade"), + ("CAM_SET_BOSS_FGANON", "Boss_phantom_ganon", "Boss_phantom_ganon"), + ("CAM_SET_BOSS_BAL", "Boss_volvagia", "Boss_volvagia"), + ("CAM_SET_BOSS_SHADES", "Boss_bongo", "Boss_bongo"), + ("CAM_SET_BOSS_MOFA", "Boss_morpha", "Boss_morpha"), + ("CAM_SET_TWIN0", "Twinrova_platform", "Twinrova_platform"), + ("CAM_SET_TWIN1", "Twinrova_floor", "Twinrova_floor"), + ("CAM_SET_BOSS_GANON1", "Boss_ganondorf", "Boss_ganondorf"), + ("CAM_SET_BOSS_GANON2", "Boss_ganon", "Boss_ganon"), + ("CAM_SET_TOWER0", "Tower_climb", "Tower_climb"), + ("CAM_SET_TOWER1", "Tower_unused", "Tower_unused"), + ("CAM_SET_FIXED0", "Market_balcony", "Market_balcony"), + ("CAM_SET_FIXED1", "Chu_bowling", "Chu_bowling"), + ("CAM_SET_CIRCLE0", "Pivot_crawlspace", "Pivot_crawlspace"), + ("CAM_SET_CIRCLE2", "Pivot_shop_browsing", "Pivot_shop_browsing"), + ("CAM_SET_CIRCLE3", "Pivot_in_front", "Pivot_in_front"), + ("CAM_SET_PREREND0", "Prerend_fixed", "Prerend_fixed"), + ("CAM_SET_PREREND1", "Prerend_pivot", "Prerend_pivot"), + ("CAM_SET_PREREND3", "Prerend_side_scroll", "Prerend_side_scroll"), + ("CAM_SET_DOOR0", "Door0", "Door0"), + ("CAM_SET_DOORC", "Doorc", "Doorc"), + ("CAM_SET_RAIL3", "Crawlspace", "Crawlspace"), + ("CAM_SET_START0", "Start0", "Start0"), + ("CAM_SET_START1", "Start1", "Start1"), + ("CAM_SET_FREE0", "Free0", "Free0"), + ("CAM_SET_FREE2", "Free2", "Free2"), + ("CAM_SET_CIRCLE4", "Pivot_corner", "Pivot_corner"), + ("CAM_SET_CIRCLE5", "Pivot_water_surface", "Pivot_water_surface"), + ("CAM_SET_DEMO0", "Cs_0", "Cs_0"), + ("CAM_SET_DEMO1", "Twisted_Hallway", "Twisted_Hallway"), + ("CAM_SET_MORI1", "Forest_birds_eye", "Forest_birds_eye"), + ("CAM_SET_ITEM0", "Slow_chest_cs", "Slow_chest_cs"), + ("CAM_SET_ITEM1", "Item_unused", "Item_unused"), + ("CAM_SET_DEMO3", "Cs_3", "Cs_3"), + ("CAM_SET_DEMO4", "Cs_attention", "Cs_attention"), + ("CAM_SET_UFOBEAN", "Bean_generic", "Bean_generic"), + ("CAM_SET_LIFTBEAN", "Bean_lost_woods", "Bean_lost_woods"), + ("CAM_SET_SCENE0", "Scene_unused", "Scene_unused"), + ("CAM_SET_SCENE1", "Scene_transition", "Scene_transition"), + ("CAM_SET_HIDAN1", "Fire_platform", "Fire_platform"), + ("CAM_SET_HIDAN2", "Fire_staircase", "Fire_staircase"), + ("CAM_SET_MORI2", "Forest_unused", "Forest_unused"), + ("CAM_SET_MORI3", "Defeat_poe", "Defeat_poe"), + ("CAM_SET_TAKO", "Big_octo", "Big_octo"), + ("CAM_SET_SPOT05A", "Meadow_birds_eye", "Meadow_birds_eye"), + ("CAM_SET_SPOT05B", "Meadow_unused", "Meadow_unused"), + ("CAM_SET_HIDAN3", "Fire_birds_eye", "Fire_birds_eye"), + ("CAM_SET_ITEM2", "Turn_around", "Turn_around"), + ("CAM_SET_CIRCLE6", "Pivot_vertical", "Pivot_vertical"), + ("CAM_SET_NORMAL2", "Normal2", "Normal2"), + ("CAM_SET_FISHING", "Fishing", "Fishing"), + ("CAM_SET_DEMOC", "Cs_c", "Cs_c"), + ("CAM_SET_UO_FIBER", "Jabu_tentacle", "Jabu_tentacle"), + ("CAM_SET_DUNGEON2", "Dungeon2", "Dungeon2"), + ("CAM_SET_TEPPEN", "Directed_yaw", "Directed_yaw"), + ("CAM_SET_CIRCLE7", "Pivot_from_side", "Pivot_from_side"), + ("CAM_SET_NORMAL4", "Normal4", "Normal4"), +] + +mm_enum_camera_setting_type = [ + ("Custom", "Custom", "Custom"), + ("CAM_SET_NONE", "None", "None"), + ("CAM_SET_NORMAL0", "Normal0", "Generic camera 0, used in various places 'NORMAL0'"), + ("CAM_SET_NORMAL3", "Normal3", "Generic camera 3, used in various places 'NORMAL3'"), + ( + "CAM_SET_PIVOT_DIVING", + "Pivot_Diving", + "Player diving from the surface of the water to underwater not as zora 'CIRCLE5'", + ), + ("CAM_SET_HORSE", "Horse", "Reiding a horse 'HORSE0'"), + ( + "CAM_SET_ZORA_DIVING", + "Zora_Diving", + "Parallel's Pivot Diving, but as Zora. However, Zora does not dive like a human. So this setting appears to not be used 'ZORA0'", + ), + ( + "CAM_SET_PREREND_FIXED", + "Prerend_Fixed", + "Unused remnant of OoT: camera is fixed in position and rotation 'PREREND0'", + ), + ( + "CAM_SET_PREREND_PIVOT", + "Prerend_Pivot", + "Unused remnant of OoT: Camera is fixed in position with fixed pitch, but is free to rotate in the yaw direction 360 degrees 'PREREND1'", + ), + ( + "CAM_SET_DOORC", + "Doorc", + "Generic room door transitions, camera moves and follows player as the door is open and closed 'DOORC'", + ), + ("CAM_SET_DEMO0", "Demo0", "Unknown, possibly related to treasure chest game as goron? 'DEMO0'"), + ("CAM_SET_FREE0", "Free0", "Free Camera, manual control is given, no auto-updating eye or at 'FREE0'"), + ("CAM_SET_BIRDS_EYE_VIEW_0", "Birds_Eye_View_0", "Appears unused. Camera is a top-down view 'FUKAN0'"), + ("CAM_SET_NORMAL1", "Normal1", "Generic camera 1, used in various places 'NORMAL1'"), + ( + "CAM_SET_NANAME", + "Naname", + "Unknown, slanted or tilted. Behaves identical to Normal0 except with added roll 'NANAME'", + ), + ("CAM_SET_CIRCLE0", "Circle0", "Used in Curiosity Shop, Pirates Fortress, Mayor's Residence 'CIRCLE0'"), + ("CAM_SET_FIXED0", "Fixed0", "Used in Sakon's Hideout puzzle rooms, milk bar stage 'FIXED0'"), + ("CAM_SET_SPIRAL_DOOR", "Spiral_Door", "Exiting a Spiral Staircase 'SPIRAL'"), + ("CAM_SET_DUNGEON0", "Dungeon0", "Generic dungeon camera 0, used in various places 'DUNGEON0'"), + ( + "CAM_SET_ITEM0", + "Item0", + "Getting an item and holding it above Player's head (from small chest, freestanding, npc, ...) 'ITEM0'", + ), + ("CAM_SET_ITEM1", "Item1", "Looking at player while playing the ocarina 'ITEM1'"), + ("CAM_SET_ITEM2", "Item2", "Bottles: drinking, releasing fairy, dropping fish 'ITEM2'"), + ("CAM_SET_ITEM3", "Item3", "Bottles: catching fish or bugs, showing an item 'ITEM3'"), + ("CAM_SET_NAVI", "Navi", "Song of Soaring, variations of playing Song of Time 'NAVI'"), + ("CAM_SET_WARP_PAD_MOON", "Warp_Pad_Moon", "Warp circles from Goron Trial on the moon 'WARP0'"), + ("CAM_SET_DEATH", "Death", "Player death animation when health goes to 0 'DEATH'"), + ("CAM_SET_REBIRTH", "Rebirth", "Unknown set with camDataId = -9 (it's not being revived by a fairy) 'REBIRTH'"), + ( + "CAM_SET_LONG_CHEST_OPENING", + "Long_Chest_Opening", + "Long cutscene when opening a big chest with a major item 'TREASURE'", + ), + ("CAM_SET_MASK_TRANSFORMATION", "Mask_Transformation", "Putting on a transformation mask 'TRANSFORM'"), + ("CAM_SET_ATTENTION", "Attention", "Unknown, set with camDataId = -15 'ATTENTION'"), + ("CAM_SET_WARP_PAD_ENTRANCE", "Warp_Pad_Entrance", "Warp pad from start of a dungeon to the boss-room 'WARP1'"), + ("CAM_SET_DUNGEON1", "Dungeon1", "Generic dungeon camera 1, used in various places 'DUNGEON1'"), + ( + "CAM_SET_FIXED1", + "Fixed1", + "Fixes camera in place, used in various places eg. entering Stock Pot Inn, hiting a switch, giving witch a red potion, shop browsing 'FIXED1'", + ), + ( + "CAM_SET_FIXED2", + "Fixed2", + "Used in Pinnacle Rock after defeating Sea Monsters, and by Tatl in Fortress 'FIXED2'", + ), + ("CAM_SET_MAZE", "Maze", "Unused. Set to use Camera_Parallel2(), which is only Camera_Noop() 'MAZE'"), + ( + "CAM_SET_REMOTEBOMB", + "Remotebomb", + "Unused. Set to use Camera_Parallel2(), which is only Camera_Noop(). But also related to Play_ChangeCameraSetting? 'REMOTEBOMB'", + ), + ("CAM_SET_CIRCLE1", "Circle1", "Unknown 'CIRCLE1'"), + ( + "CAM_SET_CIRCLE2", + "Circle2", + "Looking at far-away NPCs eg. Garo in Road to Ikana, Hungry Goron, Tingle 'CIRCLE2'", + ), + ( + "CAM_SET_CIRCLE3", + "Circle3", + "Used in curiosity shop, goron racetrack, final room in Sakon's hideout, other places 'CIRCLE3'", + ), + ("CAM_SET_CIRCLE4", "Circle4", "Used during the races on the doggy racetrack 'CIRCLE4'"), + ("CAM_SET_FIXED3", "Fixed3", "Used in Stock Pot Inn Toilet and Tatl cutscene after woodfall 'FIXED3'"), + ( + "CAM_SET_TOWER_ASCENT", + "Tower_Ascent", + "Various climbing structures (Snowhead climb to the temple entrance) 'TOWER0'", + ), + ("CAM_SET_PARALLEL0", "Parallel0", "Unknown 'PARALLEL0'"), + ("CAM_SET_NORMALD", "Normald", "Unknown, set with camDataId = -20 'NORMALD'"), + ("CAM_SET_SUBJECTD", "Subjectd", "Unknown, set with camDataId = -21 'SUBJECTD'"), + ( + "CAM_SET_START0", + "Start0", + "Entering a room, either Dawn of a New Day reload, or entering a door where the camera is fixed on the other end 'START0'", + ), + ( + "CAM_SET_START2", + "Start2", + "Entering a scene, camera is put at a low angle eg. Grottos, Deku Palace, Stock Pot Inn 'START2'", + ), + ("CAM_SET_STOP0", "Stop0", "Called in z_play 'STOP0'"), + ("CAM_SET_BOAT_CRUISE", "Boat_Cruise", " Koume's boat cruise 'JCRUISING'"), + ( + "CAM_SET_VERTICAL_CLIMB", + "Vertical_Climb", + "Large vertical climbs, such as Mountain Village wall or Pirates Fortress ladder. 'CLIMBMAZE'", + ), + ("CAM_SET_SIDED", "Sided", "Unknown, set with camDataId = -24 'SIDED'"), + ("CAM_SET_DUNGEON2", "Dungeon2", "Generic dungeon camera 2, used in various places 'DUNGEON2'"), + ("CAM_SET_BOSS_ODOLWA", "Boss_Odolwa", "Odolwa's Lair, also used in GBT entrance: 'BOSS_SHIGE'"), + ("CAM_SET_KEEPBACK", "Keepback", "Unknown. Possibly related to climbing something? 'KEEPBACK'"), + ("CAM_SET_CIRCLE6", "Circle6", "Used in select regions from Ikana 'CIRCLE6'"), + ("CAM_SET_CIRCLE7", "Circle7", "Unknown 'CIRCLE7'"), + ("CAM_SET_MINI_BOSS", "Mini_Boss", "Used during the various minibosses of the 'CHUBOSS'"), + ("CAM_SET_RFIXED1", "Rfixed1", "Talking to Koume stuck on the floor in woods of mystery 'RFIXED1'"), + ( + "CAM_SET_TREASURE_CHEST_MINIGAME", + "Treasure_Chest_Minigame", + "Treasure Chest Shop in East Clock Town, minigame location 'TRESURE1'", + ), + ("CAM_SET_HONEY_AND_DARLING_1", "Honey_And_Darling_1", "Honey and Darling Minigames 'BOMBBASKET'"), + ( + "CAM_SET_CIRCLE8", + "Circle8", + "Used by Stone Tower moving platforms, Falling eggs in Marine Lab, Bugs into soilpatch cutscene 'CIRCLE8'", + ), + ( + "CAM_SET_BIRDS_EYE_VIEW_1", + "Birds_Eye_View_1", + "Camera is a top-down view. Used in Fisherman's minigame and Deku Palace 'FUKAN1'", + ), + ("CAM_SET_DUNGEON3", "Dungeon3", "Generic dungeon camera 3, used in various places 'DUNGEON3'"), + ("CAM_SET_TELESCOPE", "Telescope", "Observatory telescope and Curiosity Shop Peep-Hole 'TELESCOPE'"), + ("CAM_SET_ROOM0", "Room0", "Certain rooms eg. inside the clock tower 'ROOM0'"), + ("CAM_SET_RCIRC0", "Rcirc0", "Used by a few NPC cutscenes, focus close on the NPC 'RCIRC0'"), + ("CAM_SET_CIRCLE9", "Circle9", "Used by Sakon Hideout entrance and Deku Palace Maze 'CIRCLE9'"), + ("CAM_SET_ONTHEPOLE", "Onthepole", "Somewhere in Snowhead Temple and Woodfall Temple 'ONTHEPOLE'"), + ( + "CAM_SET_INBUSH", + "Inbush", + "Various bush environments eg. grottos, Swamp Spider House, Termina Field grass bushes, Deku Palace near bean 'INBUSH'", + ), + ("CAM_SET_BOSS_MAJORA", "Boss_Majora", "Majora's Lair: 'BOSS_LAST'"), + ("CAM_SET_BOSS_TWINMOLD", "Boss_Twinmold", "Twinmold's Lair: 'BOSS_INI'"), + ("CAM_SET_BOSS_GOHT", "Boss_Goht", "Goht's Lair: 'BOSS_HAK'"), + ("CAM_SET_BOSS_GYORG", "Boss_Gyorg", "Gyorg's Lair: 'BOSS_KON'"), + ("CAM_SET_CONNECT0", "Connect0", "Smoothly and gradually return camera to Player after a cutscene 'CONNECT0'"), + ("CAM_SET_PINNACLE_ROCK", "Pinnacle_Rock", "Pinnacle Rock pit 'MORAY'"), + ("CAM_SET_NORMAL2", "Normal2", "Generic camera 2, used in various places 'NORMAL2'"), + ("CAM_SET_HONEY_AND_DARLING_2", "Honey_And_Darling_2", "'BOMBBOWL'"), + ("CAM_SET_CIRCLEA", "Circlea", "Unknown, Circle 10 'CIRCLEA'"), + ("CAM_SET_WHIRLPOOL", "Whirlpool", "Great Bay Temple Central Room Whirlpool 'WHIRLPOOL'"), + ("CAM_SET_CUCCO_SHACK", "Cucco_Shack", "'KOKKOGAME'"), + ("CAM_SET_GIANT", "Giant", "Giants Mask in Twinmold's Lair 'GIANT'"), + ("CAM_SET_SCENE0", "Scene0", "Entering doors to a new scene 'SCENE0'"), + ("CAM_SET_ROOM1", "Room1", "Certain rooms eg. some rooms in Stock Pot Inn 'ROOM1'"), + ("CAM_SET_WATER2", "Water2", "Swimming as Zora in Great Bay Temple 'WATER2'"), + ("CAM_SET_WOODFALL_SWAMP", "Woodfall_Swamp", "Woodfall inside the swamp, but not on the platforms, 'SOKONASI'"), + ("CAM_SET_FORCEKEEP", "Forcekeep", "Unknown 'FORCEKEEP'"), + ("CAM_SET_PARALLEL1", "Parallel1", "Unknown 'PARALLEL1'"), + ("CAM_SET_START1", "Start1", "Used when entering the lens cave 'START1'"), + ("CAM_SET_ROOM2", "Room2", "Certain rooms eg. Deku King's Chamber, Ocean Spider House 'ROOM2'"), + ("CAM_SET_NORMAL4", "Normal4", "Generic camera 4, used in Ikana Graveyard 'NORMAL4'"), + ("CAM_SET_ELEGY_SHELL", "Elegy_Shell", "cutscene after playing elegy of emptyness and spawning a shell 'SHELL'"), + ("CAM_SET_DUNGEON4", "Dungeon4", "Used in Pirates Fortress Interior, hidden room near hookshot 'DUNGEON4'"), +] + +# order here sets order on the UI +oot_enum_cs_list_type = [ + # Col 1 + ("TextList", "Text List", "Textbox", "ALIGN_BOTTOM", 0), + ("MiscList", "Misc List", "Misc", "OPTIONS", 7), + ("RumbleList", "Rumble List", "Rumble Controller", "OUTLINER_OB_FORCE_FIELD", 8), + # Col 2 + ("Transition", "Transition List", "Transition List", "COLORSET_10_VEC", 1), + ("LightSettingsList", "Light Settings List", "Lighting", "LIGHT_SUN", 2), + ("TimeList", "Time List", "Time", "TIME", 3), + # Col 3 + ("StartSeqList", "Start Seq List", "Play BGM", "PLAY", 4), + ("StopSeqList", "Stop Seq List", "Stop BGM", "SNAP_FACE", 5), + ("FadeOutSeqList", "Fade-Out Seq List", "Fade BGM", "IPO_EASE_IN_OUT", 6), +] + +mm_enum_cs_list_type = [ + # Col 1 + ("TextList", "Text List", "Textbox", "ALIGN_BOTTOM", 0), + ("MiscList", "Misc List", "Misc", "OPTIONS", 7), + ("RumbleList", "Rumble List", "Rumble Controller", "OUTLINER_OB_FORCE_FIELD", 8), + ("MotionBlurList", "Motion Blur List", "Motion Blur", "ONIONSKIN_ON", 9), + ("CreditsSceneList", "Choose Credits Scene List", "Choose Credits Scene", "WORLD", 11), + # Col 2 + ("Transition", "Transition", "Transition", "COLORSET_10_VEC", 1), + ("LightSettingsList", "Light Settings List", "Lighting", "LIGHT_SUN", 2), + ("TimeList", "Time List", "Time", "TIME", 3), + ("TransitionGeneralList", "Transition General List", "Transition General", "COLORSET_06_VEC", 12), + ("ModifySeqList", "Modify Seq List", "Modify Seq", "IPO_CONSTANT", 10), + # Col 3 + ("StartSeqList", "Start Seq List", "Play BGM", "PLAY", 4), + ("StopSeqList", "Stop Seq List", "Stop BGM", "SNAP_FACE", 5), + ("FadeOutSeqList", "Fade-Out Seq List", "Fade BGM", "IPO_EASE_IN_OUT", 6), + ("StartAmbienceList", "Start Ambience List", "Start Ambience", "SNAP_FACE", 13), + ("FadeOutAmbienceList", "Fade-Out Ambience List", "Fade-Out Ambience", "IPO_EASE_IN_OUT", 14), +] + +# Adding new rest pose entry: +# 1. Import a generic skeleton +# 2. Pose into a usable rest pose +# 3. Select skeleton, then run bpy.ops.object.oot_save_rest_pose() +# 4. Copy array data from console into an OOTSkeletonImportInfo object +# - list of tuples, first is root position, rest are euler XYZ rotations +# 5. Add object to oot_skeleton_dict/mm_skeleton_dict + +link_skeleton_names = { + "gLinkAdultSkel", + "gLinkChildSkel", + "gLinkHumanSkel", + "gLinkDekuSkel", + "gLinkGoronSkel", + "gLinkZoraSkel", + "gLinkFierceDeitySkel", +} + + +# Link overlay will be "", since Link texture array data is handled as a special case. +class OOTSkeletonImportInfo: + def __init__( + self, + skeletonName: str, + folderName: str, + actorOverlayName: str, + flipbookArrayIndex2D: int | None, + restPoseData: list[tuple[float, float, float]] | None, + ): + self.skeletonName = skeletonName + self.folderName = folderName + self.actorOverlayName = actorOverlayName # Note that overlayName = None will disable texture array reading. + self.flipbookArrayIndex2D = flipbookArrayIndex2D + self.isLink = skeletonName in link_skeleton_names + self.restPoseData = restPoseData + + +oot_skeleton_dict = OrderedDict( + { + "Adult Link": OOTSkeletonImportInfo( + "gLinkAdultSkel", + "object_link_boy", + "", + 0, + [ + (0.0, 3.6050000190734863, 0.0), + (0.0, -0.0, 0.0), + (-1.5708922147750854, -0.0, -1.5707963705062866), + (0.0, -0.0, 0.0), + (0.0, 0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (0.0, -0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (1.5707963705062866, -0.0, 1.5707963705062866), + (-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107), + (-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627), + (0.0, -0.0, 0.0), + (1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.5707963705062866, 2.611602306365967, -0.08726644515991211), + (0.0, -0.0, 0.0), + ], + ), + "Child Link": OOTSkeletonImportInfo( + "gLinkChildSkel", + "object_link_child", + "", + 1, + [ + (0.0, 2.3559017181396484, 0.0), + (0.0, -0.0, 0.0), + (-1.5708922147750854, -0.0, -1.5707963705062866), + (0.0, -0.0, 0.0), + (0.0, 0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (0.0, -0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (1.5707963705062866, -0.0, 1.5707963705062866), + (-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107), + (-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627), + (0.0, -0.0, 0.0), + (1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.5707963705062866, 2.611602306365967, -0.08726644515991211), + (0.0, -0.0, 0.0), + ], + ), + } +) +oot_enum_skeleton_mode = [ + ("Generic", "Generic", "Generic"), +] +for name, info in oot_skeleton_dict.items(): + oot_enum_skeleton_mode.append((name, name, name)) + +mm_skeleton_dict = OrderedDict( + { + "Human Link": OOTSkeletonImportInfo( + "gLinkHumanSkel", + "object_link_child", + "", + 4, + [ + (0.0, 2.3559017181396484, 0.0), + (0.0, -0.0, 0.0), + (-1.5708922147750854, -0.0, -1.5707963705062866), + (0.0, -0.0, 0.0), + (0.0, 0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (0.0, -0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (1.5707963705062866, -0.0, 1.5707963705062866), + (-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107), + (-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627), + (0.0, -0.0, 0.0), + (1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.5707963705062866, 2.611602306365967, -0.08726644515991211), + (0.0, -0.0, 0.0), + ], + ), + "Deku Link": OOTSkeletonImportInfo( + "gLinkDekuSkel", + "object_link_nuts", + None, + 3, + [ + (0.0, 2.3559017181396484, 0.0), + (0.0, -0.0, 0.0), + (-1.5708922147750854, -0.0, -1.5707963705062866), + (0.0, -0.0, 0.0), + (0.0, 0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (0.0, -0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (1.5707963705062866, -0.0, 1.5707963705062866), + (-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107), + (-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627), + (0.0, -0.0, 0.0), + (1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.5707963705062866, 2.611602306365967, -0.08726644515991211), + (0.0, -0.0, 0.0), + ], + ), + "Goron Link": OOTSkeletonImportInfo( + "gLinkGoronSkel", + "object_link_goron", + "", + 1, + [ + (0.0, 2.3559017181396484, 0.0), + (0.0, -0.0, 0.0), + (-1.5708922147750854, -0.0, -1.5707963705062866), + (0.0, -0.0, 0.0), + (0.0, 0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (0.0, -0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (1.5707963705062866, -0.0, 1.5707963705062866), + (-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107), + (-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627), + (0.0, -0.0, 0.0), + (1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.5707963705062866, 2.611602306365967, -0.08726644515991211), + (0.0, -0.0, 0.0), + ], + ), + "Zora Link": OOTSkeletonImportInfo( + "gLinkZoraSkel", + "object_link_zora", + "", + 2, + [ + (0.0, 2.3559017181396484, 0.0), + (0.0, -0.0, 0.0), + (-1.5708922147750854, -0.0, -1.5707963705062866), + (0.0, -0.0, 0.0), + (0.0, 0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (0.0, -0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (1.5707963705062866, -0.0, 1.5707963705062866), + (-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107), + (-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627), + (0.0, -0.0, 0.0), + (1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.5707963705062866, 2.611602306365967, -0.08726644515991211), + (0.0, -0.0, 0.0), + ], + ), + "Fierce Deity Link": OOTSkeletonImportInfo( + "gLinkFierceDeitySkel", + "object_link_boy", + None, + 0, + [ + (0.0, 3.6050000190734863, 0.0), + (0.0, -0.0, 0.0), + (-1.5708922147750854, -0.0, -1.5707963705062866), + (0.0, -0.0, 0.0), + (0.0, 0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (0.0, -0.05235987901687622, 0.0), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (1.5707963705062866, -0.0, 1.5707963705062866), + (-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107), + (-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627), + (0.0, -0.0, 0.0), + (1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994), + (0.0, -0.0, 0.0), + (0.0, 0.0, -1.5707964897155762), + (-1.5707963705062866, 2.611602306365967, -0.08726644515991211), + (0.0, -0.0, 0.0), + ], + ), + } +) +mm_enum_skeleton_mode = [ + ("Generic", "Generic", "Generic"), +] +for name, info in mm_skeleton_dict.items(): + mm_enum_skeleton_mode.append((name, name, name)) + +# --- + + +@dataclass +class Z64_Data: + """Contains data related to OoT/MM, like actors or objects""" + + def __init__(self, game: str): + self.game = game + self.is_registering = True + self.update(None, game, True) # forcing the update as we're in the init function + + self.enum_floor_effect = enum_floor_effect + + def is_oot(self): + self.update(bpy.context, None) + return self.game == "OOT" + + def is_mm(self): + self.update(bpy.context, None) + return self.game == "MM" + + def update(self, context: Optional[Context], game: Optional[str], force: bool = False): + if context is not None and self.is_registering: + self.is_registering = False + + if not force and self.is_registering: + next_game = "OOT" + elif game is not None: + next_game = game + elif context is not None: + next_game = context.scene.gameEditorMode + else: + raise ValueError("ERROR: invalid values for context and game") + + # don't update if the game is the same (or we don't want to force one) + if not force and next_game == self.game: + return + + self.cs_list_type_to_cmd = { + "TextList": "CS_TEXT_LIST", + "LightSettingsList": "CS_LIGHT_SETTING_LIST", + "TimeList": "CS_TIME_LIST", + "StartSeqList": "CS_START_SEQ_LIST", + "StopSeqList": "CS_STOP_SEQ_LIST", + "FadeOutSeqList": "CS_FADE_OUT_SEQ_LIST", + "MiscList": "CS_MISC_LIST", + "DestinationList": "CS_DESTINATION_LIST", + "MotionBlurList": "CS_MOTION_BLUR_LIST", + "ModifySeqList": "CS_MODIFY_SEQ_LIST", + "CreditsSceneList": "CS_CHOOSE_CREDITS_SCENES_LIST", + "TransitionGeneralList": "CS_TRANSITION_GENERAL_LIST", + "GiveTatlList": "CS_GIVE_TATL_LIST", + } + + self.game = next_game + self.enums = Z64_EnumData(self.game) + self.objects = Z64_ObjectData(self.game) + self.actors = Z64_ActorData(self.game) + + if self.game == "OOT": + self.cs_index_start = 4 + self.cs_list_type_to_cmd["Transition"] = "CS_TRANSITION" + self.cs_list_type_to_cmd["RumbleList"] = "CS_RUMBLE_CONTROLLER_LIST" + self.enum_nature_id = oot_enum_nature_id + self.enum_skybox = oot_enum_skybox + self.enum_skybox_config = oot_enum_skybox_config + self.enum_environment_type = oot_enum_environment_type + self.enum_room_type = oot_enum_room_type + self.enum_floor_property = oot_enum_floor_property + self.enum_floor_type = oot_enum_floor_type + self.enum_camera_setting_type = oot_enum_camera_setting_type + self.enum_cs_list_type = oot_enum_cs_list_type + self.skeleton_dict = oot_skeleton_dict + self.enum_skeleton_mode = oot_enum_skeleton_mode + elif self.game == "MM": + self.cs_index_start = 1 + self.cs_list_type_to_cmd["Transition"] = "CS_TRANSITION_LIST" + self.cs_list_type_to_cmd["RumbleList"] = "CS_RUMBLE_LIST" + self.enum_nature_id = enum_ambiance_id + self.enum_skybox = mm_enum_skybox + self.enum_skybox_config = mm_enum_skybox_config + self.enum_environment_type = mm_enum_environment_type + self.enum_room_type = mm_enum_room_type + self.enum_floor_property = mm_enum_floor_property + self.enum_floor_type = mm_enum_floor_type + self.enum_camera_setting_type = mm_enum_camera_setting_type + self.enum_cs_list_type = mm_enum_cs_list_type + self.skeleton_dict = mm_skeleton_dict + self.enum_skeleton_mode = mm_enum_skeleton_mode + else: + raise ValueError(f"ERROR: unsupported game {repr(self.game)}") + + self.enum_map: dict[str, list[tuple[str, str, str]]] = { + "globalObject": self.enums.enum_global_object, + "musicSeq": self.enums.enum_seq_id, + "drawConfig": self.enums.enum_draw_config, + "sound": self.enums.enum_surface_material, + "csDestination": self.enums.enum_cs_destination, + "seqId": self.enums.enum_seq_id, + "playerCueID": self.enums.enum_cs_player_cue_id, + "ocarinaAction": self.enums.enum_ocarina_song_action_id, + "csTextType": self.enums.enum_cs_text_type, + "csSeqPlayer": self.enums.enum_cs_fade_out_seq_player, + "csMiscType": self.enums.enum_cs_misc_type, + "transitionType": self.enums.enum_cs_transition_type, + "actor_cue_list_cmd_type": self.enums.enum_cs_actor_cue_list_cmd_type, + "spline_interp_type": self.enums.enum_cs_spline_interp_type, + "spline_rel_to": self.enums.enum_cs_spline_rel, + "trans_general": self.enums.enum_cs_transition_general, + "blur_type": self.enums.enum_cs_motion_blur_type, + "credits_scene_type": self.enums.enum_cs_credits_scene_type, + "mod_seq_type": self.enums.enum_cs_modify_seq_type, + "anim_mats_type": self.enums.enum_anim_mats_type, + "anim_mats_cam_type": self.enums.enum_anim_mats_cam_type, + "event_condition": self.enums.enum_event_condition, + "event_flag_type": self.enums.enum_event_flag_type, + "event_inv_type": self.enums.enum_event_inv_type, + "event_game_type": self.enums.enum_event_game_type, + "event_time_type": self.enums.enum_event_time_type, + "event_action_type": self.enums.enum_event_action_type, + "event_type": self.enums.enum_event_type, + "inventory_items": self.enums.enum_inventory_items, + "equipment_items": self.enums.enum_equipment_items, + "quest_items": self.enums.enum_quest_items, + "upgrade_type": self.enums.enum_upgrade_type, + "objectKey": self.objects.ootEnumObjectKey, + "actor_id": self.actors.ootEnumActorID, + "chest_content": self.actors.ootEnumChestContent, + "navi_msg_id": self.actors.ootEnumNaviMessageData, + "collectibles": self.actors.ootEnumCollectibleItems, + "skybox": self.enum_skybox, + "skybox_config": self.enum_skybox_config, + "nature_id": self.enum_nature_id, + "room_type": self.enum_room_type, + "environment_type": self.enum_environment_type, + "floor_property": self.enum_floor_property, + "floor_type": self.enum_floor_type, + "camera_setting_type": self.enum_camera_setting_type, + "cs_list_type": self.enum_cs_list_type, + "skeleton_mode": self.enum_skeleton_mode, + } + + def get_enum(self, prop_name: str): + self.update(bpy.context, None) + return self.enum_map[prop_name] + + def get_enum_value(self, enum_key: str, item_key: str): + enum = self.enums.enumByKey[enum_key] + + if bpy.context.scene.fast64.oot.useDecompFeatures: + return enum.item_by_key[item_key].id + else: + return str(enum.item_by_key[item_key].index) diff --git a/fast64_internal/data/z64/enum_data.py b/fast64_internal/data/z64/enum_data.py new file mode 100644 index 000000000..0dc80538b --- /dev/null +++ b/fast64_internal/data/z64/enum_data.py @@ -0,0 +1,176 @@ +from dataclasses import dataclass, field +from os import path +from pathlib import Path +from .common import Z64_BaseElement, get_xml_root + + +@dataclass +class Z64_ItemElement(Z64_BaseElement): + parentKey: str + game: str + desc: str + + def __post_init__(self): + # generate the name from the id + + if self.name is None: + keyToPrefix = { + "cs_cmd": "CS_CMD", + "cs_misc_type": "CS_MISC", + "cs_text_type": "CS_TEXT", + "cs_fade_out_seq_player": "CS_FADE_OUT", + "cs_transition_type": "CS_TRANS", + "cs_destination": ("CS_DESTINATION" if self.game == "MM" else "CS_DEST"), + "cs_player_cue_id": "PLAYER_CUEID", + "cs_modify_seq_type": "CS_MOD", + "cs_credits_scene_type": "CS_CREDITS", + "cs_motion_blur_type": "CS_MOTION_BLUR", + "cs_rumble_type": "CS_RUMBLE", + "cs_transition_general": "CS_TRANS_GENERAL", + "cs_spline_interp_type": "CS_CAM_INTERP", + "cs_spline_rel": "", # TODO: set the value to `CS_CAM_REL` once this is documented + "cs_spawn_flag": "CS_SPAWN_FLAG", + "actor_cs_end_sfx": "CS_END_SFX", + "navi_quest_hint_type": "NAVI_QUEST_HINTS", + "ocarina_song_action_id": "OCARINA_ACTION", + "seq_id": "NA_BGM", + "draw_config": ("SCENE_DRAW_CFG" if self.game == "MM" else "SDC"), + "surface_material": "SURFACE_MATERIAL", + "global_object": "OBJECT", + } + + self.name = self.id.removeprefix(f"{keyToPrefix.get(self.parentKey, '')}_") + + if self.parentKey in ["cs_cmd", "cs_player_cue_id"]: + split = self.name.split("_") + if self.parentKey == "cs_cmd" and "ACTOR_CUE" in self.id: + self.name = f"Actor Cue {split[-2]}_{split[-1]}" + else: + self.name = f"Player Cue Id {split[-1]}" + else: + self.name = self.name.replace("_", " ").title() + + +@dataclass +class Z64_EnumElement(Z64_BaseElement): + items: list[Z64_ItemElement] + item_by_key: dict[str, Z64_ItemElement] = field(default_factory=dict) + item_by_index: dict[int, Z64_ItemElement] = field(default_factory=dict) + item_by_id: dict[int, Z64_ItemElement] = field(default_factory=dict) + + def __post_init__(self): + self.item_by_key = {item.key: item for item in self.items} + self.item_by_index = {item.index: item for item in self.items} + self.item_by_id = {item.id: item for item in self.items} + + +class Z64_EnumData: + """Cutscene and misc enum data""" + + def __init__(self, game: str): + # general enumData list + self.enumDataList: list[Z64_EnumElement] = [] + + # Path to the ``EnumData.xml`` file + xml_path = Path(f"{path.dirname(path.abspath(__file__))}/xml/{game.lower()}_enum_data.xml") + enum_data_root = get_xml_root(xml_path.resolve()) + + for enum in enum_data_root.iterfind("Enum"): + self.enumDataList.append( + Z64_EnumElement( + enum.attrib["ID"], + enum.attrib["Key"], + None, + None, + [ + Z64_ItemElement( + item.attrib["ID"], + item.attrib["Key"], + # note: the name sets automatically after the init if None + item.attrib.get("Name"), + int(item.attrib["Index"]), + enum.attrib["Key"], + game, + item.attrib.get("Description", "Unset"), + ) + for item in enum + ], + ) + ) + + # create list of tuples used by Blender's enum properties + self.deletedEntry = ("None", "(Deleted from the XML)", "None") + + self.enum_cs_cmd: list[tuple[str, str, str]] = [] + self.enum_cs_misc_type: list[tuple[str, str, str]] = [] + self.enum_cs_text_type: list[tuple[str, str, str]] = [] + self.enum_cs_fade_out_seq_player: list[tuple[str, str, str]] = [] + self.enum_cs_transition_type: list[tuple[str, str, str]] = [] + self.enum_cs_destination: list[tuple[str, str, str]] = [] + self.enum_cs_player_cue_id: list[tuple[str, str, str]] = [] + self.enum_cs_modify_seq_type: list[tuple[str, str, str]] = [] + self.enum_cs_credits_scene_type: list[tuple[str, str, str]] = [] + self.enum_cs_motion_blur_type: list[tuple[str, str, str]] = [] + self.enum_cs_rumble_type: list[tuple[str, str, str]] = [] + self.enum_cs_transition_general: list[tuple[str, str, str]] = [] + self.enum_cs_spline_interp_type: list[tuple[str, str, str]] = [] + self.enum_cs_spline_rel: list[tuple[str, str, str]] = [] + self.enum_cs_spawn_flag: list[tuple[str, str, str]] = [] + self.enum_actor_cs_end_sfx: list[tuple[str, str, str]] = [] + self.enum_navi_quest_hint_type: list[tuple[str, str, str]] = [] + self.enum_ocarina_song_action_id: list[tuple[str, str, str]] = [] + self.enum_seq_id: list[tuple[str, str, str]] = [] + self.enum_draw_config: list[tuple[str, str, str]] = [] + self.enum_surface_material: list[tuple[str, str, str]] = [] + self.enum_global_object: list[tuple[str, str, str]] = [] + self.enum_floor_type: list[tuple[str, str, str]] = [] + self.enum_wall_type: list[tuple[str, str, str]] = [] + self.enum_floor_property: list[tuple[str, str, str]] = [] + self.enum_surface_sfx_offset: list[tuple[str, str, str]] = [] + self.enum_surface_material: list[tuple[str, str, str]] = [] + self.enum_floor_effect: list[tuple[str, str, str]] = [] + self.enum_conveyor_speed: list[tuple[str, str, str]] = [] + self.enum_anim_mats_type: list[tuple[str, str, str]] = [] + self.enum_anim_mats_cam_type: list[tuple[str, str, str]] = [] + self.enum_event_condition: list[tuple[str, str, str]] = [] + self.enum_event_flag_type: list[tuple[str, str, str]] = [] + self.enum_event_inv_type: list[tuple[str, str, str]] = [] + self.enum_event_game_type: list[tuple[str, str, str]] = [] + self.enum_event_time_type: list[tuple[str, str, str]] = [] + self.enum_event_action_type: list[tuple[str, str, str]] = [] + self.enum_event_type: list[tuple[str, str, str]] = [] + self.enum_inventory_items: list[tuple[str, str, str]] = [] + self.enum_equipment_items: list[tuple[str, str, str]] = [] + self.enum_quest_items: list[tuple[str, str, str]] = [] + self.enum_upgrade_type: list[tuple[str, str, str]] = [] + + self.enumByID = {enum.id: enum for enum in self.enumDataList} + self.enumByKey = {enum.key: enum for enum in self.enumDataList} + + for key in self.enumByKey.keys(): + setattr(self, f"enum_{key}", self.get_enum_data(key)) + + self.enum_cs_actor_cue_list_cmd_type = [ + item for item in self.enum_cs_cmd if "actor_cue" in item[0] or "player_cue" in item[0] + ] + self.enum_cs_actor_cue_list_cmd_type.sort() + self.enum_cs_actor_cue_list_cmd_type.insert(0, ("Custom", "Custom", "Custom")) + + def get_enum_data(self, enumKey: str): + enum = self.enumByKey[enumKey] + firstIndex = min(1, *(item.index for item in enum.items)) + lastIndex = max(1, *(item.index for item in enum.items)) + 1 + enumData = [self.deletedEntry] * lastIndex + custom = ("Custom", "Custom", "Custom") + + for item in enum.items: + if item.index < lastIndex: + identifier = item.key + enumData[item.index] = (identifier, item.name, item.id) + + if firstIndex > 0: + enumData[0] = custom + else: + enumData.insert(0, custom) + + return enumData diff --git a/fast64_internal/data/z64/object_data.py b/fast64_internal/data/z64/object_data.py new file mode 100644 index 000000000..8c42f90ff --- /dev/null +++ b/fast64_internal/data/z64/object_data.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from os import path +from pathlib import Path +from ...utility import PluginError +from .common import Z64_BaseElement, get_xml_root + +# Note: "object" in this context refers to an OoT Object file (like ``gameplay_keep``) + + +@dataclass +class Z64_ObjectElement(Z64_BaseElement): + pass + + +class Z64_ObjectData: + """Everything related to OoT objects""" + + def __init__(self, game: str): + # general object list + self.objectList: list[Z64_ObjectElement] = [] + + # Path to the ``ObjectList.xml`` file + xml_path = Path(f"{path.dirname(path.abspath(__file__))}/xml/{game.lower()}_object_list.xml") + object_root = get_xml_root(xml_path.resolve()) + + for obj in object_root.iterfind("Object"): + objName = f"{obj.attrib['Name']} - {obj.attrib['ID'].removeprefix('OBJECT_')}" + self.objectList.append( + Z64_ObjectElement(obj.attrib["ID"], obj.attrib["Key"], objName, int(obj.attrib["Index"])) + ) + + self.objects_by_id = {obj.id: obj for obj in self.objectList} + self.objects_by_key = {obj.key: obj for obj in self.objectList} + + # list of tuples used by Blender's enum properties + self.deletedEntry = ("None", "(Deleted from the XML)", "None") + lastIndex = max(1, *(obj.index for obj in self.objectList)) + self.ootEnumObjectKey = self.getObjectIDList(lastIndex + 1, False, game) + + # create the legacy object list for old blends + if game == "OOT": + self.ootEnumObjectIDLegacy = self.getObjectIDList( + self.objects_by_key["obj_timeblock"].index + 1, True, game + ) + + # validate the legacy list, if there's any None element then something's wrong + if self.deletedEntry in self.ootEnumObjectIDLegacy: + raise PluginError("ERROR: Legacy Object List doesn't match!") + else: + self.ootEnumObjectIDLegacy = [] + + def getObjectIDList(self, max: int, isLegacy: bool, game: str): + """Generates and returns the object list in the right order""" + objList = [self.deletedEntry] * max + for obj in self.objectList: + if obj.index < max: + identifier = obj.id if isLegacy else obj.key + objList[obj.index] = (identifier, obj.name, obj.id) + if game == "OOT": + objList[0] = ("Custom", "Custom Object", "Custom") + else: + objList.insert(0, ("Custom", "Custom Object", "Custom")) + return objList diff --git a/fast64_internal/data/z64/xml/mm_actor_list.xml b/fast64_internal/data/z64/xml/mm_actor_list.xml new file mode 100644 index 000000000..3593921e6 --- /dev/null +++ b/fast64_internal/data/z64/xml/mm_actor_list.xml @@ -0,0 +1,979 @@ + + + + + + + + + + + + Large Orange Flame + Large Orange Flame + Large Blue Flame + Large Green Flame + Small Orange Flame + Large Orange Flame + Large Green Flame + Large Blue Flame + Large Magenta Flame + Large Pale Orange Flame + Large Pale Yellow Flame + Large Pale Green Flame + Large Pale Pink Flame + Large Pale Purple Flame + Large Pale Indigo Flame + Large Pale Blue Flame + + + + + + + + Whole Day ('ENDOOR_TYPE_WHOLE_DAY') + Locked ('ENDOOR_TYPE_LOCKED') + Day ('ENDOOR_TYPE_DAY') + Night ('ENDOOR_TYPE_NIGHT') + Ajar ('ENDOOR_TYPE_AJAR') + Schedule ('ENDOOR_TYPE_SCHEDULE') + Unknown ('ENDOOR_TYPE_6') + Framed ('ENDOOR_TYPE_FRAMED') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Golden + Golden - Appears - Clear Flag + Boss Key Chest + Golden - Falls - Switch Flag + Golden - Invisible + Wooden + Wooden - Invisible + Wooden - Clear Flag + Wooden - Falls - Switch Flag + Crash + Crash + Golden - Appears - Switch Flag + + + + + + + + + + + + + + + Regular + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + Invisible + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/fast64_internal/data/z64/xml/mm_enum_data.xml b/fast64_internal/data/z64/xml/mm_enum_data.xml new file mode 100644 index 000000000..604b41431 --- /dev/null +++ b/fast64_internal/data/z64/xml/mm_enum_data.xml @@ -0,0 +1,756 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/fast64_internal/data/z64/xml/mm_object_list.xml b/fast64_internal/data/z64/xml/mm_object_list.xml new file mode 100644 index 000000000..71706d83c --- /dev/null +++ b/fast64_internal/data/z64/xml/mm_object_list.xml @@ -0,0 +1,653 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/fast64_internal/oot/data/xml/ActorList.xml b/fast64_internal/data/z64/xml/oot_actor_list.xml similarity index 63% rename from fast64_internal/oot/data/xml/ActorList.xml rename to fast64_internal/data/z64/xml/oot_actor_list.xml index a53b1ebfc..763545fdc 100644 --- a/fast64_internal/oot/data/xml/ActorList.xml +++ b/fast64_internal/data/z64/xml/oot_actor_list.xml @@ -30,6 +30,7 @@ for each sub element (of ) mentioned below: * Name -> display name (in the UI) * TiedActorTypes -> optional, used to use this property for the current actor type (see en_rd for an example) * Target -> optional, defines which variable should be used to store this parameter (actor.home.rot.X-Y-Z, actor.params), if none then Params is used by default +* ValueRange -> optionnal, used for actors that have parameters that shared the same location but does different things depending on the value (see en_wood02) - -> adds a bool property (checkbox) - -> adds an enum property, different from (use this if you want multiple enums for now) @@ -46,17 +47,17 @@ for each sub element (of ) mentioned below: - Invisible, Can't Control //Used for cutscenes without Link - Master Sword Pull and Drop Animation //Overwrites coordinates - Spawn: Blue Warp //used when returning from a blue warp - Spawn: Immobile - Spawn: Exit via Grotto //Y velocity = 12 - Spawn: Warp Song - Spawn: Farore's Wind - Spawn: Thrown Out - Stand - Slow Walk Forward - Retain Previous Velocity //Walk speed set by last exit + Invisible, Can't Control //Used for cutscenes without Link + Master Sword Pull and Drop Animation //Overwrites coordinates + Spawn: Blue Warp //used when returning from a blue warp + Spawn: Immobile + Spawn: Exit via Grotto //Y velocity = 12 + Spawn: Warp Song + Spawn: Farore's Wind + Spawn: Thrown Out + Stand + Slow Walk Forward + Retain Previous Velocity //Walk speed set by last exit Last 2 digits = Camera Initial Focus //Data defined by scene Link spawns in @@ -64,11 +65,12 @@ for each sub element (of ) mentioned below: - Invisible (Lens of Truth), Mini-Boss Theme - Rises from Ground, Mini-Boss Theme - Rises from Ground //Pair with 0001 - Drops From Sky on Approach - Rises from Ground, Mini-Boss Theme + Invisible (Lens of Truth), Mini-Boss Theme + Rises from Ground, Mini-Boss Theme + Rises from Ground //Pair with 0001 + Drops From Sky on Approach + Rises from Ground, Mini-Boss Theme + Unknown @@ -77,48 +79,50 @@ for each sub element (of ) mentioned below: - Green Flame - Purple Flame - Red Bubbles - Invisible Hovering Object w/Shadow, bursts into purple flames - Green Flame - Green Flame - Purple Flame - Green Flame + Green Flame + Purple Flame + Red Bubbles + Invisible Hovering Object w/Shadow, bursts into purple flames + Green Flame + Green Flame + Purple Flame + Green Flame - - Large Orange Flame - Large Orange Flame - Large Blue Flame - Large Green Flame - Small Orange Flame - Large Orange Flame - Large Green Flame - Large Blue Flame - Large Magenta Flame - Large Pale Orange Flame - Large Pale Yellow Flame - Large Pale Green Flame - Large Pale Pink Flame - Large Pale Purple Flame - Large Pale Indigo Flame - Large Pale Blue Flame - Candle Flame - Faint Blue Aura - + + Large Orange Flame + Large Orange Flame + Large Blue Flame + Large Green Flame + Small Orange Flame + Large Orange Flame + Large Green Flame + Large Blue Flame + Large Magenta Flame + Large Pale Orange Flame + Large Pale Yellow Flame + Large Pale Green Flame + Large Pale Pink Flame + Large Pale Purple Flame + Large Pale Indigo Flame + Large Pale Blue Flame + + + + + - Loads Room - Small Key Locked Door - Loads Room - Scene Transition - Ajar (slams on approach) - Displays Textbox - Time Locked (Unlocked between 18:00 and 21:00) - Loads Room + Loads Room + Small Key Locked Door + Loads Room + Scene Transition + Ajar (slams on approach) + Displays Textbox + Time Locked (Unlocked between 18:00 and 21:00) + Loads Room @@ -127,18 +131,18 @@ for each sub element (of ) mentioned below: - Large - Large, Appears, Clear Flag - Boss Key's Chest - Large, Falling, Switch Flag - Large, Invisible - Small - Small, Invisible - Small, Appears, Clear Flag - Small, Falls, Switch Flag - Large, Appears, Zelda's Lullaby - Large, Appears, Sun's Song Triggered - Large, Appears, Switch Flag + Large + Large, Appears, Clear Flag + Boss Key's Chest + Large, Falling, Switch Flag + Large, Invisible + Small + Small, Invisible + Small, Appears, Clear Flag + Small, Falls, Switch Flag + Large, Appears, Zelda's Lullaby + Large, Appears, Sun's Song Triggered + Large, Appears, Switch Flag @@ -155,11 +159,11 @@ for each sub element (of ) mentioned below: - Graveyard Poe - Graveyard Poe //Leaves a Blue Rupee at its spawn point - (006E) Sharp (Composer Brothers) //Sets Permanent Switch Flag 0B when spoken to, use object 006E - (006E) Flat (Composer Brothers) //Sets Permanent Switch Flag 0A when talked to, use object 006E - Graveyard Poe + Graveyard Poe + Graveyard Poe //Leaves a Blue Rupee at its spawn point + (006E) Sharp (Composer Brothers) //Sets Permanent Switch Flag 0B when spoken to, use object 006E + (006E) Flat (Composer Brothers) //Sets Permanent Switch Flag 0A when talked to, use object 006E + Graveyard Poe May need another object @@ -170,9 +174,9 @@ for each sub element (of ) mentioned below: - Web-Covered Hole - Vertical Web Wall - Web-Covered Hole //When burned, web becomes inverse-conical and extends upwards + Web-Covered Hole + Vertical Web Wall + Web-Covered Hole //When burned, web becomes inverse-conical and extends upwards @@ -181,105 +185,105 @@ for each sub element (of ) mentioned below: - Bomb - Bomb Shadow + Bomb + Bomb Shadow - Timer Spawn (every 6 seconds) - Proximity Spawn - Spawn on Switch Flag + Timer Spawn (every 6 seconds) + Proximity Spawn + Spawn on Switch Flag + 0x3F00 = Switch Flag (Flag to spawn on. Used by type 02 only) - Default - Two-Foot Dodongo //Emits fire and smoke from mouth upon death - Two-Foot Dodongo //Doesn't emit fire and smoke from mouth upon death + Default + Two-Foot Dodongo //Emits fire and smoke from mouth upon death + Two-Foot Dodongo //Doesn't emit fire and smoke from mouth upon death - Fire Keese - Aggressive Keese - Roosting Keese - Ice Keese + Fire Keese + Aggressive Keese + Roosting Keese + Ice Keese +8000 = Invisible - Epona - No Epona - Default + Epona + No Epona + Default - Green Rupee - Blue Rupee - Red Rupee - Recovery Heart - Bomb - Arrow (1) //Used for collecting arrows stuck in walls - Heart Piece - Deku Seeds (5) or Arrows (5) - Deku Seeds (5) or Arrows (10) - Deku Seeds (5) or Arrows (30) - Bombs (5) - Deku Nut - Deku Stick - Large Magic Jar - Small Magic Jar - Deku Seeds (5) or Arrows (5) - Small Key - Flexible Drop //Used for randomized drops, see comments. Will spawn as an invisible Recovery Heart in some situations - Giant Orange Rupee - Large Purple Rupee - Deku Shield //May require Child Link's object - Hylian Shield - Zora Tunic - Goron Tunic - Bombs (5) - Invisible Item + Green Rupee + Blue Rupee + Red Rupee + Recovery Heart + Bomb + Arrow (1) //Used for collecting arrows stuck in walls + Heart Piece + Deku Seeds (5) or Arrows (5) + Deku Seeds (5) or Arrows (10) + Deku Seeds (5) or Arrows (30) + Bombs (5) + Deku Nut + Deku Stick + Large Magic Jar + Small Magic Jar + Deku Seeds (5) or Arrows (5) + Small Key + Flexible Drop //Used for randomized drops, see comments. Will spawn as an invisible Recovery Heart in some situations + Giant Orange Rupee + Large Purple Rupee + Deku Shield //May require Child Link's object + Hylian Shield + Zora Tunic + Goron Tunic + Bombs (5) + Invisible Item +0x3F00 = Collectible Flag - Fire Arrow - Wooden Arrow + Fire Arrow + Wooden Arrow - Navi - Bottled Healing Fairy - Roaming Healing Fairy - Group of Healing Fairies - Fairy Healing You - Roaming Large Healing Fairy + Navi + Bottled Healing Fairy + Roaming Healing Fairy + Group of Healing Fairies + Fairy Healing You + Roaming Large Healing Fairy - Flees when approached //Cucco hidden in the box in Kakariko - Doesn't Flee - Non-Targetable //The non-super cuccos in the Ranch house - Default + Flees when approached //Cucco hidden in the box in Kakariko + Doesn't Flee + Non-Targetable //The non-super cuccos in the Ranch house + Default - Red Tektite - Blue Tektite - Normal Tektite - Dies Instantly + Red Tektite + Blue Tektite + Normal Tektite + Dies Instantly @@ -288,9 +292,9 @@ for each sub element (of ) mentioned below: - Flying Peahat //Spawns Larva - Peahat Larva - Burrowed Peahat + Flying Peahat //Spawns Larva + Peahat Larva + Burrowed Peahat @@ -298,17 +302,17 @@ for each sub element (of ) mentioned below: - Large Bug - Three Small Bugs - Small Bug + Large Bug + Three Small Bugs + Small Bug - Flopping - Swimming, Doesn't Flee //Used for very small pools of water - Swimming, Reacts to Link - Swimming, Flees + Flopping + Swimming, Doesn't Flee //Used for very small pools of water + Swimming, Reacts to Link + Swimming, Flees @@ -326,11 +330,11 @@ for each sub element (of ) mentioned below: - Lizalfos mini-boss, drops from ceiling //Partner to 01, in order to be killed - Lizalfos mini-boss, no mini-boss music //Partner to 00, in order to be killed - Lizalfos, no mini-boss music - Dinolfos, no mini-boss music - Lizalfos, no mini-boss music, drops from ceiling + Lizalfos mini-boss, drops from ceiling //Partner to 01, in order to be killed + Lizalfos mini-boss, no mini-boss music //Partner to 00, in order to be killed + Lizalfos, no mini-boss music + Dinolfos, no mini-boss music + Lizalfos, no mini-boss music, drops from ceiling +0xFF00 = Nullable Switch Flag Lizalfos miniboss spawns when entering room from door bound to this switch flag @@ -360,27 +364,28 @@ To differentiate the two encounters (as both are in room 3), the switch flag is - Impa's Horse - Impa on Horse - Zelda - Ganondorf on Horse (Stationary) - Ganondorf's Horse (Stationary) - Ganondorf on Horse (Riding) + Flames - Ganondorf's Horse (Galloping) - Ganondorf hands crossed //use object 009B - Ganondorf Bowing to King - Ganondorf floating (Curse You) //use object 00E1 + Impa's Horse + Impa on Horse + Zelda + Ganondorf on Horse (Stationary) + Ganondorf's Horse (Stationary) + Ganondorf on Horse (Riding) + Flames + Ganondorf's Horse (Galloping) + Ganondorf hands crossed //use object 009B + Ganondorf Bowing to King + Ganondorf floating (Curse You) //use object 00E1 They need their respective objects to load - + - Falls, lands on ground, hatches in moments - Crashes - Normal egg on ground - Invisible until it hatches (or with Lens of Truth) - Destroyed - Puffs of Blue Smoke (dies away when Link approaches) + Spawned by Gohma Boss, falls on ground, hatches quickly + Spawned by Gohma Boss (unknown 1) + Spawned by Gohma Boss (unknown 2) + Ground Gohma Egg (increase spawn count) + Ground Gohma Egg + Ceiling Gohma Egg (increase spawn count, invisible?) + Ceiling Gohma Egg (invisible?) @@ -443,9 +448,9 @@ Depending on the scene ID, they need certain objects loaded as the first one in - Normal - Big - Invisible + Normal + Big + Invisible + Normal @@ -456,80 +461,80 @@ Depending on the scene ID, they need certain objects loaded as the first one in - Small grey stone block - Large grey stone block - Huge grey stone block - Small grey stone block, rotates when you stand on it - Large grey stone block, rotates when you stand on it - Small grey stone cube - Crashes - Grass clump - Small tree stump - Oblong Signpost (unbreakable) - Arrow Signpost - Black knobby thing + Small grey stone block + Large grey stone block + Huge grey stone block + Small grey stone block, rotates when you stand on it + Large grey stone block, rotates when you stand on it + Small grey stone cube + Crashes + Grass clump + Small tree stump + Oblong Signpost (unbreakable) + Arrow Signpost + Black knobby thing +0xFF00 = Message ID (+0300) - Four spinning a circle, but once you kill one, the rest are gone as well - Three in formation, sink under floor and do not activate - Two in formation, sink under floor and do not activate - One in formation, sink under floor and do not activate - Single + Four spinning a circle, but once you kill one, the rest are gone as well + Three in formation, sink under floor and do not activate + Two in formation, sink under floor and do not activate + One in formation, sink under floor and do not activate + Single - River (Multi-Point) - Stream - Magma - Waterfall - Small Stream - Stream - Fire Temple's Lower Ambient Noise - Fire Temple's Higher Ambient Noise - Water Dripping (Well) - River - Market gibberish - Decrease current BGM volume - Proximity Saria's Song - Howling wind - Gurgling - Temple of Light's dripping sounds - Low booming-likish sound - Quake/Collapse - Fairy Fountain - Torches - Cows - Outside of the ambient noise domain + River (Multi-Point) + Stream + Magma + Waterfall + Small Stream + Stream + Fire Temple's Lower Ambient Noise + Fire Temple's Higher Ambient Noise + Water Dripping (Well) + River + Market gibberish + Decrease current BGM volume + Proximity Saria's Song + Howling wind + Gurgling + Temple of Light's dripping sounds + Low booming-likish sound + Quake/Collapse + Fairy Fountain + Torches + Cows + Outside of the ambient noise domain +0xFF00 = Path ID (Used for sound sources, Type 00, 04 and 05 only) - Tan, Runs Around - Tan, Runs Around - Tan, Runs Away - Tan, Runs Around + Tan, Runs Around + Tan, Runs Around + Tan, Runs Away + Tan, Runs Around - Kokiri Shopkeeper, Objects 0x0FC, 0x101, 0x102 - Kakariko Potion Shopkeeper, Object 0x159 - Bombchu Shopkeeper, spawn when King Dodongo is beaten, Object 0x165 - Market Potion Shopkeeper, Object 0x159 - Bazaar Shopkeeper, Object 0x05B - Unused? Shopkeeper, Object 0x05B - Unused? Shopkeeper, Object 0x05B - Zora Shopkeeper, Object 0x0FE - Goron Shopkeeper, Object 0x05B - Unused? Shopkeeper, Object 0x05B - Happy Mask Shopkeeper, Object 0x13E + Kokiri Shopkeeper, Objects 0x0FC, 0x101, 0x102 + Kakariko Potion Shopkeeper, Object 0x159 + Bombchu Shopkeeper, spawn when King Dodongo is beaten, Object 0x165 + Market Potion Shopkeeper, Object 0x159 + Bazaar Shopkeeper, Object 0x05B + Unused? Shopkeeper, Object 0x05B + Unused? Shopkeeper, Object 0x05B + Zora Shopkeeper, Object 0x0FE + Goron Shopkeeper, Object 0x05B + Unused? Shopkeeper, Object 0x05B + Happy Mask Shopkeeper, Object 0x13E @@ -545,8 +550,8 @@ Out-of-bounds Plane (typ.) - Lower Part (Flame decal) - Top Part (Face decal) + Lower Part (Flame decal) + Top Part (Face decal) +0xFF00 = Switch Flag @@ -565,52 +570,53 @@ Out-of-bounds Plane (typ.) - - Large Face Cube //Pushable - Squat Cube //Shakes and flies into the air when you land on it + + Large Face Cube //Pushable + Squat Cube //Shakes and flies into the air when you land on it + - 2-way diagonal - 4-way plus + 2-way diagonal + 4-way plus - Sinking - Sliding + Sinking + Sliding - Idle - Unknown - Unknown - Unknown - Transformation into Zelda - Nocturne CS - Minuet CS - Bolero CS - Serenade CS - First Meeting at ToT + Idle + Unknown + Unknown + Unknown + Transformation into Zelda + Nocturne CS + Minuet CS + Bolero CS + Serenade CS + First Meeting at ToT +0x0FC0 = Chest Flag - Large, turns off with room clear. - Large, plays cutscene, switches off for long time with ticking sound. Turns off permanently with chest flag. - Small, switches off for long time silently. - Large, plays cutscene, switches off for short time with ticking sound. - Disabled, small, switches on for short time. - Disabled, large, can be switched on and off. - Large, switches off permanently, won't spawn if room is cleared. - Nothing + Large, turns off with room clear. + Large, plays cutscene, switches off for long time with ticking sound. Turns off permanently with chest flag. + Small, switches off for long time silently. + Large, plays cutscene, switches off for short time with ticking sound. + Disabled, small, switches on for short time. + Disabled, large, can be switched on and off. + Large, switches off permanently, won't spawn if room is cleared. + Nothing @@ -618,30 +624,30 @@ Out-of-bounds Plane (typ.) - Chains - Drawbridge + Chains + Drawbridge - Club Moblin //Pounds the ground - Spear Moblin + Club Moblin //Pounds the ground + Spear Moblin +0xFF00 = path Id (Spear Moblins only) - Bomb - Invisible Bomb - Default + Bomb + Invisible Bomb + Default - Light Arrows CS - Pre-Credits - Post Castle Collapse + Light Arrows CS + Pre-Credits + Post Castle Collapse @@ -650,30 +656,31 @@ Out-of-bounds Plane (typ.) - Floating Platform - Water Plane - 3 Raising platforms + Floating Platform + Water Plane + 3 Raising platforms - - Rotating Spike Cylinder - Deku tree ladder falling when hit by slingshot (switchflag?) + + Rotating Spike Cylinder + Deku tree ladder falling when hit by slingshot (switchflag?) + - Statue - Enemy + Statue + Enemy - Small - Big + Small + Big @@ -686,10 +693,10 @@ Out-of-bounds Plane (typ.) - Bombable Wall, triggers intro cutscene - Bombable Wall - Bombable Floor - Lava Cover + Bombable Wall, triggers intro cutscene + Bombable Wall + Bombable Floor + Lava Cover @@ -697,7 +704,7 @@ Out-of-bounds Plane (typ.) - Lord Jabu-Jabu + Lord Jabu-Jabu The actor spawns the collision data @@ -708,27 +715,27 @@ Out-of-bounds Plane (typ.) - Normal (adult Link) - Spawning in from crystal //Combine with Link's gentle fall spawn in - Normal - Nothing - Blue warp, disappears - Giant purple crystal/magic enclosure - Yellow warp, disappears - Blue warp, doesn't warp you - Spawn in from child blue warp //Combine with Link's gentle fall - Blue warp, warping animation - Tan warp, disappears - Green warp, disappears - Red warp, disappears - Area fails to load + Normal (adult Link) + Spawning in from crystal //Combine with Link's gentle fall spawn in + Normal + Nothing + Blue warp, disappears + Giant purple crystal/magic enclosure + Yellow warp, disappears + Blue warp, doesn't warp you + Spawn in from child blue warp //Combine with Link's gentle fall + Blue warp, warping animation + Tan warp, disappears + Green warp, disappears + Red warp, disappears + Area fails to load - Golden Torch - Puzzle Torch - Wooden Torch + Golden Torch + Puzzle Torch + Wooden Torch @@ -753,14 +760,14 @@ Out-of-bounds Plane (typ.) - Floating Square platform, sloped bottom //Used in first room, on center column - Floating Square platform - Floating Square platform, tapered on bottom //Center Room floating block - Dragon Head Statue //Main Room - Dragon Head Statue //Skulltula Room - Dragon Head Statue //before Dark Link - Dragon Head Statue - Moving Square Platform with Hookshot Target + Floating Square platform, sloped bottom //Used in first room, on center column + Floating Square platform + Floating Square platform, tapered on bottom //Center Room floating block + Dragon Head Statue //Main Room + Dragon Head Statue //Skulltula Room + Dragon Head Statue //before Dark Link + Dragon Head Statue + Moving Square Platform with Hookshot Target @@ -774,12 +781,12 @@ Out-of-bounds Plane (typ.) - Switches to lowest (Triforce seems to activate itself for some reason in the ToT) - Stays at ground level (Triforce isn't affected in ToT) - Water Temple Map 3, Standard Quest - Water Temple Map 6, Standard Quest - Water Temple Map 14, Standard Quest - Water Temple (most rooms), Standard Quest + Switches to lowest (Triforce seems to activate itself for some reason in the ToT) + Stays at ground level (Triforce isn't affected in ToT) + Water Temple Map 3, Standard Quest + Water Temple Map 6, Standard Quest + Water Temple Map 14, Standard Quest + Water Temple (most rooms), Standard Quest @@ -794,19 +801,19 @@ Out-of-bounds Plane (typ.) - Large Green Bubble //requires movement path - Green Bubble //requires movement path - White Bubble //requires movement path - Fire Bubble //Proximity activated, bounces on solid surfaces, hides in lava - Blue Bubble + Large Green Bubble //requires movement path + Green Bubble //requires movement path + White Bubble //requires movement path + Fire Bubble //Proximity activated, bounces on solid surfaces, hides in lava + Blue Bubble +0xFF00 = Path Id - Normal - Invisible + Normal + Invisible @@ -828,23 +835,23 @@ Use with actors 008C and 008B for the cutscene to work - (0068) Ocarina //Unused - (0062) Light Medallion - (0063) Shadow Medallion - (0064) Fire Medallion - (0065) Water Medallion - (0066) Spirit Medallion - (0067) Forest Medallion + (0068) Ocarina //Unused + (0062) Light Medallion + (0063) Shadow Medallion + (0064) Fire Medallion + (0065) Water Medallion + (0066) Spirit Medallion + (0067) Forest Medallion Add the object in brackets for it to work - Yellow with corner removed - Green and rectangular - Yellow/green and rectangular, looks rusty - Crashes, but not without drawing a huge black thing that I can only assume is a gate... - Crashes before it can even light the area... + Yellow with corner removed + Green and rectangular + Yellow/green and rectangular, looks rusty + Crashes, but not without drawing a huge black thing that I can only assume is a gate... + Crashes before it can even light the area... + Crashes, no sign of a gate @@ -855,8 +862,8 @@ Use with actors 008C and 008B for the cutscene to work - Hammer-triggered Stone Steps - Platform, one sided + Hammer-triggered Stone Steps + Platform, one sided +0x3F00 = Switch Flag @@ -866,34 +873,36 @@ Use with actors 008C and 008B for the cutscene to work - Large tree - Medium tree - Small tree - Group of trees - Medium tree - Medium tree, dark brown trunk, greener leaves - Group of trees, dark brown trunk, yellow leaves - Medium tree, dark brown trunk, yellow leaves - Group of trees, dark brown trunk, greener leaves - Medium tree, dark brown trunk, greener leaves - Ugly tree from Kakariko Village - Bush - Large bush - Group of bushes - Bush - Group of large bushes - Large bush - Dark bush - Large dark bush - Group of dark bushes - Dark bush - Group of large dark bushes - Large dark bush - Dancing dark bush //Disappears after several repetitions - - + Large Conical Tree + Medium Conical Tree + Small Conical Tree + Conical Tree Spawner + Conical Tree Spawned + Oval Green Tree + Oval Yellow Tree Spawner + Oval Yellow Tree Spawned + Oval Green Tree Spawner + Oval Green Tree Spawned + Adult Kakariko Tree + Small Bush + Large Bush + Small Bush Spawner + Small Bush Spawned + Large Bush Spawner + Large Bush Spawned + Small Black Bush + Large Black Bush + Small Black Bush Spawner + Small Black Bush Spawned + Large Black Bush Spawner + Large Black Bush Spawned + Green leaf + Yellow leaf + + - + + +0xFF00 = Controls item(s) dropped (trees only) -0000 Random -0800 Deku seeds @@ -910,26 +919,26 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Normal, flashes blue when struck with sword - Frickin' huge, doesn't flash when struck - Even bigger? - Ridiculously huge? - Small size, flashes blue when struck + Normal, flashes blue when struck with sword + Frickin' huge, doesn't flash when struck + Even bigger? + Ridiculously huge? + Small size, flashes blue when struck Unused - Stone cube, side struck with sword flashes blue - Slightly larger + Stone cube, side struck with sword flashes blue + Slightly larger - 4-Way Attack - Line Loop - Circle Loop + 4-Way Attack + Line Loop + Circle Loop @@ -960,40 +969,40 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Attacking Beamos (2 health) - Attacking Beamos (1 health) + Attacking Beamos (2 health) + Attacking Beamos (1 health) XX00: 05 in dodongo's cavern, 08 in ganon's castle entrance - Crystal Light, used by Triforce in Hyrule Creation CS - Flame, used by Din in Hyrule Creation CS - Blue Orb, used by Din in Hyrule Creation CS - Large Green Cone, used by Farore in Hyrule Creation CS - Din - Nayru - Farore - Blue Light Ring, used by Din and Farore in Hyrule Creation CS - Triforce - Fire Medallion - Water Medallion - Forest Medallion - Spirit Medallion - Shadow Medallion - Light Medallion - Time Warp Effect from Temple of Time - Blue Light Ring that shrinks, used by Din in Hyrule Creation CS - Vertical Light Effect used by Triforce - Light Ball from the 6 Sages when sealing Ganondorf - Kokiri Emerald - Goron Ruby - Zora Sapphire - Dust, used in Light Arrow CS - Light Ball, used by Zelda - Large Block of Time Time Warp Effect - Small Block of Time Time Warp Effect + Crystal Light, used by Triforce in Hyrule Creation CS + Flame, used by Din in Hyrule Creation CS + Blue Orb, used by Din in Hyrule Creation CS + Large Green Cone, used by Farore in Hyrule Creation CS + Din + Nayru + Farore + Blue Light Ring, used by Din and Farore in Hyrule Creation CS + Triforce + Fire Medallion + Water Medallion + Forest Medallion + Spirit Medallion + Shadow Medallion + Light Medallion + Time Warp Effect from Temple of Time + Blue Light Ring that shrinks, used by Din in Hyrule Creation CS + Vertical Light Effect used by Triforce + Light Ball from the 6 Sages when sealing Ganondorf + Kokiri Emerald + Goron Ruby + Zora Sapphire + Dust, used in Light Arrow CS + Light Ball, used by Zelda + Large Block of Time Time Warp Effect + Small Block of Time Time Warp Effect @@ -1008,30 +1017,30 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Blue Rain Effect, used by Goddesses/Triforce CS - Blue Rain Effect, used in the Master Sword CS - Giant Rock 1 - Giant Rock 2 - Giant Rock 3 - Giant Rock 4 - Giant Rock 5 - Fluffy Clouds - Bolero 3D Note (not implemented) - Serenade 3D Note (not implemented) - Requiem 3D Note (not implemented) - ? 3D Note (not implemented) - ? 3D Note (not implemented) - Door of Time - Yellow Light, used in Master Sword's chamber - Warp Song leaving effect - Warp Song arriving effect - Orange Sparkly Effect, used by appearing chests + Blue Rain Effect, used by Goddesses/Triforce CS + Blue Rain Effect, used in the Master Sword CS + Giant Rock 1 + Giant Rock 2 + Giant Rock 3 + Giant Rock 4 + Giant Rock 5 + Fluffy Clouds + Bolero 3D Note (not implemented) + Serenade 3D Note (not implemented) + Requiem 3D Note (not implemented) + ? 3D Note (not implemented) + ? 3D Note (not implemented) + Door of Time + Yellow Light, used in Master Sword's chamber + Warp Song leaving effect + Warp Song arriving effect + Orange Sparkly Effect, used by appearing chests - - Stationary with switch and hardcoded camera cutscene - Follows you + + Stationary with switch and hardcoded camera cutscene + Follows Player @@ -1040,34 +1049,34 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Slow Patrolling Guard - Fast Patrolling Guard - Slow Patrolling Guard, can skip some waypoints - Fast Patrolling Guard, can skip some waypoints - Nighttime Standing Guard + Slow Patrolling Guard + Fast Patrolling Guard + Slow Patrolling Guard, can skip some waypoints + Fast Patrolling Guard, can skip some waypoints + Nighttime Standing Guard Rupees circle spawned if Path ID == 3 - Rising Gibdo - Gibdo - Redead, doesn't mourn - Redead, doesn't mourn if walking - Redead - Crying Redead - Invisible Redead + Rising Gibdo + Gibdo + Redead, doesn't mourn + Redead, doesn't mourn if walking + Redead + Crying Redead + Invisible Redead - Meg, Purple Poe - Joelle, Red Poe - Beth, Blue Poe - Amy, Green Poe + Meg, Purple Poe + Joelle, Red Poe + Beth, Blue Poe + Amy, Green Poe @@ -1075,8 +1084,8 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Sinks into the ground - Breaks on impact + Sinks into the ground + Breaks on impact @@ -1085,20 +1094,20 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - ??? Amy - ??? Amy - Joelle Painting Puzzle - Beth Painting Puzzle - ??? Amy + ??? Amy + ??? Amy + Joelle Painting Puzzle + Beth Painting Puzzle + ??? Amy +0x3F = Switch Flag - Fish - Bug - Butterfly + Fish + Bug + Butterfly @@ -1112,12 +1121,12 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - - Skullwalltula - Gold Skulltula + + Skullwalltula + Gold Skulltula - + @@ -1147,11 +1156,11 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Flying Fairies, Dustmotes - Lightning - Snow - Electric Spark Effect, object 0x0A1 - Ganon's Castle Barrier Colored Light Beams, object 0x179 + Flying Fairies, Dustmotes + Lightning + Snow + Electric Spark Effect, object 0x0A1 + Ganon's Castle Barrier Colored Light Beams, object 0x179 @@ -1162,14 +1171,13 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Visible - Appears on Song of Storms - Appears on explosion or Megaton Hammer + Visible + Appears on Song of Storms + Appears on explosion or Megaton Hammer - - + @@ -1185,47 +1193,57 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) + + + + + + + + + + - Shadow Temple's Eye of Truth Door - Small Square Patch of Ground, child only, blocks entrance to Dampe's Grave - Royal Tomb Grave, despawn if the Royal Tomb CS is watched and the scene is the graveyard - Thunderbolt used with the electric spark effect - Light Aura, appears when Royal Tomb Grave explodes + Shadow Temple's Eye of Truth Door + Small Square Patch of Ground, child only, blocks entrance to Dampe's Grave + Royal Tomb Grave, despawn if the Royal Tomb CS is watched and the scene is the graveyard + Thunderbolt used with the electric spark effect + Light Aura, appears when Royal Tomb Grave explodes +0xFF00 = Switch Flag - Default - Plays discovery sfx when pulled back - Typical + Default + Plays discovery sfx when pulled back + Typical - Normal (no sphere) - Dissipating (no sphere) + Normal (no sphere) + Dissipating (no sphere) - Blue Warp and Ruto - Leaning Ruto //no collision data, targetable - Ruto, First Encounter //Plays cutscene from Jabu-Jabu's Belly when you first meet her - Ruto, after falling down the hole in Jabu-Jabu + Blue Warp and Ruto + Leaning Ruto //no collision data, targetable + Ruto, First Encounter //Plays cutscene from Jabu-Jabu's Belly when you first meet her + Ruto, after falling down the hole in Jabu-Jabu - Flies, lands, dies in a bit //try not to run into it, Link'll catch on fire - Flies, lands, creeps towards Link, dies in a bit //it's blue, but it's still fire + Flies, lands, dies in a bit //try not to run into it, Link'll catch on fire + Flies, lands, creeps towards Link, dies in a bit //it's blue, but it's still fire @@ -1235,16 +1253,16 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Light Medallion and post-Spirit/Shadow Medallions - Ganon's Castle and post-Ganon + Light Medallion and post-Spirit/Shadow Medallions + Ganon's Castle and post-Ganon - Leever //Use object 0017, spawn on sand (max defined by code) - Red Tektite //Use object 0016 (max defined by code) - Stalchildren //Use object 0184, spawn on dirt - Wolfos //Use object 0183 + Leever //Use object 0017, spawn on sand (max defined by code) + Red Tektite //Use object 0016 (max defined by code) + Stalchildren //Use object 0184, spawn on dirt + Wolfos //Use object 0183 @@ -1258,28 +1276,28 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Fire Medallion (Default) - Goron Ruby - Chamber After Ganon - Credits + Fire Medallion (Default) + Goron Ruby + Chamber After Ganon + Credits - Shadow Medallion - Chamber After Ganon/Ganon's Castle - Escort - Hyrule Field after ZL - First Time Escort - Credits + Shadow Medallion + Chamber After Ganon/Ganon's Castle + Escort + Hyrule Field after ZL + First Time Escort + Credits - No damage - One-Hit Kill + No damage + One-Hit Kill @@ -1290,26 +1308,26 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Rock Wall with Skull //style that sometimes has glowy eyes - Black square with large skull face - Shadow Temple Boss Room platforms - Wall of Skulls - Shadow Temple Floor //bluish? texture - Massive Platform - Wall with bluish?, fat bricks texture (one sided) - Shadow Temple Diamond Room (before big key) Fake Walls - Wall with purplish?, fat brick texture (both side) - Room 11's invisible spikes, invisible hookshot point. + Rock Wall with Skull //style that sometimes has glowy eyes + Black square with large skull face + Shadow Temple Boss Room platforms + Wall of Skulls + Shadow Temple Floor //bluish? texture + Massive Platform + Wall with bluish?, fat bricks texture (one sided) + Shadow Temple Diamond Room (before big key) Fake Walls + Wall with purplish?, fat brick texture (both side) + Room 11's invisible spikes, invisible hookshot point. Need Object 0x69 - Moving stone platforms - Rising and falling stone platforms - Spinning black platform - Metal grate - Same as 0001, but graphics are glitched + Moving stone platforms + Rising and falling stone platforms + Spinning black platform + Metal grate + Same as 0001, but graphics are glitched +0xFF00 = Switch Flag //Type 03 Only @@ -1321,27 +1339,27 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Shadow Temple Scythes (Visible) //Use object 0069 - Shadow Temple Scythes (Invisible) //Use object 0069 - Ice Cavern Spinning Blade //Use object 0069 + Shadow Temple Scythes (Visible) //Use object 0069 + Shadow Temple Scythes (Invisible) //Use object 0069 + Ice Cavern Spinning Blade //Use object 0069 May require additional object - Hyrule Castle guard - Death Mountain gate guard - Ceremonial guard + Hyrule Castle guard + Death Mountain gate guard + Ceremonial guard - Large - Medium - Small - Large + Large + Medium + Small + Large @@ -1350,31 +1368,31 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Spirit Temple's Sun-Block Room - Spirit Temple's Single Cobra-Mirror Room - Spirit Temple's Four Armos Room - Spirit Temple's Topmost Room + Spirit Temple's Sun-Block Room + Spirit Temple's Single Cobra-Mirror Room + Spirit Temple's Four Armos Room + Spirit Temple's Topmost Room - Bridge Sides - Broken Bridge - Bridge as Child - Tent - Repaired Bridge + Bridge Sides + Broken Bridge + Bridge as Child + Tent + Repaired Bridge - Statue, no spear, movable with Adult Link - Spear only, climbable + Statue, no spear, movable with Adult Link + Spear only, climbable - Barinade + Barinade @@ -1383,28 +1401,28 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Guillotine Blade (Slow) - Spiked Box on Chain - Spiked Wooden Wall, moving //FIXME: moves in wrong direction? - Opposite Spiked Wooden Wall, moving //FIXME: moves in wrong direction? - Propeller, blows wind - Guillotine Blade (Fast) + Guillotine Blade (Slow) + Spiked Box on Chain + Spiked Wooden Wall, moving //FIXME: moves in wrong direction? + Opposite Spiked Wooden Wall, moving //FIXME: moves in wrong direction? + Propeller, blows wind + Guillotine Blade (Fast) + Graphical glitches, same as 2 or 3 (unsure) - Spawns gibdo //(needs object 0098) - Spawns 2 keese //(needs object 000D) + Spawns gibdo //(needs object 0098) + Spawns 2 keese //(needs object 000D) +0x3F = Switch Flag - Giant Bird Statue, bombing it will make it fall - Bombable Wall of Skulls - Bombable Rubble (Object 0x8D) + Giant Bird Statue, bombing it will make it fall + Bombable Wall of Skulls + Bombable Rubble (Object 0x8D) +0x3F00 = Switch Flag @@ -1417,19 +1435,19 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Wooden (Default) - Stone (Zora) - Granite (Goron) + Wooden (Default) + Stone (Zora) + Granite (Goron) - Spirit Medallion - Chamber After Ganon/Ganon's Castle - Kidnapping CS - Iron Knuckle - Credits - Crawlspace + Spirit Medallion + Chamber After Ganon/Ganon's Castle + Kidnapping CS + Iron Knuckle + Credits + Crawlspace @@ -1438,72 +1456,72 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Sits there, mini-boss music starts, slice it to make it move (no teleport), green target - Already running around, yellow target this time, no music + Sits there, mini-boss music starts, slice it to make it move (no teleport), green target + Already running around, yellow target this time, no music Slicing it in the back will make it jump to the y location of its room, which is typically below most floors - Normal Deku Baba - Straight Deku Baba + Normal Deku Baba + Straight Deku Baba - Giant Octo's Platform - Elevator Platform - Water Square //Rises when Switch Flag is set - Lowering Platform //Lowers into place when stepped on, sets Switch Flag + Giant Octo's Platform + Elevator Platform + Water Square //Rises when Switch Flag is set + Lowering Platform //Lowers into place when stepped on, sets Switch Flag +0xFF00 = Nullable Switch Flag - Chamber of Sages, gives Forest Medallion - Chamber After Ganon/Ganon's Castle //Use with actor 01A7 - Credits, Death Mountain Trail - Fairy Ocarina Cutscene + Chamber of Sages, gives Forest Medallion + Chamber After Ganon/Ganon's Castle //Use with actor 01A7 + Credits, Death Mountain Trail + Fairy Ocarina Cutscene - Unknown - Unknown - Unknown + Unknown + Unknown + Unknown - Unknown - Unknown - Unknown - Default + Unknown + Unknown + Unknown + Default - Koume - Kotake + Koume + Kotake - Default - Debris - Debris - Debris - Debris - Debris + Default + Debris + Debris + Debris + Debris + Debris - Cracked stone floor - Bombable stone wall - Large bombable stone wall + Cracked stone floor + Bombable stone wall + Large bombable stone wall +0x3F00 = Switch Flag @@ -1532,10 +1550,10 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Water Medallion - Chamber After Ganon/Ganon's Castle - Credits - Water Temple Meeting + Water Medallion + Chamber After Ganon/Ganon's Castle + Credits + Water Temple Meeting @@ -1545,10 +1563,10 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Temple Gate - Gate lock - Water plane - Ice Block blocking Zora's Domain Entrance (Adult only) + Temple Gate + Gate lock + Water plane + Ice Block blocking Zora's Domain Entrance (Adult only) +0xFF = Nullable Switch Flag @@ -1565,10 +1583,10 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Ingo Race - Gerudo Archery - Unused? - Malon Race + Ingo Race + Gerudo Archery + Unused? + Malon Race @@ -1577,20 +1595,20 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Reddish brown - Green - Grayish blue with some red - Already dead + Reddish brown + Green + Grayish blue with some red + Already dead - Reddish brown - Green - Grayish blue with some red - Corrupt textures, still visible and works - Dark brownish - Blackish gray + Reddish brown + Green + Grayish blue with some red + Corrupt textures, still visible and works + Dark brownish + Blackish gray @@ -1599,13 +1617,13 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Default + Default - Rotating platforms - Metal Gate + Rotating platforms + Metal Gate @@ -1614,19 +1632,19 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Gray - Expanding, gray //Unused? - Expanding, gray //Unused? - Default + Gray + Expanding, gray //Unused? + Expanding, gray //Unused? + Default - Floor Blue Switch, release when not stood on - Floor Heavy Switch (Need Ruto to press it) - Standard Floor Yellow Switch - Standard Tall Yellow Switch - On/Off Toggle Tall Yellow Switch + Floor Blue Switch, release when not stood on + Floor Heavy Switch (Need Ruto to press it) + Standard Floor Yellow Switch + Standard Tall Yellow Switch + On/Off Toggle Tall Yellow Switch +0x3F00 = Switch Flag @@ -1635,92 +1653,92 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Default - Begins battle - Ganondorf death sequence + Default + Begins battle + Ganondorf death sequence - Default + Default - Normal - Does not open //possibly waiting for some trap to spring? + Normal + Does not open //possibly waiting for some trap to spring? - Frog Game Ocarina Spot - Yellow Frog - Blue Frog - Red Frog - Purple Frog - White Frog + Frog Game Ocarina Spot + Yellow Frog + Blue Frog + Red Frog + Purple Frog + White Frog - Collectible Deku Shield - Burning Deku shield + Collectible Deku Shield + Burning Deku shield - Large crystal - Smaller crystal - Crystal platform - Meltable ice sheet - Giant crystal + Large crystal + Smaller crystal + Crystal platform + Meltable ice sheet + Giant crystal 0xFF = Nullable Switch Flag - Group of small blue flames, disappear - Blue flame, disappears - Blue flame, targetable + Group of small blue flames, disappear + Blue flame, disappears + Blue flame, targetable - Ocarina of Time being tossed higher in air - Ocarina of Time being tossed in air - Ocarina of Time - Collectible Ocarina of Time + Ocarina of Time being tossed higher in air + Ocarina of Time being tossed in air + Ocarina of Time + Collectible Ocarina of Time - Normal - No bright sphere, first one dies off quickly + Normal + No bright sphere, first one dies off quickly - Rain of multi-colored light balls, used in Rainbow Bridge CS - Lots of small particles, used with the White 'Black Hole' - White 'Black Hole' Effect, used in the Ganondorf's Sealing CS - Red Light Ball, used in Ganon's Castle - Green Light Ball, used in Ganon's Castle - Yellow Light Ball, used in Ganon's Castle - Purple Light Ball, used in Ganon's Castle - Orange Light Ball, used in Ganon's Castle - Blue Light Ball, used in Ganon's Castle - Koume/Kotake Red Light Ball Attack - Koume/Kotake Blue Light Ball Attack - 'Nabooru Disappearing' Orange Particles - Ganondorf's Light Ball Attack Purple Loading Effect - Ganondorf's Light Ball Attack, used in Zelda Fleeing CS - Red Light Ball with particles, used in the LLR/DMT part of the credits - Green Light Ball with particles, used in the LLR/DMT part of the credits - Yellow Light Ball with particles, used in ToT Cutscenes - Purple Light Ball with particles, used in the LLR/DMT part of the credits - Orange Light Ball with particles, used in the LLR/DMT part of the credits - Blue Light Ball with particles, used in the LLR/DMT part of the credits + Rain of multi-colored light balls, used in Rainbow Bridge CS + Lots of small particles, used with the White 'Black Hole' + White 'Black Hole' Effect, used in the Ganondorf's Sealing CS + Red Light Ball, used in Ganon's Castle + Green Light Ball, used in Ganon's Castle + Yellow Light Ball, used in Ganon's Castle + Purple Light Ball, used in Ganon's Castle + Orange Light Ball, used in Ganon's Castle + Blue Light Ball, used in Ganon's Castle + Koume/Kotake Red Light Ball Attack + Koume/Kotake Blue Light Ball Attack + 'Nabooru Disappearing' Orange Particles + Ganondorf's Light Ball Attack Purple Loading Effect + Ganondorf's Light Ball Attack, used in Zelda Fleeing CS + Red Light Ball with particles, used in the LLR/DMT part of the credits + Green Light Ball with particles, used in the LLR/DMT part of the credits + Yellow Light Ball with particles, used in ToT Cutscenes + Purple Light Ball with particles, used in the LLR/DMT part of the credits + Orange Light Ball with particles, used in the LLR/DMT part of the credits + Blue Light Ball with particles, used in the LLR/DMT part of the credits @@ -1728,18 +1746,18 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Rotatable Bird Statue - Circular Trap Door Platform //Opens when wrong skull is chosen - Gate //Only blocks one-way - Skull Top - Glitchy graphics, can't see object, behaves like the gate + Rotatable Bird Statue + Circular Trap Door Platform //Opens when wrong skull is chosen + Gate //Only blocks one-way + Skull Top + Glitchy graphics, can't see object, behaves like the gate +0x3F00 = Switch Flag - Default + Default Not market town gate @@ -1753,9 +1771,9 @@ ZROT 0x00FF = Gold Skulltula spawn var high byte (See Gold Skulltula Actor 0095) - Large shadow //looks like it's reproduced, well-done though - Small Shadow //CRASH? - No shadow //CRASH? + Large shadow //looks like it's reproduced, well-done though + Small Shadow //CRASH? + No shadow //CRASH? @@ -1768,19 +1786,19 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Wearing red vest over blue shirt, no hat - Normal fish - Hylian Loach - Huge Hylian Loach - Pond stuff, still no hat + Wearing red vest over blue shirt, no hat + Normal fish + Hylian Loach + Huge Hylian Loach + Pond stuff, still no hat - Small Push Block - Medium Push Block - Large Push Block - Huge Push Block + Small Push Block + Medium Push Block + Large Push Block + Huge Push Block @@ -1798,13 +1816,13 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Default + Default - Rushing white particles //Jabu-Jabu, when he opens his mouth - Rushing white particles to one point (???) + Rushing white particles //Jabu-Jabu, when he opens his mouth + Rushing white particles to one point (???) @@ -1813,15 +1831,15 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Default + Default - Purple (Meg) - Red (Joelle) - Blue (Beth) - Green (Amy) + Purple (Meg) + Red (Joelle) + Blue (Beth) + Green (Amy) +0x3F = Switch Flag @@ -1833,9 +1851,9 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Fence - Nothing //Possibly waypoint for the Ingo Race? - Fence + Fence + Nothing //Possibly waypoint for the Ingo Race? + Fence @@ -1846,14 +1864,14 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Bottle //Use group 00C6 - Bottle with Ruto's Letter //Use group 010B - Hylian shield //Use group 00DC - Quiver //Use group 00BE - Silver Scale //Use group 00DB - Golden Scale //Use group 00DB - Small Key //Use group 00AA - Fire Arrow //Use group 0158 + Bottle //Use group 00C6 + Bottle with Ruto's Letter //Use group 010B + Hylian shield //Use group 00DC + Quiver //Use group 00BE + Silver Scale //Use group 00DB + Golden Scale //Use group 00DB + Small Key //Use group 00AA + Fire Arrow //Use group 0158 @@ -1868,15 +1886,15 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Collectible gave if each tag points are reached (no order) - Tag Point (no order) - Gives collectible if in range - Invisible Collectible - Collectible gave if each tag points are reached in order - Tag Point (sets order) - Sets Switch Flag if in range (no collectible) - Bomberman Soldier - Collectible gave if player rolls + Collectible gave if each tag points are reached (no order) + Tag Point (no order) + Gives collectible if in range + Invisible Collectible + Collectible gave if each tag points are reached in order + Tag Point (sets order) + Sets Switch Flag if in range (no collectible) + Bomberman Soldier + Collectible gave if player rolls @@ -1909,42 +1927,42 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Nabooru Knuckle //May Crash - White, sitting //Stone Chair - Black, standing - White, standing - White, no armor falls off when at low health + Nabooru Knuckle //May Crash + White, sitting //Stone Chair + Black, standing + White, standing + White, no armor falls off when at low health +0xFF00 = Nullable Switch Flag - Unknown - Unknown - Unknown - Unknown - Unknown - Unknown + Unknown + Unknown + Unknown + Unknown + Unknown + Unknown - Saria's Song HP Skull Kid - Ocarina Game Left Skull Kid - Ocarina Game Right Skull Kid - Saria's Song HP Ocarina Spot - Memory Game Ocarina Spot - Enemy Skull Kid + Saria's Song HP Skull Kid + Ocarina Game Left Skull Kid + Ocarina Game Right Skull Kid + Saria's Song HP Ocarina Spot + Memory Game Ocarina Spot + Enemy Skull Kid - Silver Rupee Tracker (handles puzzle resolution) - Silver Rupee (the puzzle itself) - Horseback Archery Pot + Silver Rupee Tracker (handles puzzle resolution) + Silver Rupee (the puzzle itself) + Horseback Archery Pot @@ -1957,47 +1975,47 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Deku Nuts - Deku Sticks - Piece of Heart (10 rupees) - Deku Seeds - Deku Shield - Bombs - Deku Seeds - Red Potion - Green Potion - Deku Stick Upgrade - Deku Nut Upgrade - Never speaks, and Link is sadly very patient... - Pocket Egg? + Deku Nuts + Deku Sticks + Piece of Heart (10 rupees) + Deku Seeds + Deku Shield + Bombs + Deku Seeds + Red Potion + Green Potion + Deku Stick Upgrade + Deku Nut Upgrade + Never speaks, and Link is sadly very patient... + Pocket Egg? - Talk when in range - C-Up Prompt + Talk when in range + C-Up Prompt - Stone Eye - Heat-Seeking flame - Immobile flame, dies - Flame sound effect + Stone Eye + Heat-Seeking flame + Immobile flame, dies + Flame sound effect - Heart or Green Rupee - Bombs - Deku Seeds - Deku Nuts - Deku Seeds - Giant Purple Rupee - Goron Tunic - Nothing + Heart or Green Rupee + Bombs + Deku Seeds + Deku Nuts + Deku Seeds + Giant Purple Rupee + Goron Tunic + Nothing +0x3F = Collectible Flag @@ -2010,14 +2028,14 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Waterfall and Pool //Room 1 - King's Chamber Water //Room 0 + Waterfall and Pool //Room 1 + King's Chamber Water //Room 0 - Immobile - Mobile //Hidden When Spawned + Immobile + Mobile //Hidden When Spawned @@ -2025,8 +2043,8 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Rotating platform - Dampé Race Stone Door + Rotating platform + Dampé Race Stone Door +0xFF = Nullable Switch Flag @@ -2036,9 +2054,9 @@ The sun switch has to be between 252 and 308 units from the cobra mirror to be a - Normal shrub. Random drops. //Use object 0002 - Cut-able, regenerating grass //Use object 012B, Drops: Heart, Arrows or Deku Seeds - Cut-able grass //Use object 012B, Set drop table via Random Drop Table param + Normal shrub. Random drops. //Use object 0002 + Cut-able, regenerating grass //Use object 012B, Drops: Heart, Arrows or Deku Seeds + Cut-able grass //Use object 012B, Set drop table via Random Drop Table param @@ -2068,11 +2086,11 @@ ZROT +0x0003 = Speed, Acceleration - Floor - Rusted Floor - Eye - Crystal - Targetable Crystal + Floor + Rusted Floor + Eye + Crystal + Targetable Crystal @@ -2083,8 +2101,8 @@ ZROT +0x0003 = Speed, Acceleration - Large - Small + Large + Small @@ -2098,21 +2116,21 @@ ZROT +0x0003 = Speed, Acceleration - Hookshot Target Tower - Hookshot Target Tower, lifts up on Switch Flag - Square Hookshot Target + Hookshot Target Tower + Hookshot Target Tower, lifts up on Switch Flag + Square Hookshot Target - Zora River, Zelda's Lullaby Spot (no cutscene) - Windmill, Song of Storms spot (cutscene) - Temple of Time, Song of Time spot (cutscene) - Learn Sun's Song spot - Royal Family Tomb, Zelda's Lullaby spot - Puzzle Spot + Zora River, Zelda's Lullaby Spot (no cutscene) + Windmill, Song of Storms spot (cutscene) + Temple of Time, Song of Time spot (cutscene) + Learn Sun's Song spot + Royal Family Tomb, Zelda's Lullaby spot + Puzzle Spot @@ -2127,17 +2145,17 @@ ZROT +0x0003 = Speed, Acceleration - End Targets - Mounted Target (Small) - Mounted Target (Big) //Center one + End Targets + Mounted Target (Small) + Mounted Target (Big) //Center one - Follows path, disappears, reappears at first waypoint //simulates falling into a hole - Follows path, breaks into pieces, reappears at first waypoint //As in adult Death Mountain Trail - Follows path, treating it as a closed loop - Follows path, halts, reverses, repeat //As in Fire Temple boulder maze + Follows path, disappears, reappears at first waypoint //simulates falling into a hole + Follows path, breaks into pieces, reappears at first waypoint //As in adult Death Mountain Trail + Follows path, treating it as a closed loop + Follows path, halts, reverses, repeat //As in Fire Temple boulder maze @@ -2150,21 +2168,21 @@ ROTZ 0x0001 = Behavior after colliding with Link - Sparkling blue rupee - Huge Magenta Rupee, explodes when touched //Unused - Blue, red, and orange rupees, explode when touched //Unused - Random-colored rupees, collectible - Green rupees underground, not collectible //Unused + Sparkling blue rupee + Huge Magenta Rupee, explodes when touched //Unused + Blue, red, and orange rupees, explode when touched //Unused + Random-colored rupees, collectible + Green rupees underground, not collectible //Unused - Ichiro //Red/Purple Pants, "normal" hair - Sabooro //Light-Blue Pants - Jiro //Green Pants - Shiro //Pink/Purple Pants, two-spiked hair + Ichiro //Red/Purple Pants, "normal" hair + Sabooro //Light-Blue Pants + Jiro //Green Pants + Shiro //Pink/Purple Pants, two-spiked hair @@ -2181,12 +2199,12 @@ ROTZ 0x0001 = Behavior after colliding with Link - Shoos you away //Calls you a kid regardless of your actual age - Gate Manager (Gerudo Fortress) - Generic Guard (Gerudo Fortress) - Stands By Cow (Gerudo Valley) - Horseback Archery - Gerudo Training Grounds Guard + Shoos you away //Calls you a kid regardless of your actual age + Gate Manager (Gerudo Fortress) + Generic Guard (Gerudo Fortress) + Stands By Cow (Gerudo Valley) + Horseback Archery + Gerudo Training Grounds Guard +0x3F00 = Switch Flag @@ -2200,8 +2218,8 @@ ROTZ 0x0001 = Behavior after colliding with Link - Cutscene start - No cutscene //required if you spawn more than four or five of these + Cutscene start + No cutscene //required if you spawn more than four or five of these @@ -2213,130 +2231,130 @@ ROTZ 0x0001 = Behavior after colliding with Link - Default + Default - Default + Default Text ID 10B1 when sleeping - Standard - Cutscene Gate, Left Part - Cutscene Gate, Right Part + Standard + Cutscene Gate, Left Part + Cutscene Gate, Right Part - X - Hyrule Field - Hyrule Castle Town - The Temple of Time - Dead End - Kakariko Village / Death Mountain Trail / Starting Point - Kakariko Village Graveyard - Dark! Narrow! Scary! / Well of Three Features - Death Mountain / No passage without a / Royal Decree! - Death Mountain Trail - Dodongo's Cavern / Don't enter without permission! - Land of the Gorons / Goron City - Zora's River / Watch out for swift current / and strong undertow. - The Shadow will yield only to one / with the eye of truth, handed / down in Kakariko Village. - Zora's Domain - Zora's Fountain / Don't disturb Lord Jabu-Jabu! / –King Zora XVI - Forest Training Center / Don't recklessly cut signs– / read them carefully! - All those reckless enough to / venture into the desert–please drop by our shop. / Carpet Merchant - Just ahead: / Great Deku Tree's Meadow - Forest Temple - The Lost Woods - Talon and Malon's / Lon Lon Ranch - The Great Ingo's / Ingo Ranch - Lake Hylia - Lakeside Laboratory / Daily trying to get to the bottom / of the mysteries of Lake Hylia! / –Lake Scientist - Gerudo Valley - Horseback Archery Range / Skilled players are welcome! / Current record: # Points - Gerudo Training Ground / Only registered members are / allowed! - Haunted Wasteland / If you chase a mirage, the / desert will swallow you. / Only one path is true! - Spirit Temple - Kokiri Shop / We have original forest goods! - LINK's House - Forest folk shall not leave these woods. - Follow the trail along the edge of / the cliff and you will reach / Goron City, home of the Gorons. - Natural Wonder / Bomb Flower / Danger! Do not uproot! - Death Mountain Summit / Entrance to the crater ahead / Beware of intense heat! - King Zora's Throne Room / To hear the King's royal / proclamations, stand on the / platform and speak to him. - If you can stop my wild rolling, you might get something great. / –Hot Rodder Goron - Only one with the eye of truth / will find the stone umbrella / that protects against the / rain of blades. - Only one who has sacred feet / can cross the valley of the dead. - The record time of those / who raced against me was: / ##"##" / –Dampé the Gravekeeper - Shooting Gallery / etc. - Treasure Chest Shop / We don't necessarily sell them... - High Dive Practice Spot / Are you confident / in your diving skill? - 032c - Mountain Summit / Danger Ahead - Keep Out - Happy Mask Shop! / Now hiring happiness / delivery men! - Bombchu Bowling Alley / You can experience the / latest in Bomb technology! - Bazaar / We have a little bit of everything! - Potion Shop / We have the best quality / potions! - Goron Shop / Mountaineering supplies! - Zora Shop / We have fresh fish! - Heart-Pounding Gravedigging Tour! / From 18:00 to 21:00 Hyrule Time / –Dampé the Gravekeeper - Heart-Pounding Gravedigging Tour! / Tours are cancelled until a new / gravekeeper is found. We / apologize for any inconvenience. - Thrust Attack Signs! / To thrust with your sword, press / CS toward your target while / Z Targeting, then press B. - Hole of “Z” / Let's go through this small / hole! / Stand in front of the hole and / push CS towards it. When the / Action Icon shows “Enter,” press / A to crawl into the hole. / Pay attention to what the Action / Icon says! - Cut Grass With Your Sword / If you just swing with B, you'll / cut horizontally. If you hold Z as / you swing, you'll cut vertically. - Hyrule Castle / Lon Lon Ranch - You are here: Hyrule Castle / This way to Lon Lon Ranch - Just Ahead / King Zora's Chamber / Show the proper respect! - House of the Great Mido / Boss of the Kokiri - House of the Know-It-All Brothers - House of Twins - Saria's House - View Point with Z Targeting / When you have no object to look / at, you can just look forward / with Z. / Stop moving and then change the / direction you are facing, or hold / down Z for a little while. / This can help you get oriented in / the direction you want to face. / It's quite convenient! / If you hold down Z, you can / walk sideways while facing / straight ahead. / Walking sideways can be a very / important technique in dungeon / corridors. Turn around and try doing this right now. - Stepping Stones in the Pond / If you boldly go in the direction / you want to jump, you will leap / automatically. / If you hop around on the stones, / you'll become happier! - No Diving Allowed / –It won't do you any good! - Switch Targeting / If you see a \/ icon above an / object, you can target it with Z. / ... / You can target the stones next to this sign for practice! - Forest Stage / We are waiting to see your / beautiful face! / Win fabulous prizes! - Visit the / House of the Know-It-All Brothers / to get answers to all your / item-related questions! - Pocket Egg + X + Hyrule Field + Hyrule Castle Town + The Temple of Time + Dead End + Kakariko Village / Death Mountain Trail / Starting Point + Kakariko Village Graveyard + Dark! Narrow! Scary! / Well of Three Features + Death Mountain / No passage without a / Royal Decree! + Death Mountain Trail + Dodongo's Cavern / Don't enter without permission! + Land of the Gorons / Goron City + Zora's River / Watch out for swift current / and strong undertow. + The Shadow will yield only to one / with the eye of truth, handed / down in Kakariko Village. + Zora's Domain + Zora's Fountain / Don't disturb Lord Jabu-Jabu! / –King Zora XVI + Forest Training Center / Don't recklessly cut signs– / read them carefully! + All those reckless enough to / venture into the desert–please drop by our shop. / Carpet Merchant + Just ahead: / Great Deku Tree's Meadow + Forest Temple + The Lost Woods + Talon and Malon's / Lon Lon Ranch + The Great Ingo's / Ingo Ranch + Lake Hylia + Lakeside Laboratory / Daily trying to get to the bottom / of the mysteries of Lake Hylia! / –Lake Scientist + Gerudo Valley + Horseback Archery Range / Skilled players are welcome! / Current record: # Points + Gerudo Training Ground / Only registered members are / allowed! + Haunted Wasteland / If you chase a mirage, the / desert will swallow you. / Only one path is true! + Spirit Temple + Kokiri Shop / We have original forest goods! + LINK's House + Forest folk shall not leave these woods. + Follow the trail along the edge of / the cliff and you will reach / Goron City, home of the Gorons. + Natural Wonder / Bomb Flower / Danger! Do not uproot! + Death Mountain Summit / Entrance to the crater ahead / Beware of intense heat! + King Zora's Throne Room / To hear the King's royal / proclamations, stand on the / platform and speak to him. + If you can stop my wild rolling, you might get something great. / –Hot Rodder Goron + Only one with the eye of truth / will find the stone umbrella / that protects against the / rain of blades. + Only one who has sacred feet / can cross the valley of the dead. + The record time of those / who raced against me was: / ##"##" / –Dampé the Gravekeeper + Shooting Gallery / etc. + Treasure Chest Shop / We don't necessarily sell them... + High Dive Practice Spot / Are you confident / in your diving skill? + 032c + Mountain Summit / Danger Ahead - Keep Out + Happy Mask Shop! / Now hiring happiness / delivery men! + Bombchu Bowling Alley / You can experience the / latest in Bomb technology! + Bazaar / We have a little bit of everything! + Potion Shop / We have the best quality / potions! + Goron Shop / Mountaineering supplies! + Zora Shop / We have fresh fish! + Heart-Pounding Gravedigging Tour! / From 18:00 to 21:00 Hyrule Time / –Dampé the Gravekeeper + Heart-Pounding Gravedigging Tour! / Tours are cancelled until a new / gravekeeper is found. We / apologize for any inconvenience. + Thrust Attack Signs! / To thrust with your sword, press / CS toward your target while / Z Targeting, then press B. + Hole of “Z” / Let's go through this small / hole! / Stand in front of the hole and / push CS towards it. When the / Action Icon shows “Enter,” press / A to crawl into the hole. / Pay attention to what the Action / Icon says! + Cut Grass With Your Sword / If you just swing with B, you'll / cut horizontally. If you hold Z as / you swing, you'll cut vertically. + Hyrule Castle / Lon Lon Ranch + You are here: Hyrule Castle / This way to Lon Lon Ranch + Just Ahead / King Zora's Chamber / Show the proper respect! + House of the Great Mido / Boss of the Kokiri + House of the Know-It-All Brothers + House of Twins + Saria's House + View Point with Z Targeting / When you have no object to look / at, you can just look forward / with Z. / Stop moving and then change the / direction you are facing, or hold / down Z for a little while. / This can help you get oriented in / the direction you want to face. / It's quite convenient! / If you hold down Z, you can / walk sideways while facing / straight ahead. / Walking sideways can be a very / important technique in dungeon / corridors. Turn around and try doing this right now. + Stepping Stones in the Pond / If you boldly go in the direction / you want to jump, you will leap / automatically. / If you hop around on the stones, / you'll become happier! + No Diving Allowed / –It won't do you any good! + Switch Targeting / If you see a \/ icon above an / object, you can target it with Z. / ... / You can target the stones next to this sign for practice! + Forest Stage / We are waiting to see your / beautiful face! / Win fabulous prizes! + Visit the / House of the Know-It-All Brothers / to get answers to all your / item-related questions! + Pocket Egg Message ID //Value + 0x0300 - Whistleblower - Stands there, does nothing + Whistleblower + Stands there, does nothing - Non-solid cucco, hops oddly every once in awhile and only goes in one direction - Invisible, solid cucco, doesn't move, can be attacked but will only smoke and molt - Invisible cucco, cannot be attacked it seems, no idea what it does, but you can hear it + Non-solid cucco, hops oddly every once in awhile and only goes in one direction + Invisible, solid cucco, doesn't move, can be attacked but will only smoke and molt + Invisible cucco, cannot be attacked it seems, no idea what it does, but you can hear it - Default + Default - Default + Default - Temple of Time stone altar dialog - Gravekeeper's diary dialog //Navi hovers higher than normal - Royal Composer Sharp's grave dialog //Use Object 006E, Sets Temporary Switch Flag 09, spawns Sharp - "Royal Family Tomb" dialog - Royal Composer Flat's grave dialog //Use Object 006E, Sets Temporary Switch Flag 08, spawns Flat + Temple of Time stone altar dialog + Gravekeeper's diary dialog //Navi hovers higher than normal + Royal Composer Sharp's grave dialog //Use Object 006E, Sets Temporary Switch Flag 09, spawns Sharp + "Royal Family Tomb" dialog + Royal Composer Flat's grave dialog //Use Object 006E, Sets Temporary Switch Flag 08, spawns Flat +0x3F = Switch Flag //Spot deactivates when flag is set @@ -2353,28 +2371,28 @@ ROTZ 0x0001 = Behavior after colliding with Link - Does nothing - Outside of Kokiri Forest exit, auto talks - Near Hyrule Castle, auto talks - In front of Kakariko Village, auto talks - Between Lake Hylia and Gerudo Valley, auto talks - In front of Lake Hylia, auto talks - Nothing - Lake Hylia, manual talk, talon grab shortcut - Death Mountain summit, manual talk, talon grab shortcut - Death Mountain summit, manual talk, talon grab shortcut - Desert Colossus, auto talks - Lost Woods, before meeting Saria, auto talks - Lost Woods, after meeting Saria, auto talks - Outside of Kokiri Forest exit, auto talks, Head position alternates? + Does nothing + Outside of Kokiri Forest exit, auto talks + Near Hyrule Castle, auto talks + In front of Kakariko Village, auto talks + Between Lake Hylia and Gerudo Valley, auto talks + In front of Lake Hylia, auto talks + Nothing + Lake Hylia, manual talk, talon grab shortcut + Death Mountain summit, manual talk, talon grab shortcut + Death Mountain summit, manual talk, talon grab shortcut + Desert Colossus, auto talks + Lost Woods, before meeting Saria, auto talks + Lost Woods, after meeting Saria, auto talks + Outside of Kokiri Forest exit, auto talks, Head position alternates? +0x3F = Switch Flag - Small rock, random drops - Large light-gray rock, can't pick up as Young Link, no drop + Small rock, random drops + Large light-gray rock, can't pick up as Young Link, no drop @@ -2389,18 +2407,18 @@ ROTZ 0x0001 = Behavior after colliding with Link - Flower for grave - Non-liftable small rock - Uncuttable small shrub - Erratic collision data + Flower for grave + Non-liftable small rock + Uncuttable small shrub + Erratic collision data - Stays On - On/Off Toggle - Activated while enlightened - Burns + Stays On + On/Off Toggle + Activated while enlightened + Burns @@ -2408,25 +2426,25 @@ ROTZ 0x0001 = Behavior after colliding with Link - Circle of shrubs with one in the middle - Scattered shrubs - Circle of rocks + Circle of shrubs with one in the middle + Scattered shrubs + Circle of rocks +0xFF00 = Random Drop Table - Link the Goron - Fire Temple Generic - DMT DC Entrance - DMT Rolling - DMT Near Bomb Flower - Goron City Entrance - Goron City Island - Goron City Lost Woods - Unused - Biggoron + Link the Goron + Fire Temple Generic + DMT DC Entrance + DMT Rolling + DMT Near Bomb Flower + Goron City Entrance + Goron City Island + Goron City Lost Woods + Unused + Biggoron @@ -2482,8 +2500,8 @@ ROTZ 0x0001 = Behavior after colliding with Link - Tall and narrow - Short and wide + Tall and narrow + Short and wide @@ -2491,19 +2509,19 @@ ROTZ 0x0001 = Behavior after colliding with Link - Standing boy lifting a rock - Standing girl near Fado - Boxing boy - Blocking boy - Backflipping boy - Sitting girl on Shop - Standing girl near Mido's House - Know-It-All Bro teaching about HUD icons - Know-It-All Bro teaching about map and items - Sitting girl in house - Standing girl in Shop - Know-It-All Bro teaching about C-Up - Fado + Standing boy lifting a rock + Standing girl near Fado + Boxing boy + Blocking boy + Backflipping boy + Sitting girl on Shop + Standing girl near Mido's House + Know-It-All Bro teaching about HUD icons + Know-It-All Bro teaching about map and items + Sitting girl in house + Standing girl in Shop + Know-It-All Bro teaching about C-Up + Fado @@ -2512,14 +2530,14 @@ ROTZ 0x0001 = Behavior after colliding with Link - Cloudy Market - Cloudy Ranch - Snowy Zora's Domain - Rainy Lake Hylia - Cloudy Death Mountain - Thunderstrorm Kakariko - Sandstorm Intensity - Thunderstrorm Graveyard + Cloudy Market + Cloudy Ranch + Snowy Zora's Domain + Rainy Lake Hylia + Cloudy Death Mountain + Thunderstrorm Kakariko + Sandstorm Intensity + Thunderstrorm Graveyard @@ -2530,32 +2548,32 @@ ROTZ 0x0001 = Behavior after colliding with Link - Bomb Bag (Bombchu Bowling given prize) - Heart Piece (Bombchu Bowling given prize) - Bombchus (Bombchu Bowling given prize) - Bombs (Bombchu Bowling given prize) - Purple Rupee (Bombchu Bowling given prize) - Bomb Bag (Bombchu Bowing prize preview) - Heart Piece (Bombchu Bowing prize preview) - Bombchus (Bombchu Bowing prize preview) - Bombs (Bombchu Bowing prize preview) - Purple Rupee (Bombchu Bowing prize preview) - Green Rupee (Chest Game) - Blue Rupee (Chest Game) - Red Rupee (Chest Game) - Purple Rupee (Unused Chest Game Rupee) - Small Key (Chest Game) - Din's Fire - Farore's Wind - Nayru's Love - Bullet Bag + Bomb Bag (Bombchu Bowling given prize) + Heart Piece (Bombchu Bowling given prize) + Bombchus (Bombchu Bowling given prize) + Bombs (Bombchu Bowling given prize) + Purple Rupee (Bombchu Bowling given prize) + Bomb Bag (Bombchu Bowing prize preview) + Heart Piece (Bombchu Bowing prize preview) + Bombchus (Bombchu Bowing prize preview) + Bombs (Bombchu Bowing prize preview) + Purple Rupee (Bombchu Bowing prize preview) + Green Rupee (Chest Game) + Blue Rupee (Chest Game) + Red Rupee (Chest Game) + Purple Rupee (Unused Chest Game Rupee) + Small Key (Chest Game) + Din's Fire + Farore's Wind + Nayru's Love + Bullet Bag Each type needs its own object to load - Brick pillar - Brick throne + Brick pillar + Brick throne @@ -2568,31 +2586,31 @@ ROTZ 0x0001 = Behavior after colliding with Link - Blocking Deku Tree + Blocking Deku Tree - Richard's Owner // Use object 0105, text ID 079D - Woman in white, blues and yellow near Apothicary // Use object 018C, text ID 7016 first, then 7017 - Bearded man in white and green near Bazaar // Use object 0107, text ID 701A - Sakon (Jogging Man) // Use object 0111, needs pathway, text ID 7002 - Staunch man in black and green // Use object 0107, text ID 7023 first, then 7024 - Begging man // Use object 0111 // Use object 0111, buy things in bottles, text ID 70ED and followings - Old woman in white // Use object 010D, text ID 7021 first, then 7022 - Old man in blue near Bombchu Bowling // Use object 010C, text ID 7027 first, then 7028 - Woman in lilac near Bombchu Bowling // Use object 0108, text ID 701D first, then 701E - Laughing man in red and white // Use object 0111, text ID 701F first, then 7020 - Explaining man in blue and white // Use object 0111, text ID 7018 first, then 7019 - 'Dancing' woman in blue and yellow near Archery Game // Use object 0108, text ID 7014 - Watchtower man in crimson // Use object 0111, text ID 7015 - Red haired man in green and lilac // Use object 0107, text ID 7055 - Bearded, red haired man in green and white // Use object 0111, text ID 7089 - Old bald man in brown // Use object 010C, text ID 708A - Man in white // Use object 0111, text ID 700E - Man from Impa's House // Use object 0107, text ID 505B first, then 505C - Old bald man in purple // Use object 010C - Man in two shades of green // Use object 0107 + Richard's Owner // Use object 0105, text ID 079D + Woman in white, blues and yellow near Apothicary // Use object 018C, text ID 7016 first, then 7017 + Bearded man in white and green near Bazaar // Use object 0107, text ID 701A + Sakon (Jogging Man) // Use object 0111, needs pathway, text ID 7002 + Staunch man in black and green // Use object 0107, text ID 7023 first, then 7024 + Begging man // Use object 0111 // Use object 0111, buy things in bottles, text ID 70ED and followings + Old woman in white // Use object 010D, text ID 7021 first, then 7022 + Old man in blue near Bombchu Bowling // Use object 010C, text ID 7027 first, then 7028 + Woman in lilac near Bombchu Bowling // Use object 0108, text ID 701D first, then 701E + Laughing man in red and white // Use object 0111, text ID 701F first, then 7020 + Explaining man in blue and white // Use object 0111, text ID 7018 first, then 7019 + 'Dancing' woman in blue and yellow near Archery Game // Use object 0108, text ID 7014 + Watchtower man in crimson // Use object 0111, text ID 7015 + Red haired man in green and lilac // Use object 0107, text ID 7055 + Bearded, red haired man in green and white // Use object 0111, text ID 7089 + Old bald man in brown // Use object 010C, text ID 708A + Man in white // Use object 0111, text ID 700E + Man from Impa's House // Use object 0107, text ID 505B first, then 505C + Old bald man in purple // Use object 010C + Man in two shades of green // Use object 0107 +0x780 = Path Id //NPCs 03 and 07 only @@ -2617,24 +2635,20 @@ ROTZ 0x0001 = Behavior after colliding with Link - Unknown - Unknown - Unknown - Unknown - Unknown - Unknown - Unknown - Unknown - Unknown - Unknown + Unknown + Unknown + Unknown + Unknown + Unknown + Unknown + Unknown + Unknown + Unknown + Unknown - - Big Poe - Normal poe, circles around you - - + @@ -2644,18 +2658,18 @@ ROTZ 0x0001 = Behavior after colliding with Link - Time teller - Stands by Impa's House, tells time - Dying guard in Market Town - Stands in Market Town at night + Time teller + Stands by Impa's House, tells time + Dying guard in Market Town + Stands in Market Town at night Text ID 5063, then 5064 - Zelda in Ganondorf Fight (used by game engine) - Zelda in Ganon Fight (used by game engine) - Zelda in Castle Collapse + Zelda in Ganondorf Fight (used by game engine) + Zelda in Ganon Fight (used by game engine) + Zelda in Castle Collapse @@ -2684,70 +2698,70 @@ ROTZ 0x0001 = Behavior after colliding with Link - Drunken Ingo // Use Object 00C0 - Drunken Talon // Use Object 0088 - Windmill man breakdancing // Use Object 0133 - Encouraging Kokiri Boy // Use Object 00FC - Encouraging Kokiri Girl // Use Object 00FD - White-haired man in blue // Use Object 010C - Black-bearded man in white and green // Use Object 0107 - Bushy-haired woman in red and black // Use Object 0115 - Little old lady // Use Object 010D - Carpenter Boss, Singing // Use Object 0121 - Carpenter, Singing // Use Object 0122 - Carpenter, Singing // Use Object 0122 - Carpenter, Singing // Use Object 0122 - Carpenter, Singing // Use Object 0122 - Kokiri Boy, Dancing // Use Object 00FC - Kokiri Girl, Dancing // Use Object 00FD - Gerudo, Line-dancing 1 // Use Object 0116 - Gerudo, Line-dancing 2 // Use Object 0116 - Gerudo, spikey-haired, Line-dancing // Use Object 0116 - Dancing Zora // Use Object 00FE - King Zora // Use Object 00FF - Mido, sitting // Use Object 00FB - Floating cucco // Use Object 0013 - Floating cucco 2 // Use Object 0013 - Walking cucco // Use Object 0013 - Cucco Lady // Use Object 0110 - Potion Shopkeeper // Use Object 0159 - Happy Mask Salesman // Use Object 013E - Fisherman // Use Object 015B - Bombchu Shopkeeper // Use Object 0165 - Dancing Goron // Use Object 00C9 - Belly slapping Goron // Use Object 00C9 - Biggoron, dancing // Use Object 00C9 - Medigoron, lying down // Use Object 00C9 - Singing Malon // Use Object 00D0 + Drunken Ingo // Use Object 00C0 + Drunken Talon // Use Object 0088 + Windmill man breakdancing // Use Object 0133 + Encouraging Kokiri Boy // Use Object 00FC + Encouraging Kokiri Girl // Use Object 00FD + White-haired man in blue // Use Object 010C + Black-bearded man in white and green // Use Object 0107 + Bushy-haired woman in red and black // Use Object 0115 + Little old lady // Use Object 010D + Carpenter Boss, Singing // Use Object 0121 + Carpenter, Singing // Use Object 0122 + Carpenter, Singing // Use Object 0122 + Carpenter, Singing // Use Object 0122 + Carpenter, Singing // Use Object 0122 + Kokiri Boy, Dancing // Use Object 00FC + Kokiri Girl, Dancing // Use Object 00FD + Gerudo, Line-dancing 1 // Use Object 0116 + Gerudo, Line-dancing 2 // Use Object 0116 + Gerudo, spikey-haired, Line-dancing // Use Object 0116 + Dancing Zora // Use Object 00FE + King Zora // Use Object 00FF + Mido, sitting // Use Object 00FB + Floating cucco // Use Object 0013 + Floating cucco 2 // Use Object 0013 + Walking cucco // Use Object 0013 + Cucco Lady // Use Object 0110 + Potion Shopkeeper // Use Object 0159 + Happy Mask Salesman // Use Object 013E + Fisherman // Use Object 015B + Bombchu Shopkeeper // Use Object 0165 + Dancing Goron // Use Object 00C9 + Belly slapping Goron // Use Object 00C9 + Biggoron, dancing // Use Object 00C9 + Medigoron, lying down // Use Object 00C9 + Singing Malon // Use Object 00D0 Each variable requires it's own object - Lake Hylia's Sun Hitbox - Big Fairy, spawns with Sun's Song - Big Fairy, spawns with Song of Storms + Lake Hylia's Sun Hitbox + Big Fairy, spawns with Sun's Song + Big Fairy, spawns with Song of Storms - Pink spiral beam - Green spiral beam - Purple spiral beam - Blue spiral beam + Pink spiral beam + Green spiral beam + Purple spiral beam + Blue spiral beam + Black spiral beam - Black and green spiral beam - Gray and green spiral beam - Light blue spiral beam - Light purple spiral beam + Black and green spiral beam + Gray and green spiral beam + Light blue spiral beam + Light purple spiral beam - Checkable, Sets Switch - Instant Text - Checkable, Disappears on Switch - Z-Target, No Text + Checkable, Sets Switch + Instant Text + Checkable, Disappears on Switch + Z-Target, No Text @@ -2818,9 +2832,9 @@ ROTZ 0x0001 = Behavior after colliding with Link - Turns around, can't move, whistle-blower - Won't turn around, can't move, whistle-blower - Purple Gerudo, acts like the one that gives you the membership card + Turns around, can't move, whistle-blower + Won't turn around, can't move, whistle-blower + Purple Gerudo, acts like the one that gives you the membership card @@ -2833,57 +2847,57 @@ ROTZ 0x0001 = Behavior after colliding with Link - Main Hyliantula //(gold skulltulas lower than 100) - Hyliantula without an arm //(gold skulltulas lower than 10) - Hyliantula without an arm //(gold skulltulas lower than 20) - Hyliantula without an arm //(gold skulltulas lower than 30) - Hyliantula without an arm //(gold skulltulas lower than 40) - Hyliantula without an arm //(gold skulltulas lower than 50) - Hyliantula without an arm //(gold skulltulas lower than 60) - Hyliantula without an arm //(gold skulltulas lower than 70) - Hyliantula without an arm //(gold skulltulas lower than 80) - Hyliantula without an arm //(gold skulltulas lower than 90) - Hyliantula without an arm //(gold skulltulas lower than 100) - Hyliantula without an arm //(gold skulltulas lower than 110) + Main Hyliantula //(gold skulltulas lower than 100) + Hyliantula without an arm //(gold skulltulas lower than 10) + Hyliantula without an arm //(gold skulltulas lower than 20) + Hyliantula without an arm //(gold skulltulas lower than 30) + Hyliantula without an arm //(gold skulltulas lower than 40) + Hyliantula without an arm //(gold skulltulas lower than 50) + Hyliantula without an arm //(gold skulltulas lower than 60) + Hyliantula without an arm //(gold skulltulas lower than 70) + Hyliantula without an arm //(gold skulltulas lower than 80) + Hyliantula without an arm //(gold skulltulas lower than 90) + Hyliantula without an arm //(gold skulltulas lower than 100) + Hyliantula without an arm //(gold skulltulas lower than 110) - Main Hyliantula //(gold skulltulas lower than 100) - Hyliantula without an arm //(gold skulltulas lower than 10) - Hyliantula without an arm //(gold skulltulas lower than 20) - Hyliantula without an arm //(gold skulltulas lower than 30) - Hyliantula without an arm //(gold skulltulas lower than 40) - Hyliantula without an arm //(gold skulltulas lower than 50) - Hyliantula without an arm //(gold skulltulas lower than 60) - Hyliantula without an arm //(gold skulltulas lower than 70) - Hyliantula without an arm //(gold skulltulas lower than 80) - Hyliantula without an arm //(gold skulltulas lower than 90) - Hyliantula without an arm //(gold skulltulas lower than 100) - Hyliantula without an arm //(gold skulltulas lower than 110) + Main Hyliantula //(gold skulltulas lower than 100) + Hyliantula without an arm //(gold skulltulas lower than 10) + Hyliantula without an arm //(gold skulltulas lower than 20) + Hyliantula without an arm //(gold skulltulas lower than 30) + Hyliantula without an arm //(gold skulltulas lower than 40) + Hyliantula without an arm //(gold skulltulas lower than 50) + Hyliantula without an arm //(gold skulltulas lower than 60) + Hyliantula without an arm //(gold skulltulas lower than 70) + Hyliantula without an arm //(gold skulltulas lower than 80) + Hyliantula without an arm //(gold skulltulas lower than 90) + Hyliantula without an arm //(gold skulltulas lower than 100) + Hyliantula without an arm //(gold skulltulas lower than 110) - Purple wormhole - Blue wormhole + Purple wormhole + Blue wormhole - Regular effects, storm begins - Texture-spanning effect only, no storm, music isn't affected, effect does not dissipate + Regular effects, storm begins + Texture-spanning effect only, no storm, music isn't affected, effect does not dissipate - Default + Default - Large flat square floor tile, makes water noises when walking on it - Large vertical gate + Large flat square floor tile, makes water noises when walking on it + Large vertical gate +0x3F00 = Switch Flag //Type 01 only @@ -2894,9 +2908,9 @@ ROTZ 0x0001 = Behavior after colliding with Link - Chair Crumble - Pillar Crumble - Round stone thing, explodes as soon as area loads + Chair Crumble + Pillar Crumble + Round stone thing, explodes as soon as area loads @@ -2911,9 +2925,10 @@ ROTZ 0x0001 = Behavior after colliding with Link - Scrub on path to Deku Slingshot - 231/312 Hint scrub - Final Deku Scrub + (Invalid) + Scrub on path to Deku Slingshot + 231/312 Hint scrub + Final Deku Scrub @@ -2925,32 +2940,32 @@ ROTZ 0x0001 = Behavior after colliding with Link - Mad Scrubs - Hint Scrubs - Business Scrubs - Forest Stage Judge - Forest Stage Patron + Mad Scrubs + Hint Scrubs + Business Scrubs + Forest Stage Judge + Forest Stage Patron - Broken Drawbridge - Fences (Hyrule Field) + Broken Drawbridge + Fences (Hyrule Field) - Deku Nuts - Deku Sticks - Piece of Heart - Deku Seeds - Deku Shield - Bombs - Deku Seeds - Red Potion - Green Potion - Deku Stick Upgrade - Deku Nut Upgrade + Deku Nuts + Deku Sticks + Piece of Heart + Deku Seeds + Deku Shield + Bombs + Deku Seeds + Red Potion + Green Potion + Deku Stick Upgrade + Deku Nut Upgrade @@ -2973,22 +2988,22 @@ ROTZ 0x0001 = Behavior after colliding with Link - Dirty blond dog - Chocolate brown dog - Red dog - Multicolored, flashing dog - Red dog - Multicolored, flashing dog - Dark dog (black fur) - Green dog - Red dog - Purple dog - Red dog - Green dog - Green dog - Black dog - Black dog - Purple dog + Dirty blond dog + Chocolate brown dog + Red dog + Multicolored, flashing dog + Red dog + Multicolored, flashing dog + Dark dog (black fur) + Green dog + Red dog + Purple dog + Red dog + Green dog + Green dog + Black dog + Black dog + Purple dog @@ -3018,11 +3033,11 @@ ROTZ 0x0001 = Behavior after colliding with Link - Potion Shop Poster - Shooting Gallery Poster - Bazaar Poster - Shooting Gallery (Partially Complete) //Spawns Carpenter Sabooro (Kakariko) during the day - Shooting Gallery (Complete) + Potion Shop Poster + Shooting Gallery Poster + Bazaar Poster + Shooting Gallery (Partially Complete) //Spawns Carpenter Sabooro (Kakariko) during the day + Shooting Gallery (Complete) @@ -3045,7 +3060,7 @@ Z ROTATION +0x3F = Collectible Flag - Default + Default @@ -3053,7 +3068,7 @@ Z ROTATION +0x3F = Collectible Flag - Default + Default Yields deku seed upgrade @@ -3066,13 +3081,13 @@ Z ROTATION +0x3F = Collectible Flag - Magic Barrier - Blue Magic Core //Use with actor 00D2 (Adult Ruto), var 0002 - Yellow Magic Core //Use with actor 00A6 (Rauru), var 0002 - Red Magic Core //Use with actor 00A8 (Cutscene Darunia), var 0002 - Purple Magic Core //Use with actor 00A9 (Impa), var 0002 - Orange Magic Core //Use with actor 00C3 (Nabooru), var 0002 - Green Magic Core //Use with actor 00C9 (Cutscene Saria), var 0002 + Magic Barrier + Blue Magic Core //Use with actor 00D2 (Adult Ruto), var 0002 + Yellow Magic Core //Use with actor 00A6 (Rauru), var 0002 + Red Magic Core //Use with actor 00A8 (Cutscene Darunia), var 0002 + Purple Magic Core //Use with actor 00A9 (Impa), var 0002 + Orange Magic Core //Use with actor 00C3 (Nabooru), var 0002 + Green Magic Core //Use with actor 00C9 (Cutscene Saria), var 0002 @@ -3086,9 +3101,9 @@ Z ROTATION +0x3F = Collectible Flag - Five Blue Rupees stacked vertically - Five Green Rupees in a row - Six Green Rupees in a circle with a Red Rupee in the center + Five Blue Rupees stacked vertically + Five Green Rupees in a row + Six Green Rupees in a circle with a Red Rupee in the center @@ -3097,33 +3112,33 @@ Z ROTATION +0x3F = Collectible Flag - Blue/green textures - Green/brown textures - Green/red textures - Purple/red textures - Orange/red textures - Purple/black textures - Flashing purple/blue textures - Flashing purple/cyan/green textures, depends on movement of camera - Purple/red textures + Blue/green textures + Green/brown textures + Green/red textures + Purple/red textures + Orange/red textures + Purple/black textures + Flashing purple/blue textures + Flashing purple/cyan/green textures, depends on movement of camera + Purple/red textures - Big Rollin' Goron - Link the Goron - Biggoron - Generic Fire Temple Goron - Goron from DMT near a bomb flower - Rolling Goron from DMT - Goron near Dodongo's Cavern Entrance - Goron at the entrance of Goron City - Goron on the island from Goron City - Goron near Darunia's room - Goron in the stairwell in Goron City - Goron near Lost Woods - Goron talking about the Great Fairy in DMT - Goron in Market's Bazaar + Big Rollin' Goron + Link the Goron + Biggoron + Generic Fire Temple Goron + Goron from DMT near a bomb flower + Rolling Goron from DMT + Goron near Dodongo's Cavern Entrance + Goron at the entrance of Goron City + Goron on the island from Goron City + Goron near Darunia's room + Goron in the stairwell in Goron City + Goron near Lost Woods + Goron talking about the Great Fairy in DMT + Goron in Market's Bazaar @@ -3131,8 +3146,8 @@ Z ROTATION +0x3F = Collectible Flag - Normal - White Wolfos, mini-boss music starts + Normal + White Wolfos, mini-boss music starts +0xFF00 = Nullable Switch Flag @@ -3145,33 +3160,33 @@ Z ROTATION +0x3F = Collectible Flag - Around Arena, indestructible rubble - Rubble Pile 1 (where Ganondorf rises) - Rubble Pile 2 (where Ganondorf rises) - Rubble Pile 3 (where Ganondorf rises) - Rubble Pile 4 (where Ganondorf rises) - Rubble Pile 5 (where Ganondorf rises) - Rubble Pile 6 (where Ganondorf rises) - Rubble Pile 7 (where Ganondorf rises) - 'Sun' destructible rubble, drops collectible (draw depends on Ganon) - 'Wall' destructible rubble, drops collectible (draw depends on Ganon) - Tall destructible rubble, drops collectible (draw depends on Ganon) + Around Arena, indestructible rubble + Rubble Pile 1 (where Ganondorf rises) + Rubble Pile 2 (where Ganondorf rises) + Rubble Pile 3 (where Ganondorf rises) + Rubble Pile 4 (where Ganondorf rises) + Rubble Pile 5 (where Ganondorf rises) + Rubble Pile 6 (where Ganondorf rises) + Rubble Pile 7 (where Ganondorf rises) + 'Sun' destructible rubble, drops collectible (draw depends on Ganon) + 'Wall' destructible rubble, drops collectible (draw depends on Ganon) + Tall destructible rubble, drops collectible (draw depends on Ganon) - Large chunk - Medium chunk - Small chunk - Large chunk - Medium chunk - Small chunk - Large chunk - Medium chunk - Small chunk - Small chunk + Large chunk + Medium chunk + Small chunk + Large chunk + Medium chunk + Small chunk + Large chunk + Medium chunk + Small chunk + Small chunk non-solid, possibly Ganon's Tower rubble @@ -3180,9 +3195,9 @@ Z ROTATION +0x3F = Collectible Flag - Invisible Path - Glass Block, appears on Switch Flag - Invisible Timer + Invisible Path + Glass Block, appears on Switch Flag + Invisible Timer +0xFF00 = Nullable Switch Flag //If null, Enabled and ignoring switch flag input @@ -3198,17 +3213,17 @@ Z ROTATION +0x3F = Collectible Flag - Ceiling Web - Light Source (draws when web burned) - Light Floor (draws when web burned) + Ceiling Web + Light Source (draws when web burned) + Light Floor (draws when web burned) - Square Stone - Stone Brick - Similar to 1, but looks like a different texture + Square Stone + Stone Brick + Similar to 1, but looks like a different texture + Graphics glitchy @@ -3220,57 +3235,57 @@ Z ROTATION +0x3F = Collectible Flag - This is a Gossip Stone! - They say you can swim faster by continuously pressing B. - They say there is a secret near the lone tree which is not far from the river in the northwest part of Hyrule Field. - They say that there is a secret on the road that leads to Lake Hylia. - They say that Biggoron's Sword is super sharp and will never break. - They say that Medigoron didn't really think about his own size, so his store is really cramped. - They say that Malon set the original record in the obstacle course of Lon Lon Ranch. - They say that Malon of Lon Lon Ranch hopes a knight in shining armor will come and sweep her off her feet someday. - They say that Ruto, the Zora princess who is known for her selfish nature, likes a certain boy... - They say that players who select the “HOLD” option for “Z TARGETING” are the real “Zelda players!” - They say that there is a secret near a tree in Kakariko Village. - They say that, contrary to her elegant image, Princess Zelda of Hyrule Castle is, in fact, a tomboy! - They say that Princess Zelda's nanny is actually one of the Sheikah, who many thought had died out. - They say there is a man who can always be found running around in Hyrule Field. - They say that it is against the rules to use glasses at the Treasure Chest Shop in Hyrule Castle Town Market. - They say that the chicken lady goes to the Lakeside Laboratory to study how to breed pocket-sized Cuccos. - They say that Gerudos sometimes come to Hyrule Castle Town to look for boyfriends. - They say that the thief named Nabooru, who haunts this area, is a Gerudo. - They say that if you get close to a butterfly while holding a Deku Stick in your hands, something good will happen. - They say that you may find something new in dungeons that you have already finished. - They say that Gerudos worship Ganondorf almost like a god. - They say that there is a secret around the entrance to Gerudo Valley. - They say that the owl named Kaepora Gaebora is the reincarnation of an ancient Sage. - They say that strange owl, Kaepora Gaebora, may look big and heavy, but its character is rather lighthearted. - They say that the horse Ganondorf rides is a solid black Gerudo stallion. - They say that Ganondorf is not satisfied with ruling only the Gerudo and aims to conquer all of Hyrule! - They say that the treasure you can earn in the Gerudo's Training Ground is not as great as you would expect, given its difficulty! - They say that there is a switch that you can activate only by using the Spin Attack. - They say that it's possible to find a total of 100 Gold Skulltulas throughout Hyrule. - They say that when non-fairy folk enter the Lost Woods, they become monsters! - They say that the small holes in the ground that you can find all over Hyrule make perfect breeding ground for bugs. - They say that the Kokiri are always followed by small fairies. - They say that one Kokiri has left the forest, but he is still alive! + This is a Gossip Stone! + They say you can swim faster by continuously pressing B. + They say there is a secret near the lone tree which is not far from the river in the northwest part of Hyrule Field. + They say that there is a secret on the road that leads to Lake Hylia. + They say that Biggoron's Sword is super sharp and will never break. + They say that Medigoron didn't really think about his own size, so his store is really cramped. + They say that Malon set the original record in the obstacle course of Lon Lon Ranch. + They say that Malon of Lon Lon Ranch hopes a knight in shining armor will come and sweep her off her feet someday. + They say that Ruto, the Zora princess who is known for her selfish nature, likes a certain boy... + They say that players who select the “HOLD” option for “Z TARGETING” are the real “Zelda players!” + They say that there is a secret near a tree in Kakariko Village. + They say that, contrary to her elegant image, Princess Zelda of Hyrule Castle is, in fact, a tomboy! + They say that Princess Zelda's nanny is actually one of the Sheikah, who many thought had died out. + They say there is a man who can always be found running around in Hyrule Field. + They say that it is against the rules to use glasses at the Treasure Chest Shop in Hyrule Castle Town Market. + They say that the chicken lady goes to the Lakeside Laboratory to study how to breed pocket-sized Cuccos. + They say that Gerudos sometimes come to Hyrule Castle Town to look for boyfriends. + They say that the thief named Nabooru, who haunts this area, is a Gerudo. + They say that if you get close to a butterfly while holding a Deku Stick in your hands, something good will happen. + They say that you may find something new in dungeons that you have already finished. + They say that Gerudos worship Ganondorf almost like a god. + They say that there is a secret around the entrance to Gerudo Valley. + They say that the owl named Kaepora Gaebora is the reincarnation of an ancient Sage. + They say that strange owl, Kaepora Gaebora, may look big and heavy, but its character is rather lighthearted. + They say that the horse Ganondorf rides is a solid black Gerudo stallion. + They say that Ganondorf is not satisfied with ruling only the Gerudo and aims to conquer all of Hyrule! + They say that the treasure you can earn in the Gerudo's Training Ground is not as great as you would expect, given its difficulty! + They say that there is a switch that you can activate only by using the Spin Attack. + They say that it's possible to find a total of 100 Gold Skulltulas throughout Hyrule. + They say that when non-fairy folk enter the Lost Woods, they become monsters! + They say that the small holes in the ground that you can find all over Hyrule make perfect breeding ground for bugs. + They say that the Kokiri are always followed by small fairies. + They say that one Kokiri has left the forest, but he is still alive! +0xFF00 Collectible Flag (fairy) - Floor - Cracked Wall - Unused - Stinger Room 1 - Stinger Room 2 + Floor + Cracked Wall + Unused + Stinger Room 1 + Stinger Room 2 - Vertical - Horizontal + Vertical + Horizontal + Invalid @@ -3280,10 +3295,10 @@ Z ROTATION +0x3F = Collectible Flag - Ichiro //Red/Purple Pants, "normal" hair - Sabooro //Light-blue Pants - Jiro //Green Pants - Shiro //Pink/Purple Pants, Two-Spiked Hair + Ichiro //Red/Purple Pants, "normal" hair + Sabooro //Light-blue Pants + Jiro //Green Pants + Shiro //Pink/Purple Pants, Two-Spiked Hair +0xFF00 = Path @@ -3291,14 +3306,14 @@ Z ROTATION +0x3F = Collectible Flag - First Wall - Second Wall + First Wall + Second Wall - First Wall - Second Wall + First Wall + Second Wall @@ -3311,11 +3326,11 @@ Z ROTATION +0x3F = Collectible Flag - Fake door - Debris - Debris - Debris - Debris + Fake door + Debris + Debris + Debris + Debris @@ -3331,24 +3346,24 @@ Z ROTATION +0x3F = Collectible Flag - Cow - Tail only + Cow + Tail only - Stalagmite (floor) - Stalactite (ceiling) //falls when Link is underneath - Regrowing Stalactite (ceiling) //falls when Link is underneath + Stalagmite (floor) + Stalactite (ceiling) //falls when Link is underneath + Regrowing Stalactite (ceiling) //falls when Link is underneath - - vertical, clear flag - vertical, switch softlock - horizontal, clear flag + + vertical, clear flag + vertical, switch softlock + horizontal, clear flag - + +0x3F00 switchflag @@ -3363,29 +3378,29 @@ Z ROTATION +0x3F = Collectible Flag - Scarecrow song effect? - purple + Scarecrow song effect? + purple - Circular piece of false stone wall //Visible, invisible with Lens of Truth - Square piece of false stone wall //Oriented horizontally + Circular piece of false stone wall //Visible, invisible with Lens of Truth + Square piece of false stone wall //Oriented horizontally - Zora near the ladder/grotto - Zora near the shop - Zora near the fishes - Zora near the Lake Hylia exit - Zora near the grotto platform - Zora between ladder and grotto platform - Zora near the Lakeside Laboratory - Zora near the Domain exit - Zora from Zora Shop + Zora near the ladder/grotto + Zora near the shop + Zora near the fishes + Zora near the Lake Hylia exit + Zora near the grotto platform + Zora between ladder and grotto platform + Zora near the Lakeside Laboratory + Zora near the Domain exit + Zora from Zora Shop @@ -3399,8 +3414,8 @@ Z ROTATION +0x3F = Collectible Flag - Child Visible - Adult Visible + Child Visible + Adult Visible @@ -3416,8 +3431,8 @@ Z ROTATION +0x3F = Collectible Flag - Tent, starts the race - Waiting in Lost Woods, ends the race + Tent, starts the race + Waiting in Lost Woods, ends the race @@ -3425,8 +3440,8 @@ Z ROTATION +0x3F = Collectible Flag - Invisible - Visible + Invisible + Visible @@ -3527,6 +3542,7 @@ Z ROTATION +0x3F = Collectible Flag + diff --git a/fast64_internal/oot/data/xml/EnumData.xml b/fast64_internal/data/z64/xml/oot_enum_data.xml similarity index 58% rename from fast64_internal/oot/data/xml/EnumData.xml rename to fast64_internal/data/z64/xml/oot_enum_data.xml index c379af7a4..322aa96c7 100644 --- a/fast64_internal/oot/data/xml/EnumData.xml +++ b/fast64_internal/data/z64/xml/oot_enum_data.xml @@ -20,7 +20,7 @@ - Player Cue Ids got their own enum but not regular Actor Cues. This is because Actor Cues are among cutscene commands (``csCmd``) --> - + @@ -150,9 +150,9 @@ - + - + @@ -174,8 +174,8 @@ - - + + @@ -191,7 +191,7 @@ - + @@ -199,12 +199,12 @@ - + - + @@ -220,7 +220,7 @@ - + @@ -299,8 +299,8 @@ - - + + @@ -343,7 +343,7 @@ - + @@ -424,13 +424,13 @@ - + - + @@ -483,7 +483,7 @@ - + @@ -598,4 +598,399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast64_internal/oot/data/xml/ObjectList.xml b/fast64_internal/data/z64/xml/oot_object_list.xml similarity index 100% rename from fast64_internal/oot/data/xml/ObjectList.xml rename to fast64_internal/data/z64/xml/oot_object_list.xml diff --git a/fast64_internal/f3d/__init__.py b/fast64_internal/f3d/__init__.py index 9975de5d3..daf9c6dbf 100644 --- a/fast64_internal/f3d/__init__.py +++ b/fast64_internal/f3d/__init__.py @@ -1,4 +1,3 @@ from .f3d_parser import * from .f3d_material import * -from .f3d_render_engine import * from .f3d_gbi import * diff --git a/fast64_internal/f3d/f3d_bleed.py b/fast64_internal/f3d/f3d_bleed.py index 0e6faaa9b..5dcadcafe 100644 --- a/fast64_internal/f3d/f3d_bleed.py +++ b/fast64_internal/f3d/f3d_bleed.py @@ -7,9 +7,25 @@ from ..utility import create_or_get_world from .f3d_gbi import ( + DPPipelineMode, + DPSetAlphaCompare, + DPSetAlphaDither, + DPSetColorDither, + DPSetCombineKey, + DPSetCycleType, + DPSetDepthSource, + DPSetTextureConvert, + DPSetTextureDetail, + DPSetTextureFilter, + DPSetTextureLOD, + DPSetTextureLUT, + DPSetTexturePersp, + GfxMatWriteMethod, GfxTag, GfxListTag, + SPGeometryMode, SPMatrix, + SPSetOtherModeSub, SPVertex, SPViewport, SPDisplayList, @@ -36,6 +52,7 @@ DPLoadSync, DPTileSync, DPSetTile, + DPSetTileSize, DPLoadTile, FModel, FMesh, @@ -45,9 +62,61 @@ GfxList, FTriGroup, GbiMacro, + get_F3D_GBI, ) +def get_geo_cmds( + clear_modes: set[str], set_modes: set[str], is_ex2: bool, matWriteMethod: GfxMatWriteMethod +) -> tuple[ + list[SPLoadGeometryMode | SPGeometryMode | SPSetGeometryMode | SPClearGeometryMode], + list[SPGeometryMode | SPSetGeometryMode | SPClearGeometryMode], +]: + set_modes, clear_modes = set(set_modes), set(clear_modes) + if len(clear_modes) == 0 and len(set_modes) == 0: + return ([], []) + if is_ex2: + if matWriteMethod == GfxMatWriteMethod.WriteAll: + return ([SPLoadGeometryMode(set_modes)], []) + elif len(set_modes) > 0 and len(clear_modes) > 0: + return ([SPGeometryMode(clear_modes, set_modes)], [SPGeometryMode(set_modes, clear_modes)]) + material, revert = [], [] + if len(set_modes) > 0: + material.append(SPSetGeometryMode(set_modes)) + revert.append(SPClearGeometryMode(set_modes)) + if len(clear_modes) > 0: + material.append(SPClearGeometryMode(clear_modes)) + revert.append(SPSetGeometryMode(clear_modes)) + return (material, revert) + + +GEO_CMDS = (SPGeometryMode, SPSetGeometryMode, SPClearGeometryMode, SPLoadGeometryMode) +WRITE_DIFF_OTHERMODE_CMDS = (SPSetOtherModeSub, DPSetRenderMode) + + +def get_flags( + set_modes: set[str], clear_modes: set[str], cmd: GEO_CMDS, default_clear: SPClearGeometryMode | None = None +): + if type(cmd) == SPGeometryMode: + set_modes.update(cmd.setFlagList) + clear_modes.update(cmd.clearFlagList) + clear_modes.difference_update(set_modes) + set_modes.difference_update(clear_modes) + elif type(cmd) == SPSetGeometryMode: + set_modes.update(cmd.flagList) + clear_modes.difference_update(set_modes) + elif type(cmd) == SPClearGeometryMode: + clear_modes.update(cmd.flagList) + set_modes.difference_update(clear_modes) + elif type(cmd) == SPLoadGeometryMode: + clear_modes.update(set_modes) + clear_modes.difference_update(cmd.flagList) + if default_clear is not None: + clear_modes.update(default_clear.flagList - cmd.flagList) + set_modes.clear() + set_modes.update(cmd.flagList) + + class BleedGraphics: # bleed_state "enums" bleed_start = 1 @@ -57,7 +126,9 @@ class BleedGraphics: def __init__(self): self.bled_gfx_lists = dict() + self.reset_gfx_lists = set() # build world default cmds to compare against, f3d types needed for reset cmd building + self.f3d = get_F3D_GBI() self.is_f3d_old = bpy.context.scene.f3d_type == "F3D" self.is_f3dex2 = "F3DEX2" in bpy.context.scene.f3d_type self.build_default_geo() @@ -66,14 +137,14 @@ def __init__(self): def build_default_geo(self): defaults = create_or_get_world(bpy.context.scene).rdp_defaults - setGeo = SPSetGeometryMode([]) - clearGeo = SPClearGeometryMode([]) + setGeo = SPSetGeometryMode() + clearGeo = SPClearGeometryMode() def place_in_flaglist(flag: bool, enum: str, set_list: SPSetGeometryMode, clear_list: SPClearGeometryMode): if flag: - set_list.flagList.append(enum) + set_list.flagList.add(enum) else: - clear_list.flagList.append(enum) + clear_list.flagList.add(enum) place_in_flaglist(defaults.g_zbuffer, "G_ZBUFFER", setGeo, clearGeo) place_in_flaglist(defaults.g_shade, "G_SHADE", setGeo, clearGeo) @@ -94,32 +165,42 @@ def place_in_flaglist(flag: bool, enum: str, set_list: SPSetGeometryMode, clear_ def build_default_othermodes(self): defaults = create_or_get_world(bpy.context.scene).rdp_defaults - othermode_H = SPSetOtherMode("G_SETOTHERMODE_H", 4, 20 - self.is_f3d_old, []) + othermode_L: dict[SPSetOtherModeSub:str] = {} + othermode_L[DPSetAlphaCompare] = defaults.g_mdsft_alpha_compare + othermode_L[DPSetDepthSource] = defaults.g_mdsft_zsrcsel + + othermode_H: dict[SPSetOtherModeSub:str] = {} + othermode_H[DPSetColorDither] = defaults.g_mdsft_rgb_dither + othermode_H[DPSetAlphaDither] = defaults.g_mdsft_alpha_dither + othermode_H[DPSetCombineKey] = defaults.g_mdsft_combkey + othermode_H[DPSetTextureConvert] = defaults.g_mdsft_textconv + othermode_H[DPSetTextureFilter] = defaults.g_mdsft_text_filt + othermode_H[DPSetTextureLUT] = defaults.g_mdsft_textlut + othermode_H[DPSetTextureLOD] = defaults.g_mdsft_textlod + othermode_H[DPSetTextureDetail] = defaults.g_mdsft_textdetail + othermode_H[DPSetTexturePersp] = defaults.g_mdsft_textpersp + othermode_H[DPSetCycleType] = defaults.g_mdsft_cycletype + othermode_H[DPPipelineMode] = defaults.g_mdsft_pipeline + self.default_othermode_dict = othermode_L | othermode_H + self.default_othermode_H = SPSetOtherMode( + "G_SETOTHERMODE_H", 4, 20 - self.is_f3d_old, set(othermode_H.values()) + ) # if the render mode is set, it will be consider non-default a priori - othermode_L = SPSetOtherMode("G_SETOTHERMODE_L", 0, 3 - self.is_f3d_old, []) - - othermode_L.flagList.append(defaults.g_mdsft_alpha_compare) - othermode_L.flagList.append(defaults.g_mdsft_zsrcsel) - - othermode_H.flagList.append(defaults.g_mdsft_rgb_dither) - othermode_H.flagList.append(defaults.g_mdsft_alpha_dither) - othermode_H.flagList.append(defaults.g_mdsft_combkey) - othermode_H.flagList.append(defaults.g_mdsft_textconv) - othermode_H.flagList.append(defaults.g_mdsft_text_filt) - othermode_H.flagList.append(defaults.g_mdsft_textlut) - othermode_H.flagList.append(defaults.g_mdsft_textlod) - othermode_H.flagList.append(defaults.g_mdsft_textdetail) - othermode_H.flagList.append(defaults.g_mdsft_textpersp) - othermode_H.flagList.append(defaults.g_mdsft_cycletype) - othermode_H.flagList.append(defaults.g_mdsft_pipeline) - - self.default_othermode_L = othermode_L - self.default_othermode_H = othermode_H + self.default_othermode_L = SPSetOtherMode("G_SETOTHERMODE_L", 0, 3 - self.is_f3d_old, set(othermode_L.values())) def bleed_fModel(self, fModel: FModel, fMeshes: dict[FMesh]): # walk fModel, no order to drawing is observed, so last_mat is not kept track of for drawLayer, fMesh in fMeshes.items(): - self.bleed_fmesh(fMesh, None, fMesh.draw, fModel.getAllMaterials().items(), fModel.getRenderMode(drawLayer)) + reset_cmd_dict = {} + self.bleed_fmesh( + None, + reset_cmd_dict, + fMesh.draw, + fModel.getAllMaterials().items(), + fModel.matWriteMethod, + fModel.getRenderMode(drawLayer), + ) + self.add_reset_cmds(fMesh.draw, reset_cmd_dict, fModel.matWriteMethod, fModel.getRenderMode(drawLayer)) self.clear_gfx_lists(fModel) # clear the gfx lists so they don't export @@ -132,51 +213,96 @@ def clear_gfx_lists(self, fModel: FModel): for tri_list in fMesh.triangleGroups: tri_list.triList.tag |= GfxListTag.NoExport + def add_reset_cmd( + self, f3d: F3D, cmd: GbiMacro, reset_cmd_dict: dict[GbiMacro], mat_write_method: GfxMatWriteMethod + ): + reset_cmd_list = (DPSetRenderMode,) + if SPGeometryMode not in reset_cmd_dict: + if mat_write_method == GfxMatWriteMethod.WriteAll: + reset_cmd_dict[SPGeometryMode] = ( + self.default_set_geo.flagList.copy(), + self.default_clear_geo.flagList.copy(), + ) + else: + reset_cmd_dict[SPGeometryMode] = set(), set() + get_flags(*reset_cmd_dict[SPGeometryMode], cmd) + if isinstance(cmd, SPSetOtherModeSub): + l: SPSetOtherMode = reset_cmd_dict.get("G_SETOTHERMODE_L") + h: SPSetOtherMode = reset_cmd_dict.get("G_SETOTHERMODE_H") + if l or h: # should never be reached, but if we reach it we are prepared + if h and cmd.is_othermodeh: + for existing_mode in [mode for mode in h.flagList if str(mode).startswith(cmd.mode_prefix)]: + h.flagList.remove(existing_mode) + h.flagList.add(cmd.mode) + if l and not cmd.is_othermodeh: + for existing_mode in [mode for mode in l.flagList if str(mode).startswith(cmd.mode_prefix)]: + l.flagList.remove(existing_mode) + l.flagList.add(cmd.mode) + else: + reset_cmd_dict[type(cmd)] = cmd + + # separate other mode H and othermode L + elif type(cmd) == SPSetOtherMode: + if cmd.cmd in reset_cmd_dict: + reset_cmd_dict[cmd.cmd].add_other(f3d, cmd) + else: + reset_cmd_dict[cmd.cmd] = copy.deepcopy(cmd) + + elif type(cmd) in reset_cmd_list: + reset_cmd_dict[type(cmd)] = cmd + def bleed_fmesh( self, - fMesh: FMesh, last_mat: FMaterial, + reset_cmd_dict: dict[type, GbiMacro], cmd_list: GfxList, fmodel_materials, - default_render_mode: list[str] = None, + mat_write_method: GfxMatWriteMethod, + default_render_mode: tuple[str] = None, ): - if bled_mat := self.bled_gfx_lists.get(cmd_list, None): + if bled_mat := self.bled_gfx_lists.get(id(cmd_list)): return bled_mat - bleed_state = self.bleed_start cur_fmat = None - reset_cmd_dict = dict() bleed_gfx_lists = BleedGfxLists() fmesh_static_cmds, fmesh_jump_cmds = self.on_bleed_start(cmd_list) + start_cmds = cmd_list.commands # commands that preceed any jump list for jump_list_cmd in fmesh_jump_cmds: # bleed mat and tex + if jump_list_cmd.displayList.tag & GfxListTag.MaterialRevert: + _, mat = find_material_from_jump_cmd(fmodel_materials, jump_list_cmd) + if mat is not None: + last_mat = mat if jump_list_cmd.displayList.tag & GfxListTag.Material: - # update last_mat - if cur_fmat: - last_mat = cur_fmat _, cur_fmat = find_material_from_jump_cmd(fmodel_materials, jump_list_cmd) if not cur_fmat: # make better error msg print("could not find material used in fmesh draw") continue - bleed_gfx_lists.bled_mats = self.bleed_mat(cur_fmat, last_mat, bleed_state) if not (cur_fmat.isTexLarge[0] or cur_fmat.isTexLarge[1]): bleed_gfx_lists.bled_tex = self.bleed_textures(cur_fmat, last_mat, bleed_state) else: bleed_gfx_lists.bled_tex = cur_fmat.texture_DL.commands + bleed_gfx_lists.bled_mats = self.bleed_mat( + cur_fmat, last_mat, start_cmds, mat_write_method, default_render_mode, bleed_state + ) + start_cmds = [] + last_mat = cur_fmat # bleed tri group (for large textures) and to remove other unnecessary cmds if jump_list_cmd.displayList.tag & GfxListTag.Geometry: tri_list = jump_list_cmd.displayList self.bleed_tri_group(tri_list, cur_fmat, bleed_state) - self.inline_triGroup(tri_list, bleed_gfx_lists, cmd_list, reset_cmd_dict) + self.inline_triGroup(tri_list, bleed_gfx_lists, cmd_list) self.on_tri_group_bleed_end(tri_list, cur_fmat, bleed_gfx_lists) # reset bleed gfx lists after inlining bleed_gfx_lists = BleedGfxLists() # set bleed state for cmd reverts bleed_state = self.bleed_in_progress - - last_mat = cur_fmat - self.on_bleed_end(last_mat, cmd_list, fmesh_static_cmds, reset_cmd_dict, default_render_mode) + cmd_list.commands.extend(fmesh_static_cmds) # this is troublesome + cmd_list.commands.append(SPEndDisplayList()) + self.optimize_syncs(cmd_list) # some syncs may become redundant after bleeding + [self.add_reset_cmd(self.f3d, cmd, reset_cmd_dict, mat_write_method) for cmd in cmd_list.commands] + self.bled_gfx_lists[id(cmd_list)] = cur_fmat return last_mat def build_tmem_dict(self, cmd_list: GfxList): @@ -226,7 +352,7 @@ def bleed_textures(self, cur_fmat: FMaterial, last_mat: FMaterial, bleed_state: for j, cmd in enumerate(cur_fmat.texture_DL.commands): if not cmd: continue # some cmds are None from previous step - if self.bleed_individual_cmd(commands_bled, cmd, bleed_state, last_mat.texture_DL.commands) is True: + if self.bleed_individual_cmd(commands_bled, cmd, last_mat.texture_DL.commands) is True: commands_bled.commands[j] = None # remove Nones from list while None in commands_bled.commands: @@ -236,23 +362,98 @@ def bleed_textures(self, cur_fmat: FMaterial, last_mat: FMaterial, bleed_state: bled_tex = cur_fmat.texture_DL return bled_tex.commands - def bleed_mat(self, cur_fmat: FMaterial, last_mat: FMaterial, bleed_state: int): + def bleed_mat( + self, + cur_fmat: FMaterial, + last_mat: FMaterial, + start_cmds: list[GbiMacro], + mat_write_method: GfxMatWriteMethod, + default_render_mode: list[str], + bleed_state: int, + ): + if mat_write_method == GfxMatWriteMethod.WriteAll: + new_sets, new_clears = self.default_set_geo.flagList.copy(), self.default_clear_geo.flagList.copy() + previous_sets, previous_clears = ( + self.default_set_geo.flagList.copy(), + self.default_clear_geo.flagList.copy(), + ) + revert_sets, revert_clears = self.default_set_geo.flagList.copy(), self.default_clear_geo.flagList.copy() + else: + new_sets, new_clears = set(), set() + previous_sets, previous_clears = set(), set() + revert_sets, revert_clears = set(), set() + revert_other_diff_cmd, revert_other_load_cmd, othermode_diff_cmds, last_cmd_list = [], [], [], [] + [get_flags(new_sets, new_clears, cmd, self.default_clear_geo) for cmd in cur_fmat.mat_only_DL.commands] + if last_mat: gfx = cur_fmat.mat_only_DL # deep copy breaks on Image objects so I will only copy the levels needed commands_bled = copy.copy(gfx) commands_bled.commands = copy.copy(gfx.commands) # copy the commands also - last_cmd_list = last_mat.mat_only_DL.commands - for j, cmd in enumerate(gfx.commands): - if self.bleed_individual_cmd(commands_bled, cmd, bleed_state, last_cmd_list): - commands_bled.commands[j] = None - # remove Nones from list - while None in commands_bled.commands: - commands_bled.commands.remove(None) + last_cmd_list = last_mat.mat_only_DL.commands + start_cmds + [get_flags(previous_sets, previous_clears, cmd, self.default_clear_geo) for cmd in last_cmd_list] + + # handle write diff reverts + othermode_diff_cmds = [c for c in commands_bled.commands if isinstance(c, WRITE_DIFF_OTHERMODE_CMDS)] + if last_mat.revert: + [get_flags(revert_sets, revert_clears, cmd, self.default_clear_geo) for cmd in last_mat.revert.commands] + revert_other_diff_cmd = [ + c for c in last_mat.revert.commands if isinstance(c, WRITE_DIFF_OTHERMODE_CMDS) + ] + revert_other_load_cmd = [ + copy.deepcopy(c) for c in last_mat.revert.commands if isinstance(c, SPSetOtherMode) + ] + # while load mode is always written, they may not set the same range of values and therefor need revert + for revert_cmd in revert_other_load_cmd: + othermode_cmd = next( + (c for c in commands_bled.commands if type(c) == type(revert_cmd) and c.cmd == revert_cmd.cmd), None + ) + if othermode_cmd is None: + commands_bled.commands.insert(0, revert_cmd) + else: + index = commands_bled.commands.index(othermode_cmd) + revert_cmd.add_other(self.f3d, othermode_cmd) + commands_bled.commands[index] = revert_cmd + commands_bled.commands = [ + cmd + for cmd in commands_bled.commands + if not self.bleed_individual_cmd(commands_bled, cmd, last_cmd_list, default_render_mode) + ] else: - commands_bled = self.bleed_cmd_list(cur_fmat.mat_only_DL, bleed_state) - # some syncs may become redundant after bleeding - self.optimize_syncs(commands_bled, bleed_state) + [get_flags(previous_sets, previous_clears, cmd, self.default_clear_geo) for cmd in start_cmds] + commands_bled = self.bleed_cmd_list(cur_fmat.mat_only_DL, default_render_mode, bleed_state) + + # remove all geo cmds to add later + commands_bled.commands = [cmd for cmd in commands_bled.commands if not isinstance(cmd, GEO_CMDS)] + + # remove clears and sets from revert if they will be set later in start or this material + revert_clears, revert_sets = ( + revert_clears - previous_clears - new_sets, + revert_sets - previous_sets - new_clears, + ) + if mat_write_method == GfxMatWriteMethod.WriteAll: + if previous_clears != new_clears or previous_sets != new_sets: + set_modes, clear_modes = new_sets | revert_sets, new_clears | revert_clears + # add back removed geo cmds, reverts and start cmds + for cmd in get_geo_cmds(clear_modes, set_modes, self.f3d.F3DEX_GBI_2, mat_write_method)[0]: + commands_bled.commands.insert(0, cmd) + else: + # remove clears and sets from the material if set in start + new_clears, new_sets = new_clears - previous_clears, new_sets - previous_sets + # combine + set_modes, clear_modes = new_sets | revert_sets, new_clears | revert_clears + clear_modes, set_modes = clear_modes - set_modes, set_modes - clear_modes + + # add back removed geo cmds and reverts + for cmd in get_geo_cmds(clear_modes, set_modes, self.f3d.F3DEX_GBI_2, mat_write_method)[0]: + commands_bled.commands.insert(0, cmd) + + # if there is no equivelent othermode cmd, it must be using the revert + for revert_cmd in revert_other_diff_cmd: + othermode_cmd = next((cmd for cmd in othermode_diff_cmds if type(cmd) == type(revert_cmd)), None) + if othermode_cmd is None: + commands_bled.commands.insert(0, revert_cmd) + # remove SPEndDisplayList while SPEndDisplayList() in commands_bled.commands: commands_bled.commands.remove(SPEndDisplayList()) @@ -263,16 +464,16 @@ def bleed_tri_group(self, tri_list: GfxList, cur_fmat: fMaterial, bleed_state: i while SPEndDisplayList() in tri_list.commands: tri_list.commands.remove(SPEndDisplayList()) if not cur_fmat or (cur_fmat.isTexLarge[0] or cur_fmat.isTexLarge[1]): - tri_list = self.bleed_cmd_list(tri_list, bleed_state) + tri_list = self.bleed_cmd_list(tri_list, None, bleed_state) # this is a little less versatile than comparing by last used material - def bleed_cmd_list(self, target_cmd_list: GfxList, bleed_state: int): + def bleed_cmd_list(self, target_cmd_list: GfxList, default_render_mode: list[str], bleed_state: int): usage_dict = dict() commands_bled = copy.copy(target_cmd_list) # copy the commands commands_bled.commands = copy.copy(target_cmd_list.commands) # copy the commands for j, cmd in enumerate(target_cmd_list.commands): # some cmds you can bleed vs world defaults, others only if they repeat within this gfx list - bleed_cmd_status = self.bleed_individual_cmd(commands_bled, cmd, bleed_state) + bleed_cmd_status = self.bleed_individual_cmd(commands_bled, cmd, default_render_mode=default_render_mode) if not bleed_cmd_status: continue last_use = usage_dict.get((type(cmd), getattr(cmd, "tile", None)), None) @@ -285,20 +486,13 @@ def bleed_cmd_list(self, target_cmd_list: GfxList, bleed_state: int): return commands_bled # Put triGroup bleed gfx in the FMesh.draw object - def inline_triGroup( - self, tri_list: GfxList, bleed_gfx_lists: BleedGfxLists, cmd_list: GfxList, reset_cmd_dict: dict[GbiMacro] - ): + def inline_triGroup(self, tri_list: GfxList, bleed_gfx_lists: BleedGfxLists, cmd_list: GfxList): # add material cmd_list.commands.extend(bleed_gfx_lists.bled_mats) # add textures cmd_list.commands.extend(bleed_gfx_lists.bled_tex) # add in triangles cmd_list.commands.extend(tri_list.commands) - # skinned meshes don't draw tris sometimes, use this opportunity to save a sync - tri_cmds = [c for c in tri_list.commands if type(c) == SP1Triangle or type(c) == SP2Triangles] - if tri_cmds: - reset_cmd_dict[DPPipeSync] = DPPipeSync() - [bleed_gfx_lists.add_reset_cmd(cmd, reset_cmd_dict) for cmd in bleed_gfx_lists.bled_mats] # pre processes cmd_list and removes cmds deemed useless. subclass and override if this causes a game specific issue def on_bleed_start(self, cmd_list: GfxList): @@ -334,82 +528,104 @@ def on_bleed_start(self, cmd_list: GfxList): def on_tri_group_bleed_end(self, triGroup: FTriGroup, last_mat: FMaterial, bleed_gfx_lists: BleedGfxLists): return - def on_bleed_end( + def add_reset_cmds( self, - last_mat: FMaterial, cmd_list: GfxList, - fmesh_static_cmds: list[GbiMacro], reset_cmd_dict: dict[GbiMacro], - default_render_mode: list[str] = None, + mat_write_method: GfxMatWriteMethod, + default_render_mode: tuple[str] = None, ): + if not cmd_list or not reset_cmd_dict or id(cmd_list) in self.reset_gfx_lists: + return False # revert certain cmds for extra safety - reset_cmds = self.create_reset_cmds(reset_cmd_dict, default_render_mode) - # if pipe sync in reset list, make sure it is the first cmd - if DPPipeSync in reset_cmds: - reset_cmds.remove(DPPipeSync) - reset_cmds.insert(0, DPPipeSync) + reset_cmds = self.create_reset_cmds(reset_cmd_dict, mat_write_method, default_render_mode) + while SPEndDisplayList() in cmd_list.commands: + cmd_list.commands.remove(SPEndDisplayList()) cmd_list.commands.extend(reset_cmds) - cmd_list.commands.extend(fmesh_static_cmds) # this is troublesome cmd_list.commands.append(SPEndDisplayList()) - self.bled_gfx_lists[cmd_list] = last_mat + self.optimize_syncs(cmd_list) + self.reset_gfx_lists.add(id(cmd_list)) + return True # remove syncs if first material, or if no gsDP cmds in material - def optimize_syncs(self, cmd_list: GfxList, bleed_state: int): + def optimize_syncs(self, cmd_list: GfxList): no_syncs_needed = {"DPSetPrimColor", "DPSetPrimDepth"} # will not affect rdp - syncs_needed = {"SPSetOtherMode"} # will affect rdp - if bleed_state == self.bleed_start: - while DPPipeSync() in cmd_list.commands: - cmd_list.commands.remove(DPPipeSync()) - for cmd in cmd_list.commands: + syncs_needed = {"SPSetOtherMode", "SPTexture"} # will affect rdp + + tri_buffered = True + last_load_sync = None + old_cmds = cmd_list.commands + new_cmds = [] + cmd_list.commands = new_cmds + + for cmd in old_cmds: cmd_name = type(cmd).__name__ - if cmd == DPPipeSync(): + is_dp_cmd = ("DP" in cmd_name and cmd_name not in no_syncs_needed) or cmd_name in syncs_needed + if isinstance(cmd, (DPPipeSync, DPLoadSync, DPTileSync)): continue - if "DP" in cmd_name and cmd_name not in no_syncs_needed: - return - if cmd_name in syncs_needed: - return - while DPPipeSync() in cmd_list.commands: - cmd_list.commands.remove(DPPipeSync()) - - def create_reset_cmds(self, reset_cmd_dict: dict[GbiMacro], default_render_mode: list[str]): + elif isinstance(cmd, (DPLoadBlock, DPLoadTile, DPLoadTLUTCmd, DPSetTile, DPSetTileSize)) and tri_buffered: + last_load_sync = len(new_cmds) + new_cmds.append(DPLoadSync()) + tri_buffered = False + elif tri_buffered and is_dp_cmd: + tri_buffered = False + if last_load_sync is not None: + new_cmds[last_load_sync] = DPPipeSync() + last_load_sync = None + else: + new_cmds.append(DPPipeSync()) + elif not is_dp_cmd and isinstance(cmd, (SP2Triangles, SP1Triangle, SPLine3D, SPLineW3D)): + tri_buffered = True + last_load_sync = None + new_cmds.append(cmd) + + def create_reset_cmds( + self, reset_cmd_dict: dict[GbiMacro], mat_write_method: GfxMatWriteMethod, default_render_mode: list[str] + ): reset_cmds = [] for cmd_type, cmd_use in reset_cmd_dict.items(): - if cmd_type == DPPipeSync: - reset_cmds.append(DPPipeSync()) - - # generally either loadgeo, or a combo of set/clear is used based on microcode selected - # if you are in f3d, any selection different from the default will add a set/clear - if cmd_type == SPLoadGeometryMode and cmd_use != self.default_load_geo: - reset_cmds.append(self.default_load_geo) - - elif cmd_type == SPSetGeometryMode and cmd_use != self.default_set_geo: - reset_cmds.append(self.default_set_geo) - - elif cmd_type == SPClearGeometryMode and cmd_use != self.default_clear_geo: - reset_cmds.append(self.default_clear_geo) - + if cmd_type == SPGeometryMode: # revert cmd includes everything from the start + set_list, clear_list = cmd_use + if mat_write_method == GfxMatWriteMethod.WriteDifferingAndRevert: + clear_list = clear_list - self.default_clear_geo.flagList + set_list = set_list - self.default_set_geo.flagList + reset_cmds.extend(get_geo_cmds(clear_list, set_list, self.f3d.F3DEX_GBI_2, mat_write_method)[1]) + elif clear_list != self.default_clear_geo.flagList or set_list != self.default_set_geo.flagList: + reset_cmds.append(self.default_load_geo) elif cmd_type == "G_SETOTHERMODE_H": if cmd_use != self.default_othermode_H: reset_cmds.append(self.default_othermode_H) - # render mode takes up most bits of the lower half, so seeing high bit usage is enough to determine render mode was used - elif cmd_type == DPSetRenderMode or (cmd_type == "G_SETOTHERMODE_L" and cmd_use.length >= 31): - if default_render_mode: - reset_cmds.append( - SPSetOtherMode( - "G_SETOTHERMODE_L", - 0, - 32 - self.is_f3d_old, - [*self.default_othermode_L.flagList, *default_render_mode], - ) - ) + elif cmd_type == DPSetRenderMode: + if default_render_mode and cmd_use.flagList != default_render_mode: + reset_cmds.append(DPSetRenderMode(tuple(default_render_mode))) elif cmd_type == "G_SETOTHERMODE_L": - if cmd_use != self.default_othermode_L: - reset_cmds.append(self.default_othermode_L) + flag_list = copy.copy(self.default_othermode_L.flagList) + if cmd_use.sets_rendermode(self.f3d): + flag_list.update(default_render_mode) + default_othermode_l = SPSetOtherMode( + "G_SETOTHERMODE_L", + 0, + (32 if cmd_use.sets_rendermode(self.f3d) else 3) - self.is_f3d_old, + flag_list, + ) + if cmd_use != default_othermode_l: + reset_cmds.append(default_othermode_l) + + elif isinstance(cmd_use, SPSetOtherModeSub): + default = self.default_othermode_dict[cmd_type] + if cmd_use.mode != default: + reset_cmds.append(cmd_type(default)) return reset_cmds - def bleed_individual_cmd(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None): + def bleed_individual_cmd( + self, + cmd_list: GfxList, + cmd: GbiMacro, + last_cmd_list: GfxList = None, + default_render_mode: tuple[str] = None, + ): # never bleed these cmds if type(cmd) in [ SPMatrix, @@ -434,44 +650,28 @@ def bleed_individual_cmd(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: in ]: return False - # if no last list then calling func will own behavior of bleeding - if not last_cmd_list: - return self.bleed_self_conflict + if last_cmd_list is None: + if isinstance(cmd, SPSetOtherModeSub): + return cmd.mode == self.default_othermode_dict[type(cmd)] + elif isinstance(cmd, DPSetRenderMode): + return cmd.flagList == default_render_mode and cmd.blender is None # apply specific logic to these cmds, see functions below, otherwise default behavior is to bleed if cmd is in the last list bleed_func = getattr(self, (f"bleed_{type(cmd).__name__}"), None) if bleed_func: - return bleed_func(cmd_list, cmd, bleed_state, last_cmd_list) + return bleed_func(cmd_list, cmd, last_cmd_list) else: - return cmd in last_cmd_list + return last_cmd_list is not None and cmd in last_cmd_list # bleed these cmds only if it is the second call and cmd was in the last use list, or if they match world defaults and it is the first call - def bleed_SPLoadGeometryMode( - self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None - ): - if bleed_state != self.bleed_start: + def bleed_SPLoadGeometryMode(self, cmd_list: GfxList, cmd: GbiMacro, last_cmd_list: GfxList = None): + if last_cmd_list is not None: return cmd in last_cmd_list else: return cmd == self.default_load_geo - def bleed_SPSetGeometryMode( - self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None - ): - if bleed_state != self.bleed_start: - return cmd in last_cmd_list - else: - return cmd == self.default_set_geo - - def bleed_SPClearGeometryMode( - self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None - ): - if bleed_state != self.bleed_start: - return cmd in last_cmd_list - else: - return cmd == self.default_clear_geo - - def bleed_SPSetOtherMode(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None): - if bleed_state != self.bleed_start: + def bleed_SPSetOtherMode(self, cmd_list: GfxList, cmd: GbiMacro, last_cmd_list: GfxList = None): + if last_cmd_list is not None: return cmd in last_cmd_list else: if cmd.cmd == "G_SETOTHERMODE_H": @@ -480,43 +680,15 @@ def bleed_SPSetOtherMode(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: in return cmd == self.default_othermode_L # Don´t bleed if the cmd is used for scrolling or if the last cmd's tags are not the same (those are not hashed) - def bleed_DPSetTileSize(self, _cmd_list: GfxList, cmd: GbiMacro, _bleed_state: int, last_cmd_list: GfxList = None): + def bleed_DPSetTileSize(self, _cmd_list: GfxList, cmd: GbiMacro, last_cmd_list: GfxList = None): if cmd.tags == GfxTag.TileScroll0 or cmd.tags == GfxTag.TileScroll1: return False - if cmd in last_cmd_list: + if last_cmd_list is not None and cmd in last_cmd_list: last_size_cmd = last_cmd_list[last_cmd_list.index(cmd)] if last_size_cmd.tags == cmd.tags: return True return False - # At most, only one sync is needed after drawing tris. The f3d writer should - # already have placed the appropriate sync type required. If a second sync is - # detected between drawing cmds, then remove that sync. Remove the latest sync - # not the first seen sync. - def bleed_DPTileSync(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None): - return self.bleed_between_tris(cmd_list, cmd, bleed_state, [DPLoadSync, DPPipeSync, DPTileSync]) - - def bleed_DPPipeSync(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None): - return self.bleed_between_tris(cmd_list, cmd, bleed_state, [DPLoadSync, DPPipeSync, DPTileSync]) - - def bleed_DPLoadSync(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None): - return self.bleed_between_tris(cmd_list, cmd, bleed_state, [DPLoadSync, DPPipeSync, DPTileSync]) - - def bleed_between_tris(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, conflict_cmds: list[GbiMacro]): - tri_buffered = False - for parse_cmd in cmd_list.commands: - if parse_cmd is cmd: - return tri_buffered - if type(parse_cmd) in [SP2Triangles, SP1Triangle, SPLine3D, SPLineW3D]: - tri_buffered = False - continue - if type(parse_cmd) in conflict_cmds: - if not tri_buffered: - tri_buffered = True - else: - return True - return False - # small containers for data used in inline Gfx @dataclass @@ -524,20 +696,6 @@ class BleedGfxLists: bled_mats: GfxList = field(default_factory=list) bled_tex: GfxList = field(default_factory=list) - def add_reset_cmd(self, cmd: GbiMacro, reset_cmd_dict: dict[GbiMacro]): - reset_cmd_list = ( - SPLoadGeometryMode, - SPSetGeometryMode, - SPClearGeometryMode, - DPSetRenderMode, - ) - # separate other mode H and othermode L - if type(cmd) == SPSetOtherMode: - reset_cmd_dict[cmd.cmd] = cmd - - if type(cmd) in reset_cmd_list: - reset_cmd_dict[type(cmd)] = cmd - # helper function used for sm64 def find_material_from_jump_cmd( @@ -549,8 +707,8 @@ def find_material_from_jump_cmd( for mat in material_list: fmaterial = mat[1][0] bpy_material = mat[0][0] - if dl_jump.displayList.tag == GfxListTag.MaterialRevert and fmaterial.revert == dl_jump.displayList: + if dl_jump.displayList.tag & GfxListTag.MaterialRevert and fmaterial.revert == dl_jump.displayList: return bpy_material, fmaterial - elif fmaterial.material == dl_jump.displayList: + elif dl_jump.displayList.tag & GfxListTag.Material and fmaterial.material == dl_jump.displayList: return bpy_material, fmaterial return None, None diff --git a/fast64_internal/f3d/f3d_enums.py b/fast64_internal/f3d/f3d_enums.py index 993a87316..46e2ac678 100644 --- a/fast64_internal/f3d/f3d_enums.py +++ b/fast64_internal/f3d/f3d_enums.py @@ -322,7 +322,7 @@ ("G_RM_AA_ZB_XLU_DECAL2", "Transparent Decal", "G_RM_AA_ZB_XLU_DECAL2"), ("G_RM_AA_ZB_XLU_INTER2", "Transparent Intersecting", "G_RM_AA_ZB_XLU_INTER2"), ("G_RM_ADD2", "Add", "G_RM_ADD2"), - ("G_RM_NOOP", "No Op", "G_RM_NOOP"), + ("G_RM_NOOP2", "No Op", "G_RM_NOOP2"), ("G_RM_ZB_OPA_SURF2", "Opaque (No AA)", "G_RM_ZB_OPA_SURF2"), ("G_RM_ZB_OPA_DECAL2", "Opaque Decal (No AA)", "G_RM_ZB_OPA_DECAL2"), ("G_RM_ZB_XLU_SURF2", "Transparent (No AA)", "G_RM_ZB_XLU_SURF2"), @@ -378,13 +378,35 @@ } enumF3D = [ - ("F3D", "F3D", "Original microcode used in SM64"), - ("F3DEX/LX", "F3DEX/LX", "F3DEX version 1"), - ("F3DLX.Rej", "F3DLX.Rej", "F3DLX.Rej"), - ("F3DLP.Rej", "F3DLP.Rej", "F3DLP.Rej"), - ("F3DEX2/LX2", "F3DEX2/LX2/ZEX", "Family of microcodes used in later N64 games including OoT and MM"), - ("F3DEX2.Rej/LX2.Rej", "F3DEX2.Rej/LX2.Rej", "Variant of F3DEX2 family using vertex rejection instead of clipping"), - ("F3DEX3", "F3DEX3", "Custom microcode by Sauraen"), + ("", "F3D Family", "", 7), + ("F3D", "F3D", "Original microcode used in SM64", 0), + ("F3DEX/LX", "F3DEX/LX", "F3DEX version 1", 1), + ("F3DLX.Rej", "F3DLX.Rej", "F3DLX.Rej", 2), + ("F3DLP.Rej", "F3DLP.Rej", "F3DLP.Rej", 3), + ("F3DEX2/LX2", "F3DEX2/LX2/ZEX", "Family of microcodes used in later N64 games including OoT and MM", 4), + ( + "F3DEX2.Rej/LX2.Rej", + "F3DEX2.Rej/LX2.Rej", + "Variant of F3DEX2 family using vertex rejection instead of clipping", + 5, + ), + ("F3DEX3", "F3DEX3", "Custom microcode by Sauraen", 6), + ("", "Homebrew", "", 8), + ("RDPQ", "RDPQ", "Base libdragon microcode", 9), + ("T3D", "Tiny3D", "Custom libdragon microcode by HailToDodongo", 10), +] + +enumPackedNormalsAlgorithm = [ + ( + "565", + "565 (T3D, new EX3)", + "Packing algorithm by HailToDodongo used by Tiny3D and newer F3DEX3, simply concatenates 5, 6, and 5 bits of X, Y, Z", + ), + ( + "Octahedral", + "Octahedral (old EX3)", + "Packing algorithm by Sauraen used in older F3DEX3, based on octahedral encoding", + ), ] enumLargeEdges = [ diff --git a/fast64_internal/f3d/f3d_gbi.py b/fast64_internal/f3d/f3d_gbi.py index cb04fed6f..134338f84 100644 --- a/fast64_internal/f3d/f3d_gbi.py +++ b/fast64_internal/f3d/f3d_gbi.py @@ -71,6 +71,7 @@ class GfxMatWriteMethod(enum.Enum): "F3DEX2/LX2": (32, 32), "F3DEX2.Rej/LX2.Rej": (64, 64), "F3DEX3": (56, 56), + "T3D": (70, 70), } sm64_default_draw_layers = { @@ -145,6 +146,14 @@ def isUcodeF3DEX3(F3D_VER: str) -> bool: return F3D_VER == "F3DEX3" +def is_ucode_t3d(UCODE_VER: str) -> bool: + return UCODE_VER == "T3D" + + +def is_ucode_f3d(UCODE_VER: str) -> bool: + return UCODE_VER not in {"T3D", "RDPQ"} + + class F3D: """NOTE: do not initialize this class manually! use get_F3D_GBI so that the single instance is cached from the microcode type.""" @@ -155,6 +164,7 @@ def __init__(self, F3D_VER): F3DEX_GBI_3 = self.F3DEX_GBI_3 = isUcodeF3DEX3(F3D_VER) F3DLP_GBI = self.F3DLP_GBI = self.F3DEX_GBI self.F3D_OLD_GBI = not (F3DEX_GBI or F3DEX_GBI_2 or F3DEX_GBI_3) + self.F3D_GBI = is_ucode_f3d(F3D_VER) # F3DEX2 is F3DEX1 and F3DEX3 is F3DEX2, but F3DEX3 is not F3DEX1 if F3DEX_GBI_2: @@ -162,8 +172,12 @@ def __init__(self, F3D_VER): elif F3DEX_GBI_3: F3DEX_GBI_2 = self.F3DEX_GBI_2 = True - self.vert_buffer_size = vertexBufferSize[F3D_VER][0] - self.vert_load_size = vertexBufferSize[F3D_VER][1] + if F3D_VER in vertexBufferSize: + self.vert_buffer_size = vertexBufferSize[F3D_VER][0] + self.vert_load_size = vertexBufferSize[F3D_VER][1] + else: + self.vert_buffer_size = self.vert_load_size = None + self.G_MAX_LIGHTS = 9 if F3DEX_GBI_3 else 7 self.G_INPUT_BUFFER_CMDS = 21 @@ -2065,7 +2079,8 @@ def vertexScrollToC(self, fMaterial: FMaterial, vtxListName: str, vtxCount: int) """ return CScrollData() - def drawToC(self, f3d: F3D, gfxList: "GfxList") -> CData: + # `layer`` argument used for Z64 overrides + def drawToC(self, f3d: F3D, gfxList: "GfxList", layer: Optional[str] = None) -> CData: """ Called for building the entry point DL for drawing a model. """ @@ -2203,10 +2218,13 @@ def to_binary(self, f3d, segments): data.extend(command.to_binary(f3d, segments)) return data - def to_c_static(self): - data = f"Gfx {self.name}[] = {{\n" + 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 @@ -2235,16 +2253,19 @@ def to_xml(self, modelDirPath, objectPath, *args, **kwargs): return data - def to_c(self, f3d): + def to_c(self, f3d, name_override: Optional[str] = None): data = CData() + name = name_override if name_override is not None else self.name + if self.DLFormat == DLFormat.Static: - data.header = f"extern Gfx {self.name}[];\n" - data.source = self.to_c_static() + data.header = f"extern Gfx {name}[];\n" + data.source = self.to_c_static(name) elif self.DLFormat == DLFormat.Dynamic: - data.header = f"Gfx* {self.name}(Gfx* glistp);\n" + data.header = f"Gfx* {name}(Gfx* glistp);\n" data.source = self.to_c_dynamic() else: raise PluginError("Invalid GfxList format: " + str(self.DLFormat)) + return data @@ -2274,12 +2295,12 @@ def makeKey(self): return self.fog_data.makeKey() def requiresKey(self, material): - return self.fog_data.requiresKey(material) + return self.fog_data and self.fog_data.requiresKey(material) class FGlobalData: def __init__(self): - # dict of area index : FFogData + # dict of area index : FAreaData self.area_data = {} self.current_area_index = 1 @@ -2431,6 +2452,10 @@ def __init__( self.materialRevert: Union[GfxList, None] = None # F3D library self.f3d: F3D = get_F3D_GBI() + if not self.f3d.F3D_GBI: + raise PluginError( + f"Current microcode {self.f3d.F3D_VER} is not part of the f3d family of microcodes, fast64 cannot export it" + ) # array of FModel self.subModels: list[FModel] = [] self.parentModel: Union[FModel, None] = None @@ -2452,7 +2477,7 @@ def processTexRefNonCITextures(self, fMaterial: FMaterial, material: bpy.types.M - an object containing info about the additional textures, or None """ texProp = getattr(material.f3d_mat, f"tex{index}") - imDependencies = [] if texProp.tex is None else [texProp.tex] + imDependencies = set() if texProp.tex is None else {texProp.tex} return imDependencies, None def writeTexRefNonCITextures(self, obj, texFmt: str): @@ -2472,7 +2497,7 @@ def processTexRefCITextures(self, fMaterial: FMaterial, material: bpy.types.Mate - the palette to use (or None) """ texProp = getattr(material.f3d_mat, f"tex{index}") - imDependencies = [] if texProp.tex is None else [texProp.tex] + imDependencies = set() if texProp.tex is None else {texProp.tex} return imDependencies, None, None def writeTexRefCITextures( @@ -2522,14 +2547,17 @@ def addLight(self, key, value, fMaterial): fMaterial.usedLights.append(key) self.lights[key] = value - def addMesh(self, name, namePrefix, drawLayer, isSkinned, contextObj): - meshName = getFMeshName(self, name, namePrefix, drawLayer, isSkinned) - checkUniqueBoneNames(self, meshName, name) - self.meshes[meshName] = FMesh(meshName, self.DLFormat) - - self.onAddMesh(self.meshes[meshName], contextObj) + def addMesh(self, name, namePrefix, drawLayer, isSkinned, contextObj, dedup=False): + final_name = getFMeshName(self, name, namePrefix, drawLayer, isSkinned) + if dedup: + for i in range(1, len(self.meshes) + 2): + if final_name in self.meshes: + final_name = f"{name}_{i:03}" + checkUniqueBoneNames(self, final_name, name) + self.meshes[final_name] = mesh = FMesh(final_name, self.DLFormat) + self.onAddMesh(mesh, contextObj) + return mesh - return self.meshes[meshName] def onAddMesh(self, fMesh, contextObj): return @@ -3126,9 +3154,7 @@ def __init__(self, name, DLFormat): self.triangleGroups: list[FTriGroup] = [] # VtxList self.cullVertexList = None - # dict of (override Material, specified Material to override, - # overrideType, draw layer) : GfxList - self.drawMatOverrides = {} + self.draw_overrides: list[GfxList] = [] self.DLFormat = DLFormat # Used to avoid consecutive calls to the same material if unnecessary @@ -3151,8 +3177,8 @@ def get_ptr_addresses(self, f3d): addresses = self.draw.get_ptr_addresses(f3d) for triGroup in self.triangleGroups: addresses.extend(triGroup.get_ptr_addresses(f3d)) - for materialTuple, drawOverride in self.drawMatOverrides.items(): - addresses.extend(drawOverride.get_ptr_addresses(f3d)) + for cmd_list in self.draw_overrides: + addresses.extend(cmd_list.get_ptr_addresses(f3d)) return addresses def tri_group_new(self, fMaterial): @@ -3168,8 +3194,8 @@ def set_addr(self, startAddress, f3d): addrRange = triGroup.set_addr(addrRange[1], f3d) if self.cullVertexList is not None: addrRange = self.cullVertexList.set_addr(addrRange[1]) - for materialTuple, drawOverride in self.drawMatOverrides.items(): - addrRange = drawOverride.set_addr(addrRange[1], f3d) + for cmd_list in self.draw_overrides: + addrRange = cmd_list.set_addr(addrRange[1], f3d) return startAddress, addrRange[1] def save_binary(self, romfile, f3d, segments): @@ -3178,8 +3204,8 @@ def save_binary(self, romfile, f3d, segments): triGroup.save_binary(romfile, f3d, segments) if self.cullVertexList is not None: self.cullVertexList.save_binary(romfile) - for materialTuple, drawOverride in self.drawMatOverrides.items(): - drawOverride.save_binary(romfile, f3d, segments) + for cmd_list in self.draw_overrides: + cmd_list.save_binary(romfile, f3d, segments) # OTRTODO def to_xml(self, modelDirPath, objectPath, logging_func): @@ -3203,7 +3229,7 @@ def to_xml(self, modelDirPath, objectPath, logging_func): logging_func({"INFO"}, "FMesh.to_xml 2") # data += "\n" - for materialTuple, drawOverride in self.drawMatOverrides.items(): + for drawOverride in self.draw_overrides: data += drawOverride.to_xml(modelDirPath) # data += "\n" @@ -3221,15 +3247,21 @@ def to_xml(self, modelDirPath, objectPath, logging_func): return data - def to_c(self, f3d, gfxFormatter): + def to_c(self, f3d: F3D, gfxFormatter: GfxFormatter): staticData = CData() + if self.cullVertexList is not None: staticData.append(self.cullVertexList.to_c()) + for triGroup in self.triangleGroups: staticData.append(triGroup.to_c(f3d, gfxFormatter)) - dynamicData = gfxFormatter.drawToC(f3d, self.draw) - for materialTuple, drawOverride in self.drawMatOverrides.items(): - dynamicData.append(drawOverride.to_c(f3d)) + + draw_layer = "Opaque" if "Opaque" in self.name else "Transparent" if "Transparent" in self.name else "Overlay" + dynamicData = gfxFormatter.drawToC(f3d, self.draw, layer=draw_layer) + + for cmd_list in self.draw_overrides: + dynamicData.append(cmd_list.to_c(f3d)) + return staticData, dynamicData @@ -3250,14 +3282,17 @@ def get_ptr_addresses(self, f3d): return self.triList.get_ptr_addresses(f3d) def set_addr(self, startAddress, f3d): - addrRange = self.triList.set_addr(startAddress, f3d) + addrRange = (startAddress, startAddress) + if self.triList.tag.Export: + addrRange = self.triList.set_addr(startAddress, f3d) addrRange = self.vertexList.set_addr(addrRange[1]) return startAddress, addrRange[1] def save_binary(self, romfile, f3d, segments): for celTriList in self.celTriLists: celTriList.save_binary(romfile, f3d, segments) - self.triList.save_binary(romfile, f3d, segments) + if self.triList.tag.Export: + self.triList.save_binary(romfile, f3d, segments) self.vertexList.save_binary(romfile) def to_xml(self, modelDirPath, objectPath, logging_func): @@ -3334,7 +3369,11 @@ def __init__(self, name, DLFormat): self.material = GfxList(f"mat_{name}", GfxListTag.Material, DLFormat) self.mat_only_DL = GfxList(f"mat_only_{name}", GfxListTag.Material, DLFormat) self.texture_DL = GfxList(f"tex_{name}", GfxListTag.Material, DLFormat.Static) - self.revert = GfxList(f"mat_revert_{name}", GfxListTag.MaterialRevert, DLFormat.Static) + + self.revert: Optional[GfxList] = None + if bpy.context.scene.gameEditorMode not in {"OOT", "MM"}: + self.revert = GfxList(f"mat_revert_{name}", GfxListTag.MaterialRevert, DLFormat.Static) + self.DLFormat = DLFormat self.scrollData = FScrollData() @@ -3399,15 +3438,17 @@ def get_ptr_addresses(self, f3d): return addresses def set_addr(self, startAddress, f3d): - addrRange = self.material.set_addr(startAddress, f3d) - startAddress = addrRange[0] - if self.revert is not None: + addrRange = (startAddress, startAddress) + if self.material.tag.Export: + addrRange = self.material.set_addr(addrRange[1], f3d) + if self.revert is not None and self.revert.tag.Export: addrRange = self.revert.set_addr(addrRange[1], f3d) return startAddress, addrRange[1] def save_binary(self, romfile, f3d, segments): - self.material.save_binary(romfile, f3d, segments) - if self.revert is not None: + if self.material.tag.Export: + self.material.save_binary(romfile, f3d, segments) + if self.revert is not None and self.revert.tag.Export: self.revert.save_binary(romfile, f3d, segments) def to_xml(self, modelDirPath, objectPath): @@ -3636,6 +3677,10 @@ class FImage: isLargeTexture: bool = field(init=False, compare=False, default=False) converted: bool = field(init=False, compare=False, default=False) + @property + def aligner_name(self): + return f"{self.name}_aligner" + def size(self): return len(self.data) @@ -3654,7 +3699,7 @@ def to_c_helper(self, texData, bitsPerValue): # This is to force 8 byte alignment if bitsPerValue != 64: - code.source = f"Gfx {self.name}_aligner[] = {{gsSPEndDisplayList()}};\n" + code.source = f"Gfx {self.aligner_name}[] = {{gsSPEndDisplayList()}};\n" code.source += f"u{str(bitsPerValue)} {self.name}[] = {{\n\t" code.source += texData code.source += "\n};\n\n" @@ -3748,6 +3793,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] @@ -3763,7 +3813,7 @@ def getattr_virtual(self, field, static): else: return field.name if hasattr(field, "__iter__") and type(field) is not str: - return " | ".join(field) if len(field) else "0" + return " | ".join(map(str, field)) if len(field) else "0" if self._hex > 0 and isinstance(field, int): temp = field if field >= 0 else (1 << (self._hex * 4)) + field return f"{temp:#0{self._hex + 2}x}" # + 2 for the 0x part @@ -3773,7 +3823,8 @@ def to_c(self, static=True): if static: return f"g{'s'*static}{type(self).__name__}({', '.join( self.getargs(static) )})" else: - return f"g{'s'*static}{type(self).__name__}(glistp++, {', '.join( self.getargs(static) )})" + args = ["glistp++"] + list(self.getargs(static)) + return f"g{'s'*static}{type(self).__name__}({', '.join( args )})" def size(self, f3d): return GFX_SIZE @@ -4711,9 +4762,9 @@ def gsSPGeometryMode_Non_F3DEX_GBI_2(word, f3d): return words[0].to_bytes(4, "big") + words[1].to_bytes(4, "big") -def geoFlagListToWord(flagList, f3d): +def geoFlagListToWord(flags: tuple, f3d: F3D): word = 0 - for name in flagList: + for name in flags: if name in f3d.allGeomModeFlags: word += getattr(f3d, name) else: @@ -4727,8 +4778,8 @@ def geoFlagListToWord(flagList, f3d): @dataclass(unsafe_hash=True) class SPGeometryMode(GbiMacro): - clearFlagList: list - setFlagList: list + clearFlagList: set[str] = field(default_factory=set) + setFlagList: set[str] = field(default_factory=set) def to_binary(self, f3d, segments): if f3d.F3DEX_GBI_2: @@ -4761,7 +4812,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) class SPSetGeometryMode(GbiMacro): - flagList: list + flagList: set[str] = field(default_factory=set) def to_binary(self, f3d, segments): word = geoFlagListToWord(self.flagList, f3d) @@ -4785,7 +4836,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) class SPClearGeometryMode(GbiMacro): - flagList: list + flagList: set[str] = field(default_factory=set) def to_binary(self, f3d, segments): word = geoFlagListToWord(self.flagList, f3d) @@ -4808,7 +4859,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) class SPLoadGeometryMode(GbiMacro): - flagList: list + flagList: set[str] def to_binary(self, f3d, segments): word = geoFlagListToWord(self.flagList, f3d) @@ -4839,12 +4890,54 @@ def gsSPSetOtherMode(cmd, sft, length, data, f3d): return words[0].to_bytes(4, "big") + words[1].to_bytes(4, "big") +@dataclass(unsafe_hash=True) +class RendermodeBlender: + cycle1: tuple + cycle2: tuple + + def __str__(self): + return f"GBL_c1({', '.join(self.cycle1)}) | GBL_c2({', '.join(self.cycle2)})" + + def to_c(self, _static=True): + return str(self) + + def to_binary(self, f3d): + return GBL_c1(*[getattr(f3d, str(x), x) for x in self.cycle1]) | GBL_c2( + *[getattr(f3d, str(x), x) for x in self.cycle2] + ) + + @dataclass(unsafe_hash=True) class SPSetOtherMode(GbiMacro): cmd: str sft: int length: int - flagList: list + flagList: set + + def sets_rendermode(self, f3d): + return self.cmd == "G_SETOTHERMODE_L" and (self.sft + self.length) > (3 - f3d.F3D_OLD_GBI) + + def extend(self, flags: Iterable | str): + flags = {flags} if isinstance(flags, str) else set(flags) + self.flagList = self.flagList | flags + + def add_other(self, f3d, other: SPSetOtherMode): + min_max = min(self.sft, other.sft), max(self.sft + self.length, other.sft + other.length) + self.sft = min_max[0] + self.length = min_max[1] - min_max[0] + + for flag in self.flagList.copy(): # remove any flag overriden by other + value = flag + if isinstance(flag, RendermodeBlender): + value = flag.to_binary(f3d) + elif isinstance(flag, str): + value = getattr(f3d, flag, None) + if value is None: + raise ValueError(f"Flag {flag} not found in {f3d}") + if not value or value >> other.sft < (2**other.length): + self.flagList.remove(flag) + # add other's flags + self.extend(other.flagList) def to_binary(self, f3d, segments): data = 0 @@ -4867,10 +4960,27 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPPipelineMode(GbiMacro): - # mode is a string +class SPSetOtherModeSub(GbiMacro): mode: str + is_othermodeh = False + + @property + def mode_prefix(self): + return "_".join(self.mode.split("_")[:2]) + + +@dataclass(unsafe_hash=True) +class SPSetOtherModeLSub(SPSetOtherModeSub): + is_othermodeh = False + +@dataclass(unsafe_hash=True) +class SPSetOtherModeHSub(SPSetOtherModeSub): + is_othermodeh = True + + +@dataclass(unsafe_hash=True) +class DPPipelineMode(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_PM_1PRIMITIVE": modeVal = f3d.G_PM_1PRIMITIVE @@ -4883,10 +4993,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetCycleType(GbiMacro): - # mode is a string - mode: str - +class DPSetCycleType(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_CYC_1CYCLE": modeVal = f3d.G_CYC_1CYCLE @@ -4903,10 +5010,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetTexturePersp(GbiMacro): - # mode is a string - mode: str - +class DPSetTexturePersp(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_TP_NONE": modeVal = f3d.G_TP_NONE @@ -4919,10 +5023,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetTextureDetail(GbiMacro): - # mode is a string - mode: str - +class DPSetTextureDetail(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_TD_CLAMP": modeVal = f3d.G_TD_CLAMP @@ -4937,10 +5038,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetTextureLOD(GbiMacro): - # mode is a string - mode: str - +class DPSetTextureLOD(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_TL_TILE": modeVal = f3d.G_TL_TILE @@ -4953,10 +5051,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetTextureLUT(GbiMacro): - # mode is a string - mode: str - +class DPSetTextureLUT(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_TT_NONE": modeVal = f3d.G_TT_NONE @@ -4973,10 +5068,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetTextureFilter(GbiMacro): - # mode is a string - mode: str - +class DPSetTextureFilter(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_TF_POINT": modeVal = f3d.G_TF_POINT @@ -4991,10 +5083,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetTextureConvert(GbiMacro): - # mode is a string - mode: str - +class DPSetTextureConvert(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_TC_CONV": modeVal = f3d.G_TC_CONV @@ -5009,10 +5098,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetCombineKey(GbiMacro): - # mode is a string - mode: str - +class DPSetCombineKey(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_CK_NONE": modeVal = f3d.G_CK_NONE @@ -5025,10 +5111,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetColorDither(GbiMacro): - # mode is a string - mode: str - +class DPSetColorDither(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_CD_MAGICSQ": modeVal = f3d.G_CD_MAGICSQ @@ -5047,10 +5130,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetAlphaDither(GbiMacro): - # mode is a string - mode: str - +class DPSetAlphaDither(SPSetOtherModeHSub): def to_binary(self, f3d, segments): if self.mode == "G_AD_PATTERN": modeVal = f3d.G_AD_PATTERN @@ -5067,10 +5147,7 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetAlphaCompare(GbiMacro): - # mask is a string - mode: str - +class DPSetAlphaCompare(SPSetOtherModeLSub): def to_binary(self, f3d, segments): if self.mode == "G_AC_NONE": maskVal = f3d.G_AC_NONE @@ -5085,14 +5162,11 @@ def to_xml(self): @dataclass(unsafe_hash=True) -class DPSetDepthSource(GbiMacro): - # src is a string - src: str - +class DPSetDepthSource(SPSetOtherModeLSub): def to_binary(self, f3d, segments): - if self.src == "G_ZS_PIXEL": + if self.mode == "G_ZS_PIXEL": srcVal = f3d.G_ZS_PIXEL - elif self.src == "G_ZS_PRIM": + elif self.mode == "G_ZS_PRIM": srcVal = f3d.G_ZS_PRIM return gsSPSetOtherMode(f3d.G_SETOTHERMODE_L, f3d.G_MDSFT_ZSRCSEL, 1, srcVal, f3d) @@ -5118,37 +5192,20 @@ def GBL_c2(m1a, m1b, m2a, m2b): @dataclass(unsafe_hash=True) class DPSetRenderMode(GbiMacro): + flagList: set[str] + blender: Optional[RendermodeBlender] = None # bl0-3 are string for each blender enum - def __init__(self, flagList, blendList): - self.flagList = flagList - self.use_preset = blendList is None - if not self.use_preset: - self.bl00 = blendList[0] - self.bl01 = blendList[1] - self.bl02 = blendList[2] - self.bl03 = blendList[3] - self.bl10 = blendList[4] - self.bl11 = blendList[5] - self.bl12 = blendList[6] - self.bl13 = blendList[7] - - def getGBL_c(self, f3d): - bl00 = getattr(f3d, self.bl00) - bl01 = getattr(f3d, self.bl01) - bl02 = getattr(f3d, self.bl02) - bl03 = getattr(f3d, self.bl03) - bl10 = getattr(f3d, self.bl10) - bl11 = getattr(f3d, self.bl11) - bl12 = getattr(f3d, self.bl12) - bl13 = getattr(f3d, self.bl13) - return GBL_c1(bl00, bl01, bl02, bl03) | GBL_c2(bl10, bl11, bl12, bl13) + + @property + def use_preset(self): + return self.blender is None def to_binary(self, f3d, segments): flagWord = renderFlagListToWord(self.flagList, f3d) if not self.use_preset: return gsSPSetOtherMode( - f3d.G_SETOTHERMODE_L, f3d.G_MDSFT_RENDERMODE, 29, flagWord | self.getGBL_c(f3d), f3d + f3d.G_SETOTHERMODE_L, f3d.G_MDSFT_RENDERMODE, 29, flagWord | self.blender.to_binary(f3d), f3d ) else: return gsSPSetOtherMode(f3d.G_SETOTHERMODE_L, f3d.G_MDSFT_RENDERMODE, 29, flagWord, f3d) @@ -5170,25 +5227,7 @@ def to_c(self, static=True): data = "gsDPSetRenderMode(" if static else "gDPSetRenderMode(glistp++, " if not self.use_preset: - data += ( - "GBL_c1(" - + self.bl00 - + ", " - + self.bl01 - + ", " - + self.bl02 - + ", " - + self.bl03 - + ") | GBL_c2(" - + self.bl10 - + ", " - + self.bl11 - + ", " - + self.bl12 - + ", " - + self.bl13 - + "), " - ) + data += self.blender.to_c(static) + ", " for name in self.flagList: data += name + " | " return data[:-3] + ")" @@ -5456,8 +5495,8 @@ def to_binary(self, f3d, segments): @dataclass(unsafe_hash=True) class DPSetOtherMode(GbiMacro): - mode0: list - mode1: list + mode0: set[str] + mode1: set[str] def to_binary(self, f3d, segments): mode0 = mode1 = 0 @@ -5521,10 +5560,10 @@ class DPSetTile(GbiMacro): tmem: int tile: int palette: int - cmt: list + cmt: tuple[str, str] maskt: int shiftt: int - cms: list + cms: tuple[str, str] masks: int shifts: int @@ -5616,8 +5655,8 @@ class DPLoadTextureBlock(GbiMacro): width: int height: int pal: int - cms: list - cmt: list + cms: tuple[str, str] + cmt: tuple[str, str] masks: int maskt: int shifts: int @@ -5707,8 +5746,8 @@ class DPLoadTextureBlockYuv(GbiMacro): width: int height: int pal: int - cms: list - cmt: list + cms: tuple[str, str] + cmt: tuple[str, str] masks: int maskt: int shifts: int @@ -5804,8 +5843,8 @@ class _DPLoadTextureBlock(GbiMacro): width: int height: int pal: int - cms: list - cmt: list + cms: tuple[str, str] + cmt: tuple[str, str] masks: int maskt: int shifts: int @@ -5900,8 +5939,8 @@ class DPLoadTextureBlock_4b(GbiMacro): width: int height: int pal: int - cms: list - cmt: list + cms: tuple[str, str] + cmt: tuple[str, str] masks: int maskt: int shifts: int @@ -5993,8 +6032,8 @@ class DPLoadTextureTile(GbiMacro): lrs: int lrt: int pal: int - cms: list - cmt: list + cms: tuple[str, str] + cmt: tuple[str, str] masks: int maskt: int shifts: int @@ -6091,8 +6130,8 @@ class DPLoadTextureTile_4b(GbiMacro): lrs: int lrt: int pal: int - cms: list - cmt: list + cms: tuple[str, str] + cmt: tuple[str, str] masks: int maskt: int shifts: int diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index 1f0970553..b67acda59 100644 --- a/fast64_internal/f3d/f3d_material.py +++ b/fast64_internal/f3d/f3d_material.py @@ -31,7 +31,15 @@ from mathutils import Color from .f3d_enums import * -from .f3d_gbi import get_F3D_GBI, enumTexScroll, isUcodeF3DEX1, default_draw_layers +from .f3d_gbi import ( + get_F3D_GBI, + enumTexScroll, + isUcodeF3DEX1, + isUcodeF3DEX3, + is_ucode_f3d, + is_ucode_t3d, + default_draw_layers, +) from .f3d_material_presets import * from ..utility import * from ..render_settings import ( @@ -119,13 +127,14 @@ "Overlay": "1", } -enumF3DMenu = [ - ("Combiner", "Combiner", "Combiner"), - ("Sources", "Sources", "Sources"), - ("Geo", "Geo", "Geo"), - ("Upper", "Upper", "Upper"), - ("Lower", "Lower", "Lower"), -] + +def menu_items_enum(_self, context): + items = ["Combiner", "Sources"] + if len(geo_modes_in_ucode(context.scene.f3d_type)) > 1: + items.append("Geo") + items.extend(["Upper", "Lower"]) + return [(item, item, item) for item in items] + enumF3DSource = [ ("None", "None", "None"), @@ -144,6 +153,72 @@ "Shaded Texture": {"SM64": "Shaded Texture", "OOT": "oot_shaded_texture"}, } +F3D_GEO_MODES = { + "zBuffer": "g_zbuffer", + "shade": "g_shade", + "cullFront": "g_cull_front", + "cullBack": "g_cull_back", + "fog": "g_fog", + "lighting": "g_lighting", + "texGen": "g_tex_gen", + "texGenLinear": "g_tex_gen_linear", + "loD": "g_lod", + "shadeSmooth": "g_shade_smooth", +} + +F3DLX_GEO_MODES = { + "clipping": "g_clipping", +} + +F3DEX3_GEO_MODES = { + "ambientOcclusion": "g_ambocclusion", + "attroffsetZ": "g_attroffset_z_enable", + "attroffsetST": "g_attroffset_st_enable", + "packedNormals": "g_packed_normals", + "lightToAlpha": "g_lighttoalpha", + "specularLighting": "g_lighting_specular", + "fresnelToColor": "g_fresnel_color", + "fresnelToAlpha": "g_fresnel_alpha", +} + +T3D_GEO_MODES = { + "cullFront": "g_cull_front", + "cullBack": "g_cull_back", + "fog": "g_fog", + "texGen": "g_tex_gen", +} + + +def add_prefix(string: str, prefix: str): + """Add prefix if not already present""" + return string if string.startswith(prefix) else prefix + string + + +def geo_modes_in_ucode(UCODE_VER: str): + geo_modes = {} + if is_ucode_f3d(UCODE_VER): + geo_modes.update(F3D_GEO_MODES) + if isUcodeF3DEX1(UCODE_VER): + geo_modes.update(F3DLX_GEO_MODES) + if isUcodeF3DEX3(UCODE_VER): + geo_modes.update(F3DEX3_GEO_MODES) + if is_ucode_t3d(UCODE_VER): + geo_modes.update(T3D_GEO_MODES) + return geo_modes + + +def sources_in_ucode(UCODE_VER: str): + sources = ["Primitive", "Environment", "Key", "Convert", "Fog"] + if not is_ucode_t3d(UCODE_VER) or is_ucode_f3d(UCODE_VER): + sources.extend(["Lighting", "Clip Ratio"]) + if isUcodeF3DEX3(UCODE_VER): + sources.extend(["AO", "Fresnel", "ST Attr Offset", "Z Attr Offset"]) + return sources + + +def inherit_light_and_fog(): + return is_ucode_t3d(bpy.context.scene.f3d_type) + def getDefaultMaterialPreset(category): game = bpy.context.scene.gameEditorMode @@ -161,7 +236,7 @@ def update_draw_layer(self, context): drawLayer = material.f3d_mat.draw_layer if context.scene.gameEditorMode == "SM64": drawLayer.oot = drawLayerSM64toOOT[drawLayer.sm64] - elif context.scene.gameEditorMode == "OOT": + elif context.scene.gameEditorMode in {"OOT", "MM"}: if material.f3d_mat.draw_layer.oot == "Opaque": if int(material.f3d_mat.draw_layer.sm64) > 4: material.f3d_mat.draw_layer.sm64 = "1" @@ -443,6 +518,7 @@ def all_combiner_uses(f3d_mat: "F3DMaterialProperty") -> dict[str, bool]: def ui_geo_mode(settings, dataHolder, layout, useDropdown): + f3d = get_F3D_GBI() inputGroup = layout.column() if useDropdown: inputGroup.prop( @@ -454,7 +530,24 @@ def ui_geo_mode(settings, dataHolder, layout, useDropdown): if not useDropdown or dataHolder.menu_geo: disable_dependent = False # Don't disable dependent props in world defaults + def should_draw(*props): + return any(settings.has_prop_in_ucode(prop) for prop in props) + + def is_on(*props): + return all(settings.is_geo_mode_on(prop) for prop in props) + + def draw_mode(layout: UILayout, *props): + failed = False + for prop in props: + if settings.has_prop_in_ucode(prop): + layout.prop(settings, prop) + else: + failed = True + return not failed + def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], isText: bool) -> UILayout: + if not isText and not should_draw(textOrProp): + return parent.column(align=True) c = parent.column(align=True) if isText: c.label(text=textOrProp) @@ -468,8 +561,6 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], c.enabled = enable or not disable_dependent return c - isF3DEX3 = bpy.context.scene.f3d_type == "F3DEX3" - lightFxPrereq = isF3DEX3 and settings.g_lighting ccWarnings = shadeInCC = False blendWarnings = shadeInBlender = zInBlender = False if isinstance(dataHolder, F3DMaterialProperty): @@ -482,96 +573,103 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], shadeInBlender = settings.does_blender_use_input("G_BL_A_SHADE") zInBlender = settings.z_cmp or settings.z_upd - inputGroup.prop(settings, "g_shade_smooth") + draw_mode(inputGroup, "g_shade_smooth") c = indentGroup(inputGroup, "g_lighting", False) - if ccWarnings and not shadeInCC and settings.g_lighting and not settings.g_tex_gen: + if ccWarnings and not shadeInCC and is_on("g_lighting") and not is_on("g_tex_gen"): multilineLabel(c, "Shade not used in CC, can disable\nlighting.", icon="INFO") - if isF3DEX3: - c.prop(settings, "g_packed_normals") - c.prop(settings, "g_lighting_specular") - c.prop(settings, "g_ambocclusion") - c.prop(settings, "g_fresnel_color") - d = indentGroup(c, "g_tex_gen", False) - d.prop(settings, "g_tex_gen_linear") - - if lightFxPrereq and settings.g_fresnel_color: + draw_mode(c, "g_packed_normals", "g_lighting_specular", "g_ambocclusion", "g_fresnel_color") + if should_draw("g_tex_gen_linear"): + d = indentGroup(c, "g_tex_gen", False) + else: + draw_mode(c, "g_tex_gen") + d = c + draw_mode(d, "g_tex_gen_linear") + + if is_ucode_t3d(f3d.F3D_VER): + shadeColorLabel = "Lighting * vertex color" + if is_on("g_fresnel_color"): shadeColorLabel = "Fresnel" - elif not settings.g_lighting or (lightFxPrereq and settings.g_lighttoalpha): + elif not is_on("g_lighting") or is_on("g_lighttoalpha"): shadeColorLabel = "Vertex color" - elif lightFxPrereq and settings.g_packed_normals and not settings.g_lighttoalpha: + elif is_on("g_lighting") and is_on("g_packed_normals") and not is_on("g_lighttoalpha"): shadeColorLabel = "Lighting * vertex color" else: shadeColorLabel = "Lighting" - inputGroup.label(text=f"Shade color = {shadeColorLabel}") + inputGroup.column().label(text=f"Shade color = {shadeColorLabel}") + + draw_mode(inputGroup, "g_fog") shadowMapInShadeAlpha = False - if settings.g_fog: + if is_on("g_fog"): shadeAlphaLabel = "Fog" - elif lightFxPrereq and settings.g_fresnel_alpha: + elif is_on("g_lighting", "g_fresnel_alpha"): shadeAlphaLabel = "Fresnel" - elif lightFxPrereq and settings.g_lighttoalpha: + elif is_on("g_lighting", "g_lighttoalpha"): shadeAlphaLabel = "Light intensity" - elif lightFxPrereq and settings.g_ambocclusion: + elif is_on("g_lighting", "g_ambocclusion"): shadeAlphaLabel = "Shadow map / AO in vtx alpha" shadowMapInShadeAlpha = True else: shadeAlphaLabel = "Vtx alpha" - c = indentGroup(inputGroup, f"Shade alpha = {shadeAlphaLabel}:", True) - if isF3DEX3: - lighting_group = c.column(align=True) - lighting_group.enabled = settings.g_lighting or not disable_dependent - lighting_group.prop(settings, "g_lighttoalpha") - lighting_group.prop(settings, "g_fresnel_alpha") - c.prop(settings, "g_fog") - if lightFxPrereq and settings.g_fog and settings.g_fresnel_alpha: - c.label(text="Fog overrides Fresnel Alpha.", icon="ERROR") - if lightFxPrereq and settings.g_fog and settings.g_lighttoalpha: - c.label(text="Fog overrides Light-to-Alpha.", icon="ERROR") - if lightFxPrereq and settings.g_fresnel_alpha and settings.g_lighttoalpha: - c.label(text="Fresnel Alpha overrides Light-to-Alpha.", icon="ERROR") - if shadowMapInShadeAlpha and ccWarnings and ccUse["Shade Alpha"]: - c.label(text="Shadow map = shade alpha used in CC, probably wrong.", icon="INFO") - if settings.g_fog and ccWarnings and ccUse["Shade Alpha"]: - c.label(text="Fog = shade alpha used in CC, probably wrong.", icon="INFO") - if blendWarnings and shadeInBlender and not settings.g_fog: - c.label(text="Rendermode uses shade alpha, probably fog.", icon="INFO") - elif blendWarnings and not shadeInBlender and settings.g_fog: - c.label(text="Fog not used in rendermode / blender, can disable.", icon="INFO") - if isF3DEX3: - c = indentGroup(inputGroup, "Attribute offsets:", True) - c.prop(settings, "g_attroffset_st_enable") - c.prop(settings, "g_attroffset_z_enable") - - c = indentGroup(inputGroup, "Face culling:", True) - c.prop(settings, "g_cull_front") - c.prop(settings, "g_cull_back") - if settings.g_cull_front and settings.g_cull_back: - c.label(text="Nothing will be drawn.", icon="ERROR") - - c = indentGroup(inputGroup, "Disable if not using:", True) - c.prop(settings, "g_zbuffer") - if blendWarnings and not settings.g_zbuffer and zInBlender: - c.label(text="Rendermode / blender using Z, must enable.", icon="ERROR") - elif blendWarnings and settings.g_zbuffer and not zInBlender: - c.label(text="Z is not being used, can disable.", icon="INFO") - c.prop(settings, "g_shade") - if ccWarnings and not settings.g_shade and (shadeInCC or shadeInBlender): - if shadeInCC and shadeInBlender: - where = "CC and blender" - elif shadeInCC: - where = "CC" - else: - where = "rendermode / blender" - c.label(text=f"Shade in use in {where}, must enable.", icon="ERROR") - elif ccWarnings and settings.g_shade and not shadeInCC and not shadeInBlender: - c.label(text="Shade is not being used, can disable.", icon="INFO") + warnings, errors = [], [] + if is_on("g_lighting", "g_fog", "g_fresnel_alpha"): + errors.append("Fog overrides Fresnel Alpha.") + if is_on("g_lighting", "g_fog", "g_lighttoalpha"): + errors.append("Fog overrides Light-to-Alpha.") + if is_on("g_lighting", "g_fresnel_alpha", "g_lighttoalpha"): + errors.append("Fresnel Alpha overrides Light-to-Alpha.") + if shadowMapInShadeAlpha and ccWarnings and ccUse["Shade Alpha"]: + warnings.append("Shadow map = shade alpha used in CC, probably wrong.") + if is_on("g_fog") and ccWarnings and ccUse["Shade Alpha"]: + warnings.append("Fog = shade alpha used in CC, probably wrong.") + if blendWarnings and shadeInBlender and not is_on("g_fog"): + warnings.append("Rendermode uses shade alpha, probably fog.") + elif blendWarnings and not shadeInBlender and is_on("g_fog"): + warnings.append("Fog not used in rendermode / blender, can disable.") + + if should_draw("g_lighttoalpha", "g_fresnel_alpha") or warnings or errors: + c = indentGroup(inputGroup, f"Shade alpha = {shadeAlphaLabel}:", True) + draw_mode(c, "g_lighttoalpha", "g_fresnel_alpha") + if warnings: + multilineLabel(c, "\n".join(warnings), "INFO") + if errors: + multilineLabel(c, "\n".join(errors), "ERROR") + else: + inputGroup.column().label(text=f"Shade alpha = {shadeAlphaLabel}") + + if should_draw("g_attroffset_st_enable", "g_attroffset_z_enable"): + indentGroup(inputGroup, "Attribute offsets:", True) + draw_mode(inputGroup, "g_attroffset_st_enable", "g_attroffset_z_enable") + + if should_draw("g_cull_front", "g_cull_back"): + c = indentGroup(inputGroup, "Face culling:", True) + draw_mode(c, "g_cull_front", "g_cull_back") + if is_on("g_cull_front", "g_cull_back"): + c.label(text="Nothing will be drawn.", icon="ERROR") + + if should_draw("g_zbuffer", "g_shade"): + c = indentGroup(inputGroup, "Disable if not using:", True) + draw_mode(c, "g_zbuffer", "g_shade") + if blendWarnings and not is_on("g_zbuffer") and zInBlender: + c.label(text="Rendermode / blender using Z, must enable.", icon="ERROR") + elif blendWarnings and is_on("g_zbuffer") and not zInBlender: + c.label(text="Z is not being used, can disable.", icon="INFO") + if ccWarnings and not is_on("g_shade") and (shadeInCC or shadeInBlender): + if shadeInCC and shadeInBlender: + where = "CC and blender" + elif shadeInCC: + where = "CC" + else: + where = "rendermode / blender" + c.label(text=f"Shade in use in {where}, must enable.", icon="ERROR") + elif ccWarnings and is_on("g_shade") and not shadeInCC and not shadeInBlender: + c.label(text="Shade is not being used, can disable.", icon="INFO") - c = indentGroup(inputGroup, "Not useful:", True) - c.prop(settings, "g_lod") - if isUcodeF3DEX1(bpy.context.scene.f3d_type): - c.prop(settings, "g_clipping") + if should_draw("g_lod", "g_clipping"): + c = indentGroup(inputGroup, "Not useful:", True) + draw_mode(c, "g_lod", "g_clipping") def ui_upper_mode(settings, dataHolder, layout: UILayout, useDropdown): @@ -643,8 +741,9 @@ def ui_other(settings, dataHolder, layout, useDropdown): dataHolder, "menu_other", text="Other Settings", icon="TRIA_DOWN" if dataHolder.menu_other else "TRIA_RIGHT" ) if not useDropdown or dataHolder.menu_other: - clipRatioGroup = inputGroup.column() - prop_split(clipRatioGroup, settings, "clip_ratio", "Clip Ratio") + if "Clip Ratio" in sources_in_ucode(bpy.context.scene.f3d_type): + clipRatioGroup = inputGroup.column() + prop_split(clipRatioGroup, settings, "clip_ratio", "Clip Ratio") if isinstance(dataHolder, Material) or isinstance(dataHolder, F3DMaterialProperty): blend_color_group = layout.row() @@ -667,6 +766,28 @@ def tmemUsageUI(layout, textureProp): tmemSizeWarning.label(text="Note that width will be internally padded to 64 bit boundaries.") +def rendermode_presets_checks(material: "F3DMaterialProperty"): + rdp: RDPSettings = material.rdp_settings + + f3d = get_F3D_GBI() + no_flags_1 = rdp.rendermode_preset_cycle_1 in f3d.rendermodePresetsWithoutFlags + if rdp.g_mdsft_cycletype == "G_CYC_2CYCLE": + no_flags_2 = rdp.rendermode_preset_cycle_2 in f3d.rendermodePresetsWithoutFlags + if no_flags_1 and no_flags_2: + raise PluginError( + "Invalid combination of rendermode presets.\n" "Neither of these presets sets the rendermode flags." + ) + elif not no_flags_1 and not no_flags_2: + raise PluginError( + "Invalid combination of rendermode presets.\n" "Both of these presets set the rendermode flags." + ) + else: + if no_flags_1: + raise PluginError( + "Invalid rendermode preset in 1-cycle.\n" "This preset does not set the rendermode flags." + ) + + # UI Assumptions: # shading = 1 # lighting = 1 @@ -765,6 +886,8 @@ def ui_chroma(self, material, layout, name, setName, setProp, showCheckBox): return inputGroup def ui_lights(self, f3d_mat: "F3DMaterialProperty", layout: UILayout, name, showCheckBox): + if inherit_light_and_fog(): + return inputGroup = layout.row() prop_input_left = inputGroup.column() prop_input = inputGroup.column() @@ -773,9 +896,10 @@ def ui_lights(self, f3d_mat: "F3DMaterialProperty", layout: UILayout, name, show else: prop_input_left.label(text=name) - prop_input_left.enabled = f3d_mat.rdp_settings.g_lighting and f3d_mat.rdp_settings.g_shade + settings = f3d_mat.rdp_settings + prop_input_left.enabled = settings.is_geo_mode_on("g_lighting") and settings.is_geo_mode_on("g_shade") lightSettings: UILayout = prop_input.column() - if f3d_mat.rdp_settings.g_lighting: + if settings.is_geo_mode_on("g_lighting"): prop_input_left.separator(factor=0.25) light_controls = prop_input_left.box() light_controls.enabled = f3d_mat.set_lights @@ -805,8 +929,6 @@ def ui_lights(self, f3d_mat: "F3DMaterialProperty", layout: UILayout, name, show if f3d_mat.f3d_light6 is not None: lightSettings.prop_search(f3d_mat, "f3d_light7", bpy.data, "lights", text="") - prop_input.enabled = f3d_mat.set_lights and f3d_mat.rdp_settings.g_lighting and f3d_mat.rdp_settings.g_shade - return inputGroup def ui_convert(self, material, layout, showCheckBox): @@ -862,34 +984,10 @@ def ui_lower_render_mode(self, material, layout, useDropdown): renderGroup = inputGroup.column() renderGroup.prop(material.rdp_settings, "rendermode_advanced_enabled", text="Show Advanced Settings") if not material.rdp_settings.rendermode_advanced_enabled: - f3d = get_F3D_GBI() prop_split(renderGroup, material.rdp_settings, "rendermode_preset_cycle_1", "Render Mode") - no_flags_1 = material.rdp_settings.rendermode_preset_cycle_1 in f3d.rendermodePresetsWithoutFlags if is_two_cycle: prop_split(renderGroup, material.rdp_settings, "rendermode_preset_cycle_2", "Render Mode Cycle 2") - no_flags_2 = material.rdp_settings.rendermode_preset_cycle_2 in f3d.rendermodePresetsWithoutFlags - if no_flags_1 and no_flags_2: - multilineLabel( - renderGroup.box(), - "Invalid combination of rendermode presets.\n" - + "Neither of these presets sets the rendermode flags.", - "ERROR", - ) - elif not no_flags_1 and not no_flags_2: - multilineLabel( - renderGroup.box(), - "Invalid combination of rendermode presets.\n" - + "Both of these presets set the rendermode flags.", - "ERROR", - ) - else: - if no_flags_1: - multilineLabel( - renderGroup.box(), - "Invalid rendermode preset in 1-cycle.\n" - + "This preset does not set the rendermode flags.", - "ERROR", - ) + run_and_draw_errors(renderGroup, rendermode_presets_checks, material) else: prop_split(renderGroup, material.rdp_settings, "aa_en", "Antialiasing") prop_split(renderGroup, material.rdp_settings, "z_cmp", "Z Testing") @@ -957,11 +1055,12 @@ def ui_uvCheck(self, layout, context): def ui_draw_layer(self, material, layout, context): if context.scene.gameEditorMode == "SM64": prop_split(layout, material.f3d_mat.draw_layer, "sm64", "Draw Layer") - elif context.scene.gameEditorMode == "OOT": + elif context.scene.gameEditorMode in {"OOT", "MM"}: prop_split(layout, material.f3d_mat.draw_layer, "oot", "Draw Layer") def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBox: bool) -> None: - if f3dMat.rdp_settings.g_ambocclusion: + sources = sources_in_ucode(bpy.context.scene.f3d_type) + if "AO" in sources and f3dMat.rdp_settings.g_ambocclusion: if showCheckBox or f3dMat.set_ao: inputGroup = inputCol.column() if showCheckBox: @@ -971,7 +1070,7 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo prop_split(inputGroup.row(), f3dMat, "ao_directional", "AO Directional") prop_split(inputGroup.row(), f3dMat, "ao_point", "AO Point") - if f3dMat.rdp_settings.g_fresnel_color or f3dMat.rdp_settings.g_fresnel_alpha: + if "Fresnel" in sources and f3dMat.rdp_settings.g_fresnel_color or f3dMat.rdp_settings.g_fresnel_alpha: if showCheckBox or f3dMat.set_fresnel: inputGroup = inputCol.column() if showCheckBox: @@ -980,7 +1079,7 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo prop_split(inputGroup.row(), f3dMat, "fresnel_lo", "Fresnel Lo") prop_split(inputGroup.row(), f3dMat, "fresnel_hi", "Fresnel Hi") - if f3dMat.rdp_settings.g_attroffset_st_enable: + if "ST Attr Offset" in sources and f3dMat.rdp_settings.g_attroffset_st_enable: if showCheckBox or f3dMat.set_attroffs_st: inputGroup = inputCol.column() if showCheckBox: @@ -988,7 +1087,7 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo if f3dMat.set_attroffs_st: prop_split(inputGroup.row(), f3dMat, "attroffs_st", "ST Attr Offset") - if f3dMat.rdp_settings.g_attroffset_z_enable: + if "Z Attr Offset" in sources and f3dMat.rdp_settings.g_attroffset_z_enable: if showCheckBox or f3dMat.set_attroffs_z: inputGroup = inputCol.column() if showCheckBox: @@ -996,16 +1095,35 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo if f3dMat.set_attroffs_z: prop_split(inputGroup.row(), f3dMat, "attroffs_z", "Z Attr Offset") - if f3dMat.rdp_settings.using_fog: + if "Fog" in sources and f3dMat.rdp_settings.using_fog and not inherit_light_and_fog(): if showCheckBox or f3dMat.set_fog: inputGroup = inputCol.column() if showCheckBox: inputGroup.prop(f3dMat, "set_fog", text="Set Fog") if f3dMat.set_fog: - inputGroup.prop(f3dMat, "use_global_fog", text="Use Global Fog (SM64)") - if f3dMat.use_global_fog: - inputGroup.label(text="Only applies to levels (area fog settings).", icon="INFO") - else: + draw_fog = True + if bpy.context.scene.gameEditorMode == "SM64": + obj, area_obj = bpy.context.object.parent, None + while obj: + if obj.type == "EMPTY" and obj.sm64_obj_type == "Area Root" and obj.fast64.sm64.area.set_fog: + area_obj = obj + break + obj = obj.parent + if area_obj: + inputGroup.prop(f3dMat, "use_global_fog", text=f'Use Area "{area_obj.name}"\'s Fog') + if f3dMat.use_global_fog: + settings_col = inputGroup.column() + settings_col.enabled = not f3dMat.use_global_fog + prop_split(settings_col.row(), area_obj, "area_fog_color", "Fog Color") + prop_split(settings_col.row(), area_obj, "area_fog_position", "Fog Range") + draw_fog = False + else: + # show setting for preview + inputGroup.prop(f3dMat, "use_global_fog", text="Use Area's Fog") + inputGroup.label( + text="Preview only in this context, no area fog settings to pick up", icon="INFO" + ) + if draw_fog: prop_split(inputGroup.row(), f3dMat, "fog_color", "Fog Color") prop_split(inputGroup.row(), f3dMat, "fog_position", "Fog Range") @@ -1101,17 +1219,16 @@ def ui_cel_shading(self, material: Material, layout: UILayout): def checkDrawLayersWarnings(self, f3dMat: "F3DMaterialProperty", useDict: Dict[str, bool], layout: UILayout): settings = f3dMat.rdp_settings - isF3DEX3 = bpy.context.scene.f3d_type == "F3DEX3" - lightFxPrereq = isF3DEX3 and settings.g_lighting + f3d_type = bpy.context.scene.f3d_type anyUseShadeAlpha = useDict["Shade Alpha"] or settings.does_blender_use_input("G_BL_A_SHADE") - g_lighting = settings.g_lighting - g_fog = settings.g_fog - g_packed_normals = lightFxPrereq and settings.g_packed_normals - g_ambocclusion = lightFxPrereq and settings.g_ambocclusion - g_lighttoalpha = lightFxPrereq and settings.g_lighttoalpha - g_fresnel_color = lightFxPrereq and settings.g_fresnel_color - g_fresnel_alpha = lightFxPrereq and settings.g_fresnel_alpha + g_lighting = settings.is_geo_mode_on("g_lighting") or is_ucode_t3d(f3d_type) + g_fog = settings.is_geo_mode_on("g_fog") + g_packed_normals = settings.is_geo_mode_on("g_packed_normals") or is_ucode_t3d(f3d_type) + g_ambocclusion = settings.is_geo_mode_on("g_ambocclusion") + g_lighttoalpha = settings.is_geo_mode_on("g_lighttoalpha") + g_fresnel_color = settings.is_geo_mode_on("g_fresnel_color") + g_fresnel_alpha = settings.is_geo_mode_on("g_fresnel_alpha") usesVertexColor = useDict["Shade"] and (not g_lighting or (g_packed_normals and not g_fresnel_color)) usesVertexAlpha = anyUseShadeAlpha and (g_ambocclusion or not (g_fog or g_lighttoalpha or g_fresnel_alpha)) @@ -1129,9 +1246,7 @@ def checkDrawLayersWarnings(self, f3dMat: "F3DMaterialProperty", useDict: Dict[s noticeBox.label(text='They must be called "Col" and "Alpha".', icon="IMAGE_ALPHA") def checkDrawMixedCIWarning(self, layout, useDict, f3dMat): - useTex0 = useDict["Texture 0"] and f3dMat.tex0.tex_set - useTex1 = useDict["Texture 1"] and f3dMat.tex1.tex_set - if not useTex0 or not useTex1: + if not (f3dMat.is_multi_tex and (f3dMat.tex0.tex_set and f3dMat.tex1.tex_set)): return isTex0CI = f3dMat.tex0.tex_format[:2] == "CI" isTex1CI = f3dMat.tex1.tex_format[:2] == "CI" @@ -1141,6 +1256,7 @@ def checkDrawMixedCIWarning(self, layout, useDict, f3dMat): layout.box().column().label(text="Two CI textures must use the same CI format.", icon="ERROR") def draw_simple(self, f3dMat, material, layout, context): + f3d = get_F3D_GBI() self.ui_uvCheck(layout, context) inputCol = layout.column() @@ -1148,8 +1264,6 @@ def draw_simple(self, f3dMat, material, layout, context): self.checkDrawLayersWarnings(f3dMat, useDict, layout) - useMultitexture = useDict["Texture 0"] and useDict["Texture 1"] and f3dMat.tex0.tex_set and f3dMat.tex1.tex_set - self.checkDrawMixedCIWarning(inputCol, useDict, f3dMat) canUseLargeTextures = material.mat_ver > 3 and material.f3d_mat.use_large_textures if useDict["Texture 0"] and f3dMat.tex0.tex_set: @@ -1158,7 +1272,7 @@ def draw_simple(self, f3dMat, material, layout, context): if useDict["Texture 1"] and f3dMat.tex1.tex_set: ui_image(canUseLargeTextures, inputCol, material, f3dMat.tex1, "Texture 1", False) - if useMultitexture: + if f3dMat.is_multi_tex: inputCol.prop(f3dMat, "uv_basis", text="UV Basis") if useDict["Texture"]: @@ -1171,7 +1285,12 @@ def draw_simple(self, f3dMat, material, layout, context): if useDict["Environment"] and f3dMat.set_env: self.ui_env(material, inputCol, False) - showLightProperty = f3dMat.set_lights and f3dMat.rdp_settings.g_lighting and f3dMat.rdp_settings.g_shade + showLightProperty = ( + f3dMat.set_lights + and "Lighting" in sources_in_ucode(bpy.context.scene.f3d_type) + and f3dMat.rdp_settings.is_geo_mode_on("g_lighting") + and f3dMat.rdp_settings.is_geo_mode_on("g_shade") + ) if useDict["Shade"] and showLightProperty: self.ui_lights(f3dMat, inputCol, "Lighting", False) @@ -1187,6 +1306,7 @@ def draw_full(self, f3dMat, material, layout: UILayout, context): layout.row().prop(material, "menu_tab", expand=True) menuTab = material.menu_tab useDict = all_combiner_uses(f3dMat) + f3d = get_F3D_GBI() if menuTab == "Combiner": self.ui_draw_layer(material, layout, context) @@ -1237,8 +1357,6 @@ def drawCCProps(ui: UILayout, combiner: "CombinerProperty", isAlpha: bool, enabl inputCol = layout.column() - useMultitexture = useDict["Texture 0"] and useDict["Texture 1"] - self.checkDrawMixedCIWarning(inputCol, useDict, f3dMat) canUseLargeTextures = material.mat_ver > 3 and material.f3d_mat.use_large_textures if useDict["Texture 0"]: @@ -1247,7 +1365,7 @@ def drawCCProps(ui: UILayout, combiner: "CombinerProperty", isAlpha: bool, enabl if useDict["Texture 1"]: ui_image(canUseLargeTextures, inputCol, material, f3dMat.tex1, "Texture 1", True) - if useMultitexture: + if f3dMat.is_multi_tex: inputCol.prop(f3dMat, "uv_basis", text="UV Basis") if useDict["Texture"]: @@ -1260,7 +1378,7 @@ def drawCCProps(ui: UILayout, combiner: "CombinerProperty", isAlpha: bool, enabl if useDict["Environment"]: self.ui_env(material, inputCol, True) - if useDict["Shade"]: + if useDict["Shade"] and "Lighting" in sources_in_ucode(bpy.context.scene.f3d_type): self.ui_lights(f3dMat, inputCol, "Lighting", True) if useDict["Key"]: @@ -1652,7 +1770,11 @@ def update_fog_nodes(material: Material, context: Context): else: # If fog is not being calculated, pass in shade alpha material.node_tree.links.new(nodes["Shade Color"].outputs["Alpha"], fogBlender.inputs["FogAmount"]) - if f3dMat.use_global_fog or not f3dMat.set_fog: # Inherit fog + if ( + (bpy.context.scene.gameEditorMode == "SM64" and f3dMat.use_global_fog) + or not f3dMat.set_fog + or inherit_light_and_fog() + ): # Inherit fog link_if_none_exist(material, nodes["SceneProperties"].outputs["FogColor"], nodes["FogColor"].inputs[0]) link_if_none_exist(material, nodes["GlobalFogColor"].outputs[0], fogBlender.inputs["Fog Color"]) link_if_none_exist(material, nodes["SceneProperties"].outputs["FogNear"], nodes["CalcFog"].inputs["FogNear"]) @@ -1752,7 +1874,7 @@ def update_light_colors(material, context): f3dMat.ambient_light_color = new_amb - if f3dMat.set_lights: + if f3dMat.set_lights or inherit_light_and_fog(): remove_first_link_if_exists(material, nodes["Shade Color"].inputs["AmbientColor"].links) remove_first_link_if_exists(material, nodes["Shade Color"].inputs["Light0Color"].links) remove_first_link_if_exists(material, nodes["Shade Color"].inputs["Light1Color"].links) @@ -1806,6 +1928,7 @@ def update_node_values_of_material(material: Material, context): return f3dMat: "F3DMaterialProperty" = material.f3d_mat + settings: RDPSettings = f3dMat.rdp_settings update_combiner_connections(material, context) @@ -1813,8 +1936,8 @@ def update_node_values_of_material(material: Material, context): nodes = material.node_tree.nodes - if f3dMat.rdp_settings.g_lighting and f3dMat.rdp_settings.g_tex_gen: - if f3dMat.rdp_settings.g_tex_gen_linear: + if (settings.is_geo_mode_on("g_lighting") or inherit_light_and_fog()) and settings.is_geo_mode_on("g_tex_gen"): + if settings.is_geo_mode_on("g_tex_gen_linear"): nodes["UV"].node_tree = bpy.data.node_groups["UV_EnvMap_Linear"] else: nodes["UV"].node_tree = bpy.data.node_groups["UV_EnvMap"] @@ -1832,7 +1955,9 @@ def update_node_values_of_material(material: Material, context): "g_fog", "g_lighting", ]: - shdcol_inputs[propName.upper()].default_value = getattr(f3dMat.rdp_settings, propName) + shdcol_inputs[propName.upper()].default_value = f3dMat.rdp_settings.is_geo_mode_on(propName) + if is_ucode_t3d(bpy.context.scene.f3d_type): # Tiny3d always uses lighting * vertex color + shdcol_inputs["G_LIGHTING"].default_value = shdcol_inputs["G_PACKED_NORMALS"].default_value = True shdcol_inputs["AO Ambient"].default_value = f3dMat.ao_ambient shdcol_inputs["AO Directional"].default_value = f3dMat.ao_directional @@ -2129,14 +2254,12 @@ def update_tex_values(self, context): def get_tex_basis_size(f3d_mat: "F3DMaterialProperty"): - tex_size = None - if f3d_mat.tex0.tex is not None and f3d_mat.tex1.tex is not None: - return f3d_mat.tex0.tex.size if f3d_mat.uv_basis == "TEXEL0" else f3d_mat.tex1.tex.size - elif f3d_mat.tex0.tex is not None: - return f3d_mat.tex0.tex.size - elif f3d_mat.tex1.tex is not None: - return f3d_mat.tex1.tex.size - return tex_size + if (uv_basis := f3d_mat.uv_basis_index) is None: + return [0, 0] + tex_prop: TextureProperty | None = getattr(f3d_mat, f"tex{uv_basis}", None) + if tex_prop is None: + return [0, 0] + return tex_prop.tex_size def get_tex_gen_size(tex_size: list[int | float]): @@ -2147,9 +2270,9 @@ def get_textlut_mode(f3d_mat: "F3DMaterialProperty", inherit_from_tex: bool = Fa use_dict = all_combiner_uses(f3d_mat) textures = [f3d_mat.tex0] if use_dict["Texture 0"] and f3d_mat.tex0.tex_set else [] textures += [f3d_mat.tex1] if use_dict["Texture 1"] and f3d_mat.tex1.tex_set else [] - tlut_modes = [tex.ci_format if tex.tex_format.startswith("CI") else "NONE" for tex in textures] + tlut_modes = [tex.tlut_mode for tex in textures] if tlut_modes and tlut_modes[0] == tlut_modes[-1]: - return "G_TT_" + tlut_modes[0] + return tlut_modes[0] return None if inherit_from_tex else f3d_mat.rdp_settings.g_mdsft_textlut @@ -2194,10 +2317,7 @@ def update_tex_values_manual(material: Material, context, prop_path=None): texture_inputs["1 T TexSize"].default_value = f3dMat.tex1.tex.size[0] uv_basis: ShaderNodeGroup = nodes["UV Basis"] - if f3dMat.uv_basis == "TEXEL0": - uv_basis.node_tree = bpy.data.node_groups["UV Basis 0"] - else: - uv_basis.node_tree = bpy.data.node_groups["UV Basis 1"] + uv_basis.node_tree = bpy.data.node_groups[f"UV Basis {f3dMat.uv_basis_index}"] if not isTexGen: uv_basis.inputs["S Scale"].default_value = f3dMat.tex_scale[0] @@ -2328,8 +2448,6 @@ def load_handler(dummy): rendermode_preset_to_advanced(mat) -bpy.app.handlers.load_post.append(load_handler) - SCENE_PROPERTIES_VERSION = 2 @@ -2732,7 +2850,7 @@ def update_tex_field_prop(self: Property, context: Context): prop_path = self.path_from_id() tex_property, tex_index = get_tex_prop_from_path(material, prop_path) - tex_size = tex_property.get_tex_size() + tex_size = tex_property.tex_size if tex_size[0] > 0 and tex_size[1] > 0: update_tex_values_field(material, tex_property, tex_size, tex_index) @@ -2747,7 +2865,7 @@ def toggle_auto_prop(self, context: Context): prop_path = self.path_from_id() tex_property, tex_index = get_tex_prop_from_path(material, prop_path) if tex_property.autoprop: - tex_size = tuple([s for s in tex_property.get_tex_size()]) + tex_size = tuple([s for s in tex_property.tex_size]) if tex_size[0] > 0 and tex_size[1] > 0: update_tex_values_field(material, tex_property, tex_size, tex_index) @@ -2789,8 +2907,14 @@ class TextureFieldProperty(PropertyGroup): update=update_tex_field_prop, ) + def to_dict(self, autoprop=False): + return prop_group_to_json(self, ["low", "high", "mask", "shift"] if autoprop else None) + + def from_dict(self, data: dict): + json_to_prop_group(self, data) + def key(self): - return (self.clamp, self.mirror, round(self.low * 4), round(self.high * 4), self.mask, self.shift) + return frozenset(self.to_dict().items()) class SetTileSizeScrollProperty(PropertyGroup): @@ -2798,8 +2922,14 @@ class SetTileSizeScrollProperty(PropertyGroup): t: bpy.props.IntProperty(min=-4095, max=4095, default=0) interval: bpy.props.IntProperty(min=1, soft_max=1000, default=1) + def to_dict(self): + return prop_group_to_json(self) + + def from_dict(self, data: dict): + json_to_prop_group(self, data) + def key(self): - return (self.s, self.t, self.interval) + return frozenset(self.to_dict().items()) class TextureProperty(PropertyGroup): @@ -2862,32 +2992,98 @@ class TextureProperty(PropertyGroup): ) tile_scroll: bpy.props.PointerProperty(type=SetTileSizeScrollProperty) - def get_tex_size(self) -> list[int]: + @property + def is_ci(self): + self.tex_format: str + return self.tex_format.startswith("CI") + + @property + def is_set(self): + return self.tex_set and (self.use_tex_reference or self.tex is not None) + + @property + def tlut_mode(self): + return f"G_TT_{self.ci_format if self.is_ci else 'NONE'}" + + @property + def tex_size(self) -> list[int]: if self.tex or self.use_tex_reference: if self.tex is not None: - return self.tex.size + return list(self.tex.size) else: - return self.tex_reference_size + return list(self.tex_reference_size) return [0, 0] + @property + def word_usage(self): + return getTmemWordUsage(self.tex_format, *self.tex_size) + + @property + def format_type(self): + return texFormatOf[self.tex_format][len("G_IM_FMT_") :] + + @property + def format_size(self): + return bitSizeDict[texBitSizeF3D[self.tex_format]] + + def format_to_dict(self): + data = {"texture": self.format_type, "size": self.format_size} + if self.tex_format.startswith("CI"): + data["palette"] = self.ci_format + return data + + def format_from_dict(self, data: dict): + self.tex_format = data.get("texture", self.format_type) + str(data.get("size", self.format_size)) + self.ci_format = data.get("palette", self.ci_format) + + def reference_to_dict(self): + data = {"texture": self.tex_reference, "size": list(self.tex_reference_size)} + if self.is_ci: + data["palette"], data["paletteCount"] = self.pal_reference, self.pal_reference_size + return data + + def reference_from_dict(self, data: dict): + self.tex_reference = data.get("texture", self.tex_reference) + self.tex_reference_size = data.get("size", self.tex_reference_size) + self.pal_reference = data.get("palette", self.pal_reference) + self.pal_reference_size = data.get("paletteCount", self.pal_reference_size) + + def to_dict(self): + """Does not include actual texture and tile scroll""" + data = { + "set": self.tex_set, + "format": self.format_to_dict(), + "fields": (self.S.to_dict(self.autoprop), self.T.to_dict(self.autoprop)), + } + if self.use_tex_reference: + data["reference"] = self.reference_to_dict() + return data + + def from_dict(self, data: dict): + """Does not include actual texture and tile scroll""" + self.tex_set = data.get("set", self.tex_set) + self.format_from_dict(data.get("format", {})) + + fields = data.get("fields", []) + self.S.from_dict(fields[0] if len(fields) >= 1 else {}) + self.T.from_dict(fields[1] if len(fields) >= 2 else {}) + self.autoprop = not any( + advanced_prop in field for field in fields for advanced_prop in {"low", "high", "mask", "shift"} + ) + + self.use_tex_reference = "reference" in data + if self.use_tex_reference: + self.reference_from_dict(data["reference"]) + def key(self): - texSet = self.tex_set - isCI = self.tex_format == "CI8" or self.tex_format == "CI4" - useRef = self.use_tex_reference return ( - self.tex_set, - self.tex if texSet else None, - self.tex_format if texSet else None, - self.ci_format if texSet and isCI else None, - self.S.key() if texSet else None, - self.T.key() if texSet else None, - self.autoprop if texSet else None, - self.tile_scroll.key() if texSet else None, - self.use_tex_reference if texSet else None, - self.tex_reference if texSet and useRef else None, - self.tex_reference_size if texSet and useRef else None, - self.pal_reference if texSet and useRef and isCI else None, - self.pal_reference_size if texSet and useRef and isCI else None, + ( + self.tex if self.tex_set else None, + str(self.to_dict()), + self.tile_scroll.key(), + ) + if self.tex_set + else None ) @@ -2923,6 +3119,7 @@ def ui_image( textureProp: TextureProperty, name: str, showCheckBox: bool, + hide_lowhigh=False, ): inputGroup = layout.box().column() @@ -2970,8 +3167,7 @@ def ui_image( availTmem = 512 if textureProp.tex_format[:2] == "CI": availTmem /= 2 - useDict = all_combiner_uses(material.f3d_mat) - if useDict["Texture 0"] and useDict["Texture 1"]: + if material.f3d_mat.is_multi_tex: availTmem /= 2 isLarge = getTmemWordUsage(textureProp.tex_format, width, height) > availTmem else: @@ -3029,7 +3225,8 @@ def ui_image( shift = prop_input.row() shift.prop(textureProp.S, "shift", text="Shift S") shift.prop(textureProp.T, "shift", text="Shift T") - + if hide_lowhigh: + return low = prop_input.row() low.prop(textureProp.S, "low", text="S Low") low.prop(textureProp.T, "low", text="T Low") @@ -3104,17 +3301,37 @@ class CombinerProperty(PropertyGroup): update=update_combiner_connections_and_preset, ) + def to_dict(self): + return { + "color": [ + self.A, + self.B, + self.C, + self.D, + ], + "alpha": [ + self.A_alpha, + self.B_alpha, + self.C_alpha, + self.D_alpha, + ], + } + + def from_dict(self, data: dict): + default = self.to_dict() + color = data.get("color", default["color"]) + alpha = data.get("alpha", default["alpha"]) + self.A = color[0] + self.B = color[1] + self.C = color[2] + self.D = color[3] + self.A_alpha = alpha[0] + self.B_alpha = alpha[1] + self.C_alpha = alpha[2] + self.D_alpha = alpha[3] + def key(self): - return ( - self.A, - self.B, - self.C, - self.D, - self.A_alpha, - self.B_alpha, - self.C_alpha, - self.D_alpha, - ) + return frozenset(self.to_dict().items()) class ProceduralAnimProperty(PropertyGroup): @@ -3127,18 +3344,16 @@ class ProceduralAnimProperty(PropertyGroup): animate: bpy.props.BoolProperty() animType: bpy.props.EnumProperty(name="Type", items=enumTexScroll) + def to_dict(self): + if not self.animate: + return None + return prop_group_to_json(self, ["animate"]) + + def from_dict(self, data: dict): + json_to_prop_group(self, data, ["animate"]) + def key(self): - anim = self.animate - return ( - self.animate, - round(self.speed, 4) if anim else None, - round(self.amplitude, 4) if anim else None, - round(self.frequency, 4) if anim else None, - round(self.spaceFrequency, 4) if anim else None, - round(self.offset, 4) if anim else None, - round(self.noiseAmplitude, 4) if anim else None, - self.animType if anim else None, - ) + return frozenset(self.to_dict().items()) class ProcAnimVectorProperty(PropertyGroup): @@ -3149,15 +3364,14 @@ class ProcAnimVectorProperty(PropertyGroup): angularSpeed: bpy.props.FloatProperty(default=1, name="Angular Speed") menu: bpy.props.BoolProperty() + def to_dict(self): + return prop_group_to_json(self) + + def from_dict(self, data: dict): + json_to_prop_group(self, data) + def key(self): - return ( - self.x.key(), - self.y.key(), - self.z.key(), - round(self.pivot[0], 4), - round(self.pivot[1], 4), - round(self.angularSpeed, 4), - ) + return frozenset(self.to_dict().items()) class PrimDepthSettings(PropertyGroup): @@ -3562,88 +3776,99 @@ def blend_inputs(self): yield from self.blend_color_inputs yield from self.blend_alpha_inputs + def has_prop_in_ucode(self, prop: str) -> bool: + return prop in geo_modes_in_ucode(bpy.context.scene.f3d_type).values() + + def is_geo_mode_on(self, prop: str) -> bool: + return getattr(self, prop) if self.has_prop_in_ucode(prop) else False + def does_blender_use_input(self, setting: str) -> bool: return any(input == setting for input in self.blend_inputs) - def attributes_to_dict(self, info: dict): + def attributes_to_dict(self, info: dict, remove_prefix=False): data = {} - for key, attr, default in info: + for args in info: + key, attr, default = args[:3] value = getattr(self, attr) if value != default: + if remove_prefix and len(args) == 4: + assert len(args) == 4 + value: str = value.removeprefix(args[3]) data[key] = value return data def attributes_from_dict(self, data: dict, info: dict): - for key, attr, default in info: - setattr(self, attr, data.get(key, default)) - - geo_mode_all_attributes = [ - ("zBuffer", "g_zbuffer", False), - ("shade", "g_shade", False), - ("cullFront", "g_cull_front", False), - ("cullBack", "g_cull_back", False), - ("fog", "g_fog", False), - ("lighting", "g_lighting", False), - ("texGen", "g_tex_gen", False), - ("texGenLinear", "g_tex_gen_linear", False), - ("lod", "g_lod", False), - ("shadeSmooth", "g_shade_smooth", False), - ] - - geo_mode_f3dex_attributes = [ - ("clipping", "g_clipping", True), - ] + for args in info: + key, attr, default = args[:3] + value = data.get(key, default) + if len(args) == 4: + value = add_prefix(value, args[3]) + setattr(self, attr, value) - geo_mode_f3dex3_attributes = [ - ("ambientOcclusion", "g_ambocclusion", False), - ("attroffsetZ", "g_attroffset_z_enable", False), - ("attroffsetST", "g_attroffset_st_enable", False), - ("packedNormals", "g_packed_normals", False), - ("lightToAlpha", "g_lighttoalpha", False), - ("specularLighting", "g_lighting_specular", False), - ("fresnelToColor", "g_fresnel_color", False), - ("fresnelToAlpha", "g_fresnel_alpha", False), - ] - geo_mode_attributes = geo_mode_all_attributes + geo_mode_f3dex_attributes + geo_mode_f3dex3_attributes + geo_mode_attributes = {**F3D_GEO_MODES, **F3DLX_GEO_MODES, **F3DEX3_GEO_MODES} - def geo_mode_to_dict(self, f3d=None): - f3d = f3d if f3d else get_F3D_GBI() - data = self.attributes_to_dict(self.geo_mode_all_attributes) - if f3d.F3DEX_GBI or f3d.F3DLP_GBI: - data.update(self.attributes_to_dict(self.geo_mode_f3dex_attributes)) - if f3d.F3DEX_GBI_3: - data.update(self.attributes_to_dict(self.geo_mode_f3dex3_attributes)) + def geo_mode_dict_to_dict(self, modes: dict): + data = {} + for key, attr in modes.items(): + if getattr(self, attr): + data[key] = True return data + def geo_mode_dict_from_dict(self, data: dict, modes: dict): + for key, attr in modes.items(): + setattr(self, attr, data.get(key, False)) + + def f3d_geo_mode_to_dict(self): + return self.geo_mode_dict_to_dict(F3D_GEO_MODES) + + def f3d_geo_mode_from_dict(self, data: dict): + self.geo_mode_dict_from_dict(data, F3D_GEO_MODES) + + def f3dlx_geo_mode_to_dict(self): + return self.geo_mode_dict_to_dict(F3DLX_GEO_MODES) + + def f3dex1_geo_mode_from_dict(self, data: dict): + self.geo_mode_dict_from_dict(data, F3DLX_GEO_MODES) + + def f3dex3_geo_mode_to_dict(self): + return self.geo_mode_dict_to_dict(F3DEX3_GEO_MODES) + + def f3dex3_geo_mode_from_dict(self, data: dict): + self.geo_mode_dict_from_dict(data, F3DEX3_GEO_MODES) + + def geo_mode_to_dict(self): + return self.geo_mode_dict_to_dict(self.geo_mode_attributes) + def geo_mode_from_dict(self, data: dict): - self.attributes_from_dict(data, self.geo_mode_attributes) + for key, attr in self.geo_mode_attributes.items(): + setattr(self, attr, data.get(key, False)) other_mode_h_attributes = [ - ("alphaDither", "g_mdsft_alpha_dither", "G_AD_DISABLE"), - ("colorDither", "g_mdsft_rgb_dither", "G_CD_MAGICSQ"), - ("chromaKey", "g_mdsft_combkey", "G_CK_NONE"), - ("textureConvert", "g_mdsft_textconv", "G_TC_CONV"), - ("textureFilter", "g_mdsft_text_filt", "G_TF_POINT"), - ("lutFormat", "g_mdsft_textlut", "G_TT_NONE"), - ("textureLoD", "g_mdsft_textlod", "G_TL_TILE"), - ("textureDetail", "g_mdsft_textdetail", "G_TD_CLAMP"), - ("perspectiveCorrection", "g_mdsft_textpersp", "G_TP_NONE"), - ("cycleType", "g_mdsft_cycletype", "G_CYC_1CYCLE"), - ("pipelineMode", "g_mdsft_pipeline", "G_PM_NPRIMITIVE"), + ("alphaDither", "g_mdsft_alpha_dither", "G_AD_DISABLE", "G_AD_"), + ("colorDither", "g_mdsft_rgb_dither", "G_CD_MAGICSQ", "G_CD_"), + ("chromaKey", "g_mdsft_combkey", "G_CK_NONE", "G_CK_"), + ("textureConvert", "g_mdsft_textconv", "G_TC_CONV", "G_TC_"), + ("textureFilter", "g_mdsft_text_filt", "G_TF_POINT", "G_TF_"), + ("lutFormat", "g_mdsft_textlut", "G_TT_NONE", "G_TT_"), + ("textureLoD", "g_mdsft_textlod", "G_TL_TILE", "G_TL_"), + ("textureDetail", "g_mdsft_textdetail", "G_TD_CLAMP", "G_TD_"), + ("perspectiveCorrection", "g_mdsft_textpersp", "G_TP_NONE", "G_TP_"), + ("cycleType", "g_mdsft_cycletype", "G_CYC_1CYCLE", "G_CYC_"), + ("pipelineMode", "g_mdsft_pipeline", "G_PM_NPRIMITIVE", "G_PM_"), ] - def other_mode_h_to_dict(self, lut_format=None): - data = self.attributes_to_dict(self.other_mode_h_attributes) - if lut_format: - data["lutFormat"] = lut_format + def other_mode_h_to_dict(self, remove_prefix=False, lut_format=None): + data = self.attributes_to_dict(self.other_mode_h_attributes, remove_prefix) + if lut_format is not None and lut_format != "G_TT_NONE": + data["lutFormat"] = lut_format[len("G_TT_") :] if remove_prefix else lut_format return data def other_mode_h_from_dict(self, data: dict): self.attributes_from_dict(data, self.other_mode_h_attributes) other_mode_l_attributes = [ - ("alphaCompare", "g_mdsft_alpha_compare", "G_AC_NONE"), - ("zSourceSelection", "g_mdsft_zsrcsel", "G_ZS_PIXEL"), + ("alphaCompare", "g_mdsft_alpha_compare", "G_AC_NONE", "G_AC_"), + ("zSourceSelection", "g_mdsft_zsrcsel", "G_ZS_PIXEL", "G_ZS_"), ] rendermode_flag_attributes = [ @@ -3655,73 +3880,76 @@ def other_mode_h_from_dict(self, data: dict): ("mulCvgXAlpha", "cvg_x_alpha", False), ("forceBlend", "force_bl", False), ("readFB", "im_rd", False), - ("cvgDst", "cvg_dst", "CVG_DST_CLAMP"), - ("zMode", "zmode", "ZMODE_OPA"), + ("cvgDst", "cvg_dst", "CVG_DST_CLAMP", "CVG_DST_"), + ("zMode", "zmode", "ZMODE_OPA", "ZMODE_"), ] - def other_mode_l_to_dict(self): - data = self.attributes_to_dict(self.other_mode_l_attributes) + def other_mode_l_to_dict(self, remove_prefix=False): + data = self.attributes_to_dict(self.other_mode_l_attributes, remove_prefix) if self.g_mdsft_zsrcsel == "G_ZS_PRIM": data["primDepth"] = self.prim_depth.to_dict() if self.set_rendermode: two_cycle = self.g_mdsft_cycletype == "G_CYC_2CYCLE" if self.rendermode_advanced_enabled: blender_data = [] - for i in range(2 if two_cycle else 1): - num = i + 1 - color_attrs, alpha_attrs = (f"blend_p{num}", f"blend_m{num}"), (f"blend_a{num}", f"blend_b{num}") - blender_data.append( - { - "color": (getattr(self, color_attrs[0]), getattr(self, color_attrs[1])), - "alpha": (getattr(self, alpha_attrs[0]), getattr(self, alpha_attrs[1])), - } - ) + for i in range(1, 3 if two_cycle else 2): + colors = [getattr(self, c_attr) for c_attr in (f"blend_p{i}", f"blend_m{i}")] + alphas = [getattr(self, a_attr) for a_attr in (f"blend_a{i}", f"blend_b{i}")] + if remove_prefix: + colors = [color.removeprefix("G_BL_CLR_") for color in colors] + alphas = [alpha.removeprefix("G_BL_") for alpha in alphas] + blender_data.append({"color": colors, "alpha": alphas}) data["renderMode"] = { - "flags": self.attributes_to_dict(self.rendermode_flag_attributes), + "flags": self.attributes_to_dict(self.rendermode_flag_attributes, remove_prefix), "blender": blender_data, } else: - data["renderMode"] = { - "presets": [self.rendermode_preset_cycle_1] - + ([self.rendermode_preset_cycle_2] if two_cycle else []) - } + presets = [self.rendermode_preset_cycle_1] + if two_cycle: + presets.append(self.rendermode_preset_cycle_2) + if remove_prefix: + presets = [preset.removeprefix("G_RM_") for preset in presets] + data["renderMode"] = {"presets": presets} return data def other_mode_l_from_dict(self, data: dict): self.attributes_from_dict(data, self.other_mode_l_attributes) - self.prim_depth.from_dict(data.get("primDepth", {})) render_mode = data.get("renderMode", {}) blender = render_mode.get("blender", []) flags = render_mode.get("flags", {}) - if render_mode: - self.set_rendermode = True - if not render_mode.get("presets", None) and (flags or blender): - self.rendermode_advanced_enabled = True + self.set_rendermode = bool(render_mode) + self.rendermode_advanced_enabled = bool(blender or flags) - self.rendermode_preset_cycle_1, self.rendermode_preset_cycle_2 = render_mode.get( - "presets", [self.rendermode_preset_cycle_1, self.rendermode_preset_cycle_2] - ) + presets = render_mode.get("presets", []) + if len(presets) >= 1: + self.rendermode_preset_cycle_1 = add_prefix(presets[0], "G_RM_") + if len(presets) >= 2: + self.rendermode_preset_cycle_2 = add_prefix(presets[1], "G_RM_") self.attributes_from_dict(flags, self.rendermode_flag_attributes) - color_attrs = ("blend_p", "blend_m") - alpha_attrs = ("blend_a", "blend_b") - for i, cycle in enumerate(blender * 2 if len(blender) == 1 else blender): + + blender = blender[:2] if len(blender) > 1 else blender * 2 + for i, cycle in enumerate(blender): num = str(i + 1) - color_attrs, alpha_attrs = (f"blend_p{num}", f"blend_m{num}"), (f"blend_a{num}", f"blend_b{num}") - self[color_attrs[0]], self[color_attrs[1]] = cycle.get( - "color", [self.get(color_attrs[0]), self.get(color_attrs[1])] - ) - self[alpha_attrs[0]], self[alpha_attrs[1]] = cycle.get( - "alpha", [self.get(alpha_attrs[0]), self.get(alpha_attrs[1])] - ) + c_attrs, a_attrs = (f"blend_p{num}", f"blend_m{num}"), (f"blend_a{num}", f"blend_b{num}") + colors = cycle.get("color", [getattr(self, c_attrs[0]), getattr(self, c_attrs[1])]) + alphas = cycle.get("alpha", [getattr(self, a_attrs[0]), getattr(self, a_attrs[1])]) + + # Add back prefix if not there + colors = [add_prefix(color, "G_BL_CLR_") for color in colors] + alphas = [add_prefix(alpha, "G_BL_") for alpha in alphas] + setattr(self, c_attrs[0], colors[0]) + setattr(self, c_attrs[1], colors[1]) + setattr(self, a_attrs[0], alphas[0]) + setattr(self, a_attrs[1], alphas[1]) def other_to_dict(self): data = {} - if self.clip_ratio != 1.0: + if self.clip_ratio != 2: data["clipRatio"] = self.clip_ratio - if self.g_mdsft_textlod == "G_TL_LOD" and self.num_textures_mipmapped != 1: + if self.g_mdsft_textlod == "G_TL_LOD": data["mipmapCount"] = self.num_textures_mipmapped return data @@ -3729,11 +3957,13 @@ def other_from_dict(self, data: dict): self.clip_ratio = data.get("clipRatio", self.clip_ratio) self.num_textures_mipmapped = data.get("mipmapCount", self.num_textures_mipmapped) - def to_dict(self, f3d=None): + def to_dict(self): data = {} - data["geometryMode"] = self.geo_mode_to_dict(f3d) + data["geometryMode"] = self.geo_mode_to_dict() data["otherModeH"] = self.other_mode_h_to_dict() data["otherModeL"] = self.other_mode_l_to_dict() + if self.g_mdsft_zsrcsel == "G_ZS_PRIM": + data["primDepth"] = self.prim_depth.to_dict() data["other"] = self.other_to_dict() return data @@ -3741,6 +3971,7 @@ def from_dict(self, data: dict): self.geo_mode_from_dict(data.get("geometryMode", {})) self.other_mode_h_from_dict(data.get("otherModeH", {})) self.other_mode_l_from_dict(data.get("otherModeL", {})) + self.prim_depth.from_dict(data.get("primDepth", {})) self.other_from_dict(data.get("other", {})) def key(self): @@ -3832,6 +4063,35 @@ class CelLevelProperty(PropertyGroup): default=1, ) + def to_dict(self): + tint_data = {} + data = {"thresholdMode": self.threshMode.upper(), "threshold": self.threshold, "tint": tint_data} + tint_data["type"] = self.tintType.upper() + if self.tintType == "Fixed": + tint_data["level"] = self.tintFixedLevel + tint_data["color"] = get_clean_color(self.tintFixedColor) + elif self.tintType == "Segment": + tint_data["segment"] = self.tintSegmentNum + tint_data["offset"] = self.tintSegmentOffset + elif self.tintType == "Light": + tint_data["level"] = self.tintFixedLevel + tint_data["light"] = self.tintLightSlot + return data + + def from_dict(self, data: dict): + self.threshMode = data.get("thresholdMode", "LIGHTER").lower().capitalize() + self.threshold = data.get("threshold", 128) + tint = data.get("tint", {}) + self.tintType = tint.get("type", "FIXED").lower().capitalize() + self.tintFixedLevel = tint.get("level", 50) + self.tintFixedColor = tint.get("color", [0.0, 0.0, 0.0]) + self.tintSegmentNum = tint.get("segment", 8) + self.tintSegmentOffset = tint.get("offset", 0) + self.tintLightSlot = tint.get("light", 1) + + def key(self): + return str(self.to_dict().items()) + class CelShadingProperty(PropertyGroup): tintPipeline: bpy.props.EnumProperty(items=enumCelTintPipeline, name="Tint pipeline", default="CC") @@ -3843,6 +4103,15 @@ class CelShadingProperty(PropertyGroup): ) levels: bpy.props.CollectionProperty(type=CelLevelProperty, name="Cel levels") + def to_dict(self): + return prop_group_to_json(self) + + def from_dict(self, data: dict): + json_to_prop_group(self, data) + + def key(self): + return str(self.to_dict().items()) + def celGetMaterialLevels(materialName): material = bpy.data.materials.get(materialName) @@ -3877,10 +4146,6 @@ def execute(self, context): return {"FINISHED"} -def getCurrentPresetDir(): - return "f3d/" + bpy.context.scene.gameEditorMode.lower() - - class ApplyMaterialPresetOperator(Operator): bl_idname = "material.f3d_preset_apply" bl_label = "Apply F3D Material Preset" @@ -3892,10 +4157,6 @@ def execute(self, context: Context): return {"FINISHED"} -def getCurrentPresetDir(): - return "f3d/" + bpy.context.scene.gameEditorMode.lower() - - # modules/bpy_types.py -> Menu class MATERIAL_MT_f3d_presets(Menu): bl_label = "F3D Material Presets" @@ -3915,13 +4176,19 @@ def draw(self, _context): ext_valid = getattr(self, "preset_extensions", {".py", ".xml"}) props_default = getattr(self, "preset_operator_defaults", None) add_operator = getattr(self, "preset_add_operator", None) - presetDir = getCurrentPresetDir() + game = bpy.context.scene.gameEditorMode.lower() paths = bpy.utils.preset_paths("f3d/user") if not bpy.context.scene.f3dUserPresetsOnly: - paths += bpy.utils.preset_paths(presetDir) - if bpy.context.scene.f3d_type == "F3DEX3": - paths += bpy.utils.preset_paths(f"{presetDir}_f3dex3") + if game == "sm64": + if bpy.context.scene.fast64.sm64.lighting_engine_presets: + paths += bpy.utils.preset_paths("f3d/sm64_lighting_engine") + else: + paths += bpy.utils.preset_paths("f3d/sm64") + elif game == "oot": + paths += bpy.utils.preset_paths("f3d/oot") + if bpy.context.scene.f3d_type == "F3DEX3": + paths += bpy.utils.preset_paths("f3d/oot_f3dex3") self.path_menu( paths, self.preset_operator, @@ -4555,89 +4822,245 @@ class F3DMaterialProperty(PropertyGroup): use_cel_shading: bpy.props.BoolProperty(name="Use Cel Shading", update=update_cel_cutout_source) cel_shading: bpy.props.PointerProperty(type=CelShadingProperty) + @property + def is_multi_tex(self): + return combiner_uses_tex0(self) and combiner_uses_tex1(self) + + @property + def uv_basis_index(self) -> int | None: + uses_tex0, uses_tex1 = combiner_uses_tex0(self), combiner_uses_tex1(self) + if uses_tex0 and uses_tex1: + value = self.uv_basis.lstrip("TEXEL") + try: + return int(value) + except ValueError: + return None + return 0 if uses_tex0 else (1 if uses_tex1 else None) + + def combiner_to_dict(self): + cycles = [self.combiner1.to_dict()] + if self.rdp_settings.g_mdsft_cycletype == "G_CYC_2CYCLE": + cycles.append(self.combiner2.to_dict()) + return {"set": self.set_combiner, "cycles": cycles} + + def combiner_from_dict(self, data: dict): + self.set_combiner = data.get("set", self.set_combiner) + cycles = data.get("cycles", []) + if len(cycles) >= 1: + self.combiner1.from_dict(cycles[0]) + if len(cycles) >= 2: + self.combiner2.from_dict(cycles[1]) + + def f3dex3_colors_to_dict(self): + data = {} + rdp = self.rdp_settings + if rdp.g_ambocclusion: + data["ambientOcclusion"] = { + "set": self.set_ao, + "ambient": self.ao_ambient, + "directional": self.ao_directional, + "point": self.ao_point, + } + if rdp.g_fresnel_color or rdp.g_fresnel_alpha: + data["fresnel"] = {"set": self.set_fresnel, "low": self.fresnel_lo, "high": self.fresnel_hi} + attr_offset = {} + if rdp.g_attroffset_st_enable: + attr_offset["st"] = {"set": self.set_attroffs_st, "value": list(self.attroffs_st)} + if rdp.g_attroffset_z_enable: + attr_offset["z"] = {"set": self.set_attroffs_z, "value": self.attroffs_z} + if attr_offset: + data["attributeOffset"] = attr_offset + if self.use_cel_shading: + data["celShading"] = self.cel_shading.to_dict() + return data + + def f3dex3_colors_from_dict(self, data: dict): + ao = data.get("ambientOcclusion", {}) + self.set_ao = ao.get("set", self.set_ao) + self.ao_ambient = ao.get("ambient", self.ao_ambient) + self.ao_directional = ao.get("directional", self.ao_directional) + self.ao_point = ao.get("point", self.ao_point) + fresnel = data.get("fresnel", {}) + self.set_fresnel = fresnel.get("set", self.set_fresnel) + self.fresnel_lo = fresnel.get("low", self.fresnel_lo) + self.fresnel_hi = fresnel.get("high", self.fresnel_hi) + attr_offset = data.get("attributeOffset", {}) + st_attr_offset = attr_offset.get("st", {}) + self.set_attroffs_st = st_attr_offset.get("set", self.set_attroffs_st) + self.attroffs_st = st_attr_offset.get("value", self.attroffs_st) + z_attr_offset = attr_offset.get("z", {}) + self.set_attroffs_z = z_attr_offset.get("set", self.set_attroffs_z) + self.attroffs_z = z_attr_offset.get("value", self.attroffs_z) + cel_shading = data.get("celShading", {}) + if cel_shading: + self.use_cel_shading = True + self.cel_shading.from_dict(cel_shading) + + def lights_to_dict(self, use_dict: dict[str]): + if not (use_dict["Shade"] and self.rdp_settings.g_lighting and self.set_lights): + return {} + lights = [] + + def add_fast64_f3d_light_to_list(light: bpy.types.Light, light_list: list = lights): + if light is None: + return + for obj in bpy.context.scene.objects: + if obj.data == light.original: + light_list.append( + { + "color": get_clean_color(light.color), + "direction": list(getObjDirectionVec(obj, True)), + } + ) + + ambient = get_clean_color(self.ambient_light_color) + if self.use_default_lighting: + lights.append({"color": get_clean_color(self.default_light_color)}) + if self.set_ambient_from_light: + ambient = None + else: + for i in range(1, 8): + add_fast64_f3d_light_to_list(self.get(f"f3d_light{str(i)}"), lights) + return {"lights": lights, "ambientColor": ambient} + + def lights_from_dict(self, data: dict): + lights = data.get("lights", []) + if len(lights) == 1 and not "direction" in lights[0] and "color" in lights[0]: # Default lighting + self.use_default_lighting = True + self.default_light_color = lights[0]["color"] + [1.0] + else: + self.use_default_lighting = False + # TODO: Figure out conversion for object lights + ambient = data.get("ambientColor", []) + self.set_ambient_from_light = not ambient + if ambient: + self.ambient_light_color = ambient + [1.0] + + def f3d_colors_to_dict(self, use_dict: dict[str]): + data = {} + if self.rdp_settings.g_lighting and self.rdp_settings.g_shade and use_dict["Shade"]: + data["lights"] = {"set": self.set_lights, **self.lights_to_dict(use_dict)} + return data + + def f3d_colors_from_dict(self, data: dict): + lighting = data.get("lights", {}) + self.set_lights = lighting.get("set", self.set_lights) + self.lights_from_dict(lighting) + + def n64_colors_to_dict(self, use_dict: dict[str]): + data = {} + if use_dict["Environment"]: + data["environment"] = { + "set": self.set_env, + "color": get_clean_color(self.env_color, include_alpha=True), + } + if use_dict["Primitive"]: + data["primitive"] = { + "set": self.set_prim, + "color": get_clean_color(self.prim_color, include_alpha=True), + "minLoDRatio": self.prim_lod_min, + "loDFraction": self.prim_lod_frac, + } + if use_dict["Key"]: + data["chromaKey"] = { + "set": self.set_key, + "center": get_clean_color(self.key_center), + "scale": list(self.key_scale), + "width": list(self.key_width), + } + if use_dict["Convert"]: + data["yuvConvert"] = { + "set": self.set_k0_5, + "values": [round(k, 4) for k in (self.k0, self.k1, self.k2, self.k3, self.k4, self.k5)], + } + + rdp = self.rdp_settings + if rdp.using_fog: + data["fog"] = { + "set": self.set_fog, + "color": get_clean_color(self.fog_color, include_alpha=True), + "range": list(self.fog_position), + } + data["blend"] = { + "set": self.set_blend, + "color": get_clean_color(self.blend_color, include_alpha=True), + } + return data + + def n64_colors_from_dict(self, data: dict): + enviroment = data.get("environment", {}) + self.set_env = enviroment.get("set", self.set_env) + if "color" in enviroment: + self.env_color = enviroment.get("color") + primitive = data.get("primitive", {}) + self.set_prim = primitive.get("set", self.set_prim) + if "color" in primitive: + self.prim_color = primitive.get("color") + self.prim_lod_min, self.prim_lod_frac = ( + primitive.get("minLoDRatio", self.prim_lod_min), + primitive.get("loDFraction", self.prim_lod_frac), + ) + key = data.get("chromaKey", {}) + self.set_key = key.get("set", self.set_key) + if "center" in key: + self.key_center = key.get("center") + [1.0] + self.key_scale, self.key_width = ( + key.get("scale", list(self.key_scale)), + key.get("width", list(self.key_width)), + ) + convert = data.get("yuvConvert", {}) + self.set_k0_5 = convert.get("set", self.set_k0_5) + self.k0, self.k1, self.k2, self.k3, self.k4, self.k5 = convert.get( + "values", [self.k0, self.k1, self.k2, self.k3, self.k4, self.k5] + ) + fog = data.get("fog", {}) + self.set_fog = fog.get("set", self.set_fog) + if "color" in fog: + self.fog_color = fog.get("color") + self.fog_position = fog.get("range", list(self.fog_position)) + blend = data.get("blend", {}) + self.set_blend = blend.get("set", self.set_blend) + if "color" in blend: + self.blend_color = blend.get("color") + + def colors_to_dict(self, f3d, use_dict: dict[str]): + f3d = f3d if f3d else get_F3D_GBI() + data = {**self.n64_colors_to_dict(use_dict), **self.f3d_colors_to_dict(use_dict)} + if f3d.F3DEX_GBI_3: + data.update(self.f3dex3_colors_to_dict(f3d)) + + def colors_from_dict(self, data: dict): + self.f3d_colors_from_dict(data) + self.f3dex3_colors_from_dict(data) + + def extra_texture_settings_to_dict(self): + data = {} + if not self.scale_autoprop: + data["textureScale"] = [round(value, 4) for value in self.tex_scale] + if self.use_large_textures: + data["largeTextureMode"] = {"edges": self.large_edges.upper()} + if (uv_basis := self.uv_basis_index) is not None: + data["uvBasis"] = uv_basis + return data + + def extra_texture_settings_from_dict(self, data): + self.tex_scale = data.get("textureScale", self.tex_scale) + large_mode = data.get("largeTextureMode", {}) + self.use_large_textures = bool(large_mode) + self.large_edges = large_mode.get("edges", self.large_edges.upper()).lower().capitalize() + self.uv_basis = "TEXEL" + str(data.get("uvBasis", 0)) + def key(self) -> F3DMaterialHash: - useDefaultLighting = self.set_lights and self.use_default_lighting return ( - self.scale_autoprop, - self.uv_basis, self.UVanim0.key(), self.UVanim1.key(), - tuple([round(value, 4) for value in self.tex_scale]), self.tex0.key(), self.tex1.key(), self.rdp_settings.key(), self.draw_layer.key(), - self.use_large_textures, - self.use_cel_shading, - self.cel_shading.tintPipeline if self.use_cel_shading else None, - ( - tuple( - [ - ( - c.threshMode, - c.threshold, - c.tintType, - c.tintFixedLevel, - c.tintFixedColor, - c.tintSegmentNum, - c.tintSegmentOffset, - c.tintLightSlot, - ) - for c in self.cel_shading.levels - ] - ) - if self.use_cel_shading - else None - ), - self.use_default_lighting, - self.set_blend, - self.set_prim, - self.set_env, - self.set_key, - self.set_k0_5, - self.set_combiner, - self.set_lights, - self.set_fog, - tuple([round(value, 4) for value in self.blend_color]) if self.set_blend else None, - tuple([round(value, 4) for value in self.prim_color]) if self.set_prim else None, - round(self.prim_lod_frac, 4) if self.set_prim else None, - round(self.prim_lod_min, 4) if self.set_prim else None, - tuple([round(value, 4) for value in self.env_color]) if self.set_env else None, - tuple([round(value, 4) for value in self.key_center]) if self.set_key else None, - tuple([round(value, 4) for value in self.key_scale]) if self.set_key else None, - tuple([round(value, 4) for value in self.key_width]) if self.set_key else None, - round(self.k0, 4) if self.set_k0_5 else None, - round(self.k1, 4) if self.set_k0_5 else None, - round(self.k2, 4) if self.set_k0_5 else None, - round(self.k3, 4) if self.set_k0_5 else None, - round(self.k4, 4) if self.set_k0_5 else None, - round(self.k5, 4) if self.set_k0_5 else None, - self.combiner1.key() if self.set_combiner else None, - self.combiner2.key() if self.set_combiner else None, - ( - tuple([round(value, 4) for value in (self.ao_ambient, self.ao_directional, self.ao_point)]) - if self.set_ao - else None - ), - tuple([round(value, 4) for value in (self.fresnel_lo, self.fresnel_hi)]) if self.set_fresnel else None, - tuple([round(value, 4) for value in self.attroffs_st]) if self.set_attroffs_st else None, - self.attroffs_z if self.set_attroffs_z else None, - tuple([round(value, 4) for value in self.fog_color]) if self.set_fog else None, - tuple([round(value, 4) for value in self.fog_position]) if self.set_fog else None, - tuple([round(value, 4) for value in self.default_light_color]) if useDefaultLighting else None, - self.set_ambient_from_light if useDefaultLighting else None, - ( - tuple([round(value, 4) for value in self.ambient_light_color]) - if useDefaultLighting and not self.set_ambient_from_light - else None - ), - self.f3d_light1 if not useDefaultLighting else None, - self.f3d_light2 if not useDefaultLighting else None, - self.f3d_light3 if not useDefaultLighting else None, - self.f3d_light4 if not useDefaultLighting else None, - self.f3d_light5 if not useDefaultLighting else None, - self.f3d_light6 if not useDefaultLighting else None, - self.f3d_light7 if not useDefaultLighting else None, + str(self.extra_texture_settings_to_dict().items()), + str(self.cel_shading.to_dict()) if self.use_cel_shading else None, + str(self.colors_to_dict(get_F3D_GBI(), all_combiner_uses(self))), ) @@ -4867,8 +5290,9 @@ def mat_register(): savePresets() Scene.f3d_type = bpy.props.EnumProperty( - name="F3D Microcode", items=enumF3D, default="F3D", update=update_all_material_nodes + name="Microcode", items=enumF3D, default="F3D", update=update_all_material_nodes ) + Scene.packed_normals_algorithm = bpy.props.EnumProperty(name="Packed normals alg", items=enumPackedNormalsAlgorithm) # RDP Defaults World.rdp_defaults = bpy.props.PointerProperty(type=RDPSettings) @@ -4882,7 +5306,7 @@ def mat_register(): Material.mat_ver = bpy.props.IntProperty(default=1) Material.f3d_update_flag = bpy.props.BoolProperty() Material.f3d_mat = bpy.props.PointerProperty(type=F3DMaterialProperty) - Material.menu_tab = bpy.props.EnumProperty(items=enumF3DMenu) + Material.menu_tab = bpy.props.EnumProperty(items=menu_items_enum) Scene.f3dUserPresetsOnly = bpy.props.BoolProperty(name="User Presets Only") Scene.f3d_simple = bpy.props.BoolProperty(name="Display Simple", default=True) @@ -4905,10 +5329,13 @@ def mat_register(): Object.is_occlusion_planes = bpy.props.BoolProperty(name="Is Occlusion Planes") VIEW3D_HT_header.append(draw_f3d_render_settings) + bpy.app.handlers.load_post.append(load_handler) def mat_unregister(): VIEW3D_HT_header.remove(draw_f3d_render_settings) + while load_handler in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(load_handler) del Material.menu_tab del Material.f3d_mat diff --git a/fast64_internal/f3d/f3d_material_helpers.py b/fast64_internal/f3d/f3d_material_helpers.py index 1ab707851..da54ca9d8 100644 --- a/fast64_internal/f3d/f3d_material_helpers.py +++ b/fast64_internal/f3d/f3d_material_helpers.py @@ -37,59 +37,95 @@ def unlock_material(self): self.material.f3d_update_flag = False -EXCLUDE_FROM_NODE = ( +GENERAL_EXCLUDE = ( "rna_type", "type", + "bl_icon", + "bl_label", + "bl_idname", + "bl_description", + "bl_static_type", + "bl_height_default", + "bl_width_default", + "bl_width_max", + "bl_width_min", + "bl_height_max", + "bl_height_min", + "socket_idname", + "color_tag", + "select", + "is_inactive", + "is_icon_visible", + "parent", +) +EXCLUDE_FROM_NODE = GENERAL_EXCLUDE + ( "inputs", "outputs", "dimensions", "interface", "internal_links", + "image_user", "texture_mapping", "color_mapping", - "image_user", ) -EXCLUDE_FROM_INPUT_OUTPUT = ( - "rna_type", +EXCLUDE_FROM_GROUP_INPUT_OUTPUT = GENERAL_EXCLUDE + ( + "bl_subtype_label", + "bl_socket_idname", + "display_shape", "label", "identifier", "is_output", "is_linked", "is_multi_input", "node", - "bl_idname", - "default_value", "is_unavailable", + "show_expanded", + "link_limit", + "default_attribute_name", + "name", + "index", + "position", + "socket_type", + "in_out", + "item_type", + "default_input", # poorly documented, what does it do? ) def node_tree_copy(src: NodeTree, dst: NodeTree): def copy_attributes(src, dst, excludes=None): fails, excludes = [], excludes if excludes else [] - attributes = (attr.identifier for attr in src.bl_rna.properties if attr.identifier not in excludes) + attributes = {attr.identifier for attr in src.bl_rna.properties if attr.identifier not in excludes} for attr in attributes: try: setattr(dst, attr, getattr(src, attr)) except Exception as exc: # pylint: disable=broad-except - fails.append(exc) + fails.append((dst, attr, exc)) if fails: - raise AttributeError("Failed to copy all attributes: " + str(fails)) + print(f"Failed to copy all attributes: {fails}") dst.nodes.clear() dst.links.clear() node_mapping = {} # To not have to look up the new node for linking for src_node in src.nodes: # Copy all nodes - new_node = dst.nodes.new(src_node.bl_idname) - copy_attributes(src_node, new_node, excludes=EXCLUDE_FROM_NODE) - node_mapping[src_node] = new_node + node_mapping[src_node] = dst.nodes.new(src_node.bl_idname) + for src_node, dst_node in node_mapping.items(): - for i, src_input in enumerate(src_node.inputs): # Link all nodes - for link in src_input.links: - connected_node = dst.nodes[link.from_node.name] - dst.links.new(connected_node.outputs[link.from_socket.name], dst_node.inputs[i]) + if src_node.parent is not None: + dst_node.parent = node_mapping[src_node.parent] + copy_attributes(src_node, dst_node, excludes=EXCLUDE_FROM_NODE) + for src_node, dst_node in node_mapping.items(): + input_output_exclude = EXCLUDE_FROM_GROUP_INPUT_OUTPUT + if src_node.type == "REROUTE": + input_output_exclude += ("default_value",) for src_input, dst_input in zip(src_node.inputs, dst_node.inputs): # Copy all inputs - copy_attributes(src_input, dst_input, excludes=EXCLUDE_FROM_INPUT_OUTPUT) + copy_attributes(src_input, dst_input, excludes=input_output_exclude) for src_output, dst_output in zip(src_node.outputs, dst_node.outputs): # Copy all outputs - copy_attributes(src_output, dst_output, excludes=EXCLUDE_FROM_INPUT_OUTPUT) + copy_attributes(src_output, dst_output, excludes=input_output_exclude) + + for i, src_input in enumerate(src_node.inputs): # Link all nodes + for link in src_input.links: + connected_node = node_mapping[link.from_node] + dst.links.new(connected_node.outputs[link.from_socket.name], dst_node.inputs[i]) diff --git a/fast64_internal/f3d/f3d_material_presets.py b/fast64_internal/f3d/f3d_material_presets.py index 2a9c26a2b..89f0502eb 100644 --- a/fast64_internal/f3d/f3d_material_presets.py +++ b/fast64_internal/f3d/f3d_material_presets.py @@ -1809,13 +1809,13 @@ f3d_mat.presetName = 'Sm64 Decal' """ -sm64_environment_map = """ +sm64_unlit_environment_map = """ import bpy f3d_mat = bpy.context.material.f3d_mat bpy.context.material.f3d_update_flag = True -f3d_mat.name = '' +f3d_mat.name = 'Sm64 Unlit Environment Map' f3d_mat.combiner1.name = '' f3d_mat.combiner1.A = '0' f3d_mat.combiner1.B = '0' @@ -1918,8 +1918,26 @@ f3d_mat.draw_layer.oot = 'Opaque' bpy.context.material.f3d_update_flag = False f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update -f3d_mat.presetName = 'Sm64 Environment Map' +f3d_mat.presetName = 'Sm64 Unlit Environment Map' +""" + +sm64_shaded_environment_map = ( + sm64_unlit_environment_map + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.combiner1.A = 'TEXEL0' +f3d_mat.combiner1.B = '0' +f3d_mat.combiner1.C = 'SHADE' +f3d_mat.combiner1.D = '0' +f3d_mat.combiner1.A_alpha = '0' +f3d_mat.combiner1.B_alpha = '0' +f3d_mat.combiner1.C_alpha = '0' +f3d_mat.combiner1.D_alpha = 'ENVIRONMENT' +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +f3d_mat.presetName = 'Sm64 Shaded Environment Map' """ +) sm64_fog_shaded_texture = """ @@ -2953,7 +2971,7 @@ f3d_mat.combiner2.A_alpha = '0' f3d_mat.combiner2.B_alpha = '0' f3d_mat.combiner2.C_alpha = '0' -f3d_mat.combiner2.D_alpha = 'ENVIRONMENT' +f3d_mat.combiner2.D_alpha = 'TEXEL0' f3d_mat.tex0.tex_set = True f3d_mat.tex1.tex_set = True f3d_mat.set_prim = True @@ -6380,6 +6398,125 @@ f3d_mat.presetName = 'Oot Water Mult Specular Fresnel' """ +# SM64 Lighting Engine specific + +lighting_engine_sm64_decal = ( + sm64_decal + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.combiner1.A = 'TEXEL0' +f3d_mat.combiner1.B = 'PRIMITIVE' +f3d_mat.combiner1.C = 'TEXEL0_ALPHA' +f3d_mat.combiner1.D = 'PRIMITIVE' +f3d_mat.combiner1.A_alpha = '0' +f3d_mat.combiner1.B_alpha = '0' +f3d_mat.combiner1.C_alpha = '0' +f3d_mat.combiner1.D_alpha = 'ENVIRONMENT' +f3d_mat.combiner2.A = 'COMBINED' +f3d_mat.combiner2.B = '0' +f3d_mat.combiner2.C = 'SHADE' +f3d_mat.combiner2.D = '0' +f3d_mat.combiner2.A_alpha = '0' +f3d_mat.combiner2.B_alpha = '0' +f3d_mat.combiner2.C_alpha = '0' +f3d_mat.combiner2.D_alpha = 'COMBINED' +f3d_mat.set_prim = True +f3d_mat.set_lights = False +f3d_mat.rdp_settings.g_mdsft_cycletype = 'G_CYC_2CYCLE' +f3d_mat.rdp_settings.set_rendermode = True +f3d_mat.rdp_settings.rendermode_advanced_enabled = False +f3d_mat.rdp_settings.rendermode_preset_cycle_1 = 'G_RM_NOOP' +f3d_mat.rdp_settings.rendermode_preset_cycle_2 = 'G_RM_AA_ZB_OPA_SURF2' +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + +lighting_engine_sm64_fog_shaded_texture = ( + sm64_fog_shaded_texture + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + +lighting_engine_sm64_fog_shaded_texture_cutout = ( + sm64_fog_shaded_texture_cutout + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + +lighting_fog_shaded_texture_transparent = ( + sm64_fog_shaded_texture_transparent + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + +lighting_engine_sm64_shaded_noise = ( + sm64_shaded_noise + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + +lighting_engine_sm64_shaded_solid = ( + sm64_shaded_solid + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.combiner1.A = 'PRIMITIVE' +f3d_mat.combiner1.B = '0' +f3d_mat.combiner1.C = 'SHADE' +f3d_mat.combiner1.D = '0' +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + +lighting_engine_sm64_shaded_texture_cutout = ( + sm64_shaded_texture_cutout + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + + +lighting_engine_sm64_shaded_texture_transparent = ( + sm64_shaded_texture_transparent + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + +lighting_engine_sm64_shaded_environment_map = ( + sm64_shaded_environment_map + + """ +bpy.context.material.f3d_update_flag = True +f3d_mat.set_lights = False +bpy.context.material.f3d_update_flag = False +f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update +""" +) + homebrew_and_oot = { "shaded_environment_mapped": shaded_environment_mapped, "shaded_environment_mapped_transparent": shaded_environment_mapped_transparent, @@ -6399,6 +6536,7 @@ "vertex_colored_texture_cutout": vertex_colored_texture_cutout, "vertex_colored_texture_transparent": vertex_colored_texture_transparent, } + material_presets = { "homebrew": homebrew_and_oot, "oot": { @@ -6430,7 +6568,6 @@ }, "sm64": { "sm64_decal": sm64_decal, - "sm64_environment_map": sm64_environment_map, "sm64_fog_shaded_texture": sm64_fog_shaded_texture, "sm64_fog_shaded_texture_cutout": sm64_fog_shaded_texture_cutout, "sm64_fog_shaded_texture_transparent": sm64_fog_shaded_texture_transparent, @@ -6439,9 +6576,46 @@ "sm64_shaded_texture": sm64_shaded_texture, "sm64_shaded_texture_cutout": sm64_shaded_texture_cutout, "sm64_shaded_texture_transparent": sm64_shaded_texture_transparent, + "sm64_shaded_environment_map": sm64_shaded_environment_map, + "sm64_unlit_environment_map": sm64_unlit_environment_map, "sm64_unlit_texture": sm64_unlit_texture, "sm64_unlit_texture_cutout": sm64_unlit_texture_cutout, "sm64_vertex_colored_texture": sm64_vertex_colored_texture, "sm64_vertex_colored_texture_transparent": sm64_vertex_colored_texture_transparent, }, + "sm64_lighting_engine": { + "sm64_decal": lighting_engine_sm64_decal, + "sm64_fog_shaded_texture": lighting_engine_sm64_fog_shaded_texture, + "sm64_fog_shaded_texture_cutout": lighting_engine_sm64_fog_shaded_texture_cutout, + "sm64_fog_shaded_texture_transparent": lighting_fog_shaded_texture_transparent, + "sm64_shaded_noise": lighting_engine_sm64_shaded_noise, + "sm64_shaded_solid": lighting_engine_sm64_shaded_solid, + "sm64_shaded_texture_cutout": lighting_engine_sm64_shaded_texture_cutout, + "sm64_shaded_texture_transparent": lighting_engine_sm64_shaded_texture_transparent, + "sm64_shaded_environment_map": lighting_engine_sm64_shaded_environment_map, + "sm64_unlit_environment_map": sm64_unlit_environment_map, + "sm64_unlit_texture": sm64_unlit_texture, + "sm64_unlit_texture_cutout": sm64_unlit_texture_cutout, + "sm64_vertex_colored_texture": sm64_vertex_colored_texture, + "sm64_vertex_colored_texture_transparent": sm64_vertex_colored_texture_transparent, + }, + "mk64": { + "shaded_environment_mapped": shaded_environment_mapped, + "shaded_environment_mapped_transparent": shaded_environment_mapped_transparent, + "shaded_multitexture_lerp": shaded_multitexture_lerp, + "shaded_multitexture_lerp_transparent": shaded_multitexture_lerp_transparent, + "shaded_solid": shaded_solid, + "shaded_multitexture_lerp_transparent_vertex_alpha": shaded_multitexture_lerp_transparent_vertex_alpha, + "shaded_solid_transparent": shaded_solid_transparent, + "shaded_texture": shaded_texture, + "shaded_texture_cutout": shaded_texture_cutout, + "shaded_texture_transparent": shaded_texture_transparent, + "shaded_texture_transparent_vertex_alpha": shaded_texture_transparent_vertex_alpha, + "unlit_texture": unlit_texture, + "unlit_texture_cutout": unlit_texture_cutout, + "unlit_texture_transparent": unlit_texture_transparent, + "vertex_colored_texture": vertex_colored_texture, + "vertex_colored_texture_cutout": vertex_colored_texture_cutout, + "vertex_colored_texture_transparent": vertex_colored_texture_transparent, + }, } diff --git a/fast64_internal/f3d/f3d_parser.py b/fast64_internal/f3d/f3d_parser.py index b91154916..57f2dbb28 100644 --- a/fast64_internal/f3d/f3d_parser.py +++ b/fast64_internal/f3d/f3d_parser.py @@ -1,8 +1,22 @@ +import bmesh +import bpy +import mathutils +import re +import math +import traceback +import ast + from typing import Union, Optional, Callable, Any, TYPE_CHECKING -import bmesh, bpy, mathutils, re, math, traceback from mathutils import Vector from bpy.utils import register_class, unregister_class + +# TODO: remove `import *` +from ..utility import * from .f3d_gbi import * + +from .f3d_writer import BufferVertex, F3DVert +from .f3d_material_helpers import F3DMaterial_UpdateLock + from .f3d_material import ( createF3DMat, update_preset_manual, @@ -13,10 +27,6 @@ update_node_values_of_material, F3DMaterialHash, ) -from .f3d_writer import BufferVertex, F3DVert -from ..utility import * -import ast -from .f3d_material_helpers import F3DMaterial_UpdateLock if TYPE_CHECKING: from .f3d_material import RDPSettings @@ -91,8 +101,7 @@ def F3DtoBlenderObject(romfile, startAddress, scene, newname, transformMatrix, s mesh.update() if shadeSmooth: - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) + selectSingleObject(obj) bpy.ops.object.shade_smooth() return obj @@ -367,8 +376,6 @@ def _eval(node): elif isinstance(node, ast.Name): if hasattr(f3d, node.id): return getattr(f3d, node.id) - else: - return node.id elif isinstance(node, ast.Num): return node.n elif isinstance(node, ast.UnaryOp): @@ -406,7 +413,7 @@ def getTileSize(value, f3d): def getTileClampMirror(value, f3d): data = math_eval(value, f3d) - return [(data & f3d.G_TX_CLAMP) != 0, (data & f3d.G_TX_MIRROR) != 0] + return ((data & f3d.G_TX_CLAMP) != 0, (data & f3d.G_TX_MIRROR) != 0) def getTileMask(value, f3d): @@ -496,7 +503,7 @@ def initContext(self): # This macro has all the tile setting properties, so we reuse it self.tileSettings: list[DPSetTile] = [ - DPSetTile("G_IM_FMT_RGBA", "G_IM_SIZ_16b", 5, 0, i, 0, [False, False], 0, 0, [False, False], 0, 0) + DPSetTile("G_IM_FMT_RGBA", "G_IM_SIZ_16b", 5, 0, i, 0, (False, False), 0, 0, (False, False), 0, 0) for i in range(8) ] self.tileSizes: list[DPSetTileSize] = [DPSetTileSize(i, 0, 0, 32, 32) for i in range(8)] @@ -579,7 +586,7 @@ def clearMaterial(self): self.tmemDict = {} self.tileSettings = [ - DPSetTile("G_IM_FMT_RGBA", "G_IM_SIZ_16b", 5, 0, i, 0, [False, False], 0, 0, [False, False], 0, 0) + DPSetTile("G_IM_FMT_RGBA", "G_IM_SIZ_16b", 5, 0, i, 0, (False, False), 0, 0, (False, False), 0, 0) for i in range(8) ] @@ -847,16 +854,22 @@ def getSizeMacro(self, size: str, suffix: str): else: return getattr(self.f3d, self.f3d.IM_SIZ[size] + suffix) - def getImagePathFromInclude(self, path): + def getImagePathFromInclude(self, path, skip_base_path: bool = False): if self.basePath is None: raise PluginError("Cannot load texture from " + path + " without any provided base path.") imagePathRelative = path[:-5] + "png" + + # OoT already got the full path so no need to do that + if skip_base_path: + return imagePathRelative + imagePath = os.path.join(self.basePath, imagePathRelative) # handle custom imports, where relative paths don't make sense if not os.path.exists(imagePath): imagePath = os.path.join(self.basePath, os.path.basename(imagePathRelative)) + return imagePath def getVTXPathFromInclude(self, path): @@ -1363,7 +1376,18 @@ def loadTile(self, params): self.tmemDict[tileSettings.tmem] = self.currentTextureName self.materialChanged = True + def get_file_macro_value(self, macro: str, filedata: str): + match = re.search(rf"#\s*define\s+{macro}\s+([0-9a-fA-FxX]*)", filedata, re.DOTALL) + assert match is not None, f"match is null for {macro}" + return match.group(1) + def loadMultiBlock(self, params: "list[str | int]", dlData: str, is4bit: bool): + # handles OoT's macros + if "WIDTH" in params[5]: + params[5] = self.get_file_macro_value(params[5], dlData) + if "HEIGHT" in params[6]: + params[6] = self.get_file_macro_value(params[6], dlData) + width = math_eval(params[5], self.f3d) height = math_eval(params[6], self.f3d) siz = params[4] @@ -1885,9 +1909,7 @@ def createMesh(self, obj, removeDoubles, importNormals, callDeleteMaterialContex if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) - bpy.context.view_layer.objects.active = obj + selectSingleObject(obj) for material in self.materials: obj.data.materials.append(material) @@ -1961,6 +1983,9 @@ def parseDLData(dlData: str, dlName: str): dlCommandData = matchResult.group(1) + if "#include" in dlCommandData: + dlCommandData = get_include_data(dlCommandData, strip=True) + # recursive regex not available in re # dlCommands = [(match.group(1), [param.strip() for param in match.group(2).split(",")]) for match in \ # re.findall('(gs[A-Za-z0-9\_]*)\(((?>[^()]|(?R))*)\)', dlCommandData, re.DOTALL)] @@ -1983,8 +2008,8 @@ def parseVertexData(dlData: str, vertexDataName: str, f3dContext: F3DContext): pathMatch = re.search(r'\#include\s*"([^"]*)"', data) if pathMatch is not None: path = pathMatch.group(1) - if bpy.context.scene.gameEditorMode == "OOT": - path = f"{bpy.context.scene.fast64.oot.get_extracted_path()}/{path}" + if bpy.context.scene.gameEditorMode in {"OOT", "MM"}: + path = str(oot_get_assets_path(path, check_exists=False)) data = readFile(f3dContext.getVTXPathFromInclude(path)) f3d = f3dContext.f3d @@ -2072,7 +2097,9 @@ def CI4toRGBA32(value): def parseTextureData(dlData, textureName, f3dContext, imageFormat, imageSize, width, isLUT, f3d): matchResult = re.search( - r"([A-Za-z0-9\_]+)\s*" + re.escape(textureName) + r"\s*\[\s*[0-9a-fA-Fx]*\s*\]\s*=\s*\{([^\}]*)\s*\}\s*;\s*", + r"([A-Za-z0-9\_]+)\s*" + + re.escape(textureName) + + r"\s*\[\s*[0-9a-zA-Z_\(\),\s]*\s*\]\s*=\s*\{([^\}]*)\s*\}\s*;\s*", dlData, re.DOTALL, ) @@ -2087,9 +2114,10 @@ def parseTextureData(dlData, textureName, f3dContext, imageFormat, imageSize, wi pathMatch = re.search(r'\#include\s*"(.*?)"', data, re.DOTALL) if pathMatch is not None: path = pathMatch.group(1) - if bpy.context.scene.gameEditorMode == "OOT": - path = f"{bpy.context.scene.fast64.oot.get_extracted_path()}/{path}" - originalImage = bpy.data.images.load(f3dContext.getImagePathFromInclude(path)) + is_oot = bpy.context.scene.gameEditorMode in {"OOT", "MM"} + if is_oot: + path = str(oot_get_assets_path(path, check_exists=False)) + originalImage = bpy.data.images.load(f3dContext.getImagePathFromInclude(path, is_oot)) image = originalImage.copy() image.pack() image.filepath = "" @@ -2244,24 +2272,41 @@ def getImportData(filepaths): def parseMatrices(sceneData: str, f3dContext: F3DContext, importScale: float = 1): - for match in re.finditer(rf"Mtx\s*([a-zA-Z0-9\_]+)\s*=\s*\{{(.*?)\}}\s*;", sceneData, flags=re.DOTALL): - name = "&" + match.group(1) - values = [hexOrDecInt(value.strip()) for value in match.group(2).split(",") if value.strip() != ""] - trueValues = [] - for n in range(8): - valueInt = int.from_bytes(values[n].to_bytes(4, "big", signed=True), "big", signed=False) - valueFrac = int.from_bytes(values[n + 8].to_bytes(4, "big", signed=True), "big", signed=False) - int1 = values[n] >> 16 - int2 = int.from_bytes((valueInt & (2**16 - 1)).to_bytes(2, "big", signed=False), "big", signed=True) - frac1 = valueFrac >> 16 - frac2 = valueFrac & (2**16 - 1) - trueValues.append(int1 + (frac1 / (2**16))) - trueValues.append(int2 + (frac2 / (2**16))) + finditer = list(re.finditer(rf"Mtx\s*([a-zA-Z0-9\_]+)\s*=\s*\{{(.*?)\}}\s*;", sceneData, flags=re.DOTALL)) + + # newer assets system + if len(finditer) == 0: + finditer = list(re.finditer(r"Mtx\s*([a-zA-Z0-9\_]+)\s*=\s*(.*?)\s*;", sceneData, flags=re.DOTALL)) + for match in finditer: + name = "&" + match.group(1) + data = match.group(2) matrix = mathutils.Matrix() - for i in range(4): - for j in range(4): - matrix[j][i] = trueValues[i * 4 + j] + + if "#include" in data: + match = re.search(r".*\((.*?)\)", get_include_data(data, strip=True), re.DOTALL | re.MULTILINE) + assert match is not None + raw_matrix = match.group(1).split(",") + for i in range(4): + for j in range(4): + matrix[i][j] = float(raw_matrix[i * 4 + j].removesuffix("f")) + else: + values = [hexOrDecInt(value.strip()) for value in data.split(",") if value.strip() != ""] + + trueValues = [] + for n in range(8): + valueInt = int.from_bytes(values[n].to_bytes(4, "big", signed=True), "big", signed=False) + valueFrac = int.from_bytes(values[n + 8].to_bytes(4, "big", signed=True), "big", signed=False) + int1 = values[n] >> 16 + int2 = int.from_bytes((valueInt & (2**16 - 1)).to_bytes(2, "big", signed=False), "big", signed=True) + frac1 = valueFrac >> 16 + frac2 = valueFrac & (2**16 - 1) + trueValues.append(int1 + (frac1 / (2**16))) + trueValues.append(int2 + (frac2 / (2**16))) + + for i in range(4): + for j in range(4): + matrix[j][i] = trueValues[i * 4 + j] f3dContext.addMatrix(name, mathutils.Matrix.Scale(importScale, 4) @ matrix) diff --git a/fast64_internal/f3d/f3d_render_engine.py b/fast64_internal/f3d/f3d_render_engine.py deleted file mode 100644 index 51442a71f..000000000 --- a/fast64_internal/f3d/f3d_render_engine.py +++ /dev/null @@ -1,425 +0,0 @@ -import bpy, math, time -from mathutils import * -from bgl import * -from bpy.utils import register_class, unregister_class - -vertexShader = """ -#version 330 core -layout (location = 0) in vec3 pos; -layout (location = 1) in vec2 uv; -layout (location = 2) in vec3 colorOrNormal; - -uniform mat4 transform; - -out vec3 vertexColor; - -void main() -{ - gl_Position = transform * vec4(pos, 1.0); // see how we directly give a vec3 to vec4's constructor - vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color -} -""" - -fragmentShader = """ -#version 330 core -out vec4 color; - -in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) - -void main() -{ - color = vertexColor; -} -""" - - -class F3DRenderEngine(bpy.types.RenderEngine): - bl_idname = "f3d_renderer" - bl_label = "Fast3D" - use_preview = True - - # Init is called whenever a new render engine instance is created. Multiple - # instances may exist at the same time, for example for a viewport and final - # render. - def __init__(self): - self.draw_data = F3DDrawData() - self.first_time = False - - # When the render engine instance is destroy, this is called. Clean up any - # render engine data here, for example stopping running render threads. - def __del__(self): - pass - - # This is the method called by Blender for both final renders (F12) and - # small preview for materials, world and lights. - def render(self, depsgraph): - scene = depsgraph.scene - scale = scene.render.resolution_percentage / 100.0 - self.size_x = int(scene.render.resolution_x * scale) - self.size_y = int(scene.render.resolution_y * scale) - - # Fill the render result with a flat color. The framebuffer is - # defined as a list of pixels, each pixel itself being a list of - # R,G,B,A values. - if self.is_preview: - color = [0.1, 0.2, 0.1, 1.0] - else: - color = [0.2, 0.1, 0.1, 1.0] - - pixel_count = self.size_x * self.size_y - rect = [color] * pixel_count - - # Here we write the pixel values to the RenderResult - result = self.begin_result(0, 0, self.size_x, self.size_y) - layer = result.layers[0].passes["Combined"] - layer.rect = rect - self.end_result(result) - - # For viewport renders, this method gets called once at the start and - # whenever the scene or 3D viewport changes. This method is where data - # should be read from Blender in the same thread. Typically a render - # thread will be started to do the work while keeping Blender responsive. - - # Not called when viewport camera transform changes. - def view_update(self, context, depsgraph): - region = context.region - view3d = context.space_data - scene = depsgraph.scene - - # Get viewport dimensions - dimensions = region.width, region.height - - print("Start View Update: " + str(glGetError())) - if not self.first_time: - # First time initialization - self.first_time = True - for datablock in depsgraph.ids: - print(datablock) - if isinstance(datablock, bpy.types.Image): - self.draw_data.textures[datablock.name] = F3DRendererTexture(datablock) - print("Create Texture: " + str(glGetError())) - elif isinstance(datablock, bpy.types.Material): - self.draw_data.materials[datablock.name] = F3DRendererMaterial(datablock) - print("Create Material: " + str(glGetError())) - elif isinstance(datablock, bpy.types.Mesh): - pass - elif isinstance(datablock, bpy.types.Object) and datablock.type == "MESH": - self.draw_data.objects[datablock.name] = F3DRendererObject(datablock, self.draw_data) - print("Create Object: " + str(glGetError())) - else: - # Test which datablocks changed - for update in depsgraph.updates: - print("Datablock updated: ", update.id.name) - # if isinstance(update.id, bpy.types.Scene): - # for self.draw_data - - # Test if any material was added, removed or changed. - if depsgraph.id_type_updated("MATERIAL"): - print("Materials updated") - - # Loop over all object instances in the scene. - if self.first_time or depsgraph.id_type_updated("OBJECT"): - print("Updated object.") - for instance in depsgraph.object_instances: - pass - - context.region_data.perspective_matrix - print("End View Update: " + str(glGetError())) - - # For viewport renders, this method is called whenever Blender redraws - # the 3D viewport. The renderer is expected to quickly draw the render - # with OpenGL, and not perform other expensive work. - # Blender will draw overlays for selection and editing on top of the - # rendered image automatically. - def view_draw(self, context, depsgraph): - region = context.region - scene = depsgraph.scene - - # Get viewport dimensions - dimensions = region.width, region.height - - print("Start View Draw: " + str(glGetError())) - glEnable(GL_BLEND) - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) - self.draw_data.draw() - glDisable(GL_BLEND) - - -class F3DRendererTexture: - def __init__(self, image): - self.image = image - width, height = image.size - - self.texture_buffer = Buffer(GL_INT, 1) - image.gl_load() - self.texture_buffer[0] = image.bindcode - - # glGenTextures(1, self.texture) - glActiveTexture(GL_TEXTURE0) - glBindTexture(GL_TEXTURE_2D, self.texture_buffer[0]) - # glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, pixels) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glBindTexture(GL_TEXTURE_2D, 0) - - def __del__(self): - glBindTexture(GL_TEXTURE_2D, 0) - glDeleteTextures(1, self.texture_buffer) - - def bind(self): - glBindTexture(GL_TEXTURE_2D, self.texture_buffer[0]) - - -class F3DRendererMaterial: - def __init__(self, material): - self.material = material - - def __del__(self): - pass - - def update(self, viewport, projection, time): - pass - - def apply(self): - pass - - -class F3DRendererObject: - def __init__(self, obj, render_data): - mesh = obj.data - self.submeshes = [] - facesByMat = {} - mesh.calc_loop_triangles() - for face in mesh.loop_triangles: - if face.material_index not in facesByMat: - facesByMat[face.material_index] = [] - facesByMat[face.material_index].append(face) - - for material_index, faces in facesByMat.items(): - material = mesh.materials[material_index] - # Material should always be added in view_update - # f3d_material = render_data.materials[material] - f3d_material = None - - self.submeshes.append(F3DRendererSubmesh(f3d_material, obj, faces, render_data)) - - def draw(self): - print("Draw Object: " + str(glGetError())) - for submesh in self.submeshes: - submesh.draw() - - -class F3DRendererSubmesh: - def __init__(self, f3d_material, obj, triangles, render_data): - print("Begin submesh: " + str(glGetError())) - mesh = obj.data - loopIndices = [] - for triangle in triangles: - for loopIndex in triangle.loops: - loopIndices.append(loopIndex) - - self.material = f3d_material - self.obj = obj - - self.vertex_array = Buffer(GL_INT, 1) - glGenVertexArrays(1, self.vertex_array) - glBindVertexArray(self.vertex_array[0]) - - print("Gen/Bind VAO: " + str(glGetError())) - - self.vertex_buffer = Buffer(GL_INT, 3) - self.size = len(loopIndices) - glGenBuffers(3, self.vertex_buffer) - - position = [] - for loopIndex in loopIndices: - position.extend(mesh.vertices[mesh.loops[loopIndex].vertex_index].co[0:3]) - self.position_buffer = Buffer(GL_FLOAT, len(position), position) - - uv = [] - if "UVMap" in mesh.uv_layers: - uv = [0, 0] * len(loopIndices) - else: - for loopUV in mesh.uv_layers["UVMap"].data: - uv.extend(loopUV.uv[0:2]) - self.uv_buffer = Buffer(GL_FLOAT, len(uv), uv) - - colorOrNormal = [] - if True: # TODO: Choose normal or vertex color - for loopIndex in loopIndices: - colorOrNormal.extend(mesh.loops[loopIndex].normal[0:3]) - else: - if "Col" in mesh.vertex_colors: - color_data = mesh.vertex_colors["Col"].data - else: - color_data = [0, 0, 0] * len(loopIndices) - - if "Alpha" in mesh.vertex_colors: - alpha_data = mesh.vertex_colors["Alpha"].data - else: - alpha_data = [0, 0, 0] * len(loopIndices) - - for loopIndex in loopIndices: - # TODO: Fix Alpha - colorOrNormal.extend(color_data[loopIndex][0:3] + [alpha_data[loopIndex][0]]) - self.colorOrNormal_buffer = Buffer(GL_FLOAT, len(colorOrNormal), colorOrNormal) - - position_location = glGetAttribLocation(render_data.shaderProgram, "pos") - uv_location = glGetAttribLocation(render_data.shaderProgram, "uv") - colorOrNormal_location = glGetAttribLocation(render_data.shaderProgram, "colorOrNormal") - print( - "pos: " - + str(position_location) - + ", uv: " - + str(uv_location) - + ", colorOrNormal: " - + str(colorOrNormal_location) - ) - print("Get Attribute Locations: " + str(glGetError())) - - # Floats and Ints are 4 bytes? - glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer[0]) - print("Bind vertex buffer position: " + str(glGetError())) - glBufferData(GL_ARRAY_BUFFER, len(position) * 4, self.position_buffer, GL_STATIC_DRAW) - print("Buffer position data: " + str(glGetError())) - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None) - glEnableVertexAttribArray(0) - print("Set Attribute Pointer: " + str(glGetError())) - - glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer[1]) - glBufferData(GL_ARRAY_BUFFER, len(uv) * 4, self.uv_buffer, GL_STATIC_DRAW) - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, None) - glEnableVertexAttribArray(0) - - glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer[2]) - glBufferData(GL_ARRAY_BUFFER, len(colorOrNormal) * 4, self.colorOrNormal_buffer, GL_STATIC_DRAW) - glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, None) - glEnableVertexAttribArray(2) - - glBindBuffer(GL_ARRAY_BUFFER, 0) - glBindVertexArray(0) - - self.render_data = render_data - - print("End submesh: " + str(glGetError())) - - def draw(self): - print("Draw Start Submesh: " + str(glGetError())) - # glActiveTexture(GL_TEXTURE0) - # glBindTexture(GL_TEXTURE_2D, self.texture[0]) - - # Ignore material for now - # self.material.apply() - - # Handle modifiers? armatures? - transformLocation = glGetUniformLocation(self.render_data.shaderProgram, "transform") - glUniformMatrix4fv(transformLocation, 1, GL_FALSE, self.obj.matrix_world) - glBindVertexArray(self.vertex_array[0]) - glDrawArrays(GL_TRIANGLES, 0, self.size) - glBindVertexArray(0) - glBindTexture(GL_TEXTURE_2D, 0) - print("Draw End Submesh: " + str(glGetError())) - - def __del__(self): - glDeleteBuffers(3, self.vertex_buffer) - - glDeleteBuffers(1, self.position_buffer) - glDeleteBuffers(1, self.uv_buffer) - glDeleteBuffers(1, self.colorOrNormal_buffer) - - glDeleteVertexArrays(1, self.vertex_array) - glBindTexture(GL_TEXTURE_2D, 0) - # glDeleteTextures(1, self.texture) - - -class F3DDrawData: - def __init__(self): - self.textures = {} - self.materials = {} - self.objects = {} - print("Start: " + str(glGetError())) - - # Create shader program - vertexHandle = glCreateShader(GL_VERTEX_SHADER) - fragmentHandle = glCreateShader(GL_FRAGMENT_SHADER) - glShaderSource(vertexHandle, vertexShader) - glShaderSource(vertexHandle, fragmentShader) - glCompileShader(vertexHandle) - glCompileShader(fragmentHandle) - - print("Shader Created: " + str(glGetError())) - - self.shaderProgram = glCreateProgram() - glAttachShader(self.shaderProgram, vertexHandle) - glAttachShader(self.shaderProgram, fragmentHandle) - glLinkProgram(self.shaderProgram) - glDeleteShader(vertexHandle) - glDeleteShader(fragmentHandle) - - print("Program Created: " + str(glGetError())) - - # messageSize = Buffer(GL_INT, 1) - # message = Buffer(GL_BYTE, 1000) - # glGetShaderInfoLog(self.shaderProgram, 1000, messageSize, message) - print("End: " + str(glGetError())) - - def __del__(self): - for idName, f3dObject in self.objects.items(): - del f3dObject - - def draw(self): - print("Start Draw Data: " + str(glGetError())) - glClearColor(0.2, 0.3, 0.3, 1.0) - glClear(GL_COLOR_BUFFER_BIT) - - print("Before use program: " + str(glGetError())) - glUseProgram(self.shaderProgram) - for idName, f3dObject in self.objects.items(): - f3dObject.draw() - - -# RenderEngines also need to tell UI Panels that they are compatible with. -# We recommend to enable all panels marked as BLENDER_RENDER, and then -# exclude any panels that are replaced by custom panels registered by the -# render engine, or that are not supported. -def get_panels(): - exclude_panels = { - "VIEWLAYER_PT_filter", - "VIEWLAYER_PT_layer_passes", - } - - panels = [] - for panel in bpy.types.Panel.__subclasses__(): - if hasattr(panel, "COMPAT_ENGINES") and "BLENDER_RENDER" in panel.COMPAT_ENGINES: - if panel.__name__ not in exclude_panels: - panels.append(panel) - - return panels - - -render_engine_classes = [ - # F3DRenderEngine, -] - - -def render_engine_register(): - for cls in render_engine_classes: - register_class(cls) - - for panel in get_panels(): - panel.COMPAT_ENGINES.add("CUSTOM") - - # from bl_ui import (properties_render) - # properties_render.RENDER_PT_render.COMPAT_ENGINES.add(CustomRenderEngine.bl_idname) - - -def render_engine_unregister(): - for cls in render_engine_classes: - unregister_class(cls) - - for panel in get_panels(): - if "CUSTOM" in panel.COMPAT_ENGINES: - panel.COMPAT_ENGINES.remove("CUSTOM") - - # from bl_ui import (properties_render) - # properties_render.RENDER_PT_render.COMPAT_ENGINES.remove(CustomRenderEngine.bl_idname) diff --git a/fast64_internal/f3d/f3d_texture_writer.py b/fast64_internal/f3d/f3d_texture_writer.py index d08d80ced..599b65560 100644 --- a/fast64_internal/f3d/f3d_texture_writer.py +++ b/fast64_internal/f3d/f3d_texture_writer.py @@ -204,7 +204,7 @@ def maybeSaveSingleLargeTextureSetup( # SL, SH is * 2 for 4 bit and * 4 otherwise, because actually loading # 8 bit pairs of texels. Also written using f3d.G_TEXTURE_IMAGE_FRAC. sm = 2 if is4bit else 4 - nocm = ["G_TX_WRAP", "G_TX_NOMIRROR"] + nocm = ("G_TX_WRAP", "G_TX_NOMIRROR") if curImgSet != i: gfxOut.commands.append(DPSetTextureImage(fmt, siz, wid, fImage)) @@ -394,7 +394,7 @@ class TexInfo: # Parameters from moreSetupFromModel pal: Optional[list[int]] = None palLen: int = 0 - imDependencies: Optional[list[bpy.types.Image]] = None + imDependencies: Optional[set[bpy.types.Image]] = None flipbook: Optional["TextureFlipbook"] = None isPalRef: bool = False @@ -402,7 +402,7 @@ class TexInfo: texAddr: int = 0 palAddr: int = 0 palIndex: int = 0 - palDependencies: list[bpy.types.Image] = field(default_factory=list) + palDependencies: set[bpy.types.Image] = field(default_factory=set) palBaseName: str = "" loadPal: bool = False doTexLoad: bool = True @@ -420,10 +420,10 @@ def fromMat(self, index: int, f3dMat: F3DMaterialProperty) -> bool: texProp = getattr(f3dMat, "tex" + str(index)) return self.fromProp(texProp, index) - def fromProp(self, texProp: TextureProperty, index: int) -> bool: + def fromProp(self, texProp: TextureProperty, index: int, ignore_tex_set=False) -> bool: self.indexInMat = index self.texProp = texProp - if not texProp.tex_set: + if not texProp.tex_set and not ignore_tex_set: return True self.useTex = True @@ -463,6 +463,34 @@ def fromProp(self, texProp: TextureProperty, index: int) -> bool: return True + def materialless_setup(self) -> None: + """moreSetupFromModel equivalent that does not handle material properties like OOT flipbooks""" + if not self.useTex: + return + + if self.isTexCI: + self.imDependencies, self.flipbook, self.pal = ( + set() if self.texProp.tex is None else {self.texProp.tex}, + None, + None, + ) + if self.isTexRef: + self.palLen = self.texProp.pal_reference_size + else: + assert self.flipbook is None + self.pal = getColorsUsedInImage(self.texProp.tex, self.palFormat) + self.palLen = len(self.pal) + if self.palLen > (16 if self.texFormat == "CI4" else 256): + raise PluginError( + f"Error in Texture {self.indexInMat} uses too many unique colors to fit in format {self.texFormat}." + ) + else: + self.imDependencies = set() if self.texProp.tex is None else {self.texProp.tex} + self.flipbook = None + + self.isPalRef = self.isTexRef and self.flipbook is None + self.palDependencies = self.imDependencies + def moreSetupFromModel( self, material: bpy.types.Material, @@ -487,7 +515,7 @@ def moreSetupFromModel( self.palLen = len(self.pal) if self.palLen > (16 if self.texFormat == "CI4" else 256): raise PluginError( - f"Error in {material.name}: texture {self.indexInMat}" + f"Texture {self.indexInMat}" + (" (all flipbook textures)" if self.flipbook is not None else "") + f" uses too many unique colors to fit in format {self.texFormat}." ) @@ -504,6 +532,26 @@ def getPaletteName(self): return self.flipbook.name return getImageName(self.texProp.tex) + def setup_single_tex(self, is_ci: bool, use_large_tex: bool): + is_large = False + tmem_size = 256 if is_ci else 512 + if is_ci: + assert self.useTex # should this be here? + if self.useTex: + self.loadPal = True + self.palBaseName = self.getPaletteName() + if self.tmemSize > tmem_size: + if use_large_tex: + self.doTexLoad = False + return True + elif not bpy.context.scene.ignoreTextureRestrictions: + raise PluginError( + "Textures are too big. Max TMEM size is 4k " + "bytes, ex. 2 32x32 RGBA 16 bit textures.\n" + "Note that texture width will be internally padded to 64 bit boundaries." + ) + return is_large + def writeAll( self, fMaterial: FMaterial, @@ -514,7 +562,7 @@ def writeAll( return assert ( self.imDependencies is not None - ) # Must be set manually if didn't use moreSetupFromModel, e.g. ti.imDependencies = [tex] + ), "self.imDependencies is None, either moreSetupFromModel or materialless_setup must be called beforehand" # Get definitions imageKey, fImage = saveOrGetTextureDefinition( @@ -551,6 +599,9 @@ def writeAll( fModel.writeTexRefNonCITextures(self.flipbook, self.texFormat) else: if self.isTexCI: + assert ( + self.pal is not None + ), "self.pal is None, either moreSetupFromModel or materialless_setup must be called beforehand" writeCITextureData(self.texProp.tex, fImage, self.pal, self.palFormat, self.texFormat) else: writeNonCITextureData(self.texProp.tex, fImage, self.texFormat) @@ -566,9 +617,9 @@ def __init__( f3dMat = material.f3d_mat self.ti0, self.ti1 = TexInfo(), TexInfo() if not self.ti0.fromMat(0, f3dMat): - raise PluginError(f"In {material.name} tex0: {self.ti0.errorMsg}") + raise PluginError(f"Tex0: {self.ti0.errorMsg}") if not self.ti1.fromMat(1, f3dMat): - raise PluginError(f"In {material.name} tex1: {self.ti1.errorMsg}") + raise PluginError(f"Tex1: {self.ti1.errorMsg}") self.ti0.moreSetupFromModel(material, fMaterial, fModel) self.ti1.moreSetupFromModel(material, fMaterial, fModel) @@ -576,39 +627,24 @@ def __init__( if self.ti0.useTex and self.ti1.useTex: if self.ti0.isTexCI != self.ti1.isTexCI: - raise PluginError( - "In material " - + material.name - + ": N64 does not support CI + non-CI texture. " - + "Must be both CI or neither CI." - ) + raise PluginError("N64 does not support CI + non-CI texture. Must be both CI or neither CI.") if ( self.ti0.isTexRef and self.ti1.isTexRef and self.ti0.texProp.tex_reference == self.ti1.texProp.tex_reference and self.ti0.texProp.tex_reference_size != self.ti1.texProp.tex_reference_size ): - raise PluginError( - "In material " + material.name + ": Two textures with the same reference must have the same size." - ) + raise PluginError("Two textures with the same reference must have the same size.") if self.isCI: if self.ti0.palFormat != self.ti1.palFormat: - raise PluginError( - "In material " - + material.name - + ": Both CI textures must use the same palette format (usually RGBA16)." - ) + raise PluginError("Both CI textures must use the same palette format (usually RGBA16).") if ( self.ti0.isTexRef and self.ti1.isTexRef and self.ti0.texProp.pal_reference == self.ti1.texProp.pal_reference and self.ti0.texProp.pal_reference_size != self.ti1.texProp.pal_reference_size ): - raise PluginError( - "In material " - + material.name - + ": Two textures with the same palette reference must have the same palette size." - ) + raise PluginError("Two textures with the same palette reference must have the same palette size.") self.palFormat = self.ti0.palFormat if self.ti0.useTex else self.ti1.palFormat @@ -626,9 +662,7 @@ def writeAll( elif not convertTextureData: if self.ti0.texFormat == "CI8" or self.ti1.texFormat == "CI8": raise PluginError( - "In material " - + material.name - + ": When using export as PNGs mode, can't have multitexture with one or more CI8 textures." + "When using export as PNGs mode, can't have multitexture with one or more CI8 textures." + " Only single CI texture or two CI4 textures." ) self.ti0.loadPal = self.ti1.loadPal = True @@ -638,34 +672,26 @@ def writeAll( if self.ti0.texFormat == "CI8" and self.ti1.texFormat == "CI8": if (self.ti0.pal is None) != (self.ti1.pal is None): raise PluginError( - "In material " - + material.name - + ": can't have two CI8 textures where only one is a non-flipbook reference; " + "Can't have two CI8 textures where only one is a non-flipbook reference; " + "no way to assign the palette." ) self.ti0.loadPal = True if self.ti0.pal is None: if self.ti0.texProp.pal_reference != self.ti1.texProp.pal_reference: - raise PluginError( - "In material " - + material.name - + ": can't have two CI8 textures with different palette references." - ) + raise PluginError("Can't have two CI8 textures with different palette references.") else: self.ti0.pal = mergePalettes(self.ti0.pal, self.ti1.pal) self.ti0.palLen = len(self.ti0.pal) if self.ti0.palLen > 256: raise PluginError( - "In material " - + material.name - + ": the two CI textures together contain a total of " + "The two CI textures together contain a total of " + str(self.ti0.palLen) + " colors, which can't fit in a CI8 palette (256)." ) # self.ti0.imDependencies remains what it was; the CIs in im0 are the same as they # would be if im0 was alone. But im1 and self.ti0.pal depend on both. self.ti1.imDependencies = self.ti0.palDependencies = ( - self.ti0.imDependencies + self.ti1.imDependencies + self.ti0.imDependencies | self.ti1.imDependencies ) elif self.ti0.texFormat != self.ti1.texFormat: # One CI8, one CI4 ci8Pal, ci4Pal = ( @@ -679,9 +705,7 @@ def writeAll( if self.ti0.pal is None or self.ti1.pal is None: if ci8PalLen > 256 - 16: raise PluginError( - "In material " - + material.name - + ": the CI8 texture has over 240 colors, which can't fit together with the CI4 palette." + "The CI8 texture has over 240 colors, which can't fit together with the CI4 palette." ) self.ti0.loadPal = self.ti1.loadPal = True if self.ti0.texFormat == "CI8": @@ -697,9 +721,7 @@ def writeAll( self.ti0.palLen = len(self.ti0.pal) if self.ti0.palLen > 256: raise PluginError( - "In material " - + material.name - + ": the two CI textures together contain a total of " + "The two CI textures together contain a total of " + str(self.ti0.palLen) + " colors, which can't fit in a CI8 palette (256)." + " The CI8 texture must contain up to 240 unique colors," @@ -707,7 +729,7 @@ def writeAll( ) # The use for the CI4 texture remains what it was; its CIs are the # same as if it was alone. But both the palette and the CI8 CIs are affected. - self.ti0.palDependencies = self.ti0.imDependencies + self.ti1.imDependencies + self.ti0.palDependencies = self.ti0.imDependencies | self.ti1.imDependencies if self.ti0.texFormat == "CI8": self.ti0.imDependencies = self.ti0.palDependencies else: @@ -735,7 +757,7 @@ def writeAll( # self.ti0.imDependencies remains what it was; the CIs in im0 are the same as they # would be if im0 was alone. But im1 and self.ti0.pal depend on both. self.ti1.imDependencies = self.ti0.palDependencies = ( - self.ti0.imDependencies + self.ti1.imDependencies + self.ti0.imDependencies | self.ti1.imDependencies ) else: # Load one palette across 0-1. Put the longer in slot 0 @@ -753,7 +775,7 @@ def writeAll( self.ti0.palIndex = 1 # The up-to-32 entries in self.ti0.pal depend on both images. But the # CIs in both im0 and im1 are the same as if there was no shared palette. - self.ti0.palDependencies = self.ti0.imDependencies + self.ti1.imDependencies + self.ti0.palDependencies = self.ti0.imDependencies | self.ti1.imDependencies fMaterial.texPaletteIndex = [self.ti0.palIndex, self.ti1.palIndex] self.ti0.palBaseName = self.ti0.getPaletteName() self.ti1.palBaseName = self.ti1.getPaletteName() @@ -763,14 +785,25 @@ def writeAll( # Assign TMEM addresses sameTextures = ( - self.ti0.useTex - and self.ti1.useTex + (self.ti0.useTex and self.ti1.useTex) + and self.ti0.isTexRef == self.ti1.isTexRef + and self.ti0.tmemSize == self.ti1.tmemSize + and self.ti0.texFormat == self.ti1.texFormat and ( - (not self.ti0.isTexRef and not self.ti1.isTexRef and self.ti0.texProp.tex == self.ti1.texProp.tex) - or ( + ( # not a reference + not self.ti0.isTexRef and self.ti0.texProp.tex == self.ti1.texProp.tex # same image + ) + or ( # reference self.ti0.isTexRef - and self.ti1.isTexRef and self.ti0.texProp.tex_reference == self.ti1.texProp.tex_reference + and self.ti0.texProp.tex_reference_size == self.ti1.texProp.tex_reference_size + and ( # ci format reference + not self.isCI + or ( + self.ti0.texProp.pal_reference == self.ti1.texProp.pal_reference + and self.ti0.texProp.pal_reference_size == self.ti1.texProp.pal_reference_size + ) + ) ) ) ) @@ -779,7 +812,9 @@ def writeAll( self.ti1.texAddr = None # must be set whenever tex 1 used (and loaded or tiled) tmemOccupied = self.texDimensions = None # must be set on all codepaths if sameTextures: - assert self.ti0.tmemSize == self.ti1.tmemSize + assert ( + self.ti0.tmemSize == self.ti1.tmemSize + ), f"Unreachable code path, same textures (same image or reference) somehow not the same size" tmemOccupied = self.ti0.tmemSize self.ti1.doTexLoad = False self.ti1.texAddr = 0 @@ -788,15 +823,17 @@ def writeAll( elif not useLargeTextures or self.ti0.tmemSize + self.ti1.tmemSize <= tmemSize: self.ti1.texAddr = self.ti0.tmemSize tmemOccupied = self.ti0.tmemSize + self.ti1.tmemSize - if not self.ti0.useTex and not self.ti1.useTex: - self.texDimensions = [32, 32] - fMaterial.largeTexFmt = "RGBA16" - elif not self.ti1.useTex or f3dMat.uv_basis == "TEXEL0": + uv_basis = f3dMat.uv_basis_index + if uv_basis == 0: self.texDimensions = self.ti0.imageDims fMaterial.largeTexFmt = self.ti0.texFormat - else: + elif uv_basis == 1: self.texDimensions = self.ti1.imageDims fMaterial.largeTexFmt = self.ti1.texFormat + elif uv_basis == None: + self.texDimensions = [32, 32] + fMaterial.largeTexFmt = "RGBA16" + else: # useLargeTextures if self.ti0.useTex and self.ti1.useTex: tmemOccupied = tmemSize @@ -822,11 +859,7 @@ def writeAll( self.ti1.texAddr = tmemSize - self.ti1.tmemSize else: # Both textures large - raise PluginError( - 'Error in "' - + material.name - + '": Multitexture with two large textures is not currently supported.' - ) + raise PluginError("Multitexture with two large textures is not currently supported.") # Limited cases of 2x large textures could be supported in the # future. However, these cases are either of questionable # utility or have substantial restrictions. Most cases could be @@ -864,16 +897,10 @@ def writeAll( tmemOccupied = tmemSize if tmemOccupied > tmemSize: if sameTextures and useLargeTextures: - raise PluginError( - 'Error in "' - + material.name - + '": Using the same texture for Tex0 and Tex1 is not compatible with large textures.' - ) + raise PluginError("Using the same texture for Tex0 and Tex1 is not compatible with large textures.") elif not bpy.context.scene.ignoreTextureRestrictions: raise PluginError( - 'Error in "' - + material.name - + '": Textures are too big. Max TMEM size is 4k ' + "Textures are too big. Max TMEM size is 4k " + "bytes, ex. 2 32x32 RGBA 16 bit textures.\nNote that texture width will be internally padded to 64 bit boundaries." ) @@ -945,7 +972,7 @@ def saveTextureLoadOnly( ): fmt = texFormatOf[texProp.tex_format] siz = texBitSizeF3D[texProp.tex_format] - nocm = ["G_TX_WRAP", "G_TX_NOMIRROR"] + nocm = ("G_TX_WRAP", "G_TX_NOMIRROR") SL, TL, SH, TH, sl, tl, sh, th = getTileSizeSettings(texProp, tileSettings, f3d) # LoadTile will pad rows to 64 bit word alignment, while @@ -1015,8 +1042,8 @@ def saveTextureTile( mask_T = texProp.T.mask shift_S = texProp.S.shift shift_T = texProp.T.shift - cms = [("G_TX_CLAMP" if clamp_S else "G_TX_WRAP"), ("G_TX_MIRROR" if mirror_S else "G_TX_NOMIRROR")] - cmt = [("G_TX_CLAMP" if clamp_T else "G_TX_WRAP"), ("G_TX_MIRROR" if mirror_T else "G_TX_NOMIRROR")] + cms = (("G_TX_CLAMP" if clamp_S else "G_TX_WRAP"), ("G_TX_MIRROR" if mirror_S else "G_TX_NOMIRROR")) + cmt = (("G_TX_CLAMP" if clamp_T else "G_TX_WRAP"), ("G_TX_MIRROR" if mirror_T else "G_TX_NOMIRROR")) masks = mask_S maskt = mask_T shifts = shift_S if shift_S >= 0 else (shift_S + 16) @@ -1056,7 +1083,7 @@ def savePaletteLoad( ): assert 0 <= palAddr < 256 and (palAddr & 0xF) == 0 palFmt = texFormatOf[palFormat] - nocm = ["G_TX_WRAP", "G_TX_NOMIRROR"] + nocm = ("G_TX_WRAP", "G_TX_NOMIRROR") gfxOut.commands.extend( [ DPSetTextureImage(palFmt, "G_IM_SIZ_16b", 1, fPalette), diff --git a/fast64_internal/f3d/f3d_writer.py b/fast64_internal/f3d/f3d_writer.py index da128cb3b..89a960de0 100644 --- a/fast64_internal/f3d/f3d_writer.py +++ b/fast64_internal/f3d/f3d_writer.py @@ -9,6 +9,7 @@ from .f3d_enums import * from .f3d_material import ( all_combiner_uses, + get_tex_basis_size, getMaterialScrollDimensions, isTexturePointSampled, get_textlut_mode, @@ -16,7 +17,7 @@ ) from .f3d_texture_writer import MultitexManager, TileLoad, maybeSaveSingleLargeTextureSetup from .f3d_gbi import * -from .f3d_bleed import BleedGraphics +from .f3d_bleed import BleedGraphics, get_geo_cmds from ..utility import * @@ -91,6 +92,7 @@ def check_face_materials( obj_name: str, material_slots: "bpy.types.bpy_prop_collection[bpy.types.MaterialSlot]", faces: "bpy.types.MeshPolygons | bpy.types.MeshLoopTriangles", + requires_f3d: bool = True, ): """ Check if all faces are correctly assigned to a F3D material @@ -110,7 +112,9 @@ def check_face_materials( " Assign the faces to a valid slot." f" (0-indexed: slot {material_index}, aka the {material_index+1}th slot)." ) - material = material_slots[material_index].material + material = material_slots[material_index] + if isinstance(material, bpy.types.MaterialSlot): + material = material.material if material is None: raise PluginError( f"Mesh object {obj_name} has faces" @@ -118,7 +122,7 @@ def check_face_materials( " Set a material for the slot or assign the faces to an actual material." f" (0-indexed: slot {material_index}, aka the {material_index+1}th slot)." ) - if not material.is_f3d: + if requires_f3d and not material.is_f3d: raise PluginError( f"Mesh object {obj_name} has faces" f" assigned to a material which is not a F3D material: {material.name}" @@ -418,12 +422,12 @@ def empty_logging_func(a, b): # used when duplicating an object def saveStaticModel( triConverterInfo, - fModel, + fModel: FModel, obj, transformMatrix, - ownerName, + ownerName: str, convertTextureData, - revertMatAtEnd, + revertMatAtEnd: bool, drawLayerField, *args, **kwargs, @@ -458,7 +462,7 @@ def saveStaticModel( logging_func({"INFO"}, "saveStaticModel 5") - fMeshes = {} + fMeshes: dict[str, FMesh] = {} for material_index, faces in faces_by_mat.items(): material = obj.material_slots[material_index].material @@ -570,18 +574,18 @@ def addCullCommand(obj, fMesh, transformMatrix, matWriteMethod): defaults = create_or_get_world(bpy.context.scene).rdp_defaults if defaults.g_lighting: cullCommands = [ - SPClearGeometryMode(["G_LIGHTING"]), + SPClearGeometryMode({"G_LIGHTING"}), SPVertex(fMesh.cullVertexList, 0, 8, 0), - SPSetGeometryMode(["G_LIGHTING"]), + SPSetGeometryMode({"G_LIGHTING"}), SPCullDisplayList(0, 7), ] else: cullCommands = [SPVertex(fMesh.cullVertexList, 0, 8, 0), SPCullDisplayList(0, 7)] elif matWriteMethod == GfxMatWriteMethod.WriteAll: cullCommands = [ - SPClearGeometryMode(["G_LIGHTING"]), + SPClearGeometryMode({"G_LIGHTING"}), SPVertex(fMesh.cullVertexList, 0, 8, 0), - SPSetGeometryMode(["G_LIGHTING"]), + SPSetGeometryMode({"G_LIGHTING"}), SPCullDisplayList(0, 7), ] else: @@ -933,6 +937,32 @@ def getTransformMatrix(self, groupIndex): return self.transformMatrix @ groupMatrix +def cel_shading_checks(f3d_mat): + cel = f3d_mat.cel_shading + if f3d_mat.rdp_settings.zmode != "ZMODE_OPA": + raise PluginError("When using cel shading, the zmode in the blender / rendermode must be opaque.") + if len(cel.levels) == 0: + raise PluginError("Cel shading has no cel levels") + + # Don't want to have to change back and forth arbitrarily between decal and + # opaque mode. So if you're using both lighter and darker, need to do those + # first before switching to decal. + wrote_dark = wrote_light = uses_decal = False + for level in cel.levels: + if level.threshMode == "Darker": + if wrote_dark: + uses_decal = True + continue + wrote_dark = True + else: + if wrote_light: + uses_decal = True + continue + wrote_light = True + if uses_decal: + raise PluginError("Must use Lighter and Darker cel levels before duplicating either of them") + + # existingVertexData is used for cases where we want to assume the presence of vertex data # loaded in from a previous matrix transform (ex. sm64 skinning) class TriangleConverter: @@ -1069,33 +1099,10 @@ def writeCelLevels(self, celTriList: Optional[GfxList] = None, triCmds: Optional cel = f3dMat.cel_shading f3d = get_F3D_GBI() - # Don't want to have to change back and forth arbitrarily between decal and - # opaque mode. So if you're using both lighter and darker, need to do those - # first before switching to decal. - if f3dMat.rdp_settings.zmode != "ZMODE_OPA": - raise PluginError( - f"Material {self.material.name} with cel shading: zmode in blender / rendermode must be opaque." - ) - wroteLighter = wroteDarker = usesDecal = False - if len(cel.levels) == 0: - raise PluginError(f"Material {self.material.name} with cel shading has no cel levels") - for level in cel.levels: - if level.threshMode == "Darker": - if wroteDarker: - usesDecal = True - elif usesDecal: - raise PluginError( - f"Material {self.material.name}: must use Lighter and Darker cel levels before duplicating either of them" - ) - wroteDarker = True - else: - if wroteLighter: - usesDecal = True - elif usesDecal: - raise PluginError( - f"Material {self.material.name}: must use Lighter and Darker cel levels before duplicating either of them" - ) - wroteLighter = True + try: + cel_shading_checks(f3dMat) + except Exception as exc: + raise PluginError(f"Material {self.material.name}: {str(exc)}") from exc # Because this might not be the first tri list in the object with this # material, we have to set things even if they were set up already in @@ -1108,10 +1115,10 @@ def writeCelLevels(self, celTriList: Optional[GfxList] = None, triCmds: Optional if usesDecal: if not wroteOpaque: wroteOpaque = True - self.triList.commands.append(SPSetOtherMode("G_SETOTHERMODE_L", 10, 2, ["ZMODE_OPA"])) + self.triList.commands.append(SPSetOtherMode("G_SETOTHERMODE_L", 10, 2, {"ZMODE_OPA"})) if not wroteDecal and (darker and wroteDarker or not darker and wroteLighter): wroteDecal = True - self.triList.commands.append(SPSetOtherMode("G_SETOTHERMODE_L", 10, 2, ["ZMODE_DEC"])) + self.triList.commands.append(SPSetOtherMode("G_SETOTHERMODE_L", 10, 2, {"ZMODE_DEC"})) if darker: wroteDarker = True else: @@ -1309,37 +1316,18 @@ def getIndices(tri): def getTexDimensions(material): f3dMat = material.f3d_mat - - texDimensions0 = None - texDimensions1 = None useDict = all_combiner_uses(f3dMat) - if useDict["Texture 0"] and f3dMat.tex0.tex_set: - if f3dMat.tex0.use_tex_reference: - texDimensions0 = f3dMat.tex0.tex_reference_size - else: - if f3dMat.tex0.tex is None: - raise PluginError('In material "' + material.name + '", a texture has not been set.') - texDimensions0 = f3dMat.tex0.tex.size[0], f3dMat.tex0.tex.size[1] - if useDict["Texture 1"] and f3dMat.tex1.tex_set: - if f3dMat.tex1.use_tex_reference: - texDimensions1 = f3dMat.tex1.tex_reference_size - else: - if f3dMat.tex1.tex is None: - raise PluginError('In material "' + material.name + '", a texture has not been set.') - texDimensions1 = f3dMat.tex1.tex.size[0], f3dMat.tex1.tex.size[1] - - if texDimensions0 is not None and texDimensions1 is not None: - texDimensions = texDimensions0 if f3dMat.uv_basis == "TEXEL0" else texDimensions1 - elif texDimensions0 is not None: - texDimensions = texDimensions0 - elif texDimensions1 is not None: - texDimensions = texDimensions1 - else: - texDimensions = [32, 32] - return texDimensions + + if useDict["Texture 0"] and f3dMat.tex0.tex_set and not f3dMat.tex0.tex: + raise PluginError('In material "' + material.name + '", texture 0 has not been set.') + if useDict["Texture 1"] and f3dMat.tex1.tex_set and not f3dMat.tex1.tex: + raise PluginError('In material "' + material.name + '", texture 1 has not been set.') + + return get_tex_basis_size(f3dMat) -def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): +@wrap_func_with_error_message(lambda args: (f"In material '{args['material'].name}': ")) +def saveOrGetF3DMaterial(material, fModel, _obj, drawLayer, convertTextureData): print(f"Writing material {material.name}") if material.mat_ver > 3: f3dMat = material.f3d_mat @@ -1358,8 +1346,6 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): if materialItem is not None: return materialItem - if len(obj.data.materials) == 0: - raise PluginError("Mesh must have at least one material.") materialName = ( fModel.name + "_" @@ -1369,15 +1355,12 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): ) if not material.is_f3d: - raise PluginError("Material named " + material.name + " is not an F3D material.") + raise PluginError("Not an F3D material.") fMaterial = fModel.addMaterial(materialName) useDict = all_combiner_uses(f3dMat) defaults = create_or_get_world(bpy.context.scene).rdp_defaults - if fModel.f3d.F3DEX_GBI_2: - saveGeoModeDefinitionF3DEX2(fMaterial, f3dMat.rdp_settings, defaults, fModel.matWriteMethod) - else: - saveGeoModeDefinition(fMaterial, f3dMat.rdp_settings, defaults, fModel.matWriteMethod) + saveGeoModeDefinition(fMaterial, f3dMat.rdp_settings, defaults, fModel.matWriteMethod, fModel.f3d.F3DEX_GBI_2) # Checking for f3dMat.rdp_settings.g_lighting here will prevent accidental exports, # There may be some edge case where this isn't desired. @@ -1392,7 +1375,9 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): fMaterial.mat_only_DL.commands.extend([SPSetLights(fLights)]) fMaterial.mat_only_DL.commands.append(DPPipeSync()) - fMaterial.revert.commands.append(DPPipeSync()) + + if fMaterial.revert is not None: + fMaterial.revert.commands.append(DPPipeSync()) fMaterial.getScrollData(material, getMaterialScrollDimensions(f3dMat)) @@ -1468,8 +1453,9 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): fMaterial.mat_only_DL.commands.append(SPAttrOffsetZ(f3dMat.attroffs_z)) if f3dMat.set_fog and f3dMat.rdp_settings.using_fog: - if f3dMat.use_global_fog and fModel.global_data.getCurrentAreaData() is not None: - fogData = fModel.global_data.getCurrentAreaData().fog_data + area = fModel.global_data.getCurrentAreaData() + if f3dMat.use_global_fog and area and area.fog_data: + fogData = area.fog_data fog_position = fogData.position fog_color = fogData.color else: @@ -1575,7 +1561,7 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): fMaterial.material.commands.append(SPEndDisplayList()) # revertMatAndEndDraw(fMaterial.revert) - if len(fMaterial.revert.commands) > 1: # 1 being the pipe sync + if fMaterial.revert is not None and len(fMaterial.revert.commands) > 1: # 1 being the pipe sync if fMaterial.DLFormat == DLFormat.Static: fMaterial.revert.commands.append(SPEndDisplayList()) else: @@ -1601,20 +1587,13 @@ def getLightDefinitions(fModel, material, lightsName=""): else: lights.a = Ambient(exportColor(material.ambient_light_color)) - if material.f3d_light1 is not None: - addLightDefinition(material, material.f3d_light1, lights) - if material.f3d_light2 is not None: - addLightDefinition(material, material.f3d_light2, lights) - if material.f3d_light3 is not None: - addLightDefinition(material, material.f3d_light3, lights) - if material.f3d_light4 is not None: - addLightDefinition(material, material.f3d_light4, lights) - if material.f3d_light5 is not None: - addLightDefinition(material, material.f3d_light5, lights) - if material.f3d_light6 is not None: - addLightDefinition(material, material.f3d_light6, lights) - if material.f3d_light7 is not None: - addLightDefinition(material, material.f3d_light7, lights) + for i in range(1, 8): + try: + light_prop = getattr(material, f"f3d_light{i}") + if light_prop is not None: + addLightDefinition(light_prop, lights) + except Exception as exc: + raise PluginError(f"Failed to get custom light {i}: {exc}") from exc return lights @@ -1632,7 +1611,7 @@ def saveLightsDefinition(fModel, fMaterial, material, lightsName): return lights -def addLightDefinition(mat, f3d_light, fLights): +def addLightDefinition(f3d_light, fLights): lightObj = lightDataToObj(f3d_light) fLights.l.append( Light( @@ -1642,20 +1621,12 @@ def addLightDefinition(mat, f3d_light, fLights): ) -def saveBitGeoF3DEX2(value, defaultValue, flagName, geo, matWriteMethod): +def saveBitGeo(value, defaultValue, flagName, set_modes: list[str], clear_modes: list[str], matWriteMethod): if value != defaultValue or matWriteMethod == GfxMatWriteMethod.WriteAll: if value: - geo.setFlagList.append(flagName) + set_modes.append(flagName) else: - geo.clearFlagList.append(flagName) - - -def saveBitGeo(value, defaultValue, flagName, setGeo, clearGeo, matWriteMethod): - if value != defaultValue or matWriteMethod == GfxMatWriteMethod.WriteAll: - if value: - setGeo.flagList.append(flagName) - else: - clearGeo.flagList.append(flagName) + clear_modes.append(flagName) def saveGeoModeCommon(saveFunc: Callable, settings: RDPSettings, defaults: RDPSettings, args: Any): @@ -1682,43 +1653,25 @@ def saveGeoModeCommon(saveFunc: Callable, settings: RDPSettings, defaults: RDPSe saveFunc(settings.g_clipping, defaults.g_clipping, "G_CLIPPING", *args) -def saveGeoModeDefinitionF3DEX2(fMaterial, settings, defaults, matWriteMethod): - geo = SPGeometryMode([], []) - saveGeoModeCommon(saveBitGeoF3DEX2, settings, defaults, (geo, matWriteMethod)) +def saveGeoModeDefinition(fMaterial, settings, defaults, matWriteMethod, is_ex2: bool): + set_modes = [] + clear_modes = [] - if len(geo.clearFlagList) != 0 or len(geo.setFlagList) != 0: - if len(geo.clearFlagList) == 0: - geo.clearFlagList.append("0") - elif len(geo.setFlagList) == 0: - geo.setFlagList.append("0") + saveGeoModeCommon(saveBitGeo, settings, defaults, (set_modes, clear_modes, matWriteMethod)) - if matWriteMethod == GfxMatWriteMethod.WriteAll: - fMaterial.mat_only_DL.commands.append(SPLoadGeometryMode(geo.setFlagList)) - else: - fMaterial.mat_only_DL.commands.append(geo) - fMaterial.revert.commands.append(SPGeometryMode(geo.setFlagList, geo.clearFlagList)) + material, revert = get_geo_cmds(clear_modes, set_modes, is_ex2, matWriteMethod) + fMaterial.mat_only_DL.commands.extend(material) - -def saveGeoModeDefinition(fMaterial, settings, defaults, matWriteMethod): - setGeo = SPSetGeometryMode([]) - clearGeo = SPClearGeometryMode([]) - - saveGeoModeCommon(saveBitGeo, settings, defaults, (setGeo, clearGeo, matWriteMethod)) - - if len(setGeo.flagList) > 0: - fMaterial.mat_only_DL.commands.append(setGeo) - if matWriteMethod == GfxMatWriteMethod.WriteDifferingAndRevert: - fMaterial.revert.commands.append(SPClearGeometryMode(setGeo.flagList)) - if len(clearGeo.flagList) > 0: - fMaterial.mat_only_DL.commands.append(clearGeo) - if matWriteMethod == GfxMatWriteMethod.WriteDifferingAndRevert: - fMaterial.revert.commands.append(SPSetGeometryMode(clearGeo.flagList)) + if fMaterial.revert is not None: + fMaterial.revert.commands.extend(revert) def saveModeSetting(fMaterial, value, defaultValue, cmdClass): if value != defaultValue: fMaterial.mat_only_DL.commands.append(cmdClass(value)) - fMaterial.revert.commands.append(cmdClass(defaultValue)) + + if fMaterial.revert is not None: + fMaterial.revert.commands.append(cmdClass(defaultValue)) def saveOtherModeHDefinition(fMaterial, settings, tlut, defaults, matWriteMethod, f3d): @@ -1731,18 +1684,18 @@ def saveOtherModeHDefinition(fMaterial, settings, tlut, defaults, matWriteMethod def saveOtherModeHDefinitionAll(fMaterial, settings, tlut, defaults, f3d): - cmd = SPSetOtherMode("G_SETOTHERMODE_H", 4, 20 - f3d.F3D_OLD_GBI, []) - cmd.flagList.append(settings.g_mdsft_alpha_dither) - cmd.flagList.append(settings.g_mdsft_rgb_dither) - cmd.flagList.append(settings.g_mdsft_combkey) - cmd.flagList.append(settings.g_mdsft_textconv) - cmd.flagList.append(settings.g_mdsft_text_filt) - cmd.flagList.append(tlut) - cmd.flagList.append(settings.g_mdsft_textlod) - cmd.flagList.append(settings.g_mdsft_textdetail) - cmd.flagList.append(settings.g_mdsft_textpersp) - cmd.flagList.append(settings.g_mdsft_cycletype) - cmd.flagList.append(settings.g_mdsft_pipeline) + cmd = SPSetOtherMode("G_SETOTHERMODE_H", 4, 20 - f3d.F3D_OLD_GBI, set()) + cmd.flagList.add(settings.g_mdsft_alpha_dither) + cmd.flagList.add(settings.g_mdsft_rgb_dither) + cmd.flagList.add(settings.g_mdsft_combkey) + cmd.flagList.add(settings.g_mdsft_textconv) + cmd.flagList.add(settings.g_mdsft_text_filt) + cmd.flagList.add(tlut) + cmd.flagList.add(settings.g_mdsft_textlod) + cmd.flagList.add(settings.g_mdsft_textdetail) + cmd.flagList.add(settings.g_mdsft_textpersp) + cmd.flagList.add(settings.g_mdsft_cycletype) + cmd.flagList.add(settings.g_mdsft_pipeline) fMaterial.mat_only_DL.commands.append(cmd) @@ -1763,57 +1716,62 @@ def saveOtherModeHDefinitionIndividual(fMaterial, settings, tlut, defaults): def saveOtherModeLDefinition(fMaterial, settings, defaults, defaultRenderMode, matWriteMethod, f3d): if matWriteMethod == GfxMatWriteMethod.WriteAll: - saveOtherModeLDefinitionAll(fMaterial, settings, defaults, f3d) + saveOtherModeLDefinitionAll(fMaterial, settings, defaults, defaultRenderMode, f3d) elif matWriteMethod == GfxMatWriteMethod.WriteDifferingAndRevert: saveOtherModeLDefinitionIndividual(fMaterial, settings, defaults, defaultRenderMode) else: raise PluginError("Unhandled material write method: " + str(matWriteMethod)) -def saveOtherModeLDefinitionAll(fMaterial: FMaterial, settings, defaults, f3d): - baseLength = 3 if not settings.set_rendermode else 32 - cmd = SPSetOtherMode("G_SETOTHERMODE_L", 0, baseLength - f3d.F3D_OLD_GBI, []) - cmd.flagList.append(settings.g_mdsft_alpha_compare) - cmd.flagList.append(settings.g_mdsft_zsrcsel) +def saveOtherModeLDefinitionAll(fMaterial: FMaterial, settings, defaults, defaultRenderMode, f3d): + cmd = SPSetOtherMode("G_SETOTHERMODE_L", 0, (32 if settings.set_rendermode else 3) - f3d.F3D_OLD_GBI, set()) + cmd.flagList.add(settings.g_mdsft_alpha_compare) + cmd.flagList.add(settings.g_mdsft_zsrcsel) + + if settings.g_mdsft_zsrcsel == "G_ZS_PRIM": + fMaterial.mat_only_DL.commands.append(DPSetPrimDepth(z=settings.prim_depth.z, dz=settings.prim_depth.dz)) if settings.set_rendermode: - flagList, blendList = getRenderModeFlagList(settings, fMaterial) - cmd.flagList.extend(flagList) - if blendList is not None: - cmd.flagList.extend( - [ - "GBL_c1(" + blendList[0] + ", " + blendList[1] + ", " + blendList[2] + ", " + blendList[3] + ")", - "GBL_c2(" + blendList[4] + ", " + blendList[5] + ", " + blendList[6] + ", " + blendList[7] + ")", - ] + if defaultRenderMode: + revert_cmd = SPSetOtherMode( + "G_SETOTHERMODE_L", + 0, + 32 - f3d.F3D_OLD_GBI, + {*defaultRenderMode, defaults.g_mdsft_alpha_compare, defaults.g_mdsft_zsrcsel}, ) - fMaterial.mat_only_DL.commands.append(cmd) + if fMaterial.revert is not None: + fMaterial.revert.commands.append(revert_cmd) + flagList, blender = getRenderModeFlagList(settings, fMaterial) + cmd.flagList.update(flagList) + if blender is not None: + cmd.flagList.add(blender) - if settings.g_mdsft_zsrcsel == "G_ZS_PRIM": - fMaterial.mat_only_DL.commands.append(DPSetPrimDepth(z=settings.prim_depth.z, dz=settings.prim_depth.dz)) + fMaterial.mat_only_DL.commands.append(cmd) def saveOtherModeLDefinitionIndividual(fMaterial, settings, defaults, defaultRenderMode): saveModeSetting(fMaterial, settings.g_mdsft_alpha_compare, defaults.g_mdsft_alpha_compare, DPSetAlphaCompare) - saveModeSetting(fMaterial, settings.g_mdsft_zsrcsel, defaults.g_mdsft_zsrcsel, DPSetDepthSource) if settings.g_mdsft_zsrcsel == "G_ZS_PRIM": fMaterial.mat_only_DL.commands.append(DPSetPrimDepth(z=settings.prim_depth.z, dz=settings.prim_depth.dz)) - fMaterial.revert.commands.append(DPSetPrimDepth()) + + if fMaterial.revert is not None: + fMaterial.revert.commands.append(DPSetPrimDepth()) if settings.set_rendermode: - flagList, blendList = getRenderModeFlagList(settings, fMaterial) - renderModeSet = DPSetRenderMode(flagList, blendList) + flagList, blender = getRenderModeFlagList(settings, fMaterial) + renderModeSet = DPSetRenderMode(flagList, blender) fMaterial.mat_only_DL.commands.append(renderModeSet) - if defaultRenderMode is not None: + if defaultRenderMode is not None and fMaterial.revert is not None: fMaterial.revert.commands.append(DPSetRenderMode(defaultRenderMode, None)) def getRenderModeFlagList(settings, fMaterial): flagList = [] - blendList = None + blender = None # cycle independent if not settings.rendermode_advanced_enabled: @@ -1830,28 +1788,13 @@ def getRenderModeFlagList(settings, fMaterial): cycle2 = "G_RM_NOOP" flagList = [settings.rendermode_preset_cycle_1, cycle2] else: + cycle1 = (settings.blend_p1, settings.blend_a1, settings.blend_m1, settings.blend_b1) if settings.g_mdsft_cycletype == "G_CYC_2CYCLE": - blendList = [ - settings.blend_p1, - settings.blend_a1, - settings.blend_m1, - settings.blend_b1, - settings.blend_p2, - settings.blend_a2, - settings.blend_m2, - settings.blend_b2, - ] + blender = RendermodeBlender( + cycle1, (settings.blend_p2, settings.blend_a2, settings.blend_m2, settings.blend_b2) + ) else: - blendList = [ - settings.blend_p1, - settings.blend_a1, - settings.blend_m1, - settings.blend_b1, - settings.blend_p1, - settings.blend_a1, - settings.blend_m1, - settings.blend_b1, - ] + blender = RendermodeBlender(cycle1, cycle1) if settings.aa_en: flagList.append("AA_EN") @@ -1874,14 +1817,16 @@ def getRenderModeFlagList(settings, fMaterial): if settings.force_bl: flagList.append("FORCE_BL") - return flagList, blendList + return tuple(flagList), blender def saveOtherDefinition(fMaterial, material, defaults): settings = material.rdp_settings if settings.clip_ratio != defaults.clip_ratio: fMaterial.mat_only_DL.commands.append(SPClipRatio(settings.clip_ratio)) - fMaterial.revert.commands.append(SPClipRatio(defaults.clip_ratio)) + + if fMaterial.revert is not None: + fMaterial.revert.commands.append(SPClipRatio(defaults.clip_ratio)) if material.set_blend: fMaterial.mat_only_DL.commands.append( @@ -1973,7 +1918,7 @@ def exportF3DtoXML( def exportF3DtoC(dirPath, obj, DLFormat, transformMatrix, texDir, savePNG, texSeparate, name, matWriteMethod): inline = bpy.context.scene.exportInlineF3D - fModel = FModel(name, DLFormat, matWriteMethod if not inline else GfxMatWriteMethod.WriteAll) + fModel = FModel(name, DLFormat, matWriteMethod) fMeshes = exportF3DCommon(obj, fModel, transformMatrix, True, name, DLFormat, not savePNG) if inline: @@ -2181,10 +2126,14 @@ def f3d_writer_register(): register_class(cls) bpy.types.Scene.matWriteMethod = bpy.props.EnumProperty(items=enumMatWriteMethod) + bpy.types.Scene.DLExportPath = bpy.props.StringProperty(name="Directory", subtype="FILE_PATH") + bpy.types.Scene.DLTexDir = bpy.props.StringProperty(name="Include Path", default="levels/bob") def f3d_writer_unregister(): for cls in reversed(f3d_writer_classes): unregister_class(cls) + del bpy.types.Scene.DLTexDir + del bpy.types.Scene.DLExportPath del bpy.types.Scene.matWriteMethod diff --git a/fast64_internal/f3d/flipbook.py b/fast64_internal/f3d/flipbook.py index 86e9f3bdb..2377dd069 100644 --- a/fast64_internal/f3d/flipbook.py +++ b/fast64_internal/f3d/flipbook.py @@ -256,7 +256,7 @@ def ootFlipbookAnimUpdate(self, armatureObj: bpy.types.Object, segment: str, ind # we use a handler since update functions are not called when a property is animated. @persistent def flipbookAnimHandler(dummy): - if bpy.context.scene.gameEditorMode == "OOT": + if bpy.context.scene.gameEditorMode in {"OOT", "MM"}: for obj in bpy.data.objects: if obj.type == "ARMATURE": # we only want to update texture on keyframed armatures. @@ -291,7 +291,7 @@ def draw(self, context): mat = context.material col = layout.column() - if context.scene.gameEditorMode == "OOT": + if context.scene.gameEditorMode in {"OOT", "MM"}: checkFlipbookReference = ootFlipbookReferenceIsValid drawFlipbookRequirementMessage = ootFlipbookRequirementMessage else: diff --git a/fast64_internal/f3d/glTF/README.md b/fast64_internal/f3d/glTF/README.md new file mode 100644 index 000000000..447262482 --- /dev/null +++ b/fast64_internal/f3d/glTF/README.md @@ -0,0 +1,241 @@ +## Extension Index: + +- [N64 Material (FAST64_materials_n64](#FAST64_materials_n64) +- [N64 Sampler (FAST64_sampler_n64)](#FAST64_sampler_n64) +- F3D + - [F3D Material Properties (FAST64_materials_f3d)](#FAST64_materials_f3d) + - [F3D Mesh Properties (FAST64_mesh_f3d)](#FAST64_mesh_f3d) + - Revisions + - [F3DLX Material Properties (FAST64_materials_f3dlx)](#FAST64_materials_f3dlx) + - [F3DEX3 Material Properties (FAST64_materials_f3dex3)](#FAST64_materials_f3dex3) + - [F3DEX and up Mesh Properties (FAST64_mesh_f3d_new)](#FAST64_mesh_f3d_new) + +--- + +

FAST64_materials_n64

+ +## Contributors + +* [@Lilaa3](https://github.com/Lilaa3) + +## Status + +Draft + +## Dependencies + +Written against the glTF 2.0 spec. + +## Overview + +This extension implements a representation of the n64's basic rendering properties present in fast64 materials, properties in individual microcodes are implemented in seperate extensions: + +- [F3D](#FAST64_materials_f3d) + - [F3DLX](#FAST64_materials_f3dlx) + - [F3DEX3](#FAST64_materials_f3dex3) + +### JSON Schema + +- [material.FAST64_materials_n64.schema.json](schema/FAST64_materials_n64.schema.json) + +## Known Implementations + +* No current implementations + +## Resources + +* [RDP Command Documentation](https://n64brew.dev/wiki/Reality_Display_Processor/Commands) + +--- + +

FAST64_sampler_n64

+ +## Contributors + +* [@Lilaa3](https://github.com/Lilaa3) + +## Status + +Draft + +## Dependencies + +Written against the glTF 2.0 spec. + +## Overview + +This extension implements a representation of how individual textures are sampled on n64, including shift, mask, low and high values, clamp and mirror, format and reference information. + +### JSON Schema + +- [material.FAST64_sampler_n64.schema.json](schema/FAST64_sampler_n64.schema.json) + +## Known Implementations + +* No current implementations + +## Resources + +* [Texture Mapping Documentation](https://ultra64.ca/files/documentation/online-manuals/man/pro-man/pro13/index.html) +* [Load Tile RDP Command Documentation](https://n64brew.dev/wiki/Reality_Display_Processor/Commands#0x35_-_Set_Tile) + +--- + +

FAST64_materials_f3d

+ +## Contributors + +* [@Lilaa3](https://github.com/Lilaa3) + +## Status + +Draft + +## Dependencies + +Extension of [FAST64_materials_n64](#FAST64_materials_n64) + +## Overview + +This extension implements an abstraction of F3D material features, properties from other revisions are implemented in separate extensions: + +- [F3DLX](#FAST64_materials_f3dlx) +- [F3DEX3](#FAST64_materials_f3dex3) + +### JSON Schema + +- [material.FAST64_materials_f3d.schema.json](schema/FAST64_materials_f3d.schema.json) + +## Known Implementations + +* No current implementations + +## Resources + +* [Latest N64 Documentation](https://ultra64.ca/files/documentation/online-manuals/man-v5-2/allman52/) + +--- + +

FAST64_mesh_f3d

+ +## Contributors + +* [@Lilaa3](https://github.com/Lilaa3) + +## Status + +Draft + +## Dependencies + +Written against the glTF 2.0 spec. + +## Overview + +This extension implements a representation of fast64 mesh data, currently only contains one property from post F3DLX microcodes, which is implemented in a seperate extension: + +- [F3DEX and up](#FAST64_mesh_f3d_new) + +### JSON Schema + +- [material.FAST64_mesh_f3d.schema.json](schema/FAST64_mesh_f3d.schema.json) + +## Known Implementations + +* No current implementations + +--- + +

FAST64_materials_f3dlx

+ +## Contributors + +* [@Lilaa3](https://github.com/Lilaa3) + +## Status + +Draft + +## Dependencies + +Extension of [FAST64_materials_f3d](#FAST64_materials_f3d) + +## Overview + +This extension implements F3DLX material features, which only differs in the inclusion of the G_CLIPPING geometry mode, it mostly exists for completeness as G_CLIPPING is on by default and is crucial for performance. + +### JSON Schema + +- [material.FAST64_materials_f3dlx.schema.json](schema/FAST64_materials_f3dlx.schema.json) + +## Known Implementations + +* No current implementations + +## Resources + +* [gSPSetGeometryMode Documentation](https://ultra64.ca/files/documentation/online-manuals/man-v5-2/allman52/n64man/gsp/gSPSetGeometryMode.htm) + +--- + +

FAST64_materials_f3dex3

+ +## Contributors + +* [@Lilaa3](https://github.com/Lilaa3) + +## Status + +Draft + +## Dependencies + +Extension of [FAST64_materials_f3d](#FAST64_materials_f3d) + +## Overview + +This extension implements F3DEX3 material features such as ambient occlusion, fresnel, attribute offsets and cel shading. F3DEX3 is based of F3DEX2 but does not include G_CLIPPING as an optional geometry mode, fast64 will never export both FAST64_materials_f3dlx and FAST64_materials_f3dex3. + +### JSON Schema + +- [material.FAST64_materials_f3dex3.schema.json](schema/FAST64_materials_f3dex3.schema.json) + +## Known Implementations + +- No current implementations + +## Resources + +- [F3DEX3's Documentation](https://hackern64.github.io/F3DEX3/) + +--- + +

FAST64_mesh_f3d_new

+ +## Contributors + +* [@Lilaa3](https://github.com/Lilaa3) + +## Status + +Draft + +## Dependencies + +Extension of [FAST64_mesh_f3d](#FAST64_mesh_f3d) + +## Overview + +This extension implements f3dex and up vertex based culling, which fast64 represents per mesh. + +### JSON Schema + +- [material.FAST64_mesh_f3d_new.schema.json](schema/FAST64_mesh_f3d_new.schema.json) + +## Known Implementations + +- No current implementations + +## Resources + +- [gSPCullDisplayList Documentation](https://ultra64.ca/files/documentation/online-manuals/man-v5-2/allman52/n64man/gsp/gSPCullDisplayList.htm) \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/f3d_gltf.py b/fast64_internal/f3d/glTF/f3d_gltf.py new file mode 100644 index 000000000..ff587a6b8 --- /dev/null +++ b/fast64_internal/f3d/glTF/f3d_gltf.py @@ -0,0 +1,1485 @@ +from dataclasses import dataclass +from math import ceil, floor +import copy +import bpy +import numpy as np + +from bpy.types import NodeTree, Mesh, Material, Context, Panel, PropertyGroup, UILayout +from bpy.props import BoolProperty + +from ...utility import ( + json_to_prop_group, + multilineLabel, + prop_group_to_json, + prop_split, + PluginError, + fix_invalid_props, +) +from ...gltf_utility import ( + GlTF2SubExtension, + get_gltf_image_from_blender_image, + get_gltf_settings, + is_import_context, + swap_function, + suffix_function, + get_version, +) +from ..f3d_gbi import F3D, get_F3D_GBI +from ..f3d_material import ( + all_combiner_uses, + get_color_info_from_tex, + link_if_none_exist, + remove_first_link_if_exists, + rendermode_presets_checks, + trunc_10_2, + createScenePropertiesForMaterial, + get_f3d_node_tree, + update_all_node_values, + update_blend_method, + get_textlut_mode, + F3DMaterialProperty, + RDPSettings, + TextureProperty, + F3D_MAT_CUR_VERSION, +) +from ..f3d_material_helpers import node_tree_copy +from ..f3d_writer import cel_shading_checks, check_face_materials, getColorLayer +from ..f3d_texture_writer import UVtoSTLarge + +MATERIAL_EXTENSION_NAME = "FAST64_materials_n64" +F3D_MATERIAL_EXTENSION_NAME = "FAST64_materials_f3d" +EX1_MATERIAL_EXTENSION_NAME = "FAST64_materials_f3dlx" +EX3_MATERIAL_EXTENSION_NAME = "FAST64_materials_f3dex3" +SAMPLER_EXTENSION_NAME = "FAST64_sampler_n64" +MESH_EXTENSION_NAME = "FAST64_mesh_f3d" +NEW_MESH_EXTENSION_NAME = "FAST64_mesh_f3d_new" + + +def is_mat_f3d(mat: Material | None) -> bool: + return mat is not None and mat.is_f3d and mat.mat_ver == F3D_MAT_CUR_VERSION + + +def mesh_has_f3d_mat(mesh: Mesh): + return any(is_mat_f3d(mat) for mat in mesh.materials) + + +def get_settings(context: Context | None = None): + context = context or bpy.context + return context.scene.fast64.settings.glTF.f3d + + +def uvmap_check(mesh: Mesh): + # If there is a F3D material check if the mesh has a uvmap + if mesh_has_f3d_mat(mesh) and not any(layer.name == "UVMap" for layer in mesh.uv_layers): + raise PluginError('Object with F3D materials does not have a "UVMap" uvmap layer.') + + +def large_tex_checks(materials: list[Material], mesh: Mesh): + """ + See TileLoad.initWithFace for the usual exporter version of this function + This strips out any exporting and focous on just error checking + """ + + large_props_dict = {} + for mat in mesh.materials: # Cache info on any large tex material that needs to be checked + if not (is_mat_f3d(mat) and mat.f3d_mat.use_large_textures): + continue + f3d_mat: F3DMaterialProperty = mat.f3d_mat + use_dict = all_combiner_uses(f3d_mat) + textures: list[TextureProperty] = [] + if use_dict["Texture 0"] and f3d_mat.tex0.tex_set: + textures.append(f3d_mat.tex0) + if use_dict["Texture 1"] and f3d_mat.tex1.tex_set: + textures.append(f3d_mat.tex1) + + if len(textures) == 0: + continue + texture = textures[0] + + tex_sizes = [tex.tex_size for tex in textures] + tmem = sum(tex.word_usage for tex in textures) + tmem_size = 256 if texture.is_ci else 512 + if tmem <= tmem_size: + continue # Can fit in TMEM without large mode, so skip + widths, heights = zip(*tex_sizes) + large_props_dict[mat.name] = { + "clamp": f3d_mat.large_edges == "Clamp", + "point": f3d_mat.rdp_settings.g_mdsft_text_filt == "G_TF_POINT", + "dimensions": (min(widths), min(heights)), + "format": texture.tex_format, + "texels_per_word": 64 // sum(texture.format_size for texture in textures), + "is_4bit": any(tex.format_size == 4 for tex in textures), + "large_tex_words": tmem_size, + } + + def get_low(large_props, value, field): + value = floor(value) + if large_props["clamp"]: + value = min(max(value, 0), large_props["dimensions"][field] - 1) + if large_props["is_4bit"] and field == 0: + # Must start on an even texel (round down) + value &= ~1 + return value + + def get_high(large_props, value, field): + value = ceil(value) - (1 if large_props["point"] else 0) + if large_props["clamp"]: + value = min(max(value, 0), large_props["dimensions"][field] - 1) + if large_props["is_4bit"] and field == 0: + value |= 1 + return value + + def fix_region(large_props, sl, sh, tl, th): + dimensions = large_props["dimensions"] + assert sl <= sh and tl <= th + soffset = int(floor(sl / dimensions[0])) * dimensions[0] + toffset = int(floor(tl / dimensions[1])) * dimensions[1] + sl -= soffset + sh -= soffset + tl -= toffset + th -= toffset + assert 0 <= sl < dimensions[0] and 0 <= tl < dimensions[1] + + if sh >= 1024 or th >= 1024: + return False + texels_per_word = large_props["texels_per_word"] + if sh >= dimensions[0]: + if texels_per_word > dimensions[0]: + raise PluginError(f"Large texture must be at least {texels_per_word} wide.") + sl -= dimensions[0] + sl = int(floor(sl / texels_per_word)) * texels_per_word + sl += dimensions[0] + if th >= dimensions[1]: + tl -= dimensions[1] + tl = int(floor(tl / 2.0)) * 2 + tl += dimensions[1] + + def get_tmem_usage(width, height, texels_per_word=texels_per_word): + return (width + texels_per_word - 1) // texels_per_word * height + + tmem_usage = get_tmem_usage(sh - sl + 1, th - tl + 1) + return tmem_usage <= large_props["large_tex_words"] + + if not large_props_dict: + return + + if "UVMap" not in mesh.uv_layers: + raise PluginError('Cannot do large texture checks without a "UVMap" uvmap layer.') + uv_data = mesh.uv_layers["UVMap"].data + for face in mesh.loop_triangles: + material = materials[face.material_index] + if material is None: + continue + mat_name: str = material.name + large_props = large_props_dict.get(mat_name) + if large_props is None: + continue + + dimensions = large_props["dimensions"] + face_uvs = [UVtoSTLarge(None, loop_index, uv_data, dimensions) for loop_index in face.loops] + sl, sh, tl, th = 1000000, -1, 1000000, -1 + for point in face_uvs: + sl = min(sl, get_low(large_props, point[0], 0)) + sh = max(sh, get_high(large_props, point[0], 0)) + tl = min(tl, get_low(large_props, point[1], 1)) + th = max(th, get_high(large_props, point[1], 1)) + + if fix_region(large_props, sl, sh, tl, th): + continue # Region fits in TMEM + if sh >= 1024 or th >= 1024: + raise PluginError( + f"Large texture material {mat_name} has a face that needs" + f" to cover texels {sl}-{sh} x {tl}-{th}" + f" (image dims are {dimensions}), but image space" + " only goes up to 1024 so this cannot be represented." + ) + else: + raise PluginError( + f"Large texture material {mat_name} has a face that needs" + f" to cover texels {sl}-{sh} x {tl}-{th}" + f" ({sh-sl+1} x {th-tl+1} texels) " + f"in format {large_props['format']}, which can't fit in TMEM." + ) + + +def multitex_checks(raise_large_multitex: bool, f3d_mat: F3DMaterialProperty): + tex0: TextureProperty = f3d_mat.tex0 + tex1: TextureProperty = f3d_mat.tex1 + both_reference = tex0.use_tex_reference and tex1.use_tex_reference + same_reference = both_reference and tex0.tex_reference == tex1.tex_reference + same_textures = same_reference or (not both_reference and tex0.tex == tex1.tex) + both_ci8 = tex0.tex_format == tex1.tex_format == "CI8" + + tex0_size, tex1_size = tex0.tex_size, tex1.tex_size + tex0_tmem, tex1_tmem = tex0.word_usage, tex1.word_usage + tmem = tex0_tmem if same_textures else tex0_tmem + tex1_tmem + tmem_size = 256 if tex0.is_ci and tex1.is_ci else 512 + + if same_reference and tex0_size != tex1_size: + raise PluginError("Textures with the same reference must have the same size.") + + if raise_large_multitex and f3d_mat.use_large_textures: + if tex0_tmem > tmem_size // 2 and tex1_tmem > tmem_size // 2: + raise PluginError("Cannot multitexture with two large textures.") + if same_textures: + raise PluginError( + "Cannot use the same texture for Tex 0 and 1 when using large textures.", + ) + if not f3d_mat.use_large_textures and tmem > tmem_size: + raise PluginError( + "Textures are too big. Max TMEM size is 4kb, ex. 2 32x32 RGBA 16 bit textures.\n" + "Note that width needs to be padded to 64-bit boundaries.", + ) + + if tex0.is_ci != tex1.is_ci: + raise PluginError("Can't have a CI + non-CI texture. Must be both or neither CI.") + if not (tex0.is_ci and tex1.is_ci): + return + + # CI multitextures + same_pal_reference = both_reference and tex0.pal_reference == tex1.pal_reference + if tex0.ci_format != tex1.ci_format: + raise PluginError( + "Both CI textures must use the same palette format (usually RGBA16).", + ) + if same_pal_reference and tex0.pal_reference_size != tex1.pal_reference_size: + raise PluginError( + "Textures with the same palette reference must have the same palette size.", + ) + if tex0.use_tex_reference != tex1.use_tex_reference and both_ci8: + # TODO: If flipbook is ever implemented, check if the reference is set by the flipbook + # Theoretically possible if there was an option to have half the palette for each + raise PluginError("Can't have two CI8 textures where only one is a reference; no way to assign the palette.") + if both_reference and both_ci8 and not same_pal_reference: + raise PluginError("Can't have two CI8 textures with different palette references.") + + # TODO: When porting ac f3dzex, skip this check + rgba_colors = get_color_info_from_tex(tex0.tex)[3] + rgba_colors.update(get_color_info_from_tex(tex1.tex)[3]) + if len(rgba_colors) > 256: + raise PluginError( + f"The two CI textures together contain a total of {len(rgba_colors)} colors,\n" + "which can't fit in a CI8 palette (256)." + ) + + +# Ideally we'd use mathutils.Color here but it does not support alpha (and mul for some reason) +@dataclass +class Color: + r: float = 0.0 + g: float = 0.0 + b: float = 0.0 + a: float = 0.0 + + def wrap(self, min_value: float, max_value: float): + def wrap_value(value, min_value=min_value, max_value=max_value): + range_width = max_value - min_value + return ((value - min_value) % range_width) + min_value + + return Color(wrap_value(self.r), wrap_value(self.g), wrap_value(self.b), wrap_value(self.a)) + + def to_clean_list(self): + def round_and_clamp(value): + return round(max(min(value, 1.0), 0.0), 4) + + return [ + round_and_clamp(self.r), + round_and_clamp(self.g), + round_and_clamp(self.b), + round_and_clamp(self.a), + ] + + def __sub__(self, other): + return Color(self.r - other.r, self.g - other.g, self.b - other.b, self.a - other.a) + + def __add__(self, other): + return Color(self.r + other.r, self.g + other.g, self.b + other.b, self.a + other.a) + + def __mul__(self, other): + return Color(self.r * other.r, self.g * other.g, self.b * other.b, self.a * other.a) + + +def get_color_component(inp: str, data: dict, previous_alpha: float) -> float: + if inp == "0": + return 0.0 + elif inp == "1": + return 1.0 + elif inp.startswith("COMBINED"): + return previous_alpha + elif inp == "LOD_FRACTION": + return 0.0 # Fast64 always uses black, let's do that for now + elif inp.startswith("PRIM"): + prim = data["primitive"] + if inp == "PRIM_LOD_FRAC": + return prim["loDFraction"] + if inp == "PRIMITIVE_ALPHA": + return prim["color"][3] + elif inp == "ENV_ALPHA": + return data["environment"]["color"][3] + elif inp.startswith("K"): + values = data["yuvConvert"]["values"] + if inp == "K4": + return values[4] + if inp == "K5": + return values[5] + + +def get_color_from_input(inp: str, previous_color: Color, data: dict, is_alpha: bool, default_color: Color) -> Color: + if inp == "COMBINED" and not is_alpha: + return previous_color + elif inp == "CENTER": + return Color(*data["chromaKey"]["center"], 1.0) + elif inp == "SCALE": + return Color(*data["chromaKey"]["scale"], 1.0) + elif inp == "PRIMITIVE": + return Color(*data["primitive"]["color"]) + elif inp == "ENVIRONMENT": + return Color(*data["environment"]["color"]) + else: + value = get_color_component(inp, data, previous_color.a) + if value: + return Color(value, value, value, value) + return default_color + + +def fake_color_from_cycle(cycle: list[str], previous_color: Color, data: dict, is_alpha=False): + default_colors = [Color(1.0, 1.0, 1.0, 1.0), Color(), Color(1.0, 1.0, 1.0, 1.0), Color()] + a, b, c, d = [ + get_color_from_input(inp, previous_color, data, is_alpha, default_color) + for inp, default_color in zip(cycle, default_colors) + ] + sign_extended_c = c.wrap(-1.0, 1.0001) + unwrapped_result = (a - b) * sign_extended_c + d + result = unwrapped_result.wrap(-0.5, 1.5) + if is_alpha: + result = Color(previous_color.r, previous_color.g, previous_color.b, result.a) + return result + + +def get_fake_color(data: dict): + fake_color = Color() + for cycle in data["combiner"]["cycles"]: # Try to emulate solid colors + fake_color = fake_color_from_cycle(cycle["color"], fake_color, data) + fake_color = fake_color_from_cycle(cycle["alpha"], fake_color, data, True) + return fake_color.to_clean_list() + + +class F3DExtensions(GlTF2SubExtension): + settings: "F3DGlTFSettings" = None + gbi: F3D = None + base_node_tree: NodeTree = None + + def post_init(self): + self.settings = self.extension.settings.f3d + self.gbi: F3D = get_F3D_GBI() + + if not self.extension.importing: + return + try: + self.print_verbose("Linking F3D material library and caching F3D node tree") + self.base_node_tree = get_f3d_node_tree() + except Exception as exc: + raise PluginError(f"Failed to import F3D node tree: {str(exc)}") from exc + + def sampler_from_f3d(self, f3d_mat: F3DMaterialProperty, f3d_tex: TextureProperty): + from io_scene_gltf2.io.com import gltf2_io # pylint: disable=import-error + + if get_version() >= (4, 3, 13): + from io_scene_gltf2.io.com.constants import TextureFilter, TextureWrap # pylint: disable=import-error + else: + from io_scene_gltf2.io.com.gltf2_io_constants import ( + TextureFilter, + TextureWrap, + ) # pylint: disable=import-error + + wrap = [] + for field in ["S", "T"]: + field_prop = getattr(f3d_tex, field) + wrap.append( + TextureWrap.ClampToEdge + if field_prop.clamp + else TextureWrap.MirroredRepeat + if field_prop.mirror + else TextureWrap.Repeat + ) + + nearest = f3d_mat.rdp_settings.g_mdsft_text_filt == "G_TF_POINT" + mag_f = TextureFilter.Nearest if nearest else TextureFilter.Linear + min_f = TextureFilter.NearestMipmapNearest if nearest else TextureFilter.LinearMipmapLinear + sampler = gltf2_io.Sampler( + extensions=None, + extras=None, + mag_filter=mag_f, + min_filter=min_f, + name=None, + wrap_s=wrap[0], + wrap_t=wrap[1], + ) + self.append_extension(sampler, SAMPLER_EXTENSION_NAME, f3d_tex.to_dict()) + return sampler + + def sampler_to_f3d(self, gltf2_sampler, f3d_tex: TextureProperty): + data = self.get_extension(gltf2_sampler, SAMPLER_EXTENSION_NAME) + if data is None: + return + f3d_tex.from_dict(data) + + def f3d_to_gltf2_texture( + self, + f3d_mat: F3DMaterialProperty, + f3d_tex: TextureProperty, + export_settings: dict, + ): + from io_scene_gltf2.io.com import gltf2_io # pylint: disable=import-error + + img = f3d_tex.tex + if img is not None: + source = get_gltf_image_from_blender_image(img.name, export_settings) + + if self.settings.raise_texture_limits and f3d_tex.tex_set: + tex_size = f3d_tex.tex_size + tmem_usage = f3d_tex.word_usage + tmem_max = 256 if f3d_tex.is_ci else 512 + + if f3d_mat.use_large_textures and tex_size[0] > 1024 or tex_size[1] > 1024: + raise PluginError( + "Texture size (even large textures) limited to 1024 pixels in each dimension.", + ) + if not f3d_mat.use_large_textures and tmem_usage > tmem_max: + raise PluginError( + f"Texture is too large: {tmem_usage} / {tmem_max} bytes.\n" + "Note that width needs to be padded to 64-bit boundaries." + ) + if f3d_tex.is_ci and not f3d_tex.use_tex_reference: + _, _, _, rgba_colors = get_color_info_from_tex(img) + if len(rgba_colors) > 2**f3d_tex.format_size: + raise PluginError( + f"Too many colors for {f3d_tex.tex_format}: {len(rgba_colors)}", + ) + else: # Image isn´t set + if f3d_tex.tex_set and not f3d_tex.use_tex_reference: + raise PluginError("Non texture reference must have an image.") + source = None + sampler = self.sampler_from_f3d(f3d_mat, f3d_tex) + return gltf2_io.Texture( + extensions=None, + extras=None, + name=source.name if source else None, + sampler=sampler, + source=source, + ) + + def gltf2_to_f3d_texture(self, gltf2_texture, gltf, f3d_tex: TextureProperty): + if get_version() >= (4, 3, 13): + from io_scene_gltf2.blender.imp.image import ( + BlenderImage, + ) # pylint: disable=import-error, import-outside-toplevel + else: + from io_scene_gltf2.blender.imp.gltf2_blender_image import ( + BlenderImage, + ) # pylint: disable=import-error, import-outside-toplevel + + if gltf2_texture.sampler is not None: + sampler = gltf.data.samplers[gltf2_texture.sampler] + self.sampler_to_f3d(sampler, f3d_tex) + if gltf2_texture.source is not None: + BlenderImage.create(gltf, gltf2_texture.source) + img = gltf.data.images[gltf2_texture.source] + blender_image_name = img.blender_image_name + if blender_image_name: + f3d_tex.tex = bpy.data.images[blender_image_name] + f3d_tex.tex.colorspace_settings.name = "sRGB" + + def f3d_to_glTF2_texture_info( + self, + f3d_mat: F3DMaterialProperty, + f3d_tex: TextureProperty, + num: int, + export_settings: dict, + ): + from io_scene_gltf2.io.com import gltf2_io # pylint: disable=import-error + + try: + texture = self.f3d_to_gltf2_texture(f3d_mat, f3d_tex, export_settings) + except Exception as exc: + raise PluginError(f"Failed to create texture {num}: {str(exc)}") from exc + tex_info = gltf2_io.TextureInfo( + index=texture, + extensions=None, + extras=None, + tex_coord=None, + ) + + def to_offset(low: float, tex_size: int): + return trunc_10_2(low) * (1.0 / tex_size) + + transform_data = {} + size = f3d_tex.tex_size + if size != [0, 0]: + offset = [to_offset(f3d_tex.S.low, size[0]), to_offset(f3d_tex.T.low, size[1])] + if offset != [0.0, 0.0]: + transform_data = {"offset": offset} + + scale = [2.0 ** (f3d_tex.S.shift * -1.0), 2.0 ** (f3d_tex.T.shift * -1.0)] + if scale != [1.0, 1.0]: + transform_data["scale"] = scale + + if transform_data: + self.append_extension(tex_info, "KHR_texture_transform", transform_data) + return tex_info + + def gather_material_hook(self, gltf2_material, blender_material: Material, export_settings: dict): + if not blender_material.is_f3d: + if self.settings.raise_non_f3d_mat: + raise PluginError( + 'Material is not an F3D material. Turn off "Non F3D Material" to ignore.', + ) + return + if blender_material.mat_ver < F3D_MAT_CUR_VERSION: + raise PluginError( + f"Material is an F3D material but its version is too old ({blender_material.mat_ver}).", + ) + + f3d_mat: F3DMaterialProperty = blender_material.f3d_mat + fix_invalid_props(f3d_mat) + rdp: RDPSettings = f3d_mat.rdp_settings + + if self.settings.raise_texture_limits: + if f3d_mat.is_multi_tex and (f3d_mat.tex0.tex_set and f3d_mat.tex1.tex_set): + multitex_checks(self.settings.raise_large_multitex, f3d_mat) + if self.settings.raise_rendermode: + if rdp.set_rendermode and not rdp.rendermode_advanced_enabled: + rendermode_presets_checks(f3d_mat) + + use_dict = all_combiner_uses(f3d_mat) + n64_data = { + "combiner": f3d_mat.combiner_to_dict(), + **f3d_mat.n64_colors_to_dict(use_dict), + "otherModes": ( + { + **rdp.other_mode_h_to_dict(True, lut_format=get_textlut_mode(f3d_mat)), + **rdp.other_mode_l_to_dict(True), + } + ), + } + if rdp.g_mdsft_zsrcsel == "G_ZS_PRIM": + n64_data["primDepth"] = rdp.prim_depth.to_dict() + if rdp.g_mdsft_textlod == "G_TL_LOD": + n64_data.update({"mipmapCount": rdp.num_textures_mipmapped}) + n64_data.update(f3d_mat.extra_texture_settings_to_dict()) + + textures = {} + n64_data["textures"] = textures + if use_dict["Texture 0"]: + textures["0"] = self.f3d_to_glTF2_texture_info( + f3d_mat, + f3d_mat.tex0, + 0, + export_settings, + ) + if use_dict["Texture 1"]: + textures["1"] = self.f3d_to_glTF2_texture_info( + f3d_mat, + f3d_mat.tex1, + 1, + export_settings, + ) + n64_data["extensions"] = {} + + f3d_data = {"geometryMode": rdp.f3d_geo_mode_to_dict()} + if rdp.clip_ratio != 2: + f3d_data["clipRatio"] = rdp.clip_ratio + f3d_data.update({**f3d_mat.f3d_colors_to_dict(use_dict), "extensions": {}}) + if self.gbi.F3DEX_GBI: # F3DLX + f3d_data["extensions"][EX1_MATERIAL_EXTENSION_NAME] = self.extension.Extension( + name=EX1_MATERIAL_EXTENSION_NAME, + extension={"geometryMode": rdp.f3dlx_geo_mode_to_dict()}, + required=False, + ) + if self.gbi.F3DEX_GBI_3: # F3DEX3 + if f3d_mat.use_cel_shading: + cel_shading_checks(f3d_mat) + f3d_data["extensions"][EX3_MATERIAL_EXTENSION_NAME] = self.extension.Extension( + name=EX3_MATERIAL_EXTENSION_NAME, + extension={ + "geometryMode": rdp.f3dex3_geo_mode_to_dict(), + **f3d_mat.f3dex3_colors_to_dict(), + }, + required=False, + ) + + n64_data["extensions"][F3D_MATERIAL_EXTENSION_NAME] = self.extension.Extension( + name=F3D_MATERIAL_EXTENSION_NAME, + extension=f3d_data, + required=False, + ) + + self.append_extension(gltf2_material, MATERIAL_EXTENSION_NAME, n64_data) + + # glTF Standard + pbr = gltf2_material.pbr_metallic_roughness + if f3d_mat.is_multi_tex: + pbr.base_color_texture = textures["0"] + pbr.metallic_roughness_texture = textures["1"] + elif textures: + pbr.base_color_texture = list(textures.values())[0] + pbr.base_color_factor = get_fake_color(n64_data) + + if not f3d_mat.rdp_settings.g_lighting: + self.append_extension(gltf2_material, "KHR_materials_unlit") + + def gather_mesh_hook( + self, gltf2_mesh, blender_mesh, _blender_object, _vertex_groups, _modifiers, materials, _export_settings + ): + if self.settings.raise_bad_mat_slot: + if len(blender_mesh.materials) == 0 or len(materials) == 0: + raise PluginError("Object does not have any materials.") + check_face_materials( + gltf2_mesh.name, + materials, + blender_mesh.polygons, + self.settings.raise_non_f3d_mat, + ) + if self.settings.raise_no_uvmap: + uvmap_check(blender_mesh) + if self.settings.raise_large_tex: + large_tex_checks(materials, blender_mesh) + + def gather_node_hook(self, gltf2_node, blender_object, _export_settings: dict): + if gltf2_node.mesh and not self.gbi.F3D_OLD_GBI and not blender_object.use_f3d_culling: + self.append_extension( + gltf2_node.mesh, + MESH_EXTENSION_NAME, + { + "extensions": { + NEW_MESH_EXTENSION_NAME: self.extension.Extension( + name=NEW_MESH_EXTENSION_NAME, + extension={"use_culling": False}, + required=False, + ) + } + }, + ) + + # Importing + + def gather_import_material_after_hook( + self, + gltf_material, + _vertex_color, + blender_material: Material, + gltf, + ): + n64_data = self.get_extension(gltf_material, MATERIAL_EXTENSION_NAME) + if n64_data is None: + return + + try: + blender_material.f3d_update_flag = True + + f3d_mat: F3DMaterialProperty = blender_material.f3d_mat + rdp: RDPSettings = f3d_mat.rdp_settings + f3d_mat.combiner_from_dict(n64_data.get("combiner", {})) + f3d_mat.n64_colors_from_dict(n64_data) + other_modes = n64_data.get("otherModes", {}) + rdp.other_mode_h_from_dict(other_modes) + rdp.other_mode_l_from_dict(other_modes) + rdp.prim_depth.from_dict(n64_data.get("primDepth", {})) + f3d_mat.extra_texture_settings_from_dict(n64_data) + rdp.num_textures_mipmapped = n64_data.get("mipmapCount", 2) + + for num, tex_info in n64_data.get("textures", {}).items(): + index = tex_info["index"] + self.print_verbose(f"Importing F3D texture {index}") + gltf2_texture = gltf.data.textures[index] + if num == "0": + self.gltf2_to_f3d_texture(gltf2_texture, gltf, f3d_mat.tex0) + elif num == "1": + self.gltf2_to_f3d_texture(gltf2_texture, gltf, f3d_mat.tex1) + else: + raise PluginError("Fast64 currently only supports the first two textures") + + f3d_data = n64_data.get("extensions", {}).get(F3D_MATERIAL_EXTENSION_NAME, None) + if f3d_data: + rdp.clip_ratio = f3d_data.get("clipRatio", 2) + f3d_mat.f3d_colors_from_dict(f3d_data) + rdp.f3d_geo_mode_from_dict(f3d_data.get("geometryMode", [])) + + ex1_data = f3d_data.get("extensions", {}).get(EX1_MATERIAL_EXTENSION_NAME, None) + if ex1_data is not None: # F3DLX + f3d_mat.rdp_settings.f3dex1_geo_mode_from_dict(ex1_data.get("geometryMode", {})) + + ex3_data = f3d_data.get("extensions", {}).get(EX3_MATERIAL_EXTENSION_NAME, None) + if ex3_data is not None: # F3DEX3 + f3d_mat.rdp_settings.f3dex3_geo_mode_from_dict(ex3_data.get("geometryMode", {})) + f3d_mat.f3dex3_colors_from_dict(ex3_data) + except Exception as exc: + raise Exception( # pylint: disable=broad-exception-raised + f"Failed to import fast64 extension data:\n{str(exc)}", + ) from exc + finally: + blender_material.f3d_update_flag = False + blender_material.is_f3d = True + blender_material.mat_ver = F3D_MAT_CUR_VERSION + + self.print_verbose( + "Copying F3D node tree, creating scene properties and updating all nodes", + ) + try: + node_tree_copy(self.base_node_tree, blender_material.node_tree) + createScenePropertiesForMaterial(blender_material) + with bpy.context.temp_override(material=blender_material): + update_all_node_values(blender_material, bpy.context) + except Exception as exc: + raise Exception( # pylint: disable=broad-exception-raised + f"Error creating F3D node tree:\n{str(exc)}", + ) from exc + + def gather_import_node_after_hook(self, _vnode, gltf_node, blender_object, _gltf): + data = self.get_extension(gltf_node, MESH_EXTENSION_NAME) + if data is None: + return + new_data = data.get("extensions", {}).get(NEW_MESH_EXTENSION_NAME, None) + if new_data: + blender_object.use_f3d_culling = new_data.get("use_culling", True) + + def gather_import_mesh_after_hook(self, _gltf_mesh, blender_mesh, _gltf): + if len(blender_mesh.vertex_colors) < 1 or not mesh_has_f3d_mat(blender_mesh): + return + color_layer = blender_mesh.vertex_colors[0] + color_layer.name = "Col" + color = np.empty((len(blender_mesh.loops), 4), dtype=np.float32) + color_layer.data.foreach_get("color", color.ravel()) + + alpha = color[:, 3] + alpha_rgba = np.repeat(alpha[:, np.newaxis], 4, axis=1).flatten() + alpha_layer = blender_mesh.vertex_colors.new(name="Alpha").data + alpha_layer.foreach_set("color", alpha_rgba) + + +class F3DGlTFSettings(PropertyGroup): + use: BoolProperty(default=True, name="Export/Import F3D extensions") + use_3_2_hacks_prop: BoolProperty( + name="Use 3.2 vertex color hacks", + description="Blender version 3.2 ships with the last version of the glTF 2.0 addon to not support " + "float colors (3.2.40).\n" + "This hack will override the primitive gathering function in the glTF addon with a custom one", + default=True, + ) + apply_alpha_to_col: BoolProperty( + name='Apply alpha to "Col" layer', + description='"Col" color attribute will have alpha applied for a single color accessor', + default=True, + ) + raise_texture_limits: BoolProperty( + name="Tex Limits", + description="Raises errors when texture limits are exceeded,\n" + "such as texture resolution, palette size, format conflicts, etc", + default=True, + ) + raise_large_multitex: BoolProperty( + name="Large Multi", + description="Raise an error when a multitexture has two large textures.\n" + "This can theoretically be supported", + default=True, + ) + raise_large_tex: BoolProperty( + name="Large Tex", + description="Raise an error when a polygon's textures in large texture mode can´t fit in\n" + "one full TMEM load", + default=True, + ) + raise_rendermode: BoolProperty( + name="Rendermode", + description="Raise an error when a material uses an invalid combination of rendermode presets.\n" + "Does not raise in the normal exporter", + default=True, + ) + raise_non_f3d_mat: BoolProperty( + name="Non F3D", + description="Raise an error when a material is not an F3D material. Useful for tiny3d", + default=False, + ) + raise_bad_mat_slot: BoolProperty( + name="Bad Slot", + description="Raise an error when the mesh has no materials, " "a face's material slot is empty or invalid", + default=False, + ) + raise_no_uvmap: BoolProperty( + name="No UVMap", + description="Raise an error when a mesh with F3D materials has no uv layer named UVMap", + default=True, + ) + + @property + def use_3_2_hacks(self): + return self.use and self.use_3_2_hacks_prop + + def to_dict(self): + return prop_group_to_json(self, ["use_3_2_hacks_prop"]) + + def from_dict(self, data: dict): + json_to_prop_group(self, data) + + def draw_props(self, layout: UILayout, import_context=False): + col = layout.column() + action = "Import" if import_context else "Export" + if not self.use: + col.box().label(text="Not enabled", icon="ERROR") + return + + if not import_context: + scene = bpy.context.scene + prop_split(col, scene, "f3d_type", "Scene Microcode") + if not scene.f3d_type: + col.box().label(text="No microcode selected", icon="ERROR") + gbi = get_F3D_GBI() + extensions = [MATERIAL_EXTENSION_NAME, SAMPLER_EXTENSION_NAME, MESH_EXTENSION_NAME, F3D_MATERIAL_EXTENSION_NAME] + if import_context or gbi.F3DEX_GBI: + extensions.append(EX1_MATERIAL_EXTENSION_NAME) + if import_context or gbi.F3DEX_GBI_3: + extensions.append(EX3_MATERIAL_EXTENSION_NAME) + if import_context or not gbi.F3D_OLD_GBI: + extensions.append(NEW_MESH_EXTENSION_NAME) + multilineLabel(col.box(), f"Will {action}:\n" + ",\n".join(extensions), icon=action.upper()) + + if import_context: + return + col.separator() + + col.box().label(text="See tooltips for more info", icon="INFO") + + if get_version() == (3, 2, 40): + col.prop(self, "use_3_2_hacks_prop") + col.prop(self, "apply_alpha_to_col") + + box = col.box().column() + box.box().label(text="Raise Errors:", icon="ERROR") + + row = box.row(align=True) + row.prop(self, "raise_texture_limits", toggle=True) + limits_row = row.row(align=True) + limits_row.enabled = self.raise_texture_limits + limits_row.prop(self, "raise_large_multitex", toggle=True) + limits_row.prop(self, "raise_large_tex", toggle=True) + + row = box.row(align=True) + row.prop(self, "raise_rendermode", toggle=True) + row.prop(self, "raise_non_f3d_mat", toggle=True) + row.prop(self, "raise_no_uvmap", toggle=True) + + row = box.split(factor=1.0 / 3.0, align=True) + row.prop(self, "raise_bad_mat_slot", toggle=True) + + +class F3DGlTFPanel(Panel): + bl_idname = "GLTF_PT_F3D" + bl_space_type = "FILE_BROWSER" + bl_region_type = "TOOL_PROPS" + bl_label = "" + bl_parent_id = "GLTF_PT_Fast64" + bl_options = {"DEFAULT_CLOSED"} + + def draw_header(self, context: Context): + row = self.layout.row() + row.separator(factor=0.25) + row.prop( + get_settings(context), + "use", + text=("Import" if is_import_context(context) else "Export") + " F3D extensions", + ) + + def draw(self, context: Context): + self.layout.use_property_decorate = False # No animation. + get_gltf_settings(context).f3d.draw_props(self.layout, is_import_context(context)) + + +def modify_f3d_nodes_for_export(use: bool): + """ + HACK: For 4.1 and 4.2, we create new, way simpler nodes that glTF can use to gather the correct vertex color layer. + We can´t have glTF interacting with the f3d nodes either, otherwise an infinite recursion occurs in texture gathering + this is also called in gather_gltf_extensions_hook (glTF2_post_export_callback can fail) + """ + for mat in bpy.data.materials: + if not is_mat_f3d(mat): + continue + node_tree = mat.node_tree + nodes = node_tree.nodes + f3d_output = nodes.get("OUTPUT") + if not f3d_output: + mat.use_nodes = use + continue + + material_output = next((node for node in nodes if node.bl_idname == "ShaderNodeOutputMaterial"), None) + if material_output is None: + material_output = nodes.new("ShaderNodeOutputMaterial") + + bsdf = next((node for node in nodes if node.bl_idname == "ShaderNodeBsdfPrincipled"), None) + if bsdf is None: + bsdf = nodes.new("ShaderNodeBsdfPrincipled") + bsdf["f3d_gltf_owned"] = True + bsdf.location = (1260, 900) + + if bpy.data.version < (4, 1, 0): + mix_name = "ShaderNodeMixRGB" + else: + mix_name = "ShaderNodeMix" + # we need to use a mix node because 4.1 + mix = next((node for node in nodes if node.bl_idname == mix_name and node.get("f3d_gltf_owned")), None) + if mix is None: + mix = nodes.new(mix_name) + mix["f3d_gltf_owned"] = True + mix.location = (1075, 850) + mix.blend_type = "MULTIPLY" + if bpy.data.version >= (4, 1, 0): + mix.data_type = "FLOAT" + mix.inputs["B"].default_value = 1.0 + else: + mix.inputs[2].default_value = [1.0, 1.0, 1.0, 1.0] + mix.inputs[0].default_value = 1.0 + + vertex_color = next( + (node for node in nodes if node.bl_idname == "ShaderNodeVertexColor" and node.get("f3d_gltf_owned")), None + ) + if vertex_color is None: + vertex_color = nodes.new("ShaderNodeVertexColor") + vertex_color["f3d_gltf_owned"] = True + vertex_color.location = (900, 850) + vertex_color.layer_name = "Col" + + remove_first_link_if_exists(mat, material_output.inputs["Surface"].links) + if use: + link_if_none_exist(mat, f3d_output.outputs["Shader"], material_output.inputs["Surface"]) + update_blend_method(mat, bpy.context) + else: + mat.blend_method = "BLEND" # HACK: same thing, 4.1 is weird with alpha + link_if_none_exist( + mat, vertex_color.outputs["Color"], bsdf.inputs.get("Color") or bsdf.inputs.get("Base Color") + ) + if bpy.app.version >= (4, 3, 0): + link_if_none_exist(mat, vertex_color.outputs["Alpha"], bsdf.inputs["Alpha"]) + else: + link_if_none_exist( + mat, vertex_color.outputs["Alpha"], mix.inputs["A" if bpy.data.version >= (4, 1, 0) else 1] + ) + link_if_none_exist(mat, mix.outputs[0], bsdf.inputs["Alpha"]) + link_if_none_exist(mat, bsdf.outputs["BSDF"], material_output.inputs["Surface"]) + + +def get_gamma_corrected(layer): + colors = np.empty((len(layer), 4), dtype=np.float32) + if bpy.app.version > (3, 2, 0): + layer.foreach_get("color", colors.ravel()) + else: # vectorized linear -> sRGB conversion + layer.foreach_get("color", colors.ravel()) + mask = colors > 0.0031308 + colors[mask] = 1.055 * (np.power(colors[mask], (1.0 / 2.4))) - 0.055 + colors[~mask] *= 12.0 + return colors.reshape((-1, 4)) + + +RGB_TO_LUM_COEF = np.array([0.2126729, 0.7151522, 0.0721750], np.float32) # blender rgb -> lum coefficient + + +def pre_gather_mesh_hook(blender_mesh: Mesh, *_args): + """HACK: Runs right before the actual gather_mesh func in the addon, we need to join col and alpha""" + if not get_settings().apply_alpha_to_col: + return + if not mesh_has_f3d_mat(blender_mesh): + return + print("F3D glTF: Applying alpha") + if get_version() != (3, 2, 40) and get_version() < (4, 1, 0) and "Col" in blender_mesh.color_attributes: + blender_mesh.color_attributes.active = blender_mesh.color_attributes["Col"] + color_layer = getColorLayer(blender_mesh, layer="Col") + alpha_layer = getColorLayer(blender_mesh, layer="Alpha") + if not color_layer or not alpha_layer: + return + color = get_gamma_corrected(color_layer) + rgb_alpha = get_gamma_corrected(alpha_layer) + + alpha_median = np.dot(rgb_alpha[:, :3], RGB_TO_LUM_COEF) + color[:, 3] = alpha_median + + color = color.flatten() + color = color.clip(0.0, 1.0) # clamp + color_layer.foreach_set("color", color) + + +def get_fast64_custom_colors(blender_mesh): + color_layer = getColorLayer(blender_mesh, layer="Col") # assume Col already has alpha from other hack + if color_layer is not None: + return np.zeros(len(blender_mesh.loops) * 4, dtype=np.float32) + else: + return get_gamma_corrected(color_layer) + + +def extract_primitives_fast64( + original_function, blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings +): + """ + https://github.com/KhronosGroup/glTF-Blender-IO/blob/bb0e780711f2021defb06c5650d5490f3771f252/addons/io_scene_gltf2/blender/exp/gltf2_blender_extract.py#L23-L383 + SPDX-License-Identifier: Apache-2.0 + Copyright 2018-2021 The glTF-Blender-IO authors. + + All changes are marked by "# FAST64 CHANGE/END:" + We must gather fast64 colors manually since they are corner float colors (unsupported in 3.2 glTF 2.0 addon) + + Extract primitives from a mesh. + """ + # FAST64 CHANGE: Local imports + from io_scene_gltf2.blender.exp.gltf2_blender_extract import ( # pylint: disable=import-error + __get_positions, + __get_bone_data, + __get_normals, + __get_tangents, + __get_bitangent_signs, + __get_uvs, + __get_colors, + __calc_morph_tangents, + ) + from io_scene_gltf2.blender.exp import gltf2_blender_export_keys # pylint: disable=import-error + from io_scene_gltf2.io.com.gltf2_io_debug import print_console # pylint: disable=import-error + from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins # pylint: disable=import-error + + # FAST64 END + # FAST64 CHANGE: Use custom fast64 function or use original + if not (get_settings().use_3_2_hacks and mesh_has_f3d_mat(blender_mesh)): + return original_function(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings) + # FAST64 END + + # FAST64 CHANGE: Changed print so the user knows the custom function is being used + print_console("INFO", "(Fast64) Extracting primitive: " + blender_mesh.name) + + blender_object = None + if uuid_for_skined_data: + blender_object = export_settings["vtree"].nodes[uuid_for_skined_data].blender_object + + use_normals = export_settings[gltf2_blender_export_keys.NORMALS] + if use_normals: + blender_mesh.calc_normals_split() + + use_tangents = False + if use_normals and export_settings[gltf2_blender_export_keys.TANGENTS]: + if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0: + try: + blender_mesh.calc_tangents() + use_tangents = True + except Exception: + print_console("WARNING", "Could not calculate tangents. Please try to triangulate the mesh first.") + + tex_coord_max = 0 + if export_settings[gltf2_blender_export_keys.TEX_COORDS]: + if blender_mesh.uv_layers.active: + tex_coord_max = len(blender_mesh.uv_layers) + + color_max = 0 + if export_settings[gltf2_blender_export_keys.COLORS]: + color_max = len(blender_mesh.vertex_colors) + + armature = None + skin = None + if blender_vertex_groups and export_settings[gltf2_blender_export_keys.SKINS]: + if modifiers is not None: + modifiers_dict = {m.type: m for m in modifiers} + if "ARMATURE" in modifiers_dict: + modifier = modifiers_dict["ARMATURE"] + armature = modifier.object + + # Skin must be ignored if the object is parented to a bone of the armature + # (This creates an infinite recursive error) + # So ignoring skin in that case + is_child_of_arma = ( + armature + and blender_object + and blender_object.parent_type == "BONE" + and blender_object.parent.name == armature.name + ) + if is_child_of_arma: + armature = None + + if armature: + skin = gltf2_blender_gather_skins.gather_skin( + export_settings["vtree"].nodes[uuid_for_skined_data].armature, export_settings + ) + if not skin: + armature = None + + use_morph_normals = use_normals and export_settings[gltf2_blender_export_keys.MORPH_NORMAL] + use_morph_tangents = use_morph_normals and use_tangents and export_settings[gltf2_blender_export_keys.MORPH_TANGENT] + + key_blocks = [] + if blender_mesh.shape_keys and export_settings[gltf2_blender_export_keys.MORPH]: + key_blocks = [ + key_block + for key_block in blender_mesh.shape_keys.key_blocks + if not (key_block == key_block.relative_key or key_block.mute) + ] + + use_materials = export_settings[gltf2_blender_export_keys.MATERIALS] + + # Fetch vert positions and bone data (joint,weights) + + locs, morph_locs = __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings) + if skin: + vert_bones, num_joint_sets, need_neutral_bone = __get_bone_data(blender_mesh, skin, blender_vertex_groups) + if need_neutral_bone is True: + # Need to create a fake joint at root of armature + # In order to assign not assigned vertices to it + # But for now, this is not yet possible, we need to wait the armature node is created + # Just store this, to be used later + armature_uuid = export_settings["vtree"].nodes[uuid_for_skined_data].armature + export_settings["vtree"].nodes[armature_uuid].need_neutral_bone = True + + # In Blender there is both per-vert data, like position, and also per-loop + # (loop=corner-of-poly) data, like normals or UVs. glTF only has per-vert + # data, so we need to split Blender verts up into potentially-multiple glTF + # verts. + # + # First, we'll collect a "dot" for every loop: a struct that stores all the + # attributes at that loop, namely the vertex index (which determines all + # per-vert data), and all the per-loop data like UVs, etc. + # + # Each unique dot will become one unique glTF vert. + + # List all fields the dot struct needs. + dot_fields = [("vertex_index", np.uint32)] + if use_normals: + dot_fields += [("nx", np.float32), ("ny", np.float32), ("nz", np.float32)] + if use_tangents: + dot_fields += [("tx", np.float32), ("ty", np.float32), ("tz", np.float32), ("tw", np.float32)] + for uv_i in range(tex_coord_max): + dot_fields += [("uv%dx" % uv_i, np.float32), ("uv%dy" % uv_i, np.float32)] + for col_i in range(color_max): + dot_fields += [ + ("color%dr" % col_i, np.float32), + ("color%dg" % col_i, np.float32), + ("color%db" % col_i, np.float32), + ("color%da" % col_i, np.float32), + ] + # FAST64 CHANGE: Add fields for custom fast64 color + dot_fields += [ + ("fast64_color_r", np.float32), + ("fast64_color_g", np.float32), + ("fast64_color_b", np.float32), + ("fast64_color_a", np.float32), + ] + # FAST64 CHANGE: END + if use_morph_normals: + for morph_i, _ in enumerate(key_blocks): + dot_fields += [ + ("morph%dnx" % morph_i, np.float32), + ("morph%dny" % morph_i, np.float32), + ("morph%dnz" % morph_i, np.float32), + ] + + dots = np.empty(len(blender_mesh.loops), dtype=np.dtype(dot_fields)) + + vidxs = np.empty(len(blender_mesh.loops)) + blender_mesh.loops.foreach_get("vertex_index", vidxs) + dots["vertex_index"] = vidxs + del vidxs + + if use_normals: + kbs = key_blocks if use_morph_normals else [] + normals, morph_normals = __get_normals(blender_mesh, kbs, armature, blender_object, export_settings) + dots["nx"] = normals[:, 0] + dots["ny"] = normals[:, 1] + dots["nz"] = normals[:, 2] + del normals + for morph_i, ns in enumerate(morph_normals): + dots["morph%dnx" % morph_i] = ns[:, 0] + dots["morph%dny" % morph_i] = ns[:, 1] + dots["morph%dnz" % morph_i] = ns[:, 2] + del morph_normals + + if use_tangents: + tangents = __get_tangents(blender_mesh, armature, blender_object, export_settings) + dots["tx"] = tangents[:, 0] + dots["ty"] = tangents[:, 1] + dots["tz"] = tangents[:, 2] + del tangents + signs = __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings) + dots["tw"] = signs + del signs + + for uv_i in range(tex_coord_max): + uvs = __get_uvs(blender_mesh, uv_i) + dots["uv%dx" % uv_i] = uvs[:, 0] + dots["uv%dy" % uv_i] = uvs[:, 1] + del uvs + + for col_i in range(color_max): + colors = __get_colors(blender_mesh, col_i) + dots["color%dr" % col_i] = colors[:, 0] + dots["color%dg" % col_i] = colors[:, 1] + dots["color%db" % col_i] = colors[:, 2] + dots["color%da" % col_i] = colors[:, 3] + del colors + + # FAST64 CHANGE: Add custom fast64 color + colors = get_fast64_custom_colors(blender_mesh) + dots["fast64_color_r"] = colors[:, 0] + dots["fast64_color_g"] = colors[:, 1] + dots["fast64_color_b"] = colors[:, 2] + dots["fast64_color_a"] = colors[:, 3] + del colors + # FAST64 CHANGE: End + + # Calculate triangles and sort them into primitives. + + blender_mesh.calc_loop_triangles() + loop_indices = np.empty(len(blender_mesh.loop_triangles) * 3, dtype=np.uint32) + blender_mesh.loop_triangles.foreach_get("loops", loop_indices) + + prim_indices = {} # maps material index to TRIANGLES-style indices into dots + + if use_materials == "NONE": # Only for None. For placeholder and export, keep primitives + # Put all vertices into one primitive + prim_indices[-1] = loop_indices + + else: + # Bucket by material index. + + tri_material_idxs = np.empty(len(blender_mesh.loop_triangles), dtype=np.uint32) + blender_mesh.loop_triangles.foreach_get("material_index", tri_material_idxs) + loop_material_idxs = np.repeat(tri_material_idxs, 3) # material index for every loop + unique_material_idxs = np.unique(tri_material_idxs) + del tri_material_idxs + + for material_idx in unique_material_idxs: + prim_indices[material_idx] = loop_indices[loop_material_idxs == material_idx] + + # Create all the primitives. + + primitives = [] + + for material_idx, dot_indices in prim_indices.items(): + # Extract just dots used by this primitive, deduplicate them, and + # calculate indices into this deduplicated list. + prim_dots = dots[dot_indices] + prim_dots, indices = np.unique(prim_dots, return_inverse=True) + + if len(prim_dots) == 0: + continue + + # Now just move all the data for prim_dots into attribute arrays + + attributes = {} + + blender_idxs = prim_dots["vertex_index"] + + attributes["POSITION"] = locs[blender_idxs] + + for morph_i, vs in enumerate(morph_locs): + attributes["MORPH_POSITION_%d" % morph_i] = vs[blender_idxs] + + if use_normals: + normals = np.empty((len(prim_dots), 3), dtype=np.float32) + normals[:, 0] = prim_dots["nx"] + normals[:, 1] = prim_dots["ny"] + normals[:, 2] = prim_dots["nz"] + attributes["NORMAL"] = normals + + if use_tangents: + tangents = np.empty((len(prim_dots), 4), dtype=np.float32) + tangents[:, 0] = prim_dots["tx"] + tangents[:, 1] = prim_dots["ty"] + tangents[:, 2] = prim_dots["tz"] + tangents[:, 3] = prim_dots["tw"] + attributes["TANGENT"] = tangents + + if use_morph_normals: + for morph_i, _ in enumerate(key_blocks): + ns = np.empty((len(prim_dots), 3), dtype=np.float32) + ns[:, 0] = prim_dots["morph%dnx" % morph_i] + ns[:, 1] = prim_dots["morph%dny" % morph_i] + ns[:, 2] = prim_dots["morph%dnz" % morph_i] + attributes["MORPH_NORMAL_%d" % morph_i] = ns + + if use_morph_tangents: + attributes["MORPH_TANGENT_%d" % morph_i] = __calc_morph_tangents(normals, ns, tangents) + + for tex_coord_i in range(tex_coord_max): + uvs = np.empty((len(prim_dots), 2), dtype=np.float32) + uvs[:, 0] = prim_dots["uv%dx" % tex_coord_i] + uvs[:, 1] = prim_dots["uv%dy" % tex_coord_i] + attributes["TEXCOORD_%d" % tex_coord_i] = uvs + + for color_i in range(color_max): + colors = np.empty((len(prim_dots), 4), dtype=np.float32) + colors[:, 0] = prim_dots["color%dr" % color_i] + colors[:, 1] = prim_dots["color%dg" % color_i] + colors[:, 2] = prim_dots["color%db" % color_i] + colors[:, 3] = prim_dots["color%da" % color_i] + attributes["COLOR_%d" % color_i] = colors + + # FAST64 CHANGE: Start + mat = blender_mesh.materials[material_idx] + if is_mat_f3d(mat): + colors = np.empty((len(prim_dots), 4), dtype=np.float32) + colors[:, 0] = prim_dots["fast64_color_r"] + colors[:, 1] = prim_dots["fast64_color_g"] + colors[:, 2] = prim_dots["fast64_color_b"] + colors[:, 3] = prim_dots["fast64_color_a"] + attributes["FAST64_COLOR"] = colors + del colors + # FAST64 CHANGE: End + + if skin: + joints = [[] for _ in range(num_joint_sets)] + weights = [[] for _ in range(num_joint_sets)] + + for vi in blender_idxs: + bones = vert_bones[vi] + for j in range(0, 4 * num_joint_sets): + if j < len(bones): + joint, weight = bones[j] + else: + joint, weight = 0, 0.0 + joints[j // 4].append(joint) + weights[j // 4].append(weight) + + for i, (js, ws) in enumerate(zip(joints, weights)): + attributes["JOINTS_%d" % i] = js + attributes["WEIGHTS_%d" % i] = ws + + primitives.append( + { + "attributes": attributes, + "indices": indices, + "material": material_idx, + } + ) + + if export_settings["gltf_loose_edges"]: + # Find loose edges + loose_edges = [e for e in blender_mesh.edges if e.is_loose] + blender_idxs = [vi for e in loose_edges for vi in e.vertices] + + if blender_idxs: + # Export one glTF vert per unique Blender vert in a loose edge + blender_idxs = np.array(blender_idxs, dtype=np.uint32) + blender_idxs, indices = np.unique(blender_idxs, return_inverse=True) + + attributes = {} + + attributes["POSITION"] = locs[blender_idxs] + + for morph_i, vs in enumerate(morph_locs): + attributes["MORPH_POSITION_%d" % morph_i] = vs[blender_idxs] + + if skin: + joints = [[] for _ in range(num_joint_sets)] + weights = [[] for _ in range(num_joint_sets)] + + for vi in blender_idxs: + bones = vert_bones[vi] + for j in range(0, 4 * num_joint_sets): + if j < len(bones): + joint, weight = bones[j] + else: + joint, weight = 0, 0.0 + joints[j // 4].append(joint) + weights[j // 4].append(weight) + + for i, (js, ws) in enumerate(zip(joints, weights)): + attributes["JOINTS_%d" % i] = js + attributes["WEIGHTS_%d" % i] = ws + + primitives.append( + { + "attributes": attributes, + "indices": indices, + "mode": 1, # LINES + "material": 0, + } + ) + + if export_settings["gltf_loose_points"]: + # Find loose points + verts_in_edge = set(vi for e in blender_mesh.edges for vi in e.vertices) + blender_idxs = [vi for vi, _ in enumerate(blender_mesh.vertices) if vi not in verts_in_edge] + + if blender_idxs: + blender_idxs = np.array(blender_idxs, dtype=np.uint32) + + attributes = {} + + attributes["POSITION"] = locs[blender_idxs] + + for morph_i, vs in enumerate(morph_locs): + attributes["MORPH_POSITION_%d" % morph_i] = vs[blender_idxs] + + if skin: + joints = [[] for _ in range(num_joint_sets)] + weights = [[] for _ in range(num_joint_sets)] + + for vi in blender_idxs: + bones = vert_bones[vi] + for j in range(0, 4 * num_joint_sets): + if j < len(bones): + joint, weight = bones[j] + else: + joint, weight = 0, 0.0 + joints[j // 4].append(joint) + weights[j // 4].append(weight) + + for i, (js, ws) in enumerate(zip(joints, weights)): + attributes["JOINTS_%d" % i] = js + attributes["WEIGHTS_%d" % i] = ws + + primitives.append( + { + "attributes": attributes, + "mode": 0, # POINTS + "material": 0, + } + ) + + print_console("INFO", "Primitives created: %d" % len(primitives)) + + return primitives + + +def post__gather_colors(results, blender_primitive, _export_settings): + from io_scene_gltf2.io.com import gltf2_io, gltf2_io_constants # pylint: disable=import-error + from io_scene_gltf2.io.exp import gltf2_io_binary_data # pylint: disable=import-error + + attributes = blender_primitive["attributes"] + colors = attributes.get("FAST64_COLOR", None) + if colors is not None: + # Rename other attributes + for attr_name, values in copy.copy(attributes).items(): + if attr_name.startswith("COLOR_"): + num = int(attr_name.lstrip("COLOR_")) + attributes.pop(attr_name) + attributes["COLOR_%d" % num] = values + results["COLOR_0"] = gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData(colors.tobytes()), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(colors), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=True, + sparse=None, + type=gltf2_io_constants.DataType.Vec4, + ) + return results + + +def add_3_2_hooks(): + """3.2 hack for float colors""" + if get_version() == (3, 2, 40): + import io_scene_gltf2.blender.exp.gltf2_blender_gather_primitive_attributes as __gather_colors_owner # pylint: disable=import-error, import-outside-toplevel + import io_scene_gltf2.blender.exp.gltf2_blender_extract as extract_primitives_owner # pylint: disable=import-error, import-outside-toplevel + + extract_primitives_owner.extract_primitives = swap_function( + extract_primitives_owner.extract_primitives, extract_primitives_fast64 + ) + __gather_colors_owner.__gather_colors = suffix_function( + __gather_colors_owner.__gather_colors, post__gather_colors + ) diff --git a/fast64_internal/f3d/glTF/schema/FAST64_materials_f3d.schema.json b/fast64_internal/f3d/glTF/schema/FAST64_materials_f3d.schema.json new file mode 100644 index 000000000..e4b397c2b --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/FAST64_materials_f3d.schema.json @@ -0,0 +1,130 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_materials_f3d glTF Material Extension", + "type": "object", + "description": "Sub-extension to FAST64_materials_n64 that implements F3D properties.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "fog": { + "type": "object", + "description": "The fog color and range", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the fog color and position" + }, + "color": { + "$ref": "color.schema.json" + }, + "range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 65536 + }, + "minItems": 2, + "maxItems": 2, + "default": [ + 985, + 1000 + ] + } + } + }, + "lights": { + "type": "object", + "description": "The lighting data for the material", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the lighting data" + }, + "lights": { + "$ref": "f3d_light.schema.json" + }, + "ambientColor": { + "$ref": "color.schema.json", + "description": "Defaults to the main light's color divided by 4.672" + } + } + }, + "geometryMode": { + "type": "object", + "properties": { + "zBuffer": { + "type": "boolean", + "description": "Enables calculation of Z value for primitives. Disable if not reading or writing Z-Buffer in the blender", + "default": false + }, + "shade": { + "type": "boolean", + "description": "Computes shade coordinates for primitives. Disable if not using lighting, vertex colors or fog", + "default": false + }, + "shadeSmooth": { + "type": "boolean", + "description": "Shades primitive smoothly using interpolation between shade values for each vertex (Gouraud shading)", + "default": false + }, + "cullFront": { + "type": "boolean", + "description": "Cull front faces", + "default": false + }, + "cullBack": { + "type": "boolean", + "description": "Cull back faces", + "default": false + }, + "fog": { + "type": "boolean", + "description": "Turns on/off fog calculation. Fog variable gets stored into shade alpha", + "default": false + }, + "lighting": { + "type": "boolean", + "description": "Enables calculating shade color using lights. Turn off for vertex colors as shade color", + "default": false + }, + "texGen": { + "type": "boolean", + "description": "Generates texture coordinates for reflection mapping based on vertex normals and lookat direction. On a skybox texture, maps the sky to the center of the texture and the ground to a circle inscribed in the border. Requires lighting enabled to use", + "default": false + }, + "texGenLinear": { + "type": "boolean", + "description": "Modifies the texgen mapping; enable with texgen. Use a normal panorama image for the texture, with the sky at the top and the ground at the bottom. Requires lighting enabled to use", + "default": false + }, + "loD": { + "type": "boolean", + "description": "Not implemented in any known microcodes. No effect whether enabled or disabled", + "default": false + } + } + }, + "clipRatio": { + "type": "integer", + "minimum": 1, + "maximum": 32767, + "default": 1 + }, + "extensions": { + "type": "object", + "properties": { + "FAST64_materials_f3dlx": { + "$ref": "FAST64_materials_f3dlx.schema.json" + }, + "FAST64_materials_f3dex3": { + "$ref": "FAST64_materials_f3dex3.schema.json" + } + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/FAST64_materials_f3dex3.schema.json b/fast64_internal/f3d/glTF/schema/FAST64_materials_f3dex3.schema.json new file mode 100644 index 000000000..93237c5d4 --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/FAST64_materials_f3dex3.schema.json @@ -0,0 +1,187 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_materials_f3dex3 glTF Material Extension", + "type": "object", + "description": "Sub-extension to FAST64_materials_f3d that implements F3DEX3 properties", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "celShading": { + "type": "object", + "properties": { + "tintPipeline": { + "type": "string", + "enum": [ + "CC", + "TINT" + ], + "default": "CC" + }, + "cutoutSource": { + "type": "string", + "enum": [ + "TEXEL0", + "TEXEL1", + "ENVIRONMENT" + ], + "default": "ENVIRONMENT" + }, + "levels": { + "type": "array", + "items": { + "$ref": "f3dex3_cel_level.schema.json" + }, + "minItems": 1 + } + } + }, + "ambientOcclusion": { + "type": "object", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the ambient occlusion values" + }, + "ambient": { + "type": "number", + "description": "How much ambient occlusion (vertex alpha) affects ambient light intensity", + "minimum": 0.0, + "maximum": 1.0, + "default": 1.0 + }, + "directional": { + "type": "number", + "description": "How much ambient occlusion (vertex alpha) affects directional light intensity", + "minimum": 0.0, + "maximum": 1.0, + "default": 0.625 + }, + "point": { + "type": "number", + "description": "How much ambient occlusion (vertex alpha) affects point light intensity", + "minimum": 0.0, + "maximum": 1.0, + "default": 0.0 + } + } + }, + "fresnel": { + "type": "object", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the fresnel values" + }, + "low": { + "type": "number", + "description": "Dot product value which gives shade alpha = 0. The dot product ranges from 1 when the normal points directly at the camera, to 0 when it points sideways", + "minimum": -1000.0, + "maximum": 1000.0, + "default": 0.7 + }, + "high": { + "type": "number", + "description": "Dot product value which gives shade alpha = FF. The dot product ranges from 1 when the normal points directly at the camera, to 0 when it points sideways", + "minimum": -1000.0, + "maximum": 1000.0, + "default": 0.4 + } + } + }, + "attributeOffset": { + "type": "object", + "properties": { + "st": { + "type": "object", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the ST offset values" + }, + "value": { + "type": "array", + "description": "Offset applied to ST (UV) coordinates, after texture scale. Units are texels. Usually for UV scrolling", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "number", + "minimum": -1024.0, + "maximum": 1024.0, + "default": 0.0 + }, + "default": [ + 0.0, + 0.0 + ] + } + }, + "z": { + "type": "object", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the Z offset values" + }, + "value": { + "type": "integer", + "description": "Offset applied to Z coordinate. To fix decals, set Z mode to opaque and set Z attr offset to something like -2", + "minimum": -32768, + "maximum": 32767, + "default": -2 + } + } + } + } + } + }, + "geometryMode": { + "type": "object", + "properties": { + "ambientOcclusion": { + "type": "boolean", + "description": "Scales each type light intensity differently with vertex alpha. Bake scene shadows / AO into vertex alpha, not vertex color", + "default": false + }, + "attroffsetZ": { + "type": "boolean", + "description": "Enables offsets to vertex ST values, usually for UV scrolling", + "default": false + }, + "attroffsetST": { + "type": "boolean", + "description": "Enables offset to vertex Z. To fix decals, set the Z mode to opaque and enable this", + "default": false + }, + "packedNormals": { + "type": "boolean", + "description": "Packs vertex normals in unused 16 bits of each vertex, enabling simultaneous vertex colors and lighting", + "default": false + }, + "lightToAlpha": { + "type": "boolean", + "description": "Moves light intensity to shade alpha, used for cel shading and other effects", + "default": false + }, + "specularLighting": { + "type": "boolean", + "description": "Microcode lighting computes specular instead of diffuse component. If using, must set size field of every light in code", + "default": false + }, + "fresnelToColor": { + "type": "boolean", + "description": "Shade color derived from how much each vertex normal faces the camera. For bump mapping", + "default": false + }, + "fresnelToAlpha": { + "type": "boolean", + "description": "Shade alpha derived from how much each vertex normal faces the camera. For water, glass, ghosts, etc., or toon outlines", + "default": false + } + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/FAST64_materials_f3dlx.schema.json b/fast64_internal/f3d/glTF/schema/FAST64_materials_f3dlx.schema.json new file mode 100644 index 000000000..da9da5b7d --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/FAST64_materials_f3dlx.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_materials_f3dlx.schema glTF Material Extension", + "type": "object", + "description": "Sub-extension to FAST64_materials_f3d that implements F3DLX properties", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "geometryMode": { + "type": "object", + "properties": { + "clipping": { + "type": "boolean", + "description": "This mode is enabled in the initial state. When disabled, clipping is not performed", + "default": true + } + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/FAST64_materials_n64.json b/fast64_internal/f3d/glTF/schema/FAST64_materials_n64.json new file mode 100644 index 000000000..9fee37ae4 --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/FAST64_materials_n64.json @@ -0,0 +1,652 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_materials_n64 glTF Material Extension", + "type": "object", + "description": "glTF extension that provides a representation of fast64 basic n64 material data. With extensions for microcodes.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "combiner": { + "type": "object", + "description": "The n64 color combiner inputs", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the combiner inputs" + }, + "cycles": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "description": "Can have one or two cycles depending on cycleType.", + "items": { + "type": "object", + "properties": { + "color": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "prefixItems": [ + { + "type": "string", + "default": "0", + "enum": [ + "COMBINED", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "1", + "NOISE", + "0" + ] + }, + { + "type": "string", + "default": "0", + "enum": [ + "COMBINED", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "CENTER", + "K4", + "0" + ] + }, + { + "type": "string", + "default": "0", + "enum": [ + "COMBINED", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "SCALE", + "COMBINED_ALPHA", + "TEXEL0_ALPHA", + "TEXEL1_ALPHA", + "PRIMITIVE_ALPHA", + "SHADE_ALPHA", + "ENV_ALPHA", + "LOD_FRACTION", + "PRIM_LOD_FRAC", + "K5", + "0" + ] + }, + { + "type": "string", + "default": "0", + "enum": [ + "COMBINED", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "1", + "0" + ] + } + ] + }, + "alpha": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "prefixItems": [ + { + "type": "string", + "default": "0", + "enum": [ + "COMBINED", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "1", + "0" + ] + }, + { + "type": "string", + "default": "0", + "enum": [ + "COMBINED", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "1", + "0" + ] + }, + { + "type": "string", + "default": "0", + "enum": [ + "LOD_FRACTION", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "PRIM_LOD_FRAC", + "0" + ] + }, + { + "type": "string", + "default": "0", + "enum": [ + "COMBINED", + "TEXEL0", + "TEXEL1", + "PRIMITIVE", + "SHADE", + "ENVIRONMENT", + "1", + "0" + ] + } + ] + } + } + } + } + } + }, + "environment": { + "type": "object", + "description": "The environment color, available in the combiner as the color inputs ENVIRONMENT and ENV_ALPHA", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the current environment color" + }, + "color": { + "$ref": "color.schema.json" + } + } + }, + "primitive": { + "type": "object", + "description": "The primitive color values, available in the combiner as the color inputs PRIMITIVE, PRIMITIVE_ALPHA and PRIM_LOD_FRAC", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the current primitive color" + }, + "color": { + "$ref": "color.schema.json" + }, + "minLoDRatio": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "loDFraction": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + } + } + }, + "chromaKey": { + "type": "object", + "description": "The chroma key values, available in the combiner as the color inputs CENTER and SCALE", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the chroma key values and set the h othermode for chroma keying (G_MDSFT_COMBKEY)" + }, + "center": { + "description": "Defines the color intensity at which the key is active.", + "$ref": "solid_color.schema.json" + }, + "scale": { + "type": "array", + "description": "For hard edge keying, set scale to maximum", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 3, + "maxItems": 3, + "default": [ + 0.0, + 0.0, + 0.0 + ] + }, + "width": { + "type": "array", + "description": "Size of half the key window. If width > 255, then keying is disabled for that channel", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 16.0 + }, + "minItems": 3, + "maxItems": 3, + "default": [ + 0.0, + 0.0, + 0.0 + ] + } + } + }, + "yuvConvert": { + "type": "object", + "description": "The yuv to rgb coefficients, available in the combiner as the color inputs K4 and K5", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the yuv to rgb coefficients and set the h othermode for texture conversion (G_TC_FILTCONV)" + }, + "values": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "items": { + "type": "number", + "minimum": -1.0, + "maximum": 1.0 + }, + "default": [ + 0.6863, + -0.1686, + -0.3490, + 0.8706, + 0.4475, + 0.1647 + ] + } + } + }, + "fog": { + "type": "object", + "description": "The fog color and range", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the fog color and position" + }, + "color": { + "$ref": "color.schema.json" + }, + "range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 65536 + }, + "minItems": 2, + "maxItems": 2, + "default": [ + 985, + 1000 + ] + } + } + }, + "blend": { + "type": "object", + "description": "The blend color, used in the blenderer as the input G_BL_CLR_BL", + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should update the current blend color" + }, + "color": { + "$ref": "color.schema.json" + } + } + }, + "otherModes": { + "type": "object", + "properties": { + "alphaDither": { + "type": "string", + "description": "Applies your choice dithering type to output framebuffer alpha. Dithering is used to convert high precision source colors into lower precision framebuffer values", + "default": "DISABLE", + "enum": [ + "PATTERN", + "NOTPATTERN", + "NOISE", + "DISABLE" + ] + }, + "colorDither": { + "type": "string", + "description": "Applies your choice dithering type to output framebuffer color. Dithering is used to convert high precision source colors into lower precision framebuffer values", + "default": "MAGICSQ", + "enum": [ + "MAGICSQ", + "BAYER", + "NOISE", + "DISABLE", + "ENABLE" + ] + }, + "chromaKey": { + "type": "string", + "description": "Turns on/off the chroma key. Chroma key requires a special setup to work properly", + "default": "NONE", + "enum": [ + "NONE", + "KEY" + ] + }, + "textureConvert": { + "type": "string", + "description": "Sets the function of the texture convert unit, to do texture filtering, YUV to RGB conversion, or both", + "default": "CONV", + "enum": [ + "CONV", + "FILTCONV", + "FILT" + ] + }, + "textureFilter": { + "type": "string", + "description": "Applies your choice of filtering to texels", + "default": "POINT", + "enum": [ + "POINT", + "AVERAGE", + "BILERP" + ] + }, + "textureLoD": { + "type": "string", + "description": "Turns on/off the use of LoD on textures. LoD textures change the used tile based on the texel/pixel ratio", + "default": "TILE", + "enum": [ + "TILE", + "LOD" + ] + }, + "textureLUT": { + "type": "string", + "description": "Changes texture look up table (LUT) behavior. This property is auto set by fast64", + "default": "NONE", + "enum": [ + "NONE", + "RGBA16", + "IA16" + ] + }, + "textureDetail": { + "type": "string", + "description": "Changes type of LoD usage. Affects how tiles are selected based on texel magnification. Only works when G_TL_LOD is selected", + "default": "CLAMP", + "enum": [ + "CLAMP", + "SHARPEN", + "DETAIL" + ] + }, + "perspectiveCorrection": { + "type": "string", + "description": "Turns on/off texture perspective correction", + "default": "NONE", + "enum": [ + "NONE", + "PERSP" + ] + }, + "cycleType": { + "type": "string", + "description": "Changes RDP pipeline configuration. For normal textured triangles use one or two cycle mode", + "default": "1CYCLE", + "enum": [ + "1CYCLE", + "2CYCLES", + "COPY", + "FILL" + ] + }, + "pipelineMode": { + "type": "string", + "description": "Changes primitive rasterization timing by adding syncs after tri draws.", + "default": "1PASS", + "enum": [ + "1PRIMITIVE", + "NPRIMITIVE" + ] + }, + "alphaCompare": { + "type": "string", + "enum": [ + "NONE", + "THRESHOLD", + "DITHER" + ], + "default": "NONE" + }, + "zSourceSelection": { + "type": "string", + "enum": [ + "PIXEL", + "PRIM" + ], + "default": "PIXEL" + }, + "renderMode": { + "type": "object", + "properties": { + "presets": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "description": "Can have one or two preset combos depending on cycleType.", + "items": { + "type": "string" + } + }, + "flags": { + "type": "object", + "properties": { + "aa": { + "type": "boolean", + "description": "Enables anti-aliasing to rasterized primitive edges. Uses coverage to determine edges", + "default": false + }, + "zTest": { + "type": "boolean", + "description": "Checks pixel Z value against Z-Buffer to test writing", + "default": false + }, + "zWrite": { + "type": "boolean", + "description": "Updates the Z-Buffer with the most recently written pixel Z value", + "default": false + }, + "colorOnCvg": { + "type": "boolean", + "description": "Only draw on coverage (amount primitive covers target pixel) overflow", + "default": false + }, + "alphaOnCvg": { + "type": "boolean", + "description": "Use coverage (amount primitive covers target pixel) as alpha instead of color combiner alpha", + "default": false + }, + "mulCvgXAlpha": { + "type": "boolean", + "description": "Multiply coverage (amount primitive covers target pixel) with alpha and store result as coverage", + "default": false + }, + "forceBlend": { + "type": "boolean", + "description": "Always uses blending on. Default blending is conditionally only applied during partial coverage. Forcing blending will disable division step of the blender, so B input must be 1-A or there may be rendering issues. Always use this option when Z Buffering is off", + "default": false + }, + "readFB": { + "type": "boolean", + "description": "Enables reading from framebuffer for blending calculations", + "default": false + }, + "cvgDst": { + "type": "string", + "description": "Changes how coverage (amount primitive covers target pixel) gets retrieved/stored", + "enum": [ + "CLAMP", + "WRAP", + "FULL", + "SAVE" + ], + "default": "CLAMP" + }, + "zMode": { + "type": "boolean", + "description": "Changes Z calculation for different types of primitives", + "enum": [ + "OPA", + "INTER", + "XLU", + "DEC" + ], + "default": "OPA" + } + } + }, + "blender": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "description": "Can have one or two cycles depending on cycleType.", + "items": { + "type": "object", + "properties": { + "color": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string", + "enum": [ + "IN", + "MEM", + "BL", + "FOG" + ] + } + }, + "alpha": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string", + "enum": [ + "A_IN", + "A_FOG", + "A_SHADE", + "0" + ] + } + } + } + } + } + } + } + } + }, + "primDepth": { + "properties": { + "z": { + "type": "integer", + "description": "The value to use for z is the screen Z position of the object you are rendering. This is a value ranging from 0x0000 to 0x7fff, where 0x0000 usually corresponds to the near clipping plane and 0x7fff usually corresponds to the far clipping plane. You can use -1 to force Z to be at the far clipping plane.", + "default": 0, + "minimum": -1, + "maximum": 32767 + }, + "dz": { + "type": "integer", + "description": "The dz value should be set to 0. This value is used for antialiasing and objects drawn in decal render mode and must always be a power of 2 (0, 1, 2, 4, 8, ... 0x4000). If you are using decal mode and part of the decaled object is not being rendered correctly, try setting this to powers of 2. Otherwise use 0.", + "default": 0, + "minimum": 0, + "maximum": 16384 + } + } + }, + "mipmapCount": { + "type": "integer", + "minimum": 2, + "maximum": 8, + "default": 2 + }, + "textures": { + "type": "object", + "description": "Dict of texture info objects for each tile, could support up to 8 tiles in the future", + "minItems": 0, + "maxItems": 2, + "additionalProperties": { + "allOf": [ + { + "$ref": "textureInfo.schema.json" + } + ] + } + }, + "textureScale": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "number" + }, + "default": [ + 1, + 1 + ] + }, + "uvBasis": { + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 1 + }, + "large": { + "type": "object", + "description": "Settings for large textures, if property exists large texture mode is being used", + "properties": { + "edges": { + "type": "string", + "description": "Clamp outside image bounds or wrap outside image bounds (more expensive)", + "enum": [ + "CLAMP", + "WRAP" + ], + "default": "CLAMP" + } + } + }, + "extensions": { + "type": "object", + "properties": { + "FAST64_materials_f3d": { + "$ref": "FAST64_materials_f3d.schema.json" + } + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/FAST64_mesh_f3d.schema.json b/fast64_internal/f3d/glTF/schema/FAST64_mesh_f3d.schema.json new file mode 100644 index 000000000..652255cd2 --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/FAST64_mesh_f3d.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_materials_f3d glTF Material Extension", + "type": "object", + "description": "glTF extension that provides a representation of F3d fast64 mesh data. Right now only includes the sub-extension FAST64_mesh_f3d_new that implements post F3DLX properties", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "extensions": { + "type": "object", + "properties": { + "FAST64_mesh_f3d_new": { + "$ref": "FAST64_mesh_f3d_new.schema.json" + } + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/FAST64_mesh_f3d_new.schema.json b/fast64_internal/f3d/glTF/schema/FAST64_mesh_f3d_new.schema.json new file mode 100644 index 000000000..f6d2460fd --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/FAST64_mesh_f3d_new.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_materials_f3dlx.schema glTF Material Extension", + "type": "object", + "description": "Sub-extension to FAST64_mesh_f3d that implements post F3DLX properties", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "use_culling": { + "type": "object", + "properties": { + "clipping": { + "type": "boolean", + "description": "Adds culling vertices for SPCullDisplayList", + "default": true + } + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/FAST64_sampler_n64.schema.json b/fast64_internal/f3d/glTF/schema/FAST64_sampler_n64.schema.json new file mode 100644 index 000000000..d56c7bd4b --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/FAST64_sampler_n64.schema.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "FAST64_sampler_n64 glTF Sampler Extension", + "type": "object", + "description": "glTF extension that provides a representation of fast64 F3D texture samplers.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "set": { + "type": "boolean", + "description": "When toggled the material should actually load the texture source." + }, + "format": { + "type": "object", + "properties": { + "texture": { + "type": "string", + "enum": [ + "I", + "IA", + "CI", + "RGBA", + "YUV" + ], + "description": "The texture format, CI and YUV rely on other modes textureConvert and textureLUT", + "default": "RGBA" + }, + "size": { + "type": "integer", + "minimum": 4, + "maximum": 32, + "default": 16 + }, + "palette": { + "type": "string", + "enum": [ + "IA", + "RGBA" + ], + "default": "RGBA" + } + }, + "required": [ + "texture", + "size" + ] + }, + "fields": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "object", + "properties": { + "clamp": { + "type": "boolean", + "default": true + }, + "mirror": { + "type": "boolean", + "default": false + } + }, + "required": [ + "clamp", + "mirror" + ] + } + }, + "reference": { + "type": "object", + "properties": { + "texture": { + "type": "string", + "default": "0x08000000" + }, + "size": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 2, + "maxItems": 2, + "default": [ + 32, + 32 + ] + }, + "palette": { + "type": "string", + "default": "0x08000000" + }, + "paletteCount": { + "type": "integer", + "minimum": 1, + "default": 16 + } + }, + "required": [ + "texture", + "size" + ] + } + }, + "required": [ + "set", + "format", + "fields" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/color.schema.json b/fast64_internal/f3d/glTF/schema/color.schema.json new file mode 100644 index 000000000..8740afa88 --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/color.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "N64 Color", + "type": "array", + "description": "The color (not including alpha) is linear instead of sRGB so it must be converted for N64.", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 4, + "maxItems": 4, + "default": [ + 1.0, + 1.0, + 1.0, + 1.0 + ] +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/f3d_light.schema.json b/fast64_internal/f3d/glTF/schema/f3d_light.schema.json new file mode 100644 index 000000000..f71759663 --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/f3d_light.schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "F3D Light", + "type": "object", + "description": "glTF extension that provides a representation of F3D light data. Color must be set, direction and positional are both optional but can't be both set.", + "required": [ + "color" + ], + "color": { + "$ref": "color.schema.json" + }, + "direction": { + "type": "array", + "description": "The light's quaternion rotation in the order (x, y, z, w), where w is the scalar.", + "items": { + "type": "number" + }, + "minItems": 4, + "maxItems": 4, + "default": [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + "positional": { + "position": { + "type": "array", + "description": "The light's position in the order (x, y, z).", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3, + "default": [ + 0.0, + 0.0, + 0.0 + ] + }, + "constant_attenuation": { + "type": "integer", + "description": "Must be greater than 0 for the light to be recognized as a point light.", + "minimum": 1, + "maximum": 255, + "default": 1.0 + }, + "linear_attenuation": { + "type": "integer", + "minimum": 0, + "maximum": 255, + "default": 0 + }, + "quadratic_attenuation": { + "type": "number", + "minimum": 0, + "maximum": 255, + "default": 0 + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/f3dex3_cel_level.schema.json b/fast64_internal/f3d/glTF/schema/f3dex3_cel_level.schema.json new file mode 100644 index 000000000..eb7d81490 --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/f3dex3_cel_level.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "F3DEX3 Cel Level", + "type": "array", + "properties": { + "thresholdMode": { + "type": "string", + "description": "This cel level is drawn when the lighting level per-pixel is LIGHTER or DARKER than (>=) the threshold.", + "enum": [ + "LIGHTER", + "DARKER" + ], + "default": "LIGHTER" + }, + "threshold": { + "type": "integer", + "description": "Light level at which the boundary between cel levels occurs. One level is >= this value, the other is < it.", + "minimum": 2, + "maximum": 255, + "default": 128 + }, + "tint": { + "type": "object", + "description": "Tint settings for the cel level.", + "properties": { + "type": { + "type": "string", + "enum": [ + "FIXED", + "SEGMENT", + "LIGHT" + ], + "default": "FIXED" + }, + "level": { + "type": "integer", + "description": "0: original color <=> 255: fully tint color. Applies if tint type is Fixed or Light.", + "minimum": 0, + "maximum": 255, + "default": 50 + }, + "color": { + "$ref": "color.schema.json", + "description": "The fixed color tint to apply. Applies if tint type is Fixed." + }, + "segment": { + "type": "integer", + "description": "Segment number to store tint DL in. Applies if tint type is Segment.", + "minimum": 8, + "maximum": 13, + "default": 8 + }, + "offset": { + "type": "integer", + "description": "Number of instructions (8 bytes) within this DL to jump to. Applies if tint type is Segment.", + "minimum": 0, + "maximum": 1000, + "default": 0 + }, + "light": { + "type": "integer", + "description": "Which light to load RGB color from, counting from the end. 0 = ambient, 1 = last directional / point light, 2 = second-to-last, etc. Applies if tint type is Light.", + "minimum": 0, + "maximum": 9, + "default": 1 + } + }, + "required": [ + "type" + ] + } + }, + "required": [ + "thresholdMode", + "threshold", + "tint" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/glTF/schema/opaque_color.schema.json b/fast64_internal/f3d/glTF/schema/opaque_color.schema.json new file mode 100644 index 000000000..2cc427915 --- /dev/null +++ b/fast64_internal/f3d/glTF/schema/opaque_color.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "title": "Opaque N64 Color", + "type": "array", + "description": "The color is linear instead of sRGB so it must be converted for N64.", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 3, + "maxItems": 3, + "default": [ + 1.0, + 1.0, + 1.0 + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/fast64_internal/f3d/op_largetexture.py b/fast64_internal/f3d/op_largetexture.py index d36ccdd6c..9c3e55c4c 100644 --- a/fast64_internal/f3d/op_largetexture.py +++ b/fast64_internal/f3d/op_largetexture.py @@ -292,7 +292,7 @@ class CreateLargeTextureMesh(bpy.types.Operator): bl_options = {"REGISTER", "UNDO", "PRESET"} def execute(self, context): - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() prop = context.scene.opLargeTextureProperty assert prop.mat is not None name = prop.mat.name + "Mesh" diff --git a/fast64_internal/game_data.py b/fast64_internal/game_data.py new file mode 100644 index 000000000..8777b2886 --- /dev/null +++ b/fast64_internal/game_data.py @@ -0,0 +1,36 @@ +import bpy + +from typing import Optional + + +class GameData: + def __init__(self, game_editor_mode: Optional[str] = None): + from .data import Z64_Data + + self.z64 = Z64_Data("OOT") + + if game_editor_mode is not None: + self.update(game_editor_mode) + + def update(self, game_editor_mode: str): + from .z64.utility import getObjectList + + if game_editor_mode is not None and game_editor_mode in {"OOT", "MM"}: + self.z64.update(None, game_editor_mode, True) + + # ensure `currentCutsceneIndex` is set to a correct value + if bpy.context.scene.gameEditorMode in {"OOT", "MM"}: + for scene_obj in bpy.data.objects: + scene_obj.ootAlternateSceneHeaders.currentCutsceneIndex = game_data.z64.cs_index_start + + if scene_obj.type == "EMPTY" and scene_obj.ootEmptyType == "Scene": + room_obj_list = getObjectList(scene_obj.children_recursive, "EMPTY", "Room") + + for room_obj in room_obj_list: + room_obj.ootAlternateRoomHeaders.currentCutsceneIndex = game_data.z64.cs_index_start + + if game_editor_mode in {"OOT", "MM"} and game_editor_mode != self.z64.game: + raise ValueError(f"ERROR: Z64 game mismatch: {game_editor_mode}, {game_data.z64.game}") + + +game_data = GameData() diff --git a/fast64_internal/gltf_extension.py b/fast64_internal/gltf_extension.py new file mode 100644 index 000000000..098a8b875 --- /dev/null +++ b/fast64_internal/gltf_extension.py @@ -0,0 +1,233 @@ +import traceback + +import bpy +from bpy.types import PropertyGroup, UILayout, Panel, Context +from bpy.props import BoolProperty, PointerProperty + +from .utility import multilineLabel, prop_group_to_json, json_to_prop_group +from .gltf_utility import get_gltf_settings, prefix_function, update_gltf2_addon +from .f3d.glTF.f3d_gltf import ( + F3DGlTFSettings, + F3DGlTFPanel, + F3DExtensions, + pre_gather_mesh_hook, + modify_f3d_nodes_for_export, + add_3_2_hooks, + get_version, +) + +# Original implementation from github.com/Mr-Wiseguy/gltf64-blender + +# Changes made from the original glTF64: +# Property names (keys) now all use the glTF standard naming, camelCase. +# Extension names all follow the glTF2 naming convention, PREFIX_scope_feature. +# Full fast64 v6 material support. +# Extendability improvements. +# Doesn´t use world defaults, as those should be left to the repo to handle. +# Hacks for broken versions +# Importing +# Better and more extensive errors + + +def glTF2_pre_export_callback(_gltf): + update_gltf2_addon() + add_3_2_hooks() + modify_f3d_nodes_for_export(False) + + if get_version() >= (4, 3, 13): + import io_scene_gltf2.blender.exp.mesh as gather_mesh_owner # pylint: disable=import-error, import-outside-toplevel + else: + import io_scene_gltf2.blender.exp.gltf2_blender_gather_mesh as gather_mesh_owner # pylint: disable=import-error, import-outside-toplevel + + gather_mesh_owner.gather_mesh = prefix_function(gather_mesh_owner.gather_mesh, pre_gather_mesh_hook) + del gather_mesh_owner + + +def glTF2_post_export_callback(_gltf): + modify_f3d_nodes_for_export(True) + + +def error_popup_handler(simple_error: str, full_error: str): + def handler(self, _context): + col = self.layout.column() + multilineLabel(col, simple_error, icon="INFO") + col.separator() + multilineLabel(col, full_error) + + return handler + + +class GlTF2Extension: + def call_hooks(self, hook: str, message_template: str, *args): + for extension in self.sub_extensions: + try: + if hasattr(extension, hook): + getattr(extension, hook)(*args) + except Exception as exc: + wm = bpy.context.window_manager + message = f"Error in {message_template.format(self=self, args=args)}" + error_location = f"{extension.__class__.__name__}.{hook}" + full_error = f"{error_location}:\n{traceback.format_exc().rstrip()}" + + wm.popup_menu( + error_popup_handler(str(exc), full_error), + title=message, + icon="ERROR", + ) + print(full_error) + # TODO: Force glTF exports and imports to fail somehow? + + def __init__(self): + from io_scene_gltf2.io.com.gltf2_io_extensions import Extension # pylint: disable=import-error + + self.Extension = Extension + + self.settings = bpy.context.scene.fast64.settings.glTF + self.verbose = self.settings.verbose + self.sub_extensions = [] + if self.settings.f3d.use: + self.sub_extensions.append(F3DExtensions(self)) + + +class glTF2ExportUserExtension(GlTF2Extension): + importing = False + + def gather_node_hook(self, gltf2_node, blender_object, export_settings): + self.call_hooks( + "gather_node_hook", + 'Object "{args[1].name}"', + gltf2_node, + blender_object, + export_settings, + ) + + def gather_mesh_hook(self, gltf2_mesh, blender_mesh, blender_object, vertex_groups, modifiers, *last_args): + materials, export_settings = last_args[-2:] # 3.2 + self.call_hooks( + "gather_mesh_hook", + 'Mesh "{args[1].name}"', + gltf2_mesh, + blender_mesh, + blender_object, + vertex_groups, + modifiers, + materials, + export_settings, + ) + + def gather_material_hook(self, gltf2_material, blender_material, export_settings): + self.call_hooks( + "gather_material_hook", + 'Material "{args[1].name}"', + gltf2_material, + blender_material, + export_settings, + ) + + def gather_gltf_extensions_hook(self, _gltf, _export_settings): + modify_f3d_nodes_for_export(True) + + +class glTF2ImportUserExtension(GlTF2Extension): + importing = True + + def gather_import_material_after_hook(self, gltf_material, vertex_color, blender_mat, gltf): + self.call_hooks( + "gather_import_material_after_hook", + 'Material "{args[2].name}""', + gltf_material, + vertex_color, + blender_mat, + gltf, + ) + + def gather_import_node_after_hook(self, vnode, gltf_node, blender_object, gltf): + self.call_hooks( + "gather_import_node_after_hook", + 'Object "{args[2].name}"', + vnode, + gltf_node, + blender_object, + gltf, + ) + + def gather_import_mesh_after_hook(self, gltf_mesh, blender_mesh, gltf): + self.call_hooks( + "gather_import_mesh_after_hook", + 'Mesh "{args[1].name}"', + gltf_mesh, + blender_mesh, + gltf, + ) + + +class Fast64GlTFSettings(PropertyGroup): + verbose: BoolProperty( + name="Verbose", + description="Print all appended extension data, useful for troubleshooting", + ) + f3d: PointerProperty(type=F3DGlTFSettings) + game: BoolProperty(default=True, name="Export current game mode") + + def to_dict(self): + return prop_group_to_json(self) + + def from_dict(self, data: dict): + json_to_prop_group(self, data) + + def draw_props(self, scene, layout: UILayout): + col = layout.column() + multilineLabel( + col, + "TIP: Create a repo settings file in the\n" + "fast64 tab to save these settings for your\n" + "repo.", # pylint: disable=line-too-long + icon="INFO", + ) + col.separator() + + col.prop(self, "verbose") + + game_mode = scene.gameEditorMode + if game_mode == "Homebrew": + multilineLabel( + col.box(), + "Homebrew mode does not\nimplement any extensions", + icon="INFO", + ) + elif not getattr(self, game_mode.lower(), None): + multilineLabel( + col.box(), + f"Current game mode ({game_mode})\nnot implemented", + icon="INFO", + ) + + +class Fast64GlTFPanel(Panel): + bl_idname = "GLTF_PT_Fast64" + bl_space_type = "FILE_BROWSER" + bl_region_type = "TOOL_PROPS" + bl_label = "Fast64" + bl_parent_id = "FILE_PT_operator" + + @classmethod + def poll(cls, context: Context): + operator_idname = context.space_data.active_operator.bl_idname + return operator_idname in ["EXPORT_SCENE_OT_gltf", "IMPORT_SCENE_OT_gltf"] + + def draw(self, context: Context): + self.layout.use_property_decorate = False # No animation. + get_gltf_settings(context).draw_props(context.scene, self.layout) + + +classes = (F3DGlTFSettings, Fast64GlTFPanel, Fast64GlTFSettings, F3DGlTFPanel) + + +def gltf_extension_register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def gltf_extension_unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/fast64_internal/gltf_utility.py b/fast64_internal/gltf_utility.py new file mode 100644 index 000000000..e8bf56296 --- /dev/null +++ b/fast64_internal/gltf_utility.py @@ -0,0 +1,204 @@ +from pprint import pprint +from typing import Callable +import functools + +import addon_utils +import bpy +from bpy.types import Image + + +def find_glTF2_addon(): + for mod in addon_utils.modules(): # pylint: disable=not-an-iterable + if mod.__name__ == "io_scene_gltf2": + return mod + raise ValueError("glTF2 addon not found") + + +CUR_GLTF2_ADDON = None + + +def update_gltf2_addon(): + global CUR_GLTF2_ADDON + CUR_GLTF2_ADDON = find_glTF2_addon() + + +def get_version() -> tuple[int, int, int]: + global CUR_GLTF2_ADDON + if CUR_GLTF2_ADDON is None: + CUR_GLTF2_ADDON = find_glTF2_addon() + return CUR_GLTF2_ADDON.bl_info.get("version", (-1, -1, -1)) + + +def is_blender_image_a_webp(image: Image) -> bool: + if get_version() >= (4, 3, 13): + from io_scene_gltf2.blender.exp.material.image import ( # type: ignore # pylint: disable=import-error, import-outside-toplevel + __is_blender_image_a_webp, + ) + elif get_version() >= (3, 6, 5): + from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_image import ( # type: ignore # pylint: disable=import-error, import-outside-toplevel + __is_blender_image_a_webp, + ) + + return __is_blender_image_a_webp(image) + return False + + +def __get_mime_type_of_image(name: str, export_settings: dict): + image = bpy.data.images[name] + if image.channels == 4: # Has alpha channel, doesn´t actually check for transparency + if is_blender_image_a_webp(image): + return "image/webp" + return "image/png" + + if export_settings["gltf_image_format"] == "AUTO": + if get_version() >= (4, 3, 13): + from io_scene_gltf2.blender.exp.material.image import ( # pylint: disable=import-error, import-outside-toplevel # type: ignore + __is_blender_image_a_jpeg, + ) + elif get_version() >= (3, 6, 0): + from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_image import ( # pylint: disable=import-error, import-outside-toplevel # type: ignore + __is_blender_image_a_jpeg, + ) + else: + from io_scene_gltf2.blender.exp.gltf2_blender_gather_image import ( # pylint: disable=import-error, import-outside-toplevel # type: ignore + __is_blender_image_a_jpeg, + ) + if __is_blender_image_a_jpeg(image): + return "image/jpeg" + elif is_blender_image_a_webp(image): + return "image/webp" + return "image/png" + + elif export_settings["gltf_image_format"] == "JPEG": + return "image/jpeg" + + +def get_gltf_image_from_blender_image(blender_image_name: str, export_settings: dict): + if get_version() >= (4, 3, 13): + from io_scene_gltf2.blender.exp.material.encode_image import ( # type: ignore # pylint: disable=import-error, import-outside-toplevel + ExportImage, + ) + from io_scene_gltf2.blender.exp.material.image import ( # pylint: disable=import-error, import-outside-toplevel # type: ignore + __gather_name, + __make_image, + __gather_uri, + __gather_buffer_view, + ) + elif get_version() >= (3, 6, 0): + from io_scene_gltf2.blender.exp.material.extensions.gltf2_blender_image import ( # type: ignore # pylint: disable=import-error, import-outside-toplevel + ExportImage, + ) + from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_image import ( # pylint: disable=import-error, import-outside-toplevel # type: ignore + __gather_name, + __make_image, + __gather_uri, + __gather_buffer_view, + ) + else: + from io_scene_gltf2.blender.exp.gltf2_blender_image import ExportImage # type: ignore # pylint: disable=import-error, import-outside-toplevel + from io_scene_gltf2.blender.exp.gltf2_blender_gather_image import ( # pylint: disable=import-error, import-outside-toplevel # type: ignore + __gather_name, + __make_image, + __gather_uri, + __gather_buffer_view, + ) + image_data = ExportImage.from_blender_image(bpy.data.images[blender_image_name]) + + if bpy.app.version > (4, 1, 1): + name = __gather_name(image_data, None, export_settings) + else: + name = __gather_name(image_data, export_settings) + mime_type = __get_mime_type_of_image(blender_image_name, export_settings) + + uri = __gather_uri(image_data, mime_type, name, export_settings) + buffer_view = __gather_buffer_view(image_data, mime_type, name, export_settings) + + if get_version() >= (3, 3, 0): + buffer_view, _factor_buffer_view = buffer_view + uri, _factor_uri = uri + + image = __make_image(buffer_view, None, None, mime_type, name, uri, export_settings) + return image + + +class GlTF2SubExtension: + required: bool = False + + def post_init(self): + pass + + def __init__(self, extension): + self.extension = extension + self.post_init() + + def print_verbose(self, content): + if self.extension.verbose: + pprint(content) + + def append_extension(self, gltf_prop, name: str, data: dict | None = None, required=False, skip_if_empty=True): + if skip_if_empty and not data and data is not None: # If none, assume it shouldn´t skip + return + self.print_verbose(f"Appending {name} extension") + if data: + self.print_verbose(data) + if gltf_prop.extensions is None: + gltf_prop.extensions = {} + gltf_prop.extensions[name] = self.extension.Extension( + name=name, + extension=data if data else {}, + required=required if required else self.required, + ) + return gltf_prop.extensions[name] + + def get_extension(self, gltf_prop, name: str): + if gltf_prop.extensions is None: + return None + data = gltf_prop.extensions.get(name, None) + if data and any(data): + self.print_verbose(data) + return data + + +def get_gltf_settings(context): + return context.scene.fast64.settings.glTF + + +def is_import_context(context): + return context.space_data.active_operator.bl_idname == "IMPORT_SCENE_OT_gltf" + + +def prefix_function(original: Callable, prefix: Callable): + original = getattr(original, "fast64_og_func", original) + + @functools.wraps(original) + def run(*args, **kwargs): + prefix(*args, **kwargs) + return original(*args, **kwargs) + + setattr(run, "fast64_og_func", original) + return run + + +def suffix_function(original: Callable, suffix: Callable): + """Passes in result as the first arg""" + original = getattr(original, "fast64_og_func", original) + + @functools.wraps(original) + def run(*args, **kwargs): + results = original(*args, **kwargs) + return suffix(results, *args, **kwargs) + + setattr(run, "fast64_og_func", original) + return run + + +def swap_function(original: Callable, new: Callable): + """Passes in the original function as the first arg""" + original = getattr(original, "fast64_og_func", original) + + @functools.wraps(original) + def run(*args, **kwargs): + return new(original, *args, **kwargs) + + setattr(run, "fast64_og_func", original) + return run diff --git a/fast64_internal/mm/data/xml/ActorList.xml b/fast64_internal/mm/data/xml/ActorList.xml deleted file mode 100644 index 11e286839..000000000 --- a/fast64_internal/mm/data/xml/ActorList.xml +++ /dev/null @@ -1,928 +0,0 @@ - - - - - - - - - - - - Large Orange Flame - Large Orange Flame - Large Blue Flame - Large Green Flame - Small Orange Flame - Large Orange Flame - Large Green Flame - Large Blue Flame - Large Magenta Flame - Large Pale Orange Flame - Large Pale Yellow Flame - Large Pale Green Flame - Large Pale Pink Flame - Large Pale Purple Flame - Large Pale Indigo Flame - Large Pale Blue Flame - - - - - - - - - - Golden - Golden - Appears - Clear Flag - Boss Key Chest - Golden - Falls - Switch Flag - Golden - Invisible - Wooden - Wooden - Invisible - Wooden - Clear Flag - Wooden - Falls - Switch Flag - Crash - Crash - Golden - Appears - Switch Flag - - - - - - - - - - - - - - - Regular - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - Invisible - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/fast64_internal/oot/actor/operators.py b/fast64_internal/oot/actor/operators.py deleted file mode 100644 index e1702a8af..000000000 --- a/fast64_internal/oot/actor/operators.py +++ /dev/null @@ -1,49 +0,0 @@ -import bpy -from bpy.types import Operator -from bpy.props import EnumProperty, StringProperty -from bpy.utils import register_class, unregister_class -from ...utility import PluginError -from ..oot_constants import ootData - - -class OOT_SearchActorIDEnumOperator(Operator): - bl_idname = "object.oot_search_actor_id_enum_operator" - bl_label = "Select Actor ID" - bl_property = "actorID" - bl_options = {"REGISTER", "UNDO"} - - actorID: EnumProperty(items=ootData.actorData.ootEnumActorID, default="ACTOR_PLAYER") - actorUser: StringProperty(default="Actor") - objName: StringProperty() - - def execute(self, context): - obj = bpy.data.objects[self.objName] - if self.actorUser == "Transition Actor": - obj.ootTransitionActorProperty.actor.actorID = self.actorID - elif self.actorUser == "Actor": - obj.ootActorProperty.actorID = self.actorID - elif self.actorUser == "Entrance": - obj.ootEntranceProperty.actor.actorID = self.actorID - else: - raise PluginError("Invalid actor user for search: " + str(self.actorUser)) - - context.region.tag_redraw() - self.report({"INFO"}, "Selected: " + self.actorID) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.invoke_search_popup(self) - return {"RUNNING_MODAL"} - - -classes = (OOT_SearchActorIDEnumOperator,) - - -def actor_ops_register(): - for cls in classes: - register_class(cls) - - -def actor_ops_unregister(): - for cls in reversed(classes): - unregister_class(cls) diff --git a/fast64_internal/oot/actor/properties.py b/fast64_internal/oot/actor/properties.py deleted file mode 100644 index 50f0a7054..000000000 --- a/fast64_internal/oot/actor/properties.py +++ /dev/null @@ -1,266 +0,0 @@ -from bpy.types import Object, PropertyGroup, UILayout -from bpy.utils import register_class, unregister_class -from bpy.props import EnumProperty, StringProperty, IntProperty, BoolProperty, CollectionProperty, PointerProperty -from ...utility import prop_split, label_split -from ..oot_constants import ootData, ootEnumCamTransition -from ..oot_upgrade import upgradeActors -from ..scene.properties import OOTAlternateSceneHeaderProperty -from ..room.properties import OOTAlternateRoomHeaderProperty -from .operators import OOT_SearchActorIDEnumOperator - -from ..oot_utility import ( - getRoomObj, - getEnumName, - drawAddButton, - drawCollectionOps, - drawEnumWithCustom, -) - -ootEnumSceneSetupPreset = [ - ("Custom", "Custom", "Custom"), - ("All Scene Setups", "All Scene Setups", "All Scene Setups"), - ("All Non-Cutscene Scene Setups", "All Non-Cutscene Scene Setups", "All Non-Cutscene Scene Setups"), -] - - -class OOTActorHeaderItemProperty(PropertyGroup): - headerIndex: IntProperty(name="Scene Setup", min=4, default=4) - expandTab: BoolProperty(name="Expand Tab") - - def draw_props( - self, - layout: UILayout, - propUser: str, - index: int, - altProp: OOTAlternateSceneHeaderProperty | OOTAlternateRoomHeaderProperty, - objName: str, - ): - box = layout.column() - row = box.row() - row.prop(self, "headerIndex", text="") - drawCollectionOps(row.row(align=True), index, propUser, None, objName, compact=True) - if altProp is not None and self.headerIndex >= len(altProp.cutsceneHeaders) + 4: - box.label(text="Above header does not exist.", icon="QUESTION") - - -class OOTActorHeaderProperty(PropertyGroup): - sceneSetupPreset: EnumProperty(name="Scene Setup Preset", items=ootEnumSceneSetupPreset, default="All Scene Setups") - childDayHeader: BoolProperty(name="Child Day Header", default=True) - childNightHeader: BoolProperty(name="Child Night Header", default=True) - adultDayHeader: BoolProperty(name="Adult Day Header", default=True) - adultNightHeader: BoolProperty(name="Adult Night Header", default=True) - cutsceneHeaders: CollectionProperty(type=OOTActorHeaderItemProperty) - - def checkHeader(self, index: int) -> bool: - if index == 0: - return self.childDayHeader - elif index == 1: - return self.childNightHeader - elif index == 2: - return self.adultDayHeader - elif index == 3: - return self.adultNightHeader - else: - return index in [value.headerIndex for value in self.cutsceneHeaders] - - def draw_props( - self, - layout: UILayout, - propUser: str, - altProp: OOTAlternateSceneHeaderProperty | OOTAlternateRoomHeaderProperty, - objName: str, - ): - headerSetup = layout.column() - # headerSetup.box().label(text = "Alternate Headers") - prop_split(headerSetup, self, "sceneSetupPreset", "Scene Setup Preset") - if self.sceneSetupPreset == "Custom": - headerSetupBox = headerSetup.column() - headerSetupBox.prop(self, "childDayHeader", text="Child Day") - prevHeaderName = "childDayHeader" - childNightRow = headerSetupBox.row() - if altProp is None or altProp.childNightHeader.usePreviousHeader: - # Draw previous header checkbox (so get previous state), but labeled - # as current one and grayed out - childNightRow.prop(self, prevHeaderName, text="Child Night") - childNightRow.enabled = False - else: - childNightRow.prop(self, "childNightHeader", text="Child Night") - prevHeaderName = "childNightHeader" - adultDayRow = headerSetupBox.row() - if altProp is None or altProp.adultDayHeader.usePreviousHeader: - adultDayRow.prop(self, prevHeaderName, text="Adult Day") - adultDayRow.enabled = False - else: - adultDayRow.prop(self, "adultDayHeader", text="Adult Day") - prevHeaderName = "adultDayHeader" - adultNightRow = headerSetupBox.row() - if altProp is None or altProp.adultNightHeader.usePreviousHeader: - adultNightRow.prop(self, prevHeaderName, text="Adult Night") - adultNightRow.enabled = False - else: - adultNightRow.prop(self, "adultNightHeader", text="Adult Night") - - headerSetupBox.row().label(text="Cutscene headers to include this actor in:") - for i in range(len(self.cutsceneHeaders)): - headerItemProps: OOTActorHeaderItemProperty = self.cutsceneHeaders[i] - headerItemProps.draw_props(headerSetup, propUser, i, altProp, objName) - drawAddButton(headerSetup, len(self.cutsceneHeaders), propUser, None, objName) - - -class OOTActorProperty(PropertyGroup): - actorID: EnumProperty(name="Actor", items=ootData.actorData.ootEnumActorID, default="ACTOR_PLAYER") - actorIDCustom: StringProperty(name="Actor ID", default="ACTOR_PLAYER") - actorParam: StringProperty(name="Actor Parameter", default="0x0000") - rotOverride: BoolProperty(name="Override Rotation", default=False) - rotOverrideX: StringProperty(name="Rot X", default="0") - rotOverrideY: StringProperty(name="Rot Y", default="0") - rotOverrideZ: StringProperty(name="Rot Z", default="0") - headerSettings: PointerProperty(type=OOTActorHeaderProperty) - - @staticmethod - def upgrade_object(obj: Object): - print(f"Processing '{obj.name}'...") - upgradeActors(obj) - - def draw_props(self, layout: UILayout, altRoomProp: OOTAlternateRoomHeaderProperty, objName: str): - # prop_split(layout, actorProp, 'actorID', 'Actor') - actorIDBox = layout.column() - # actorIDBox.box().label(text = "Settings") - searchOp = actorIDBox.operator(OOT_SearchActorIDEnumOperator.bl_idname, icon="VIEWZOOM") - searchOp.actorUser = "Actor" - searchOp.objName = objName - - split = actorIDBox.split(factor=0.5) - - if self.actorID == "None": - actorIDBox.box().label(text="This Actor was deleted from the XML file.") - return - - split.label(text="Actor ID") - split.label(text=getEnumName(ootData.actorData.ootEnumActorID, self.actorID)) - - if self.actorID == "Custom": - # actorIDBox.prop(actorProp, 'actorIDCustom', text = 'Actor ID') - prop_split(actorIDBox, self, "actorIDCustom", "") - - # layout.box().label(text = 'Actor IDs defined in include/z64actors.h.') - prop_split(actorIDBox, self, "actorParam", "Actor Parameter") - - actorIDBox.prop(self, "rotOverride", text="Override Rotation (ignore Blender rot)") - if self.rotOverride: - prop_split(actorIDBox, self, "rotOverrideX", "Rot X") - prop_split(actorIDBox, self, "rotOverrideY", "Rot Y") - prop_split(actorIDBox, self, "rotOverrideZ", "Rot Z") - - headerProp: OOTActorHeaderProperty = self.headerSettings - headerProp.draw_props(actorIDBox, "Actor", altRoomProp, objName) - - -class OOTTransitionActorProperty(PropertyGroup): - fromRoom: PointerProperty(type=Object, poll=lambda self, object: self.isRoomEmptyObject(object)) - toRoom: PointerProperty(type=Object, poll=lambda self, object: self.isRoomEmptyObject(object)) - cameraTransitionFront: EnumProperty(items=ootEnumCamTransition, default="0x00") - cameraTransitionFrontCustom: StringProperty(default="0x00") - cameraTransitionBack: EnumProperty(items=ootEnumCamTransition, default="0x00") - cameraTransitionBackCustom: StringProperty(default="0x00") - isRoomTransition: BoolProperty(name="Is Room Transition", default=True) - - actor: PointerProperty(type=OOTActorProperty) - - def isRoomEmptyObject(self, obj: Object): - return obj.type == "EMPTY" and obj.ootEmptyType == "Room" - - def draw_props( - self, layout: UILayout, altSceneProp: OOTAlternateSceneHeaderProperty, roomObj: Object, objName: str - ): - actorIDBox = layout.column() - searchOp = actorIDBox.operator(OOT_SearchActorIDEnumOperator.bl_idname, icon="VIEWZOOM") - searchOp.actorUser = "Transition Actor" - searchOp.objName = objName - - split = actorIDBox.split(factor=0.5) - split.label(text="Actor ID") - split.label(text=getEnumName(ootData.actorData.ootEnumActorID, self.actor.actorID)) - - if self.actor.actorID == "Custom": - prop_split(actorIDBox, self.actor, "actorIDCustom", "") - - # layout.box().label(text = 'Actor IDs defined in include/z64actors.h.') - prop_split(actorIDBox, self.actor, "actorParam", "Actor Parameter") - - if roomObj is None: - actorIDBox.label(text="This must be part of a Room empty's hierarchy.", icon="OUTLINER") - else: - actorIDBox.prop(self, "isRoomTransition") - if self.isRoomTransition: - prop_split(actorIDBox, self, "fromRoom", "Room To Transition From") - prop_split(actorIDBox, self, "toRoom", "Room To Transition To") - if self.fromRoom == self.toRoom: - actorIDBox.label(text="Warning: You selected the same room!", icon="ERROR") - actorIDBox.label(text='Y+ side of door faces toward the "from" room.', icon="ORIENTATION_NORMAL") - drawEnumWithCustom(actorIDBox, self, "cameraTransitionFront", "Camera Transition Front", "") - drawEnumWithCustom(actorIDBox, self, "cameraTransitionBack", "Camera Transition Back", "") - - headerProps: OOTActorHeaderProperty = self.actor.headerSettings - headerProps.draw_props(actorIDBox, "Transition Actor", altSceneProp, objName) - - -class OOTEntranceProperty(PropertyGroup): - # This is also used in entrance list. - spawnIndex: IntProperty(min=0) - customActor: BoolProperty(name="Use Custom Actor") - actor: PointerProperty(type=OOTActorProperty) - - tiedRoom: PointerProperty( - type=Object, - poll=lambda self, object: self.isRoomEmptyObject(object), - description="Used to set the room index", - ) - - def isRoomEmptyObject(self, obj: Object): - return obj.type == "EMPTY" and obj.ootEmptyType == "Room" - - def draw_props(self, layout: UILayout, obj: Object, altSceneProp: OOTAlternateSceneHeaderProperty, objName: str): - box = layout.column() - # box.box().label(text = "Properties") - roomObj = getRoomObj(obj) - if roomObj is None: - box.label(text="This must be part of a Room empty's hierarchy.", icon="OUTLINER") - - entranceProp = obj.ootEntranceProperty - prop_split(box, entranceProp, "tiedRoom", "Room") - prop_split(box, entranceProp, "spawnIndex", "Spawn Index") - prop_split(box, entranceProp.actor, "actorParam", "Actor Param") - box.prop(entranceProp, "customActor") - if entranceProp.customActor: - prop_split(box, entranceProp.actor, "actorIDCustom", "Actor ID Custom") - - headerProps: OOTActorHeaderProperty = entranceProp.actor.headerSettings - headerProps.draw_props(box, "Entrance", altSceneProp, objName) - - -classes = ( - OOTActorHeaderItemProperty, - OOTActorHeaderProperty, - OOTActorProperty, - OOTTransitionActorProperty, - OOTEntranceProperty, -) - - -def actor_props_register(): - for cls in classes: - register_class(cls) - - Object.ootActorProperty = PointerProperty(type=OOTActorProperty) - Object.ootTransitionActorProperty = PointerProperty(type=OOTTransitionActorProperty) - Object.ootEntranceProperty = PointerProperty(type=OOTEntranceProperty) - - -def actor_props_unregister(): - del Object.ootActorProperty - del Object.ootTransitionActorProperty - del Object.ootEntranceProperty - - for cls in reversed(classes): - unregister_class(cls) diff --git a/fast64_internal/oot/collision/constants.py b/fast64_internal/oot/collision/constants.py deleted file mode 100644 index 031125a54..000000000 --- a/fast64_internal/oot/collision/constants.py +++ /dev/null @@ -1,206 +0,0 @@ -ootEnumConveyer = [ - ("None", "None", "None"), - ("Land", "Land", "Land"), - ("Water", "Water", "Water"), -] - -ootEnumFloorSetting = [ - ("Custom", "Custom", "Custom"), - ("0x00", "Default", "Default"), - ("0x05", "Void (Small)", "Void (Small)"), - ("0x06", "Grab Wall", "Grab Wall"), - ("0x08", "Stop Air Momentum", "Stop Air Momentum"), - ("0x09", "Fall Instead Of Jumping", "Fall Instead Of Jumping"), - ("0x0B", "Dive", "Dive"), - ("0x0C", "Void (Large)", "Void (Large)"), -] - -ootEnumWallSetting = [ - ("Custom", "Custom", "Custom"), - ("0x00", "None", "None"), - ("0x01", "No Ledge Grab", "No Ledge Grab"), - ("0x02", "Ladder", "Ladder"), - ("0x03", "Ladder Top", "Ladder Top"), - ("0x04", "Vines", "Vines"), - ("0x05", "Crawl Space", "Crawl Space"), - ("0x06", "Crawl Space 2", "Crawl Space 2"), - ("0x07", "Push Block", "Push Block"), -] - -ootEnumFloorProperty = [ - ("Custom", "Custom", "Custom"), - ("0x00", "None", "None"), - ("0x01", "Haunted Wasteland Camera", "Haunted Wasteland Camera"), - ("0x02", "Hurt Floor (Spikes)", "Hurt Floor (Spikes)"), - ("0x03", "Hurt Floor (Lava)", "Hurt Floor (Lava)"), - ("0x04", "Shallow Sand", "Shallow Sand"), - ("0x05", "Slippery", "Slippery"), - ("0x06", "No Fall Damage", "No Fall Damage"), - ("0x07", "Quicksand Crossing (Epona Uncrossable)", "Quicksand Crossing (Epona Uncrossable)"), - ("0x08", "Jabu Jabu's Belly", "Jabu Jabu's Belly"), - ("0x09", "Void", "Void"), - ("0x0A", "Link Looks Up", "Link Looks Up"), - ("0x0B", "Quicksand Crossing (Epona Crossable)", "Quicksand Crossing (Epona Crossable)"), -] - -ootEnumCollisionTerrain = [ - ("Custom", "Custom", "Custom"), - ("0x00", "Walkable", "Walkable"), - ("0x01", "Steep", "Steep"), - ("0x02", "Walkable (Preserves Exit Flags)", "Walkable (Preserves Exit Flags)"), - ("0x03", "Walkable (?)", "Walkable (?)"), -] - -ootEnumCollisionSound = [ - ("Custom", "Custom", "Custom"), - ("0x00", "Dirt", "Dirt (aka Earth)"), - ("0x01", "Sand", "Sand"), - ("0x02", "Stone", "Stone"), - ("0x03", "Jabu", "Jabu-Jabu flesh (aka Wet Stone)"), - ("0x04", "Shallow Water", "Shallow Water"), - ("0x05", "Deep Water", "Deep Water"), - ("0x06", "Tall Grass", "Tall Grass"), - ("0x07", "Lava", "Lava (aka Goo)"), - ("0x08", "Grass", "Grass (aka Earth 2)"), - ("0x09", "Bridge", "Bridge (aka Wooden Plank)"), - ("0x0A", "Wood", "Wood (aka Packed Earth)"), - ("0x0B", "Soft Dirt", "Soft Dirt (aka Earth 3)"), - ("0x0C", "Ice", "Ice (aka Ceramic)"), - ("0x0D", "Carpet", "Carpet (aka Loose Earth)"), -] - -ootEnumConveyorSpeed = [ - ("Custom", "Custom", "Custom"), - ("0x00", "None", "None"), - ("0x01", "Slow", "Slow"), - ("0x02", "Medium", "Medium"), - ("0x03", "Fast", "Fast"), -] - -ootEnumCameraCrawlspaceSType = [ - ("Custom", "Custom", "Custom"), - ("CAM_SET_CRAWLSPACE", "Crawlspace", "Crawlspace"), -] - -ootEnumCameraSType = [ - ("Custom", "Custom", "Custom"), - ("CAM_SET_NONE", "None", "None"), - ("CAM_SET_NORMAL0", "Normal0", "Normal0"), - ("CAM_SET_NORMAL1", "Normal1", "Normal1"), - ("CAM_SET_DUNGEON0", "Dungeon0", "Dungeon0"), - ("CAM_SET_DUNGEON1", "Dungeon1", "Dungeon1"), - ("CAM_SET_NORMAL3", "Normal3", "Normal3"), - ("CAM_SET_HORSE0", "Horse", "Horse"), - ("CAM_SET_BOSS_GOMA", "Boss_gohma", "Boss_gohma"), - ("CAM_SET_BOSS_DODO", "Boss_dodongo", "Boss_dodongo"), - ("CAM_SET_BOSS_BARI", "Boss_barinade", "Boss_barinade"), - ("CAM_SET_BOSS_FGANON", "Boss_phantom_ganon", "Boss_phantom_ganon"), - ("CAM_SET_BOSS_BAL", "Boss_volvagia", "Boss_volvagia"), - ("CAM_SET_BOSS_SHADES", "Boss_bongo", "Boss_bongo"), - ("CAM_SET_BOSS_MOFA", "Boss_morpha", "Boss_morpha"), - ("CAM_SET_TWIN0", "Twinrova_platform", "Twinrova_platform"), - ("CAM_SET_TWIN1", "Twinrova_floor", "Twinrova_floor"), - ("CAM_SET_BOSS_GANON1", "Boss_ganondorf", "Boss_ganondorf"), - ("CAM_SET_BOSS_GANON2", "Boss_ganon", "Boss_ganon"), - ("CAM_SET_TOWER0", "Tower_climb", "Tower_climb"), - ("CAM_SET_TOWER1", "Tower_unused", "Tower_unused"), - ("CAM_SET_FIXED0", "Market_balcony", "Market_balcony"), - ("CAM_SET_FIXED1", "Chu_bowling", "Chu_bowling"), - ("CAM_SET_CIRCLE0", "Pivot_crawlspace", "Pivot_crawlspace"), - ("CAM_SET_CIRCLE2", "Pivot_shop_browsing", "Pivot_shop_browsing"), - ("CAM_SET_CIRCLE3", "Pivot_in_front", "Pivot_in_front"), - ("CAM_SET_PREREND0", "Prerend_fixed", "Prerend_fixed"), - ("CAM_SET_PREREND1", "Prerend_pivot", "Prerend_pivot"), - ("CAM_SET_PREREND3", "Prerend_side_scroll", "Prerend_side_scroll"), - ("CAM_SET_DOOR0", "Door0", "Door0"), - ("CAM_SET_DOORC", "Doorc", "Doorc"), - ("CAM_SET_RAIL3", "Crawlspace", "Crawlspace"), - ("CAM_SET_START0", "Start0", "Start0"), - ("CAM_SET_START1", "Start1", "Start1"), - ("CAM_SET_FREE0", "Free0", "Free0"), - ("CAM_SET_FREE2", "Free2", "Free2"), - ("CAM_SET_CIRCLE4", "Pivot_corner", "Pivot_corner"), - ("CAM_SET_CIRCLE5", "Pivot_water_surface", "Pivot_water_surface"), - ("CAM_SET_DEMO0", "Cs_0", "Cs_0"), - ("CAM_SET_DEMO1", "Twisted_Hallway", "Twisted_Hallway"), - ("CAM_SET_MORI1", "Forest_birds_eye", "Forest_birds_eye"), - ("CAM_SET_ITEM0", "Slow_chest_cs", "Slow_chest_cs"), - ("CAM_SET_ITEM1", "Item_unused", "Item_unused"), - ("CAM_SET_DEMO3", "Cs_3", "Cs_3"), - ("CAM_SET_DEMO4", "Cs_attention", "Cs_attention"), - ("CAM_SET_UFOBEAN", "Bean_generic", "Bean_generic"), - ("CAM_SET_LIFTBEAN", "Bean_lost_woods", "Bean_lost_woods"), - ("CAM_SET_SCENE0", "Scene_unused", "Scene_unused"), - ("CAM_SET_SCENE1", "Scene_transition", "Scene_transition"), - ("CAM_SET_HIDAN1", "Fire_platform", "Fire_platform"), - ("CAM_SET_HIDAN2", "Fire_staircase", "Fire_staircase"), - ("CAM_SET_MORI2", "Forest_unused", "Forest_unused"), - ("CAM_SET_MORI3", "Defeat_poe", "Defeat_poe"), - ("CAM_SET_TAKO", "Big_octo", "Big_octo"), - ("CAM_SET_SPOT05A", "Meadow_birds_eye", "Meadow_birds_eye"), - ("CAM_SET_SPOT05B", "Meadow_unused", "Meadow_unused"), - ("CAM_SET_HIDAN3", "Fire_birds_eye", "Fire_birds_eye"), - ("CAM_SET_ITEM2", "Turn_around", "Turn_around"), - ("CAM_SET_CIRCLE6", "Pivot_vertical", "Pivot_vertical"), - ("CAM_SET_NORMAL2", "Normal2", "Normal2"), - ("CAM_SET_FISHING", "Fishing", "Fishing"), - ("CAM_SET_DEMOC", "Cs_c", "Cs_c"), - ("CAM_SET_UO_FIBER", "Jabu_tentacle", "Jabu_tentacle"), - ("CAM_SET_DUNGEON2", "Dungeon2", "Dungeon2"), - ("CAM_SET_TEPPEN", "Directed_yaw", "Directed_yaw"), - ("CAM_SET_CIRCLE7", "Pivot_from_side", "Pivot_from_side"), - ("CAM_SET_NORMAL4", "Normal4", "Normal4"), -] - -decomp_compat_map_CameraSType = { - "CAM_SET_HORSE0": "CAM_SET_HORSE", - "CAM_SET_BOSS_GOMA": "CAM_SET_BOSS_GOHMA", - "CAM_SET_BOSS_DODO": "CAM_SET_BOSS_DODONGO", - "CAM_SET_BOSS_BARI": "CAM_SET_BOSS_BARINADE", - "CAM_SET_BOSS_FGANON": "CAM_SET_BOSS_PHANTOM_GANON", - "CAM_SET_BOSS_BAL": "CAM_SET_BOSS_VOLVAGIA", - "CAM_SET_BOSS_SHADES": "CAM_SET_BOSS_BONGO", - "CAM_SET_BOSS_MOFA": "CAM_SET_BOSS_MORPHA", - "CAM_SET_TWIN0": "CAM_SET_BOSS_TWINROVA_PLATFORM", - "CAM_SET_TWIN1": "CAM_SET_BOSS_TWINROVA_FLOOR", - "CAM_SET_BOSS_GANON1": "CAM_SET_BOSS_GANONDORF", - "CAM_SET_BOSS_GANON2": "CAM_SET_BOSS_GANON", - "CAM_SET_TOWER0": "CAM_SET_TOWER_CLIMB", - "CAM_SET_TOWER1": "CAM_SET_TOWER_UNUSED", - "CAM_SET_FIXED0": "CAM_SET_MARKET_BALCONY", - "CAM_SET_FIXED1": "CAM_SET_CHU_BOWLING", - "CAM_SET_CIRCLE0": "CAM_SET_PIVOT_CRAWLSPACE", - "CAM_SET_CIRCLE2": "CAM_SET_PIVOT_SHOP_BROWSING", - "CAM_SET_CIRCLE3": "CAM_SET_PIVOT_IN_FRONT", - "CAM_SET_PREREND0": "CAM_SET_PREREND_FIXED", - "CAM_SET_PREREND1": "CAM_SET_PREREND_PIVOT", - "CAM_SET_PREREND3": "CAM_SET_PREREND_SIDE_SCROLL", - "CAM_SET_RAIL3": "CAM_SET_CRAWLSPACE", - "CAM_SET_CIRCLE4": "CAM_SET_PIVOT_CORNER", - "CAM_SET_CIRCLE5": "CAM_SET_PIVOT_WATER_SURFACE", - "CAM_SET_DEMO0": "CAM_SET_CS_0", - "CAM_SET_DEMO1": "CAM_SET_CS_TWISTED_HALLWAY", - "CAM_SET_MORI1": "CAM_SET_FOREST_BIRDS_EYE", - "CAM_SET_ITEM0": "CAM_SET_SLOW_CHEST_CS", - "CAM_SET_ITEM1": "CAM_SET_ITEM_UNUSED", - "CAM_SET_DEMO3": "CAM_SET_CS_3", - "CAM_SET_DEMO4": "CAM_SET_CS_ATTENTION", - "CAM_SET_UFOBEAN": "CAM_SET_BEAN_GENERIC", - "CAM_SET_LIFTBEAN": "CAM_SET_BEAN_LOST_WOODS", - "CAM_SET_SCENE0": "CAM_SET_SCENE_UNUSED", - "CAM_SET_SCENE1": "CAM_SET_SCENE_TRANSITION", - "CAM_SET_HIDAN1": "CAM_SET_ELEVATOR_PLATFORM", - "CAM_SET_HIDAN2": "CAM_SET_FIRE_STAIRCASE", - "CAM_SET_MORI2": "CAM_SET_FOREST_UNUSED", - "CAM_SET_MORI3": "CAM_SET_FOREST_DEFEAT_POE", - "CAM_SET_TAKO": "CAM_SET_BIG_OCTO", - "CAM_SET_SPOT05A": "CAM_SET_MEADOW_BIRDS_EYE", - "CAM_SET_SPOT05B": "CAM_SET_MEADOW_UNUSED", - "CAM_SET_HIDAN3": "CAM_SET_FIRE_BIRDS_EYE", - "CAM_SET_ITEM2": "CAM_SET_TURN_AROUND", - "CAM_SET_CIRCLE6": "CAM_SET_PIVOT_VERTICAL", - "CAM_SET_DEMOC": "CAM_SET_CS_C", - "CAM_SET_UO_FIBER": "CAM_SET_JABU_TENTACLE", - "CAM_SET_TEPPEN": "CAM_SET_DIRECTED_YAW", - "CAM_SET_CIRCLE7": "CAM_SET_PIVOT_FROM_SIDE", -} diff --git a/fast64_internal/oot/collision/exporter/__init__.py b/fast64_internal/oot/collision/exporter/__init__.py deleted file mode 100644 index 29566e74b..000000000 --- a/fast64_internal/oot/collision/exporter/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .functions import exportCollisionCommon - -from .classes import ( - OOTCollision, - OOTCameraData, - OOTCameraPosData, - OOTWaterBox, - OOTCrawlspaceData, -) diff --git a/fast64_internal/oot/collision/exporter/classes.py b/fast64_internal/oot/collision/exporter/classes.py deleted file mode 100644 index af8ba58ec..000000000 --- a/fast64_internal/oot/collision/exporter/classes.py +++ /dev/null @@ -1,275 +0,0 @@ -import math -from ....utility import PluginError -from ...oot_utility import BoxEmpty, convertIntTo2sComplement, getCustomProperty - - -class OOTCollisionVertex: - def __init__(self, position): - self.position = position - - -class OOTCollisionPolygon: - def __init__(self, indices, normal, distance): - self.indices = indices - self.normal = normal - self.distance = distance - - def convertShort02(self, ignoreCamera, ignoreActor, ignoreProjectile): - vertPart = self.indices[0] & 0x1FFF - colPart = (1 if ignoreCamera else 0) + (2 if ignoreActor else 0) + (4 if ignoreProjectile else 0) - - return vertPart | (colPart << 13) - - def convertShort04(self, enableConveyor): - vertPart = self.indices[1] & 0x1FFF - conveyorPart = 1 if enableConveyor else 0 - - return vertPart | (conveyorPart << 13) - - def convertShort06(self): - return self.indices[2] & 0x1FFF - - -class OOTPolygonType: - def __eq__(self, other): - return ( - self.eponaBlock == other.eponaBlock - and self.decreaseHeight == other.decreaseHeight - and self.floorSetting == other.floorSetting - and self.wallSetting == other.wallSetting - and self.floorProperty == other.floorProperty - and self.exitID == other.exitID - and self.cameraID == other.cameraID - and self.isWallDamage == other.isWallDamage - and self.enableConveyor == other.enableConveyor - and self.conveyorRotation == other.conveyorRotation - and self.conveyorSpeed == other.conveyorSpeed - and self.hookshotable == other.hookshotable - and self.echo == other.echo - and self.lightingSetting == other.lightingSetting - and self.terrain == other.terrain - and self.sound == other.sound - and self.ignoreCameraCollision == other.ignoreCameraCollision - and self.ignoreActorCollision == other.ignoreActorCollision - and self.ignoreProjectileCollision == other.ignoreProjectileCollision - ) - - def __ne__(self, other): - return ( - self.eponaBlock != other.eponaBlock - or self.decreaseHeight != other.decreaseHeight - or self.floorSetting != other.floorSetting - or self.wallSetting != other.wallSetting - or self.floorProperty != other.floorProperty - or self.exitID != other.exitID - or self.cameraID != other.cameraID - or self.isWallDamage != other.isWallDamage - or self.enableConveyor != other.enableConveyor - or self.conveyorRotation != other.conveyorRotation - or self.conveyorSpeed != other.conveyorSpeed - or self.hookshotable != other.hookshotable - or self.echo != other.echo - or self.lightingSetting != other.lightingSetting - or self.terrain != other.terrain - or self.sound != other.sound - or self.ignoreCameraCollision != other.ignoreCameraCollision - or self.ignoreActorCollision != other.ignoreActorCollision - or self.ignoreProjectileCollision != other.ignoreProjectileCollision - ) - - def __hash__(self): - return hash( - ( - self.eponaBlock, - self.decreaseHeight, - self.floorSetting, - self.wallSetting, - self.floorProperty, - self.exitID, - self.cameraID, - self.isWallDamage, - self.enableConveyor, - self.conveyorRotation, - self.conveyorSpeed, - self.hookshotable, - self.echo, - self.lightingSetting, - self.terrain, - self.sound, - self.ignoreCameraCollision, - self.ignoreActorCollision, - self.ignoreProjectileCollision, - ) - ) - - def __init__(self): - self.eponaBlock = None # eponaBlock - self.decreaseHeight = None # decreaseHeight - self.floorSetting = None # floorSetting - self.wallSetting = None # wallSetting - self.floorProperty = None # floorProperty - self.exitID = None # exitID - self.cameraID = None # cameraID - self.isWallDamage = None # isWallDamage - self.enableConveyor = None - self.conveyorRotation = None # conveyorDirection - self.conveyorSpeed = None # conveyorSpeed - self.hookshotable = None # hookshotable - self.echo = None # echo - self.lightingSetting = None # lightingSetting - self.terrain = None # terrain - self.sound = None # sound - self.ignoreCameraCollision = None - self.ignoreActorCollision = None - self.ignoreProjectileCollision = None - - def convertHigh(self): - value = ( - ((1 if self.eponaBlock else 0) << 31) - | ((1 if self.decreaseHeight else 0) << 30) - | (int(self.floorSetting, 16) << 26) - | (int(self.wallSetting, 16) << 21) - | (int(self.floorProperty, 16) << 13) - | (self.exitID << 8) - | (self.cameraID << 0) - ) - - return convertIntTo2sComplement(value, 4, False) - - def convertLow(self): - value = ( - ((1 if self.isWallDamage else 0) << 27) - | (self.conveyorRotation << 21) - | (self.conveyorSpeed << 18) - | ((1 if self.hookshotable else 0) << 17) - | (int(self.echo, 16) << 11) - | (self.lightingSetting << 6) - | (int(self.terrain, 16) << 4) - | (int(self.sound, 16) << 0) - ) - - return convertIntTo2sComplement(value, 4, False) - - -class OOTCollision: - def __init__(self, ownerName): - self.ownerName = ownerName - self.bounds = [] - self.vertices = [] - # dict of polygon type : polygon list - self.polygonGroups = {} - self.cameraData = None - self.waterBoxes = [] - - def polygonCount(self): - count = 0 - for polygonType, polygons in self.polygonGroups.items(): - count += len(polygons) - return count - - def headerName(self): - return self.ownerName + "_collisionHeader" - - def verticesName(self): - return self.ownerName + "_vertices" - - def polygonsName(self): - return self.ownerName + "_polygons" - - def polygonTypesName(self): - return self.ownerName + "_polygonTypes" - - def camDataName(self): - return self.ownerName + "_camData" - - def waterBoxesName(self): - return self.ownerName + "_waterBoxes" - - -def getPolygonType(collisionProp): - polygonType = OOTPolygonType() - polygonType.ignoreCameraCollision = collisionProp.ignoreCameraCollision - polygonType.ignoreActorCollision = collisionProp.ignoreActorCollision - polygonType.ignoreProjectileCollision = collisionProp.ignoreProjectileCollision - polygonType.eponaBlock = collisionProp.eponaBlock - polygonType.decreaseHeight = collisionProp.decreaseHeight - polygonType.floorSetting = getCustomProperty(collisionProp, "floorSetting") - polygonType.wallSetting = getCustomProperty(collisionProp, "wallSetting") - polygonType.floorProperty = getCustomProperty(collisionProp, "floorProperty") - polygonType.exitID = collisionProp.exitID - polygonType.cameraID = collisionProp.cameraID - polygonType.isWallDamage = collisionProp.isWallDamage - polygonType.enableConveyor = collisionProp.conveyorOption == "Land" - if collisionProp.conveyorOption != "None": - polygonType.conveyorRotation = int(collisionProp.conveyorRotation / (2 * math.pi) * 0x3F) - polygonType.conveyorSpeed = int(getCustomProperty(collisionProp, "conveyorSpeed"), 16) + ( - 4 if collisionProp.conveyorKeepMomentum else 0 - ) - else: - polygonType.conveyorRotation = 0 - polygonType.conveyorSpeed = 0 - - polygonType.hookshotable = collisionProp.hookshotable - polygonType.echo = collisionProp.echo - polygonType.lightingSetting = collisionProp.lightingSetting - polygonType.terrain = getCustomProperty(collisionProp, "terrain") - polygonType.sound = getCustomProperty(collisionProp, "sound") - return polygonType - - -class OOTWaterBox(BoxEmpty): - def __init__(self, roomIndex, lightingSetting, cameraSetting, flag19, position, scale, emptyScale): - self.roomIndex = roomIndex - self.lightingSetting = lightingSetting - self.cameraSetting = cameraSetting - self.flag19 = flag19 - BoxEmpty.__init__(self, position, scale, emptyScale) - - def propertyData(self): - value = ( - ((1 if self.flag19 else 0) << 19) - | (int(self.roomIndex) << 13) - | (self.lightingSetting << 8) - | (self.cameraSetting << 0) - ) - return convertIntTo2sComplement(value, 4, False) - - -class OOTCameraData: - def __init__(self, ownerName): - self.ownerName = ownerName - self.camPosDict = {} - - def camDataName(self): - return self.ownerName + "_camData" - - def camPositionsName(self): - return self.ownerName + "_camPosData" - - def validateCamPositions(self): - count = 0 - while count < len(self.camPosDict): - if count not in self.camPosDict: - raise PluginError( - "Error: Camera positions do not have a consecutive list of indices.\n" - + "Missing index: " - + str(count) - ) - count = count + 1 - - -class OOTCameraPosData: - def __init__(self, camSType, hasPositionData, position, rotation, fov, bgImageOverrideIndex): - self.camSType = camSType - self.position = position - self.rotation = rotation - self.fov = fov - self.bgImageOverrideIndex = bgImageOverrideIndex - self.unknown = -1 - self.hasPositionData = hasPositionData - - -class OOTCrawlspaceData: - def __init__(self, camSType): - self.camSType = camSType - self.points = [] diff --git a/fast64_internal/oot/collision/exporter/functions.py b/fast64_internal/oot/collision/exporter/functions.py deleted file mode 100644 index 19364ddd9..000000000 --- a/fast64_internal/oot/collision/exporter/functions.py +++ /dev/null @@ -1,131 +0,0 @@ -import bpy -import mathutils - -from ....utility import PluginError -from ...oot_utility import convertIntTo2sComplement -from .classes import OOTCollisionVertex, OOTCollisionPolygon, getPolygonType - - -def updateBounds(position, bounds): - if len(bounds) == 0: - bounds.append([position[0], position[1], position[2]]) - bounds.append([position[0], position[1], position[2]]) - return - - minBounds = bounds[0] - maxBounds = bounds[1] - for i in range(3): - if position[i] < minBounds[i]: - minBounds[i] = position[i] - if position[i] > maxBounds[i]: - maxBounds[i] = position[i] - - -def collisionVertIndex(vert, vertArray): - for i in range(len(vertArray)): - colVert = vertArray[i] - if colVert.position == vert: - return i - return None - - -def roundPosition(position): - # return [int.from_bytes(int(round(value)).to_bytes(2, 'big', signed = True), 'big') for value in position] - return (int(round(position[0])), int(round(position[1])), int(round(position[2]))) - - -def addCollisionTriangles(obj, collisionDict, includeChildren, transformMatrix, bounds): - if obj.type == "MESH" and not obj.ignore_collision: - if len(obj.data.materials) == 0: - raise PluginError(obj.name + " must have a material associated with it.") - obj.data.calc_loop_triangles() - for face in obj.data.loop_triangles: - material = obj.material_slots[face.material_index].material - polygonType = getPolygonType(material.ootCollisionProperty) - - planePoint = transformMatrix @ obj.data.vertices[face.vertices[0]].co - (x1, y1, z1) = roundPosition(planePoint) - (x2, y2, z2) = roundPosition(transformMatrix @ obj.data.vertices[face.vertices[1]].co) - (x3, y3, z3) = roundPosition(transformMatrix @ obj.data.vertices[face.vertices[2]].co) - - updateBounds((x1, y1, z1), bounds) - updateBounds((x2, y2, z2), bounds) - updateBounds((x3, y3, z3), bounds) - - faceNormal = (transformMatrix.inverted().transposed() @ face.normal).normalized() - distance = int( - round( - -1 * (faceNormal[0] * planePoint[0] + faceNormal[1] * planePoint[1] + faceNormal[2] * planePoint[2]) - ) - ) - distance = convertIntTo2sComplement(distance, 2, True) - - nx = (y2 - y1) * (z3 - z2) - (z2 - z1) * (y3 - y2) - ny = (z2 - z1) * (x3 - x2) - (x2 - x1) * (z3 - z2) - nz = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2) - magSqr = nx * nx + ny * ny + nz * nz - - if magSqr <= 0: - print("Ignore denormalized triangle.") - continue - - if polygonType not in collisionDict: - collisionDict[polygonType] = [] - - positions = ((x1, y1, z1), (x2, y2, z2), (x3, y3, z3)) - - collisionDict[polygonType].append((positions, faceNormal, distance)) - - if includeChildren: - for child in obj.children: - addCollisionTriangles(child, collisionDict, includeChildren, transformMatrix @ child.matrix_local, bounds) - - -# water boxes handled by level writer -def exportCollisionCommon(collision, obj, transformMatrix, includeChildren, name): - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) - - # dict of collisionType : faces - collisionDict = {} - - addCollisionTriangles(obj, collisionDict, includeChildren, transformMatrix, collision.bounds) - for polygonType, faces in collisionDict.items(): - collision.polygonGroups[polygonType] = [] - for faceVerts, normal, distance in faces: - assert len(faceVerts) == 3 - indices = [] - for roundedPosition in faceVerts: - index = collisionVertIndex(roundedPosition, collision.vertices) - if index is None: - collision.vertices.append(OOTCollisionVertex(roundedPosition)) - indices.append(len(collision.vertices) - 1) - else: - indices.append(index) - assert len(indices) == 3 - - # We need to ensure two things about the order in which the vertex indices are: - # - # 1) The vertex with the minimum y coordinate should be first. - # This prevents a bug due to an optimization in OoT's CollisionPoly_GetMinY. - # https://github.com/zeldaret/oot/blob/791d9018c09925138b9f830f7ae8142119905c05/src/code/z_bgcheck.c#L161 - # - # 2) The vertices should wrap around the polygon normal **counter-clockwise**. - # This is needed for OoT's dynapoly, which is collision that can move. - # When it moves, the vertex coordinates and normals are recomputed. - # The normal is computed based on the vertex coordinates, which makes the order of vertices matter. - # https://github.com/zeldaret/oot/blob/791d9018c09925138b9f830f7ae8142119905c05/src/code/z_bgcheck.c#L2888 - - # Address 1): sort by ascending y coordinate - indices.sort(key=lambda index: collision.vertices[index].position[1]) - - # Address 2): - # swap indices[1] and indices[2], - # if the normal computed from the vertices in the current order is the wrong way. - v0 = mathutils.Vector(collision.vertices[indices[0]].position) - v1 = mathutils.Vector(collision.vertices[indices[1]].position) - v2 = mathutils.Vector(collision.vertices[indices[2]].position) - if (v1 - v0).cross(v2 - v0).dot(mathutils.Vector(normal)) < 0: - indices[1], indices[2] = indices[2], indices[1] - - collision.polygonGroups[polygonType].append(OOTCollisionPolygon(indices, normal, distance)) diff --git a/fast64_internal/oot/collision/exporter/to_c/__init__.py b/fast64_internal/oot/collision/exporter/to_c/__init__.py deleted file mode 100644 index 0f36623eb..000000000 --- a/fast64_internal/oot/collision/exporter/to_c/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .collision import ootCollisionToC, exportCollisionToC diff --git a/fast64_internal/oot/collision/exporter/to_c/collision.py b/fast64_internal/oot/collision/exporter/to_c/collision.py deleted file mode 100644 index 563263c5d..000000000 --- a/fast64_internal/oot/collision/exporter/to_c/collision.py +++ /dev/null @@ -1,321 +0,0 @@ -import bpy -import os -import mathutils - -from ...exporter import OOTCollision, OOTCameraData -from ...properties import OOTCollisionExportSettings -from ..classes import OOTCameraData, OOTCameraPosData, OOTCrawlspaceData -from ....exporter.collision import CollisionHeader - -from .....utility import ( - PluginError, - CData, - unhideAllAndGetHiddenState, - restoreHiddenState, - writeCData, - toAlnum, -) - -from ....oot_utility import ( - OOTObjectCategorizer, - addIncludeFiles, - ootDuplicateHierarchy, - ootCleanupScene, - ootGetPath, - ootGetObjectPath, -) - - -def ootCollisionVertexToC(vertex): - return "{ " + str(vertex.position[0]) + ", " + str(vertex.position[1]) + ", " + str(vertex.position[2]) + " },\n" - - -def ootCollisionPolygonToC(polygon, ignoreCamera, ignoreActor, ignoreProjectile, enableConveyor, polygonTypeIndex): - return ( - "{ " - + ", ".join( - ( - format(polygonTypeIndex, "#06x"), - format(polygon.convertShort02(ignoreCamera, ignoreActor, ignoreProjectile), "#06x"), - format(polygon.convertShort04(enableConveyor), "#06x"), - format(polygon.convertShort06(), "#06x"), - "COLPOLY_SNORMAL({})".format(polygon.normal[0]), - "COLPOLY_SNORMAL({})".format(polygon.normal[1]), - "COLPOLY_SNORMAL({})".format(polygon.normal[2]), - format(polygon.distance, "#06x"), - ) - ) - + " },\n" - ) - - -def ootPolygonTypeToC(polygonType): - return ( - "{ " + format(polygonType.convertHigh(), "#010x") + ", " + format(polygonType.convertLow(), "#010x") + " },\n" - ) - - -def ootWaterBoxToC(waterBox): - return ( - "{ " - + str(waterBox.low[0]) - + ", " - + str(waterBox.height) - + ", " - + str(waterBox.low[1]) - + ", " - + str(waterBox.high[0] - waterBox.low[0]) - + ", " - + str(waterBox.high[1] - waterBox.low[1]) - + ", " - + format(waterBox.propertyData(), "#010x") - + " },\n" - ) - - -def ootCameraDataToC(camData): - posC = CData() - camC = CData() - if len(camData.camPosDict) > 0: - camDataName = "BgCamInfo " + camData.camDataName() + "[" + str(len(camData.camPosDict)) + "]" - - camC.source = camDataName + " = {\n" - camC.header = "extern " + camDataName + ";\n" - - camPosIndex = 0 - - for i in range(len(camData.camPosDict)): - camItem = camData.camPosDict[i] - if isinstance(camItem, OOTCameraPosData): - camC.source += "\t" + ootCameraEntryToC(camItem, camData, camPosIndex) + ",\n" - if camItem.hasPositionData: - posC.source += ootCameraPosToC(camItem) - camPosIndex += 3 - elif isinstance(camItem, OOTCrawlspaceData): - camC.source += "\t" + ootCrawlspaceEntryToC(camItem, camData, camPosIndex) + ",\n" - posC.source += ootCrawlspaceToC(camItem) - camPosIndex += len(camItem.points) * 3 - else: - raise PluginError(f"Invalid object type in camera position dict: {type(camItem)}") - posC.source += "};\n\n" - camC.source += "};\n\n" - - if camPosIndex > 0: - posDataName = "Vec3s " + camData.camPositionsName() + "[" + str(camPosIndex) + "]" - posC.header = "extern " + posDataName + ";\n" - posC.source = posDataName + " = {\n" + posC.source - else: - posC = CData() - - return posC, camC - - -def ootCameraPosToC(camPos): - return ( - "\t{ " - + str(camPos.position[0]) - + ", " - + str(camPos.position[1]) - + ", " - + str(camPos.position[2]) - + " },\n\t{ " - + str(camPos.rotation[0]) - + ", " - + str(camPos.rotation[1]) - + ", " - + str(camPos.rotation[2]) - + " },\n\t{ " - + str(camPos.fov) - + ", " - + str(camPos.bgImageOverrideIndex) - + ", " - + str(camPos.unknown) - + " },\n" - ) - - -def ootCameraEntryToC(camPos, camData, camPosIndex): - return " ".join( - ( - "{", - camPos.camSType + ",", - ("3" if camPos.hasPositionData else "0") + ",", - ("&" + camData.camPositionsName() + "[" + str(camPosIndex) + "]" if camPos.hasPositionData else "NULL"), - "}", - ) - ) - - -def ootCrawlspaceToC(camItem: OOTCrawlspaceData): - data = "" - for point in camItem.points: - data += f"\t{{{point[0]}, {point[1]}, {point[2]}}},\n" * 3 - - return data - - -def ootCrawlspaceEntryToC(camItem: OOTCrawlspaceData, camData: OOTCameraData, camPosIndex: int): - return " ".join( - ( - "{", - camItem.camSType + ",", - str((len(camItem.points) * 3)) + ",", - (("&" + camData.camPositionsName() + "[" + str(camPosIndex) + "]") if len(camItem.points) > 0 else "NULL"), - "}", - ) - ) - - -def ootCollisionToC(collision): - data = CData() - posC, camC = ootCameraDataToC(collision.cameraData) - - data.append(posC) - data.append(camC) - - if len(collision.polygonGroups) > 0: - data.header += "extern SurfaceType " + collision.polygonTypesName() + "[];\n" - data.header += "extern CollisionPoly " + collision.polygonsName() + "[];\n" - polygonTypeC = "SurfaceType " + collision.polygonTypesName() + "[] = {\n" - polygonC = "CollisionPoly " + collision.polygonsName() + "[] = {\n" - polygonIndex = 0 - for polygonType, polygons in collision.polygonGroups.items(): - polygonTypeC += "\t" + ootPolygonTypeToC(polygonType) - for polygon in polygons: - polygonC += "\t" + ootCollisionPolygonToC( - polygon, - polygonType.ignoreCameraCollision, - polygonType.ignoreActorCollision, - polygonType.ignoreProjectileCollision, - polygonType.enableConveyor, - polygonIndex, - ) - polygonIndex += 1 - polygonTypeC += "};\n\n" - polygonC += "};\n\n" - - data.source += polygonTypeC + polygonC - polygonTypesName = collision.polygonTypesName() - polygonsName = collision.polygonsName() - else: - polygonTypesName = "0" - polygonsName = "0" - - if len(collision.vertices) > 0: - data.header += "extern Vec3s " + collision.verticesName() + "[" + str(len(collision.vertices)) + "];\n" - data.source += "Vec3s " + collision.verticesName() + "[" + str(len(collision.vertices)) + "] = {\n" - for vertex in collision.vertices: - data.source += "\t" + ootCollisionVertexToC(vertex) - data.source += "};\n\n" - collisionVerticesName = collision.verticesName() - else: - collisionVerticesName = "0" - - if len(collision.waterBoxes) > 0: - data.header += "extern WaterBox " + collision.waterBoxesName() + "[];\n" - data.source += "WaterBox " + collision.waterBoxesName() + "[] = {\n" - for waterBox in collision.waterBoxes: - data.source += "\t" + ootWaterBoxToC(waterBox) - data.source += "};\n\n" - waterBoxesName = collision.waterBoxesName() - else: - waterBoxesName = "0" - - if len(collision.cameraData.camPosDict) > 0: - camDataName = collision.camDataName() - else: - camDataName = "0" - - data.header += "extern CollisionHeader " + collision.headerName() + ";\n" - data.source += "CollisionHeader " + collision.headerName() + " = {\n" - - if len(collision.bounds) == 2: - for bound in range(2): # min, max bound - for field in range(3): # x, y, z - data.source += "\t" + str(collision.bounds[bound][field]) + ",\n" - else: - data.source += "0, 0, 0, 0, 0, 0, " - - data.source += ( - "\t" - + str(len(collision.vertices)) - + ",\n" - + "\t" - + collisionVerticesName - + ",\n" - + "\t" - + str(collision.polygonCount()) - + ",\n" - + "\t" - + polygonsName - + ",\n" - + "\t" - + polygonTypesName - + ",\n" - + "\t" - + camDataName - + ",\n" - + "\t" - + str(len(collision.waterBoxes)) - + ",\n" - + "\t" - + waterBoxesName - + "\n" - + "};\n\n" - ) - - return data - - -def exportCollisionToC( - originalObj: bpy.types.Object, transformMatrix: mathutils.Matrix, exportSettings: OOTCollisionExportSettings -): - name = toAlnum(originalObj.name) - isCustomExport = exportSettings.customExport - folderName = exportSettings.folder - exportPath = ootGetObjectPath(isCustomExport, bpy.path.abspath(exportSettings.exportPath), folderName, False) - - collision = OOTCollision(name) - collision.cameraData = OOTCameraData(name) - - if bpy.context.scene.exportHiddenGeometry: - hiddenState = unhideAllAndGetHiddenState(bpy.context.scene) - - # Don't remove ignore_render, as we want to resuse this for collision - obj, allObjs = ootDuplicateHierarchy(originalObj, None, True, OOTObjectCategorizer()) - - if bpy.context.scene.exportHiddenGeometry: - restoreHiddenState(hiddenState) - - try: - if not obj.ignore_collision: - # get C data - colData = CData() - colData.source = '#include "ultra64.h"\n#include "z64.h"\n#include "macros.h"\n' - if not isCustomExport: - colData.source += f'#include "{folderName}.h"\n\n' - else: - colData.source += "\n" - colData.append( - CollisionHeader.new( - f"{name}_collisionHeader", - name, - obj, - transformMatrix, - bpy.context.scene.fast64.oot.useDecompFeatures, - exportSettings.includeChildren, - ).getC() - ) - - # write file - path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, True) - filename = exportSettings.filename if exportSettings.isCustomFilename else f"{name}_collision" - writeCData(colData, os.path.join(path, f"{filename}.h"), os.path.join(path, f"{filename}.c")) - if not isCustomExport: - addIncludeFiles(folderName, path, name) - else: - raise PluginError("ERROR: The selected mesh object ignores collision!") - except Exception as e: - raise Exception(str(e)) - finally: - ootCleanupScene(originalObj, allObjs) diff --git a/fast64_internal/oot/collision/operators.py b/fast64_internal/oot/collision/operators.py deleted file mode 100644 index 38fe2a5df..000000000 --- a/fast64_internal/oot/collision/operators.py +++ /dev/null @@ -1,53 +0,0 @@ -from bpy.types import Operator -from bpy.utils import register_class, unregister_class -from bpy.ops import object -from mathutils import Matrix -from ...utility import PluginError, raisePluginError -from ..oot_utility import getOOTScale -from ..collision.exporter.to_c import exportCollisionToC -from .properties import OOTCollisionExportSettings - - -class OOT_ExportCollision(Operator): - # set bl_ properties - bl_idname = "object.oot_export_collision" - bl_label = "Export Collision" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - def execute(self, context): - obj = None - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - if len(context.selected_objects) == 0: - raise PluginError("No object selected.") - obj = context.active_object - if obj.type != "MESH": - raise PluginError("No mesh object selected.") - - finalTransform = Matrix.Scale(getOOTScale(obj.ootActorScale), 4) - - try: - exportSettings: OOTCollisionExportSettings = context.scene.fast64.oot.collisionExportSettings - exportCollisionToC(obj, finalTransform, exportSettings) - - self.report({"INFO"}, "Success!") - return {"FINISHED"} - - except Exception as e: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - -oot_col_classes = (OOT_ExportCollision,) - - -def collision_ops_register(): - for cls in oot_col_classes: - register_class(cls) - - -def collision_ops_unregister(): - for cls in reversed(oot_col_classes): - unregister_class(cls) diff --git a/fast64_internal/oot/cutscene/exporter/__init__.py b/fast64_internal/oot/cutscene/exporter/__init__.py deleted file mode 100644 index 6a6aed48e..000000000 --- a/fast64_internal/oot/cutscene/exporter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .functions import getNewCutsceneExport diff --git a/fast64_internal/oot/cutscene/exporter/classes.py b/fast64_internal/oot/cutscene/exporter/classes.py deleted file mode 100644 index f655b5483..000000000 --- a/fast64_internal/oot/cutscene/exporter/classes.py +++ /dev/null @@ -1,462 +0,0 @@ -import math -import bpy - -from dataclasses import dataclass -from typing import TYPE_CHECKING -from bpy.types import Object -from ....utility import PluginError, indent -from ...oot_constants import ootData -from ..constants import ootEnumCSListTypeListC - -if TYPE_CHECKING: - from ..properties import OOTCutsceneProperty, OOTCSTextProperty - -from ..classes import ( - CutsceneCmdTransition, - CutsceneCmdRumbleController, - CutsceneCmdMisc, - CutsceneCmdTime, - CutsceneCmdLightSetting, - CutsceneCmdText, - CutsceneCmdTextNone, - CutsceneCmdTextOcarinaAction, - CutsceneCmdActorCueList, - CutsceneCmdActorCue, - CutsceneCmdCamEyeSpline, - CutsceneCmdCamATSpline, - CutsceneCmdCamEyeSplineRelToPlayer, - CutsceneCmdCamATSplineRelToPlayer, - CutsceneCmdCamEye, - CutsceneCmdCamAT, - CutsceneCmdCamPoint, -) - - -def cs_export_float(v: float): - return f"{v:f}f" - - -class CutsceneCmdToC: - """This class contains functions to create the cutscene commands""" - - def getEnumValue(self, enumKey: str, owner, propName: str): - item = ootData.enumData.enumByKey[enumKey].itemByKey.get(getattr(owner, propName)) - return item.id if item is not None else getattr(owner, f"{propName}Custom") - - def getGenericListCmd(self, cmdName: str, entryTotal: int): - return indent * 2 + f"{cmdName}({entryTotal}),\n" - - def getGenericSeqCmd(self, cmdName: str, type: str, startFrame: int, endFrame: int): - return indent * 3 + f"{cmdName}({type}, {startFrame}, {endFrame}" + ", 0" * 8 + "),\n" - - def getTransitionCmd(self, transition: CutsceneCmdTransition): - return indent * 2 + f"CS_TRANSITION({transition.type}, {transition.startFrame}, {transition.endFrame}),\n" - - def getRumbleControllerCmd(self, rumble: CutsceneCmdRumbleController): - return indent * 3 + ( - f"CS_RUMBLE_CONTROLLER(" - + f"0, {rumble.startFrame}, 0, " - + f"{rumble.sourceStrength}, {rumble.duration}, {rumble.decreaseRate}, 0, 0),\n" - ) - - def getMiscCmd(self, misc: CutsceneCmdMisc): - return indent * 3 + (f"CS_MISC(" + f"{misc.type}, {misc.startFrame}, {misc.endFrame}" + ", 0" * 11 + "),\n") - - def getTimeCmd(self, time: CutsceneCmdTime): - return indent * 3 + (f"CS_TIME(" + f"0, {time.startFrame}, 0, {time.hour}, {time.minute}" + "),\n") - - def getLightSettingCmd(self, lightSetting: CutsceneCmdLightSetting): - return indent * 3 + ( - f"CS_LIGHT_SETTING(" + f"{lightSetting.lightSetting}, {lightSetting.startFrame}" + ", 0" * 9 + "),\n" - ) - - def getTextCmd(self, text: CutsceneCmdText): - return indent * 3 + ( - f"CS_TEXT(" - + f"{text.textId}, {text.startFrame}, {text.endFrame}, {text.type}, {text.altTextId1}, {text.altTextId2}" - + "),\n" - ) - - def getTextNoneCmd(self, textNone: CutsceneCmdTextNone): - return indent * 3 + f"CS_TEXT_NONE({textNone.startFrame}, {textNone.endFrame}),\n" - - def getTextOcarinaActionCmd(self, ocarinaAction: CutsceneCmdTextOcarinaAction): - return indent * 3 + ( - f"CS_TEXT_OCARINA_ACTION(" - + f"{ocarinaAction.ocarinaActionId}, {ocarinaAction.startFrame}, " - + f"{ocarinaAction.endFrame}, {ocarinaAction.messageId}" - + "),\n" - ) - - def getDestinationCmd(self, csProp: "OOTCutsceneProperty"): - dest = self.getEnumValue("csDestination", csProp, "csDestination") - return indent * 2 + f"CS_DESTINATION({dest}, {csProp.csDestinationStartFrame}, 0),\n" - - def getActorCueListCmd(self, actorCueList: CutsceneCmdActorCueList, isPlayerActor: bool): - return indent * 2 + ( - f"CS_{'PLAYER' if isPlayerActor else 'ACTOR'}_CUE_LIST(" - + f"{actorCueList.commandType + ', ' if not isPlayerActor else ''}" - + f"{actorCueList.entryTotal}),\n" - ) - - def getActorCueCmd(self, actorCue: CutsceneCmdActorCue, isPlayerActor: bool): - return indent * 3 + ( - f"CS_{'PLAYER' if isPlayerActor else 'ACTOR'}_CUE(" - + f"{actorCue.actionID}, {actorCue.startFrame}, {actorCue.endFrame}, " - + "".join(f"{rot}, " for rot in actorCue.rot) - + "".join(f"{pos}, " for pos in actorCue.startPos) - + "".join(f"{pos}, " for pos in actorCue.endPos) - + f"{cs_export_float(0)}, {cs_export_float(0)}, {cs_export_float(0)}),\n" - ) - - def getCamListCmd(self, cmdName: str, startFrame: int, endFrame: int): - return indent * 2 + f"{cmdName}({startFrame}, {endFrame}),\n" - - def getCamEyeSplineCmd(self, camEyeSpline: CutsceneCmdCamEyeSpline): - return self.getCamListCmd("CS_CAM_EYE_SPLINE", camEyeSpline.startFrame, camEyeSpline.endFrame) - - def getCamATSplineCmd(self, camATSpline: CutsceneCmdCamATSpline): - return self.getCamListCmd("CS_CAM_AT_SPLINE", camATSpline.startFrame, camATSpline.endFrame) - - def getCamEyeSplineRelToPlayerCmd(self, camEyeSplinePlayer: CutsceneCmdCamEyeSplineRelToPlayer): - return self.getCamListCmd( - "CS_CAM_EYE_SPLINE_REL_TO_PLAYER", camEyeSplinePlayer.startFrame, camEyeSplinePlayer.endFrame - ) - - def getCamATSplineRelToPlayerCmd(self, camATSplinePlayer: CutsceneCmdCamATSplineRelToPlayer): - return self.getCamListCmd( - "CS_CAM_AT_SPLINE_REL_TO_PLAYER", camATSplinePlayer.startFrame, camATSplinePlayer.endFrame - ) - - def getCamEyeCmd(self, camEye: CutsceneCmdCamEye): - return self.getCamListCmd("CS_CAM_EYE", camEye.startFrame, camEye.endFrame) - - def getCamATCmd(self, camAT: CutsceneCmdCamAT): - return self.getCamListCmd("CS_CAM_AT", camAT.startFrame, camAT.endFrame) - - def getCamPointCmd(self, camPoint: CutsceneCmdCamPoint): - return indent * 3 + ( - f"CS_CAM_POINT(" - + f"{camPoint.continueFlag}, {camPoint.camRoll}, {camPoint.frame}, {cs_export_float(camPoint.viewAngle)}, " - + "".join(f"{pos}, " for pos in camPoint.pos) - + "0),\n" - ) - - -@dataclass -class CutsceneExport(CutsceneCmdToC): - """This class contains functions to create the new cutscene data""" - - csObjects: dict[str, list[Object]] - useDecomp: bool - motionOnly: bool - entryTotal: int = 0 - frameCount: int = 0 - motionFrameCount: int = 0 - camEndFrame: int = 0 - - def getOoTRotation(self, obj: Object): - """Returns the converted Blender rotation""" - - def conv(r): - r /= 2.0 * math.pi - r -= math.floor(r) - r = round(r * 0x10000) - - if r >= 0x8000: - r += 0xFFFF0000 - - assert r >= 0 and r <= 0xFFFFFFFF and (r <= 0x7FFF or r >= 0xFFFF8000) - - return hex(r & 0xFFFF) - - rotXYZ = [conv(obj.rotation_euler[0]), conv(obj.rotation_euler[2]), conv(obj.rotation_euler[1])] - return [f"DEG_TO_BINANG({(int(rot, base=16) * (180 / 0x8000)):.3f})" for rot in rotXYZ] - - def getOoTPosition(self, pos): - """Returns the converted Blender position""" - - scale = bpy.context.scene.ootBlenderScale - - x = round(pos[0] * scale) - y = round(pos[2] * scale) - z = round(-pos[1] * scale) - - if any(v < -0x8000 or v >= 0x8000 for v in (x, y, z)): - raise RuntimeError(f"Position(s) too large, out of range: {x}, {y}, {z}") - - return [x, y, z] - - def getActorCueListData(self, isPlayer: bool): - """Returns the Actor Cue List commands from the corresponding objects""" - - playerOrActor = f"{'Player' if isPlayer else 'Actor'}" - actorCueListObjects = self.csObjects[f"CS {playerOrActor} Cue List"] - actorCueListObjects.sort(key=lambda o: o.ootCSMotionProperty.actorCueProp.cueStartFrame) - actorCueData = "" - - self.entryTotal += len(actorCueListObjects) - for obj in actorCueListObjects: - entryTotal = len(obj.children) - - if entryTotal == 0: - raise PluginError("ERROR: The Actor Cue List does not contain any child Actor Cue objects") - - if obj.children[-1].ootEmptyType != "CS Dummy Cue": - # we need an extra point that won't be exported to get the real last cue's - # end frame and end position - raise PluginError("ERROR: The Actor Cue List is missing the extra dummy point!") - - commandType = obj.ootCSMotionProperty.actorCueListProp.commandType - - if commandType == "Custom": - commandType = obj.ootCSMotionProperty.actorCueListProp.commandTypeCustom - elif self.useDecomp: - commandType = ootData.enumData.enumByKey["csCmd"].itemByKey[commandType].id - - # ignoring dummy cue - actorCueList = CutsceneCmdActorCueList(None, entryTotal=entryTotal - 1, commandType=commandType) - actorCueData += self.getActorCueListCmd(actorCueList, isPlayer) - - for i, childObj in enumerate(obj.children, 1): - startFrame = childObj.ootCSMotionProperty.actorCueProp.cueStartFrame - if i < len(obj.children) and childObj.ootEmptyType != "CS Dummy Cue": - endFrame = obj.children[i].ootCSMotionProperty.actorCueProp.cueStartFrame - actionID = None - - if isPlayer: - cueID = childObj.ootCSMotionProperty.actorCueProp.playerCueID - if cueID != "Custom": - actionID = ootData.enumData.enumByKey["csPlayerCueId"].itemByKey[cueID].id - - if actionID is None: - actionID = childObj.ootCSMotionProperty.actorCueProp.cueActionID - - actorCue = CutsceneCmdActorCue( - None, - startFrame, - endFrame, - actionID, - self.getOoTRotation(childObj), - self.getOoTPosition(childObj.location), - self.getOoTPosition(obj.children[i].location), - ) - actorCueData += self.getActorCueCmd(actorCue, isPlayer) - - return actorCueData - - def getCameraShotPointData(self, bones, useAT: bool): - """Returns the Camera Point data from the bone data""" - - shotPoints: list[CutsceneCmdCamPoint] = [] - - if len(bones) < 4: - raise RuntimeError("Camera Armature needs at least 4 bones!") - - for bone in bones: - if bone.parent is not None: - raise RuntimeError("Camera Armature bones are not allowed to have parent bones!") - - shotPoints.append( - CutsceneCmdCamPoint( - None, - None, - None, - ("CS_CAM_CONTINUE" if self.useDecomp else "0"), - bone.ootCamShotPointProp.shotPointRoll if useAT else 0, - bone.ootCamShotPointProp.shotPointFrame, - bone.ootCamShotPointProp.shotPointViewAngle, - self.getOoTPosition(bone.head if not useAT else bone.tail), - ) - ) - - # NOTE: because of the game's bug explained in the importer we need to add an extra dummy point when exporting - shotPoints.append( - CutsceneCmdCamPoint(None, None, None, "CS_CAM_STOP" if self.useDecomp else "-1", 0, 0, 0.0, [0, 0, 0]) - ) - return shotPoints - - def getCamCmdFunc(self, camMode: str, useAT: bool): - """Returns the camera get function depending on the camera mode""" - - camCmdFuncMap = { - "splineEyeOrAT": self.getCamATSplineCmd if useAT else self.getCamEyeSplineCmd, - "splineEyeOrATRelPlayer": self.getCamATSplineRelToPlayerCmd - if useAT - else self.getCamEyeSplineRelToPlayerCmd, - "eyeOrAT": self.getCamATCmd if useAT else self.getCamEyeCmd, - } - - return camCmdFuncMap[camMode] - - def getCamClass(self, camMode: str, useAT: bool): - """Returns the camera dataclass depending on the camera mode""" - - camCmdClassMap = { - "splineEyeOrAT": CutsceneCmdCamATSpline if useAT else CutsceneCmdCamEyeSpline, - "splineEyeOrATRelPlayer": CutsceneCmdCamATSplineRelToPlayer - if useAT - else CutsceneCmdCamEyeSplineRelToPlayer, - "eyeOrAT": CutsceneCmdCamAT if useAT else CutsceneCmdCamEye, - } - - return camCmdClassMap[camMode] - - def getCamListData(self, shotObj: Object, useAT: bool): - """Returns the Camera Shot data from the corresponding Armatures""" - - camPointList = self.getCameraShotPointData(shotObj.data.bones, useAT) - startFrame = shotObj.data.ootCamShotProp.shotStartFrame - - # "fake" end frame - endFrame = ( - startFrame + max(2, sum(point.frame for point in camPointList)) + (camPointList[-2].frame if useAT else 1) - ) - - if not useAT: - for pointData in camPointList: - pointData.frame = 0 - self.camEndFrame = endFrame - - camData = self.getCamClass(shotObj.data.ootCamShotProp.shotCamMode, useAT)(None, startFrame, endFrame) - return self.getCamCmdFunc(shotObj.data.ootCamShotProp.shotCamMode, useAT)(camData) + "".join( - self.getCamPointCmd(pointData) for pointData in camPointList - ) - - def getCameraShotData(self): - """Returns every Camera Shot commands""" - - shotObjects = self.csObjects["camShot"] - cameraShotData = "" - - if len(shotObjects) > 0: - motionFrameCount = -1 - for shotObj in shotObjects: - cameraShotData += self.getCamListData(shotObj, False) + self.getCamListData(shotObj, True) - motionFrameCount = max(motionFrameCount, self.camEndFrame + 1) - self.motionFrameCount += motionFrameCount - self.entryTotal += len(shotObjects) * 2 - - return cameraShotData - - def getTextListData(self, textEntry: "OOTCSTextProperty"): - match textEntry.textboxType: - case "Text": - return self.getTextCmd( - CutsceneCmdText( - None, - textEntry.startFrame, - textEntry.endFrame, - textEntry.textID, - self.getEnumValue("csTextType", textEntry, "csTextType"), - textEntry.topOptionTextID, - textEntry.bottomOptionTextID, - ) - ) - case "None": - return self.getTextNoneCmd(CutsceneCmdTextNone(None, textEntry.startFrame, textEntry.endFrame)) - case "OcarinaAction": - return self.getTextOcarinaActionCmd( - CutsceneCmdTextOcarinaAction( - None, - textEntry.startFrame, - textEntry.endFrame, - self.getEnumValue("ocarinaSongActionId", textEntry, "ocarinaAction"), - textEntry.ocarinaMessageId, - ) - ) - - def getCutsceneData(self): - csProp: "OOTCutsceneProperty" = self.csObjects["Cutscene"][0].ootCutsceneProperty - self.frameCount = csProp.csEndFrame - data = "" - - if csProp.csUseDestination: - data += self.getDestinationCmd(csProp) - self.entryTotal += 1 - - for entry in csProp.csLists: - subData = "" - listCmd = "" - entryTotal = 0 - match entry.listType: - case "StartSeqList" | "StopSeqList" | "FadeOutSeqList": - entryTotal = len(entry.seqList) - for elem in entry.seqList: - enumKey = "csFadeOutSeqPlayer" if entry.listType == "FadeOutSeqList" else "seqId" - propName = "csSeqPlayer" if entry.listType == "FadeOutSeqList" else "csSeqID" - subData += self.getGenericSeqCmd( - ootEnumCSListTypeListC[entry.listType].removesuffix("_LIST"), - self.getEnumValue(enumKey, elem, propName), - elem.startFrame, - elem.endFrame, - ) - case "Transition": - subData += self.getTransitionCmd( - CutsceneCmdTransition( - None, - entry.transitionStartFrame, - entry.transitionEndFrame, - self.getEnumValue("csTransitionType", entry, "transitionType"), - ) - ) - case _: - curList = getattr(entry, (entry.listType[0].lower() + entry.listType[1:])) - entryTotal = len(curList) - for elem in curList: - match entry.listType: - case "TextList": - subData += self.getTextListData(elem) - case "LightSettingsList": - subData += self.getLightSettingCmd( - CutsceneCmdLightSetting( - None, elem.startFrame, elem.endFrame, None, elem.lightSettingsIndex - ) - ) - case "TimeList": - subData += self.getTimeCmd( - CutsceneCmdTime(None, elem.startFrame, elem.endFrame, elem.hour, elem.minute) - ) - case "MiscList": - subData += self.getMiscCmd( - CutsceneCmdMisc( - None, - elem.startFrame, - elem.endFrame, - self.getEnumValue("csMiscType", elem, "csMiscType"), - ) - ) - case "RumbleList": - subData += self.getRumbleControllerCmd( - CutsceneCmdRumbleController( - None, - elem.startFrame, - elem.endFrame, - elem.rumbleSourceStrength, - elem.rumbleDuration, - elem.rumbleDecreaseRate, - ) - ) - case _: - raise PluginError("ERROR: Unknown Cutscene List Type!") - if entry.listType != "Transition": - listCmd = self.getGenericListCmd(ootEnumCSListTypeListC[entry.listType], entryTotal) - self.entryTotal += 1 - data += listCmd + subData - - return data - - def getExportData(self): - """Returns the cutscene data""" - - csData = "" - if not self.motionOnly: - csData = self.getCutsceneData() - csData += self.getActorCueListData(False) + self.getActorCueListData(True) + self.getCameraShotData() - - if self.motionFrameCount > self.frameCount: - self.frameCount += self.motionFrameCount - self.frameCount - - return ( - (indent + f"CS_BEGIN_CUTSCENE({self.entryTotal}, {self.frameCount}),\n") + csData + (indent + "CS_END(),\n") - ) diff --git a/fast64_internal/oot/cutscene/exporter/functions.py b/fast64_internal/oot/cutscene/exporter/functions.py deleted file mode 100644 index b2f9e2454..000000000 --- a/fast64_internal/oot/cutscene/exporter/functions.py +++ /dev/null @@ -1,49 +0,0 @@ -import bpy - -from bpy.types import Object -from ....utility import PluginError -from .classes import CutsceneExport - - -def getCutsceneObjects(csName: str): - """Returns the object list containing every object from the cutscene to export""" - - csObjects: dict[str, list[Object]] = { - "Cutscene": [], - "CS Actor Cue List": [], - "CS Player Cue List": [], - "camShot": [], - } - - if csName is None: - raise PluginError("ERROR: The cutscene name is None!") - - for obj in bpy.data.objects: - isEmptyObj = obj.type == "EMPTY" - - # look for the cutscene object based on the cutscene name - parentCheck = obj.parent is not None and obj.parent.name == f"Cutscene.{csName}" - csObjCheck = isEmptyObj and obj.ootEmptyType == "Cutscene" and obj.name == f"Cutscene.{csName}" - if parentCheck or csObjCheck: - # add the relevant objects based on the empty type or if it's an armature - if isEmptyObj and obj.ootEmptyType in csObjects.keys(): - csObjects[obj.ootEmptyType].append(obj) - - if obj.type == "ARMATURE" and obj.parent.ootEmptyType == "Cutscene": - csObjects["camShot"].append(obj) - - if len(csObjects["Cutscene"]) != 1: - raise PluginError(f"ERROR: Expected 1 Cutscene Object, found {len(csObjects['Cutscene'])} ({csName}).") - - return csObjects - - -def getNewCutsceneExport(csName: str, motionOnly: bool): - """Returns the initialised cutscene exporter""" - - # this allows us to change the exporter's variables to get what we need - return CutsceneExport( - getCutsceneObjects(csName), - (bpy.context.scene.fast64.oot.featureSet == "HackerOOT") or bpy.context.scene.fast64.oot.useDecompFeatures, - motionOnly, - ) diff --git a/fast64_internal/oot/cutscene/operators.py b/fast64_internal/oot/cutscene/operators.py deleted file mode 100644 index d8ac08975..000000000 --- a/fast64_internal/oot/cutscene/operators.py +++ /dev/null @@ -1,312 +0,0 @@ -import os -import re -import bpy - -from bpy.path import abspath -from bpy.ops import object -from bpy.props import StringProperty, EnumProperty, IntProperty -from bpy.types import Scene, Operator, Context -from bpy.utils import register_class, unregister_class -from ...utility import CData, PluginError, writeCData, raisePluginError -from ..oot_utility import getCollection -from ..oot_constants import ootData -from .constants import ootEnumCSTextboxType, ootEnumCSListType -from .importer import importCutsceneData -from .exporter import getNewCutsceneExport -from ..exporter.cutscene import Cutscene - - -def checkGetFilePaths(context: Context): - cpath = abspath(context.scene.ootCutsceneExportPath) - - if not cpath.endswith(".c"): - raise PluginError("Output file must end with .c") - - hpath = cpath[:-1] + "h" - headerfilename = os.path.basename(hpath) - - return cpath, hpath, headerfilename - - -def ootCutsceneIncludes(headerfilename): - ret = CData() - ret.source = ( - '#include "ultra64.h"\n' - + '#include "z64.h"\n' - + '#include "macros.h"\n' - + '#include "command_macros_base.h"\n' - + '#include "z64cutscene_commands.h"\n\n' - + '#include "' - + headerfilename - + '"\n\n' - ) - return ret - - -def insertCutsceneData(filePath: str, csName: str): - """Inserts the motion data in the cutscene and returns the new data""" - fileLines = [] - includes = ootCutsceneIncludes("").source.split("\n") - - # if the file is not found then it's likely a new file that needs to be created - try: - with open(filePath, "r") as inputFile: - fileLines = inputFile.readlines() - fileLines = fileLines[len(includes) - 1 :] - except FileNotFoundError: - fileLines = [] - - foundCutscene = False - motionExporter = getNewCutsceneExport(csName) - beginIndex = 0 - - for i, line in enumerate(fileLines): - # skip commented lines - if not line.startswith("//") and not line.startswith("/*"): - if f"CutsceneData {csName}" in line: - foundCutscene = True - - if foundCutscene: - if "CS_BEGIN_CUTSCENE" in line: - # save the index of the line that contains the entry total and the framecount for later use - beginIndex = i - - # looking at next line to see if we reached the end of the cs script - index = i + 1 - if index < len(fileLines) and "CS_END" in fileLines[index]: - # exporting first to get the new framecount and the total of entries values - fileLines.insert(index, motionExporter.getExportData()) - - # update framecount and entry total values - beginLine = fileLines[beginIndex] - reMatch = re.search(r"\b\(([0-9a-fA-F, ]*)\b", beginLine) - if reMatch is not None: - params = reMatch[1].split(", ") - entryTotal = int(params[0], base=0) - frameCount = int(params[1], base=0) - entries = re.sub( - r"\b\(([0-9a-fA-F]*)\b", f"({entryTotal + motionExporter.entryTotal}", beginLine - ) - frames = re.sub(r"\b([0-9a-fA-F]*)\)", f"{frameCount + motionExporter.frameCount})", beginLine) - fileLines[beginIndex] = f"{entries.split(', ')[0]}, {frames.split(', ')[1]}" - else: - raise PluginError("ERROR: Can't find `CS_BEGIN_CUTSCENE()` parameters!") - break - - fileData = CData() - - if not foundCutscene: - print(f"WARNING: Can't find Cutscene ``{csName}``, inserting data at the end of the file.") - motionExporter.addBeginEndCmds = True - csArrayName = f"CutsceneData {csName}[]" - fileLines.append("\n" + csArrayName + " = {\n" + motionExporter.getExportData() + "};\n") - fileData.header = f"{csArrayName};\n" - - fileData.source = "".join(line for line in fileLines) - return fileData - - -class OOTCSTextAdd(Operator): - bl_idname = "object.oot_cstextbox_add" - bl_label = "Add CS Textbox" - bl_options = {"REGISTER", "UNDO"} - - collectionType: StringProperty() - textboxType: EnumProperty(items=ootEnumCSTextboxType) - listIndex: IntProperty() - objName: StringProperty() - - def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.listIndex) - newTextboxElement = collection.add() - newTextboxElement.textboxType = self.textboxType - return {"FINISHED"} - - -class OOTCSListAdd(Operator): - bl_idname = "object.oot_cslist_add" - bl_label = "Add CS List" - bl_options = {"REGISTER", "UNDO"} - - collectionType: StringProperty() - listType: EnumProperty(items=ootEnumCSListType) - objName: StringProperty() - - def execute(self, context): - collection = getCollection(self.objName, self.collectionType, None) - newList = collection.add() - newList.listType = self.listType - return {"FINISHED"} - - -class OOT_ImportCutscene(Operator): - bl_idname = "object.oot_import_cutscenes" - bl_label = "Import Cutscenes" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - try: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - - path = abspath(context.scene.ootCutsceneImportPath) - csName = context.scene.ootCSImportName if len(context.scene.ootCSImportName) > 0 else None - context.scene.ootCSNumber = importCutsceneData(path, None, csName) - - self.report({"INFO"}, "Successfully imported cutscenes") - return {"FINISHED"} - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - -class OOT_ExportCutscene(Operator): - bl_idname = "object.oot_export_cutscene" - bl_label = "Export Cutscene" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - def execute(self, context): - try: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - - activeObj = context.view_layer.objects.active - - if activeObj is None or activeObj.type != "EMPTY" or activeObj.ootEmptyType != "Cutscene": - raise PluginError("You must select a cutscene object") - - if activeObj.parent is not None: - raise PluginError("Cutscene object must not be parented to anything") - - cpath, hpath, headerfilename = checkGetFilePaths(context) - csdata = ootCutsceneIncludes(headerfilename) - - if context.scene.fast64.oot.exportMotionOnly: - # TODO: improve this - csdata.append(insertCutsceneData(cpath, activeObj.name.removeprefix("Cutscene."))) - else: - csdata.append(Cutscene(activeObj, context.scene.fast64.oot.useDecompFeatures).getC()) - writeCData(csdata, hpath, cpath) - - self.report({"INFO"}, "Successfully exported cutscene") - return {"FINISHED"} - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - -class OOT_ExportAllCutscenes(Operator): - bl_idname = "object.oot_export_all_cutscenes" - bl_label = "Export All Cutscenes" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - def execute(self, context): - try: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - - cpath, hpath, headerfilename = checkGetFilePaths(context) - csdata = ootCutsceneIncludes(headerfilename) - count = 0 - - for obj in context.view_layer.objects: - if obj.type == "EMPTY" and obj.ootEmptyType == "Cutscene": - if obj.parent is not None: - print(f"Parent: {obj.parent.name}, Object: {obj.name}") - raise PluginError("Cutscene object must not be parented to anything") - - if context.scene.fast64.oot.exportMotionOnly: - raise PluginError("ERROR: Not implemented yet.") - else: - csdata.append(Cutscene(obj, context.scene.fast64.oot.useDecompFeatures).getC()) - count += 1 - - if count == 0: - raise PluginError("Could not find any cutscenes to export") - - writeCData(csdata, hpath, cpath) - self.report({"INFO"}, "Successfully exported " + str(count) + " cutscenes") - return {"FINISHED"} - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - -class OOT_SearchCSDestinationEnumOperator(Operator): - bl_idname = "object.oot_search_cs_dest_enum_operator" - bl_label = "Choose Destination" - bl_property = "csDestination" - bl_options = {"REGISTER", "UNDO"} - - csDestination: EnumProperty(items=ootData.enumData.ootEnumCsDestination, default="cutscene_map_ganon_horse") - objName: StringProperty() - - def execute(self, context): - obj = bpy.data.objects[self.objName] - obj.ootCutsceneProperty.csDestination = self.csDestination - - context.region.tag_redraw() - self.report({"INFO"}, "Selected: " + self.csDestination) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.invoke_search_popup(self) - return {"RUNNING_MODAL"} - - -class OOT_SearchCSSeqOperator(Operator): - bl_idname = "object.oot_search_cs_seq_enum_operator" - bl_label = "Search Music Sequence" - bl_property = "seqId" - bl_options = {"REGISTER", "UNDO"} - - seqId: EnumProperty(items=ootData.enumData.ootEnumSeqId, default="general_sfx") - itemIndex: IntProperty() - listType: StringProperty() - - def execute(self, context): - csProp = context.view_layer.objects.active.ootCutsceneProperty - for elem in csProp.csLists: - if elem.listType == self.listType: - elem.seqList[self.itemIndex].csSeqID = self.seqId - break - context.region.tag_redraw() - self.report({"INFO"}, "Selected: " + self.seqId) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.invoke_search_popup(self) - return {"RUNNING_MODAL"} - - -oot_cutscene_classes = ( - OOTCSTextAdd, - OOTCSListAdd, - OOT_ImportCutscene, - OOT_ExportCutscene, - OOT_ExportAllCutscenes, - OOT_SearchCSDestinationEnumOperator, - OOT_SearchCSSeqOperator, -) - - -def cutscene_ops_register(): - for cls in oot_cutscene_classes: - register_class(cls) - - Scene.ootCutsceneExportPath = StringProperty(name="File", subtype="FILE_PATH") - Scene.ootCutsceneImportPath = StringProperty(name="File", subtype="FILE_PATH") - Scene.ootCSNumber = IntProperty(default=1, min=0) - Scene.ootCSImportName = StringProperty( - name="CS Name", description="Used to import a single cutscene, can be ``None``" - ) - - -def cutscene_ops_unregister(): - for cls in reversed(oot_cutscene_classes): - unregister_class(cls) - - del Scene.ootCSImportName - del Scene.ootCSNumber - del Scene.ootCutsceneImportPath - del Scene.ootCutsceneExportPath diff --git a/fast64_internal/oot/cutscene/panels.py b/fast64_internal/oot/cutscene/panels.py deleted file mode 100644 index 5cf84a908..000000000 --- a/fast64_internal/oot/cutscene/panels.py +++ /dev/null @@ -1,73 +0,0 @@ -from bpy.utils import register_class, unregister_class -from bpy.types import Scene -from bpy.props import BoolProperty -from ...utility import prop_split -from ...panels import OOT_Panel -from .operators import OOT_ExportCutscene, OOT_ExportAllCutscenes, OOT_ImportCutscene - - -class OoT_PreviewSettingsPanel(OOT_Panel): - bl_idname = "OOT_PT_preview_settings" - bl_label = "OOT CS Preview Settings" - - def draw(self, context): - context.scene.ootPreviewSettingsProperty.draw_props(self.layout) - - -class OOT_CutscenePanel(OOT_Panel): - bl_idname = "OOT_PT_export_cutscene" - bl_label = "OOT Cutscene Exporter" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "OOT" - - def draw(self, context): - layout = self.layout - - exportBox = layout.box() - exportBox.label(text="Cutscene Exporter") - - prop_split(exportBox, context.scene, "ootCutsceneExportPath", "Export To") - - activeObj = context.view_layer.objects.active - label = None - col = exportBox.column() - colcol = col.column() - if activeObj is None or activeObj.type != "EMPTY" or activeObj.ootEmptyType != "Cutscene": - label = "Select a cutscene object" - - if activeObj is not None and activeObj.parent is not None: - label = "Cutscene object must not be parented to anything" - - if label is not None: - col.label(text=label) - colcol.enabled = False - - colcol.operator(OOT_ExportCutscene.bl_idname) - col.operator(OOT_ExportAllCutscenes.bl_idname) - - importBox = layout.box() - importBox.label(text="Cutscene Importer") - prop_split(importBox, context.scene, "ootCSImportName", "Import") - prop_split(importBox, context.scene, "ootCutsceneImportPath", "From") - - col = importBox.column() - if len(context.scene.ootCSImportName) == 0: - col.label(text="All Cutscenes will be imported.") - col.operator(OOT_ImportCutscene.bl_idname) - - -oot_cutscene_panel_classes = ( - OoT_PreviewSettingsPanel, - OOT_CutscenePanel, -) - - -def cutscene_panels_register(): - for cls in oot_cutscene_panel_classes: - register_class(cls) - - -def cutscene_panels_unregister(): - for cls in oot_cutscene_panel_classes: - unregister_class(cls) diff --git a/fast64_internal/oot/data/__init__.py b/fast64_internal/oot/data/__init__.py deleted file mode 100644 index ce6e88b62..000000000 --- a/fast64_internal/oot/data/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .oot_data import OoT_Data -from .oot_object_data import OoT_ObjectData diff --git a/fast64_internal/oot/data/oot_actor_data.py b/fast64_internal/oot/data/oot_actor_data.py deleted file mode 100644 index 7f7642b2a..000000000 --- a/fast64_internal/oot/data/oot_actor_data.py +++ /dev/null @@ -1,48 +0,0 @@ -from os import path -from dataclasses import dataclass -from .oot_getters import getXMLRoot -from .oot_data import OoT_BaseElement - - -@dataclass -class OoT_ActorElement(OoT_BaseElement): - category: str - tiedObjects: list[str] - - -class OoT_ActorData: - """Everything related to OoT Actors""" - - def __init__(self): - # Path to the ``ActorList.xml`` file - actorXML = path.dirname(path.abspath(__file__)) + "/xml/ActorList.xml" - actorRoot = getXMLRoot(actorXML) - - # general actor list - self.actorList: list[OoT_ActorElement] = [] - - for actor in actorRoot.iterfind("Actor"): - tiedObjects = [] - objKey = actor.get("ObjectKey") - actorName = f"{actor.attrib['Name']} - {actor.attrib['ID'].removeprefix('ACTOR_')}" - if objKey is not None: # actors don't always use an object - tiedObjects = objKey.split(",") - self.actorList.append( - OoT_ActorElement( - actor.attrib["ID"], - actor.attrib["Key"], - actorName, - int(actor.attrib["Index"]), - actor.attrib["Category"], - tiedObjects, - ) - ) - self.actorsByKey = {actor.key: actor for actor in self.actorList} - self.actorsByID = {actor.id: actor for actor in self.actorList} - - # list of tuples used by Blender's enum properties - lastIndex = max(1, *(actor.index for actor in self.actorList)) - self.ootEnumActorID = [("None", f"{i} (Deleted from the XML)", "None") for i in range(lastIndex)] - self.ootEnumActorID.insert(0, ("Custom", "Custom Actor", "Custom")) - for actor in self.actorList: - self.ootEnumActorID[actor.index] = (actor.id, actor.name, actor.id) diff --git a/fast64_internal/oot/data/oot_data.py b/fast64_internal/oot/data/oot_data.py deleted file mode 100644 index c4619a925..000000000 --- a/fast64_internal/oot/data/oot_data.py +++ /dev/null @@ -1,23 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class OoT_BaseElement: - id: str - key: str - name: str - index: int - - -@dataclass -class OoT_Data: - """Contains data related to OoT, like actors or objects""" - - def __init__(self): - from .oot_enum_data import OoT_EnumData - from .oot_object_data import OoT_ObjectData - from .oot_actor_data import OoT_ActorData - - self.enumData = OoT_EnumData() - self.objectData = OoT_ObjectData() - self.actorData = OoT_ActorData() diff --git a/fast64_internal/oot/data/oot_enum_data.py b/fast64_internal/oot/data/oot_enum_data.py deleted file mode 100644 index 0644c6f4b..000000000 --- a/fast64_internal/oot/data/oot_enum_data.py +++ /dev/null @@ -1,123 +0,0 @@ -from dataclasses import dataclass, field -from os import path -from .oot_getters import getXMLRoot -from .oot_data import OoT_BaseElement - -# Note: "enumData" in this context refers to an OoT Object file (like ``gameplay_keep``) - - -@dataclass -class OoT_ItemElement(OoT_BaseElement): - parentKey: str - - def __post_init__(self): - # generate the name from the id - - if self.name is None: - keyToPrefix = { - "csCmd": "CS_CMD", - "csMiscType": "CS_MISC", - "csTextType": "CS_TEXT", - "csFadeOutSeqPlayer": "CS_FADE_OUT", - "csTransitionType": "CS_TRANS", - "csDestination": "CS_DEST", - "csPlayerCueId": "PLAYER_CUEID", - "naviQuestHintType": "NAVI_QUEST_HINTS", - "ocarinaSongActionId": "OCARINA_ACTION", - } - - self.name = self.id.removeprefix(f"{keyToPrefix[self.parentKey]}_") - - if self.parentKey in ["csCmd", "csPlayerCueId"]: - split = self.name.split("_") - if self.parentKey == "csCmd" and "ACTOR_CUE" in self.id: - self.name = f"Actor Cue {split[-2]}_{split[-1]}" - else: - self.name = f"Player Cue Id {split[-1]}" - else: - self.name = self.name.replace("_", " ").title() - - -@dataclass -class OoT_EnumElement(OoT_BaseElement): - items: list[OoT_ItemElement] - itemByKey: dict[str, OoT_ItemElement] = field(default_factory=dict) - itemByIndex: dict[int, OoT_ItemElement] = field(default_factory=dict) - itemById: dict[int, OoT_ItemElement] = field(default_factory=dict) - - def __post_init__(self): - self.itemByKey = {item.key: item for item in self.items} - self.itemByIndex = {item.index: item for item in self.items} - self.itemById = {item.id: item for item in self.items} - - -class OoT_EnumData: - """Cutscene and misc enum data""" - - def __init__(self): - # general enumData list - self.enumDataList: list[OoT_EnumElement] = [] - - # Path to the ``EnumData.xml`` file - enumDataXML = path.dirname(path.abspath(__file__)) + "/xml/EnumData.xml" - enumDataRoot = getXMLRoot(enumDataXML) - - for enum in enumDataRoot.iterfind("Enum"): - self.enumDataList.append( - OoT_EnumElement( - enum.attrib["ID"], - enum.attrib["Key"], - None, - None, - [ - OoT_ItemElement( - 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, - int(item.attrib["Index"]), - enum.attrib["Key"], - ) - for item in enum - ], - ) - ) - - # create list of tuples used by Blender's enum properties - self.deletedEntry = ("None", "(Deleted from the XML)", "None") - - self.ootEnumCsCmd: list[tuple[str, str, str]] = [] - self.ootEnumCsMiscType: list[tuple[str, str, str]] = [] - self.ootEnumCsTextType: list[tuple[str, str, str]] = [] - self.ootEnumCsFadeOutSeqPlayer: list[tuple[str, str, str]] = [] - self.ootEnumCsTransitionType: list[tuple[str, str, str]] = [] - self.ootEnumCsDestination: list[tuple[str, str, str]] = [] - self.ootEnumCsPlayerCueId: list[tuple[str, str, str]] = [] - self.ootEnumNaviQuestHintType: list[tuple[str, str, str]] = [] - self.ootEnumOcarinaSongActionId: list[tuple[str, str, str]] = [] - self.ootEnumSeqId: list[tuple[str, str, str]] = [] - - self.enumByID = {enum.id: enum for enum in self.enumDataList} - self.enumByKey = {enum.key: enum for enum in self.enumDataList} - - for key in self.enumByKey.keys(): - setattr(self, "ootEnum" + key[0].upper() + key[1:], self.getOoTEnumData(key)) - - def getOoTEnumData(self, enumKey: str): - enum = self.enumByKey[enumKey] - firstIndex = min(1, *(item.index for item in enum.items)) - lastIndex = max(1, *(item.index for item in enum.items)) + 1 - enumData = [self.deletedEntry] * lastIndex - custom = ("Custom", "Custom", "Custom") - - for item in enum.items: - if item.index < lastIndex: - identifier = item.key - enumData[item.index] = (identifier, item.name, item.id) - - if firstIndex > 0: - enumData[0] = custom - else: - enumData.insert(0, custom) - - return enumData diff --git a/fast64_internal/oot/data/oot_object_data.py b/fast64_internal/oot/data/oot_object_data.py deleted file mode 100644 index 28970a80c..000000000 --- a/fast64_internal/oot/data/oot_object_data.py +++ /dev/null @@ -1,55 +0,0 @@ -from dataclasses import dataclass -from os import path -from ...utility import PluginError -from .oot_getters import getXMLRoot -from .oot_data import OoT_BaseElement - -# Note: "object" in this context refers to an OoT Object file (like ``gameplay_keep``) - - -@dataclass -class OoT_ObjectElement(OoT_BaseElement): - pass - - -class OoT_ObjectData: - """Everything related to OoT objects""" - - def __init__(self): - # general object list - self.objectList: list[OoT_ObjectElement] = [] - - # Path to the ``ObjectList.xml`` file - objectXML = path.dirname(path.abspath(__file__)) + "/xml/ObjectList.xml" - objectRoot = getXMLRoot(objectXML) - - for obj in objectRoot.iterfind("Object"): - objName = f"{obj.attrib['Name']} - {obj.attrib['ID'].removeprefix('OBJECT_')}" - self.objectList.append( - OoT_ObjectElement(obj.attrib["ID"], obj.attrib["Key"], objName, int(obj.attrib["Index"])) - ) - - self.objectsByID = {obj.id: obj for obj in self.objectList} - self.objectsByKey = {obj.key: obj for obj in self.objectList} - - # list of tuples used by Blender's enum properties - self.deletedEntry = ("None", "(Deleted from the XML)", "None") - lastIndex = max(1, *(obj.index for obj in self.objectList)) - self.ootEnumObjectKey = self.getObjectIDList(lastIndex + 1, False) - - # create the legacy object list for old blends - self.ootEnumObjectIDLegacy = self.getObjectIDList(self.objectsByKey["obj_timeblock"].index + 1, True) - - # validate the legacy list, if there's any None element then something's wrong - if self.deletedEntry in self.ootEnumObjectIDLegacy: - raise PluginError("ERROR: Legacy Object List doesn't match!") - - def getObjectIDList(self, max: int, isLegacy: bool): - """Generates and returns the object list in the right order""" - objList = [self.deletedEntry] * max - for obj in self.objectList: - if obj.index < max: - identifier = obj.id if isLegacy else obj.key - objList[obj.index] = (identifier, obj.name, obj.id) - objList[0] = ("Custom", "Custom Object", "Custom") - return objList diff --git a/fast64_internal/oot/exporter/scene/__init__.py b/fast64_internal/oot/exporter/scene/__init__.py deleted file mode 100644 index f8b83cdd2..000000000 --- a/fast64_internal/oot/exporter/scene/__init__.py +++ /dev/null @@ -1,242 +0,0 @@ -from dataclasses import dataclass -from mathutils import Matrix -from bpy.types import Object -from typing import Optional -from ....utility import PluginError, CData, indent -from ....f3d.f3d_gbi import TextureExportSettings, ScrollMethod -from ...scene.properties import OOTSceneHeaderProperty -from ...oot_model_classes import OOTModel, OOTGfxFormatter -from ..file import SceneFile -from ..utility import Utility, altHeaderList -from ..collision import CollisionHeader -from .header import SceneAlternateHeader, SceneHeader -from .rooms import RoomEntries - - -@dataclass -class Scene: - """This class defines a scene""" - - name: str - model: OOTModel - mainHeader: Optional[SceneHeader] - altHeader: Optional[SceneAlternateHeader] - rooms: Optional[RoomEntries] - colHeader: Optional[CollisionHeader] - hasAlternateHeaders: bool - - @staticmethod - def new(name: str, sceneObj: Object, transform: Matrix, useMacros: bool, saveTexturesAsPNG: bool, model: OOTModel): - i = 0 - rooms = RoomEntries.new( - f"{name}_roomList", name.removesuffix("_scene"), model, sceneObj, transform, saveTexturesAsPNG - ) - - colHeader = CollisionHeader.new( - f"{name}_collisionHeader", - name, - sceneObj, - transform, - useMacros, - True, - ) - - mainHeader = SceneHeader.new(f"{name}_header{i:02}", sceneObj.ootSceneHeader, sceneObj, transform, i, useMacros) - hasAlternateHeaders = False - altHeader = SceneAlternateHeader(f"{name}_alternateHeaders") - altProp = sceneObj.ootAlternateSceneHeaders - - for i, header in enumerate(altHeaderList, 1): - altP: OOTSceneHeaderProperty = getattr(altProp, f"{header}Header") - if not altP.usePreviousHeader: - setattr( - altHeader, header, SceneHeader.new(f"{name}_header{i:02}", altP, sceneObj, transform, i, useMacros) - ) - hasAlternateHeaders = True - - altHeader.cutscenes = [ - SceneHeader.new(f"{name}_header{i:02}", csHeader, sceneObj, transform, i, useMacros) - for i, csHeader in enumerate(altProp.cutsceneHeaders, 4) - ] - - hasAlternateHeaders = True if len(altHeader.cutscenes) > 0 else hasAlternateHeaders - altHeader = altHeader if hasAlternateHeaders else None - return Scene(name, model, mainHeader, altHeader, rooms, colHeader, hasAlternateHeaders) - - def validateRoomIndices(self): - """Checks if there are multiple rooms with the same room index""" - - for i, room in enumerate(self.rooms.entries): - if i != room.roomIndex: - return False - return True - - def validateScene(self): - """Performs safety checks related to the scene data""" - - if not len(self.rooms.entries) > 0: - raise PluginError("ERROR: This scene does not have any rooms!") - - if not self.validateRoomIndices(): - raise PluginError("ERROR: Room indices do not have a consecutive list of indices.") - - def getSceneHeaderFromIndex(self, headerIndex: int) -> SceneHeader | None: - """Returns the scene header based on the header index""" - - if headerIndex == 0: - return self.mainHeader - - for i, header in enumerate(altHeaderList, 1): - if headerIndex == i: - return getattr(self.altHeader, header) - - for i, csHeader in enumerate(self.altHeader.cutscenes, 4): - if headerIndex == i: - return csHeader - - return None - - def getCmdList(self, curHeader: SceneHeader, hasAltHeaders: bool): - """Returns the scene's commands list""" - - cmdListData = CData() - listName = f"SceneCmd {curHeader.name}" - - # .h - cmdListData.header = f"extern {listName}[]" + ";\n" - - # .c - cmdListData.source = ( - (f"{listName}[]" + " = {\n") - + (Utility.getAltHeaderListCmd(self.altHeader.name) if hasAltHeaders else "") - + self.colHeader.getCmd() - + self.rooms.getCmd() - + curHeader.infos.getCmds(curHeader.lighting) - + curHeader.lighting.getCmd() - + curHeader.path.getCmd() - + (curHeader.transitionActors.getCmd() if len(curHeader.transitionActors.entries) > 0 else "") - + curHeader.spawns.getCmd() - + curHeader.entranceActors.getCmd() - + (curHeader.exits.getCmd() if len(curHeader.exits.exitList) > 0 else "") - + (curHeader.cutscene.getCmd() if len(curHeader.cutscene.entries) > 0 else "") - + Utility.getEndCmd() - + "};\n\n" - ) - - return cmdListData - - def getSceneMainC(self): - """Returns the main informations of the scene as ``CData``""" - - sceneC = CData() - headers: list[tuple[SceneHeader, str]] = [] - altHeaderPtrs = None - - if self.hasAlternateHeaders: - headers = [ - (self.altHeader.childNight, "Child Night"), - (self.altHeader.adultDay, "Adult Day"), - (self.altHeader.adultNight, "Adult Night"), - ] - - for i, csHeader in enumerate(self.altHeader.cutscenes): - 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 "" - for i, (curHeader, _) in enumerate(headers, 1) - ) - - headers.insert(0, (self.mainHeader, "Child Day (Default)")) - for i, (curHeader, headerDesc) in enumerate(headers): - if curHeader is not None: - sceneC.source += "/**\n * " + f"Header {headerDesc}\n" + "*/\n" - sceneC.append(self.getCmdList(curHeader, i == 0 and self.hasAlternateHeaders)) - - if i == 0: - if self.hasAlternateHeaders and altHeaderPtrs is not None: - altHeaderListName = f"SceneCmd* {self.altHeader.name}[]" - sceneC.header += f"extern {altHeaderListName};\n" - sceneC.source += altHeaderListName + " = {\n" + altHeaderPtrs + "\n};\n\n" - - # Write the room segment list - sceneC.append(self.rooms.getC(self.mainHeader.infos.useDummyRoomList)) - - sceneC.append(curHeader.getC()) - - return sceneC - - def getSceneCutscenesC(self): - """Returns the cutscene informations of the scene as ``CData``""" - - csDataList: list[CData] = [] - headers: list[SceneHeader] = [ - self.mainHeader, - ] - - if self.altHeader is not None: - headers.extend( - [ - self.altHeader.childNight, - self.altHeader.adultDay, - self.altHeader.adultNight, - ] - ) - headers.extend(self.altHeader.cutscenes) - - for curHeader in headers: - if curHeader is not None: - for csEntry in curHeader.cutscene.entries: - csDataList.append(csEntry.getC()) - - return csDataList - - def getSceneTexturesC(self, textureExportSettings: TextureExportSettings): - """ - Writes the textures and material setup displaylists that are shared between multiple rooms - (is written to the scene) - """ - - return self.model.to_c(textureExportSettings, OOTGfxFormatter(ScrollMethod.Vertex)).all() - - def getNewSceneFile(self, path: str, isSingleFile: bool, textureExportSettings: TextureExportSettings): - """Returns a new scene file containing the C data""" - - sceneMainData = self.getSceneMainC() - sceneCollisionData = self.colHeader.getC() - sceneCutsceneData = self.getSceneCutscenesC() - sceneTexturesData = self.getSceneTexturesC(textureExportSettings) - - includes = ( - "\n".join( - [ - '#include "ultra64.h"', - '#include "macros.h"', - '#include "z64.h"', - ] - ) - + "\n\n\n" - ) - - return SceneFile( - self.name, - sceneMainData.source, - sceneCollisionData.source, - [cs.source for cs in sceneCutsceneData], - sceneTexturesData.source, - { - room.roomIndex: room.getNewRoomFile(path, isSingleFile, textureExportSettings) - for room in self.rooms.entries - }, - isSingleFile, - path, - ( - f"#ifndef {self.name.upper()}_H\n" - + f"#define {self.name.upper()}_H\n\n" - + includes - + sceneMainData.header - + "".join(cs.header for cs in sceneCutsceneData) - + sceneCollisionData.header - + sceneTexturesData.header - ), - ) diff --git a/fast64_internal/oot/f3d/operators.py b/fast64_internal/oot/f3d/operators.py deleted file mode 100644 index 92208755e..000000000 --- a/fast64_internal/oot/f3d/operators.py +++ /dev/null @@ -1,328 +0,0 @@ -import bpy, os, mathutils -from bpy.types import Operator, Mesh -from bpy.ops import object -from bpy.path import abspath -from bpy.utils import register_class, unregister_class -from mathutils import Matrix -from ...utility import CData, PluginError, raisePluginError, writeCData, writeXMLData, toAlnum -from ...f3d.f3d_parser import importMeshC, getImportData -from ...f3d.f3d_gbi import DLFormat, F3D, TextureExportSettings, ScrollMethod, get_F3D_GBI -from ...f3d.f3d_writer import TriangleConverterInfo, removeDL, saveStaticModel, getInfoDict -from ..oot_utility import ootGetObjectPath, getOOTScale -from ..oot_model_classes import OOTF3DContext, ootGetIncludedAssetData -from ..oot_texture_array import ootReadTextureArrays -from ..oot_model_classes import OOTModel, OOTGfxFormatter -from ..oot_f3d_writer import ootReadActorScale, writeTextureArraysNew, writeTextureArraysExisting -from .properties import OOTDLImportSettings, OOTDLExportSettings - -from ..oot_utility import ( - OOTObjectCategorizer, - ootDuplicateHierarchy, - ootCleanupScene, - ootGetPath, - addIncludeFiles, - getOOTScale, -) - - -def ootConvertMeshToC( - originalObj: bpy.types.Object, - finalTransform: mathutils.Matrix, - DLFormat: DLFormat, - saveTextures: bool, - settings: OOTDLExportSettings, -): - folderName = settings.folder - exportPath = bpy.path.abspath(settings.customPath) - isCustomExport = settings.isCustom - drawLayer = settings.drawLayer - removeVanillaData = settings.removeVanillaData - name = toAlnum(originalObj.name) - overlayName = settings.actorOverlayName - flipbookUses2DArray = settings.flipbookUses2DArray - flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None - - try: - obj, allObjs = ootDuplicateHierarchy(originalObj, None, False, OOTObjectCategorizer()) - - fModel = OOTModel(name, DLFormat, drawLayer) - triConverterInfo = TriangleConverterInfo(obj, None, fModel.f3d, finalTransform, getInfoDict(obj)) - fMeshes = saveStaticModel( - triConverterInfo, fModel, obj, finalTransform, fModel.name, not saveTextures, False, "oot" - ) - - # Since we provide a draw layer override, there should only be one fMesh. - for drawLayer, fMesh in fMeshes.items(): - fMesh.draw.name = name - - ootCleanupScene(originalObj, allObjs) - - except Exception as e: - ootCleanupScene(originalObj, allObjs) - raise Exception(str(e)) - - data = CData() - data.source += '#include "ultra64.h"\n#include "global.h"\n' - if not isCustomExport: - data.source += '#include "' + folderName + '.h"\n\n' - else: - data.source += "\n" - - path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, True) - includeDir = settings.customAssetIncludeDir if settings.isCustom else f"assets/objects/{folderName}" - exportData = fModel.to_c( - TextureExportSettings(False, saveTextures, includeDir, path), OOTGfxFormatter(ScrollMethod.Vertex) - ) - - data.append(exportData.all()) - - if isCustomExport: - textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D) - data.append(textureArrayData) - - filename = settings.filename if settings.isCustomFilename else name - writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) - - if not isCustomExport: - writeTextureArraysExisting(bpy.context.scene.ootDecompPath, overlayName, False, flipbookArrayIndex2D, fModel) - addIncludeFiles(folderName, path, name) - if removeVanillaData: - headerPath = os.path.join(path, folderName + ".h") - sourcePath = os.path.join(path, folderName + ".c") - removeDL(sourcePath, headerPath, name) - - -def ootConvertMeshToXML( - originalObj: bpy.types.Object, - finalTransform: mathutils.Matrix, - DLFormat: DLFormat, - saveTextures: bool, - settings: OOTDLExportSettings, - logging_func, -): - logging_func({"INFO"}, "ootConvertMeshToXML 1") - - folderName = settings.folder - exportPath = bpy.path.abspath(settings.customPath) - isCustomExport = settings.isCustom - drawLayer = settings.drawLayer - removeVanillaData = settings.removeVanillaData - name = toAlnum(originalObj.name) - overlayName = settings.actorOverlayName - flipbookUses2DArray = settings.flipbookUses2DArray - flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None - - logging_func({"INFO"}, "ootConvertMeshToXML 2") - - try: - obj, allObjs = ootDuplicateHierarchy(originalObj, None, False, OOTObjectCategorizer()) - - logging_func({"INFO"}, "ootConvertMeshToXML 3") - - fModel = OOTModel(name, DLFormat, drawLayer) - - logging_func({"INFO"}, "ootConvertMeshToXML 4") - - triConverterInfo = TriangleConverterInfo(obj, None, fModel.f3d, finalTransform, getInfoDict(obj)) - - logging_func({"INFO"}, "ootConvertMeshToXML 5") - - fMeshes = saveStaticModel( - triConverterInfo, - fModel, - obj, - finalTransform, - fModel.name, - not saveTextures, - False, - "oot", - logging_func=logging_func, - ) - - logging_func({"INFO"}, "ootConvertMeshToXML 6") - - # Since we provide a draw layer override, there should only be one fMesh. - for drawLayer, fMesh in fMeshes.items(): - fMesh.draw.name = name - - logging_func({"INFO"}, "ootConvertMeshToXML 7") - - ootCleanupScene(originalObj, allObjs) - - logging_func({"INFO"}, "ootConvertMeshToXML 8") - - except Exception as e: - ootCleanupScene(originalObj, allObjs) - raise Exception(str(e)) - - logging_func( - {"INFO"}, "ootConvertMeshToXML 9.1 exportPath=" + (str(exportPath) if exportPath is not None else "None") - ) - logging_func( - {"INFO"}, - "ootConvertMeshToXML 9.2 settings.customAssetIncludeDir=" - + (str(settings.customAssetIncludeDir) if settings.customAssetIncludeDir is not None else "None"), - ) - - path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, True) - - logging_func({"INFO"}, "ootConvertMeshToXML 10.1 path=" + (str(path) if path is not None else "None")) - logging_func( - {"INFO"}, "ootConvertMeshToXML 10.2 folderName=" + (str(folderName) if folderName is not None else "None") - ) - - data = fModel.to_xml(exportPath, folderName, logging_func) - - logging_func({"INFO"}, "ootConvertMeshToXML 11") - - if isCustomExport: - textureArrayData = writeTextureArraysNewXML(fModel, flipbookArrayIndex2D) - data += textureArrayData - - logging_func({"INFO"}, "ootConvertMeshToXML 12") - - -def writeTextureArraysNewXML(fModel: OOTModel, arrayIndex: int): - textureArrayData = "" - # for flipbook in fModel.flipbooks: - # if flipbook.exportMode == "Array": - # if arrayIndex is not None: - # textureArrayData += flipbook_2d_to_xml(flipbook, True, arrayIndex + 1) + "\n" - # else: - # textureArrayData += flipbook_to_xml(flipbook, True) + "\n" - return textureArrayData - - -class OOT_ImportDL(Operator): - # set bl_ properties - bl_idname = "object.oot_import_dl" - bl_label = "Import DL" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - obj = None - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - - try: - settings: OOTDLImportSettings = context.scene.fast64.oot.DLImportSettings - name = settings.name - folderName = settings.folder - importPath = abspath(settings.customPath) - isCustomImport = settings.isCustom - basePath = abspath(context.scene.ootDecompPath) if not isCustomImport else os.path.dirname(importPath) - removeDoubles = settings.removeDoubles - importNormals = settings.importNormals - drawLayer = settings.drawLayer - overlayName = settings.actorOverlayName - flipbookUses2DArray = settings.flipbookUses2DArray - flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None - - paths = [ootGetObjectPath(isCustomImport, importPath, folderName, True)] - filedata = getImportData(paths) - f3dContext = OOTF3DContext(get_F3D_GBI(), [name], basePath) - - scale = getOOTScale(settings.actorScale) - if not isCustomImport: - filedata = ootGetIncludedAssetData(basePath, paths, filedata) + filedata - - if overlayName is not None: - ootReadTextureArrays(basePath, overlayName, name, f3dContext, False, flipbookArrayIndex2D) - if settings.autoDetectActorScale: - scale = ootReadActorScale(basePath, overlayName, False) - - obj = importMeshC( - filedata, - name, - scale, - removeDoubles, - importNormals, - drawLayer, - f3dContext, - ) - obj.ootActorScale = scale / context.scene.ootBlenderScale - - self.report({"INFO"}, "Success!") - return {"FINISHED"} - - except Exception as e: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - -class OOT_ExportDL(Operator): - # set bl_ properties - bl_idname = "object.oot_export_dl" - bl_label = "Export DL" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - self.report({"INFO"}, "OOT_ExportDL execute 0") - obj = None - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - self.report({"INFO"}, "OOT_ExportDL execute 1") - if len(context.selected_objects) == 0: - raise PluginError("Mesh not selected.") - self.report({"INFO"}, "OOT_ExportDL execute 2") - obj = context.active_object - if obj.type != "MESH": - raise PluginError("Mesh not selected.") - - self.report({"INFO"}, "OOT_ExportDL execute 3") - - finalTransform = Matrix.Scale(getOOTScale(obj.ootActorScale), 4) - - try: - # exportPath, levelName = getPathAndLevel(context.scene.geoCustomExport, - # context.scene.geoExportPath, context.scene.geoLevelName, - # context.scene.geoLevelOption) - - saveTextures = context.scene.saveTextures - exportSettings = context.scene.fast64.oot.DLExportSettings - - self.report({"INFO"}, "OOT_ExportDL execute 4") - - if context.scene.fast64.oot.featureSet == "HM64": - ootConvertMeshToXML(obj, finalTransform, DLFormat.Static, saveTextures, exportSettings, self.report) - elif context.scene.fast64.mk64.featureSet == "HM64": - ootConvertMeshToXML(obj, finalTransform, DLFormat.Static, saveTextures, exportSettings, self.report) - else: - ootConvertMeshToC( - obj, - finalTransform, - DLFormat.Static, - saveTextures, - exportSettings, - ) - - self.report({"INFO"}, "Success!") - return {"FINISHED"} - - except Exception as e: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - -oot_dl_writer_classes = ( - OOT_ImportDL, - OOT_ExportDL, -) - - -def f3d_ops_register(): - for cls in oot_dl_writer_classes: - register_class(cls) - - -def f3d_ops_unregister(): - for cls in reversed(oot_dl_writer_classes): - unregister_class(cls) diff --git a/fast64_internal/oot/importer/scene_collision.py b/fast64_internal/oot/importer/scene_collision.py deleted file mode 100644 index 2cf26200e..000000000 --- a/fast64_internal/oot/importer/scene_collision.py +++ /dev/null @@ -1,363 +0,0 @@ -import math -import re -import bpy -import mathutils - -from random import random -from collections import OrderedDict -from ...utility import PluginError, parentObject, hexOrDecInt, yUpToZUp -from ..collision.properties import OOTMaterialCollisionProperty -from ..oot_f3d_writer import getColliderMat -from ..oot_utility import setCustomProperty, ootParseRotation -from .utility import getDataMatch, getBits, checkBit, createCurveFromPoints, stripName -from .classes import SharedSceneData - -from ..collision.constants import ( - ootEnumFloorSetting, - ootEnumWallSetting, - ootEnumFloorProperty, - ootEnumCollisionTerrain, - ootEnumCollisionSound, - ootEnumCameraSType, - ootEnumCameraCrawlspaceSType, -) - - -def parseCrawlSpaceData( - setting: str, sceneData: str, posDataName: str, index: int, count: int, objName: str, orderIndex: str -): - camPosData = getDataMatch(sceneData, posDataName, "Vec3s", "camera position list") - camPosList = [value.replace("{", "").strip() for value in camPosData.split("},") if value.strip() != ""] - posData = [camPosList[index : index + count][i] for i in range(0, count, 3)] - - points = [] - for posDataItem in posData: - points.append([hexOrDecInt(value.strip()) for value in posDataItem.split(",")]) - - # name is important for alphabetical ordering - curveObj = createCurveFromPoints(points, objName) - curveObj.show_name = True - crawlProp = curveObj.ootSplineProperty - crawlProp.splineType = "Crawlspace" - crawlProp.index = orderIndex - setCustomProperty(crawlProp, "camSType", "CAM_SET_CRAWLSPACE", ootEnumCameraCrawlspaceSType) - - return curveObj - - -def parseCamDataList(sceneObj: bpy.types.Object, camDataListName: str, sceneData: str): - camMatchData = getDataMatch(sceneData, camDataListName, ["CamData", "BgCamInfo"], "camera data list") - camDataList = [value.replace("{", "").strip() for value in camMatchData.split("},") if value.strip() != ""] - - # orderIndex used for naming cameras in alphabetical order - orderIndex = 0 - for camEntry in camDataList: - setting, count, posDataName = [value.strip() for value in camEntry.split(",")] - index = None - - objName = f"{sceneObj.name}_camPos_{format(orderIndex, '03')}" - - if posDataName != "NULL" and posDataName != "0": - index = hexOrDecInt(posDataName[posDataName.index("[") + 1 : -1]) - posDataName = posDataName[1 : posDataName.index("[")] # remove '&' and '[n]' - - if setting == "CAM_SET_CRAWLSPACE" or setting == "0x001E": - obj = parseCrawlSpaceData(setting, sceneData, posDataName, index, hexOrDecInt(count), objName, orderIndex) - else: - obj = parseCamPosData(setting, sceneData, posDataName, index, objName, orderIndex) - - parentObject(sceneObj, obj) - orderIndex += 1 - - -def parseCamPosData(setting: str, sceneData: str, posDataName: str, index: int, objName: str, orderIndex: str): - camera = bpy.data.cameras.new("Camera") - camObj = bpy.data.objects.new(objName, camera) - bpy.context.scene.collection.objects.link(camObj) - camProp = camObj.ootCameraPositionProperty - setCustomProperty(camProp, "camSType", setting, ootEnumCameraSType) - camProp.hasPositionData = posDataName != "NULL" and posDataName != "0" - camProp.index = orderIndex - - # name is important for alphabetical ordering - camObj.name = objName - - if index is None: - camObj.location = [0, 0, 0] - return camObj - - camPosData = getDataMatch(sceneData, posDataName, "Vec3s", "camera position list") - camPosList = [value.replace("{", "").strip() for value in camPosData.split("},") if value.strip() != ""] - - posData = camPosList[index : index + 3] - position = yUpToZUp @ mathutils.Vector( - [hexOrDecInt(value.strip()) / bpy.context.scene.ootBlenderScale for value in posData[0].split(",")] - ) - - # camera faces opposite direction - rotation = ( - yUpToZUp.to_quaternion() - @ mathutils.Euler( - ootParseRotation([hexOrDecInt(value.strip()) for value in posData[1].split(",")]) - ).to_quaternion() - @ mathutils.Quaternion((0, 1, 0), math.radians(180.0)) - ).to_euler() - - fov, bgImageOverrideIndex, unknown = [value.strip() for value in posData[2].split(",")] - - camObj.location = position - camObj.rotation_euler = rotation - camObj.show_name = True - - camProp = camObj.ootCameraPositionProperty - camProp.bgImageOverrideIndex = hexOrDecInt(bgImageOverrideIndex) - - fovValue = hexOrDecInt(fov) - fovValue = int.from_bytes(fovValue.to_bytes(2, "big", signed=fovValue < 0x8000), "big", signed=True) - if fovValue > 360: - fovValue *= 0.01 # see CAM_DATA_SCALED() macro - camObj.data.angle = math.radians(fovValue) - - return camObj - - -def parseWaterBoxes( - sceneObj: bpy.types.Object, - roomObjs: list[bpy.types.Object], - sceneData: str, - waterBoxListName: str, -): - waterBoxListData = getDataMatch(sceneData, waterBoxListName, "WaterBox", "water box list") - waterBoxList = [value.replace("{", "").strip() for value in waterBoxListData.split("},") if value.strip() != ""] - - # orderIndex used for naming cameras in alphabetical order - orderIndex = 0 - for waterBoxData in waterBoxList: - objName = f"{sceneObj.name}_waterBox_{format(orderIndex, '03')}" - params = [value.strip() for value in waterBoxData.split(",")] - topCorner = yUpToZUp @ mathutils.Vector( - [hexOrDecInt(value) / bpy.context.scene.ootBlenderScale for value in params[0:3]] - ) - dimensions = [hexOrDecInt(value) / bpy.context.scene.ootBlenderScale for value in params[3:5]] - properties = hexOrDecInt(params[5]) - - height = 1000 / bpy.context.scene.ootBlenderScale # just to add volume - - location = mathutils.Vector([0, 0, 0]) - scale = [dimensions[0] / 2, dimensions[1] / 2, height / 2] - location.x = topCorner[0] + scale[0] # x - location.y = topCorner[1] - scale[1] # -z - location.z = topCorner.z - scale[2] # y - - waterBoxObj = bpy.data.objects.new(objName, None) - bpy.context.scene.collection.objects.link(waterBoxObj) - waterBoxObj.location = location - waterBoxObj.scale = scale - waterBoxProp = waterBoxObj.ootWaterBoxProperty - - waterBoxObj.show_name = True - waterBoxObj.ootEmptyType = "Water Box" - flag19 = checkBit(properties, 19) - roomIndex = getBits(properties, 13, 6) - waterBoxProp.lighting = getBits(properties, 8, 5) - waterBoxProp.camera = getBits(properties, 0, 8) - waterBoxProp.flag19 = flag19 - - # 0x3F = -1 in 6bit value - parentObject(roomObjs[roomIndex] if roomIndex != 0x3F else sceneObj, waterBoxObj) - orderIndex += 1 - - -def parseSurfaceParams( - surface: tuple[int, int], polygonParams: tuple[bool, bool, bool, bool], collision: OOTMaterialCollisionProperty -): - params = surface - ignoreCamera, ignoreActor, ignoreProjectile, enableConveyor = polygonParams - - collision.eponaBlock = checkBit(params[0], 31) - collision.decreaseHeight = checkBit(params[0], 30) - setCustomProperty(collision, "floorSetting", str(getBits(params[0], 26, 4)), ootEnumFloorSetting) - setCustomProperty(collision, "wallSetting", str(getBits(params[0], 21, 5)), ootEnumWallSetting) - setCustomProperty(collision, "floorProperty", str(getBits(params[0], 13, 8)), ootEnumFloorProperty) - collision.exitID = getBits(params[0], 8, 5) - collision.cameraID = getBits(params[0], 0, 8) - collision.isWallDamage = checkBit(params[1], 27) - - collision.conveyorRotation = (getBits(params[1], 21, 6) / 0x3F) * (2 * math.pi) - collision.conveyorSpeed = "Custom" - collision.conveyorSpeedCustom = str(getBits(params[1], 18, 3)) - - if collision.conveyorRotation == 0 and collision.conveyorSpeedCustom == "0": - collision.conveyorOption = "None" - elif enableConveyor: - collision.conveyorOption = "Land" - else: - collision.conveyorOption = "Water" - - collision.hookshotable = checkBit(params[1], 17) - collision.echo = str(getBits(params[1], 11, 6)) - collision.lightingSetting = getBits(params[1], 6, 5) - setCustomProperty(collision, "terrain", str(getBits(params[1], 4, 2)), ootEnumCollisionTerrain) - setCustomProperty(collision, "sound", str(getBits(params[1], 0, 4)), ootEnumCollisionSound) - - collision.ignoreCameraCollision = ignoreCamera - collision.ignoreActorCollision = ignoreActor - collision.ignoreProjectileCollision = ignoreProjectile - - -def parseSurfaces(surfaceList: list[str]): - surfaces = [] - for surfaceData in surfaceList: - params = [hexOrDecInt(value.strip()) for value in surfaceData.split(",")] - surfaces.append(tuple(params)) - - return surfaces - - -def parseVertices(vertexList: list[str]): - vertices = [] - for vertexData in vertexList: - vertex = [hexOrDecInt(value.strip()) / bpy.context.scene.ootBlenderScale for value in vertexData.split(",")] - position = yUpToZUp @ mathutils.Vector(vertex) - vertices.append(position) - - return vertices - - -def parsePolygon(polygonData: str): - shorts = [ - hexOrDecInt(value.strip()) if "COLPOLY_SNORMAL" not in value else value.strip() - for value in polygonData.split(",") - ] - vertIndices = [0, 0, 0] - - # 00 - surfaceIndex = shorts[0] - - # 02 - vertIndices[0] = shorts[1] & 0x1FFF - ignoreCamera = 1 & (shorts[1] >> 13) == 1 - ignoreActor = 1 & (shorts[1] >> 14) == 1 - ignoreProjectile = 1 & (shorts[1] >> 15) == 1 - - # 04 - vertIndices[1] = shorts[2] & 0x1FFF - enableConveyor = 1 & (shorts[2] >> 13) == 1 - - # 06 - vertIndices[2] = shorts[3] & 0x1FFF - - # 08-0C - normal = [] - for value in shorts[4:7]: - if isinstance(value, str) and "COLPOLY_SNORMAL" in value: - normal.append(float(value[value.index("(") + 1 : value.index(")")])) - else: - normal.append(int.from_bytes(value.to_bytes(2, "big", signed=value < 0x8000), "big", signed=True) / 0x7FFF) - - # 0E - distance = shorts[7] - - return (ignoreCamera, ignoreActor, ignoreProjectile, enableConveyor), surfaceIndex, vertIndices, normal - - -def parseCollisionHeader( - sceneObj: bpy.types.Object, - roomObjs: list[bpy.types.Object], - sceneData: str, - collisionHeaderName: str, - sharedSceneData: SharedSceneData, -): - match = re.search( - rf"CollisionHeader\s*{re.escape(collisionHeaderName)}\s*=\s*\{{\s*\{{(.*?)\}}\s*,\s*\{{(.*?)\}}\s*,(.*?)\}}\s*;", - sceneData, - flags=re.DOTALL, - ) - - if not match: - match = re.search( - rf"CollisionHeader\s*{re.escape(collisionHeaderName)}\s*=\s*\{{(.*?)\}}\s*;", - sceneData, - flags=re.DOTALL, - ) - if not match: - raise PluginError(f"Could not find collision header {collisionHeaderName}.") - - params = [value.strip() for value in match.group(1).split(",")] - minBounds = [hexOrDecInt(value.strip()) for value in params[0:3]] - maxBounds = [hexOrDecInt(value.strip()) for value in params[3:6]] - otherParams = [value.strip() for value in params[6:]] - - else: - minBounds = [hexOrDecInt(value.strip()) for value in match.group(1).split(",")] - maxBounds = [hexOrDecInt(value.strip()) for value in match.group(2).split(",")] - otherParams = [value.strip() for value in match.group(3).split(",")] - - vertexListName = stripName(otherParams[1]) - polygonListName = stripName(otherParams[3]) - surfaceTypeListName = stripName(otherParams[4]) - camDataListName = stripName(otherParams[5]) - waterBoxListName = stripName(otherParams[7]) - - if sharedSceneData.includeCollision: - parseCollision(sceneObj, vertexListName, polygonListName, surfaceTypeListName, sceneData) - if sharedSceneData.includeCameras and camDataListName != "NULL" and camDataListName != "0": - parseCamDataList(sceneObj, camDataListName, sceneData) - if sharedSceneData.includeWaterBoxes and waterBoxListName != "NULL" and waterBoxListName != "0": - parseWaterBoxes(sceneObj, roomObjs, sceneData, waterBoxListName) - - -def parseCollision( - sceneObj: bpy.types.Object, vertexListName: str, polygonListName: str, surfaceTypeListName: str, sceneData: str -): - vertMatchData = getDataMatch(sceneData, vertexListName, "Vec3s", "vertex list") - polyMatchData = getDataMatch(sceneData, polygonListName, "CollisionPoly", "polygon list") - surfMatchData = getDataMatch(sceneData, surfaceTypeListName, "SurfaceType", "surface type list") - - vertexList = [value.replace("{", "").strip() for value in vertMatchData.split("},") if value.strip() != ""] - polygonList = [value.replace("{", "").strip() for value in polyMatchData.split("},") if value.strip() != ""] - surfaceList = [value.replace("{", "").strip() for value in surfMatchData.split("},") if value.strip() != ""] - - # Although polygon params are geometry based, we will group them with surface. - collisionDict = OrderedDict() # (surface, polygonParams) : list[triangles] - - surfaces = parseSurfaces(surfaceList) - vertices = parseVertices(vertexList) - - for polygonData in polygonList: - polygonParams, surfaceIndex, vertIndices, normal = parsePolygon(polygonData) - key = (surfaces[surfaceIndex], polygonParams) - if key not in collisionDict: - collisionDict[key] = [] - - collisionDict[key].append((vertIndices, normal)) - - collisionName = f"{sceneObj.name}_collision" - mesh = bpy.data.meshes.new(collisionName) - obj = bpy.data.objects.new(collisionName, mesh) - bpy.context.scene.collection.objects.link(obj) - - triData = [] - triMatData = [] - - surfaceIndex = 0 - for (surface, polygonParams), triList in collisionDict.items(): - randomColor = mathutils.Color((1, 1, 1)) - randomColor.hsv = (random(), 0.5, 0.5) - collisionMat = getColliderMat(f"oot_collision_mat_{surfaceIndex}", randomColor[:] + (0.5,)) - collision = collisionMat.ootCollisionProperty - parseSurfaceParams(surface, polygonParams, collision) - - mesh.materials.append(collisionMat) - for j in range(len(triList)): - triData.append(triList[j][0]) - triMatData += [surfaceIndex] - surfaceIndex += 1 - - mesh.from_pydata(vertices=vertices, edges=[], faces=triData) - for i in range(len(mesh.polygons)): - mesh.polygons[i].material_index = triMatData[i] - - obj.ignore_render = True - - parentObject(sceneObj, obj) diff --git a/fast64_internal/oot/importer/scene_header.py b/fast64_internal/oot/importer/scene_header.py deleted file mode 100644 index 017667b33..000000000 --- a/fast64_internal/oot/importer/scene_header.py +++ /dev/null @@ -1,347 +0,0 @@ -import math -import os -import re -import bpy -import mathutils - -from ...utility import PluginError, readFile, parentObject, hexOrDecInt, gammaInverse -from ...f3d.f3d_parser import parseMatrices -from ..oot_model_classes import OOTF3DContext -from ..scene.properties import OOTSceneHeaderProperty, OOTLightProperty -from ..oot_utility import getEvalParams, setCustomProperty -from .constants import headerNames -from .utility import getDataMatch, stripName -from .classes import SharedSceneData -from .room_header import parseRoomCommands -from .actor import parseTransActorList, parseSpawnList, parseEntranceList -from .scene_collision import parseCollisionHeader -from .scene_pathways import parsePathList - -from ..oot_constants import ( - ootEnumAudioSessionPreset, - ootEnumNightSeq, - ootEnumMusicSeq, - ootEnumCameraMode, - ootEnumMapLocation, - ootEnumNaviHints, - ootEnumGlobalObject, - ootEnumSkybox, - ootEnumCloudiness, - ootEnumSkyboxLighting, -) - - -def parseColor(values: tuple[str, str, str]) -> tuple[float, float, float]: - return tuple(gammaInverse([hexOrDecInt(value) / 0xFF for value in values])) - - -def parseDirection(index: int, values: tuple[str, str, str]) -> tuple[float, float, float] | int: - values = [hexOrDecInt(value) for value in values] - - if tuple(values) == (0, 0, 0): - return "Zero" - elif index == 0 and tuple(values) == (0x49, 0x49, 0x49): - return "Default" - elif index == 1 and tuple(values) == (0xB7, 0xB7, 0xB7): - return "Default" - else: - direction = mathutils.Vector( - [int.from_bytes(value.to_bytes(1, "big", signed=value < 127), "big", signed=True) / 127 for value in values] - ) - - return ( - mathutils.Euler((0, 0, math.pi)).to_quaternion() - @ (mathutils.Euler((math.pi / 2, 0, 0)).to_quaternion() @ direction).rotation_difference( - mathutils.Vector((0, 0, 1)) - ) - ).to_euler() - - -def parseLight( - lightHeader: OOTLightProperty, index: int, rotation: mathutils.Euler, color: mathutils.Vector -) -> bpy.types.Object | None: - setattr(lightHeader, f"useCustomDiffuse{index}", rotation != "Zero" and rotation != "Default") - - if rotation == "Zero" or rotation == "Default": - setattr(lightHeader, f"zeroDiffuse{index}", rotation == "Zero") - setattr(lightHeader, f"diffuse{index}", color + (1,)) - return None - else: - light = bpy.data.lights.new("Light", "SUN") - lightObj = bpy.data.objects.new("Light", light) - bpy.context.scene.collection.objects.link(lightObj) - setattr(lightHeader, f"diffuse{index}Custom", lightObj.data) - lightObj.rotation_euler = rotation - lightObj.data.color = color - lightObj.data.type = "SUN" - return lightObj - - -def parseLightList( - sceneObj: bpy.types.Object, - sceneHeader: OOTSceneHeaderProperty, - sceneData: str, - lightListName: str, - headerIndex: int, -): - lightData = getDataMatch(sceneData, lightListName, ["LightSettings", "EnvLightSettings"], "light list") - - # I currently don't understand the light list format in respect to this lighting flag. - # So we'll set it to custom instead. - if sceneHeader.skyboxLighting != "Custom": - sceneHeader.skyboxLightingCustom = sceneHeader.skyboxLighting - sceneHeader.skyboxLighting = "Custom" - sceneHeader.lightList.clear() - - # convert string to ZAPD format if using new Fast64 output - if "// Ambient Color" in sceneData: - i = 0 - lightData = lightData.replace("{", "").replace("}", "").replace("\n", "").replace(" ", "").replace(",,", ",") - data = "{ " - for part in lightData.split(","): - if i < 20: - if i == 18: - part = getEvalParams(part) - data += part + ", " - if i == 19: - data = data[:-2] - else: - data += "},\n{ " + part + ", " - i = 0 - i += 1 - lightData = data[:-4] - - lightList = [ - value.replace("{", "").replace("\n", "").replace(" ", "") - for value in lightData.split("},") - if value.strip() != "" - ] - - index = 0 - for lightEntry in lightList: - lightParams = [value.strip() for value in lightEntry.split(",")] - - ambientColor = parseColor(lightParams[0:3]) - diffuseDir0 = parseDirection(0, lightParams[3:6]) - diffuseColor0 = parseColor(lightParams[6:9]) - diffuseDir1 = parseDirection(1, lightParams[9:12]) - diffuseColor1 = parseColor(lightParams[12:15]) - fogColor = parseColor(lightParams[15:18]) - - blendFogShort = hexOrDecInt(lightParams[18]) - fogNear = blendFogShort & ((1 << 10) - 1) - transitionSpeed = blendFogShort >> 10 - z_far = hexOrDecInt(lightParams[19]) - - lightHeader = sceneHeader.lightList.add() - lightHeader.ambient = ambientColor + (1,) - - lightObj0 = parseLight(lightHeader, 0, diffuseDir0, diffuseColor0) - lightObj1 = parseLight(lightHeader, 1, diffuseDir1, diffuseColor1) - - if lightObj0 is not None: - parentObject(sceneObj, lightObj0) - lightObj0.location = [4 + headerIndex * 2, 0, -index * 2] - if lightObj1 is not None: - parentObject(sceneObj, lightObj1) - lightObj1.location = [4 + headerIndex * 2, 2, -index * 2] - - lightHeader.fogColor = fogColor + (1,) - lightHeader.fogNear = fogNear - lightHeader.z_far = z_far - lightHeader.transitionSpeed = transitionSpeed - - index += 1 - - -def parseExitList(sceneHeader: OOTSceneHeaderProperty, sceneData: str, exitListName: str): - exitData = getDataMatch(sceneData, exitListName, "u16", "exit list") - - # see also start position list - exitList = [value.strip() for value in exitData.split(",") if value.strip() != ""] - for exit in exitList: - exitProp = sceneHeader.exitList.add() - exitProp.exitIndex = "Custom" - exitProp.exitIndexCustom = exit - - -def parseRoomList( - sceneObj: bpy.types.Object, - sceneData: str, - roomListName: str, - f3dContext: OOTF3DContext, - sharedSceneData: SharedSceneData, - headerIndex: int, -): - roomList = getDataMatch(sceneData, roomListName, "RomFile", "room list") - index = 0 - roomObjs = [] - - # Assumption that alternate scene headers all use the same room list. - for roomMatch in re.finditer( - rf"\{{([\(\)\sA-Za-z0-9\_]*),([\(\)\sA-Za-z0-9\_]*)\}}\s*,", roomList, flags=re.DOTALL - ): - roomName = roomMatch.group(1).strip().replace("SegmentRomStart", "") - if "(u32)" in roomName: - roomName = roomName[5:].strip()[1:] # includes leading underscore - elif "(uintptr_t)" in roomName: - roomName = roomName[11:].strip()[1:] - else: - roomName = roomName[1:] - - roomPath = os.path.join(sharedSceneData.scenePath, f"{roomName}.c") - roomData = readFile(roomPath) - parseMatrices(roomData, f3dContext, 1 / bpy.context.scene.ootBlenderScale) - - roomCommandsName = f"{roomName}Commands" - if roomCommandsName not in roomData: - roomCommandsName = f"{roomName}_header00" # fast64 naming - - # Assumption that any shared textures are stored after the CollisionHeader. - # This is done to avoid including large collision data in regex searches. - try: - collisionHeaderIndex = sceneData.index("CollisionHeader ") - except: - collisionHeaderIndex = 0 - sharedRoomData = sceneData[collisionHeaderIndex:] - roomObj = parseRoomCommands( - roomName, - None, - sharedRoomData + roomData, - roomCommandsName, - index, - f3dContext, - sharedSceneData, - headerIndex, - ) - parentObject(sceneObj, roomObj) - index += 1 - roomObjs.append(roomObj) - - return roomObjs - - -def parseAlternateSceneHeaders( - sceneObj: bpy.types.Object, - roomObjs: list[bpy.types.Object], - sceneData: str, - altHeadersListName: str, - f3dContext: OOTF3DContext, - sharedSceneData: SharedSceneData, -): - altHeadersData = getDataMatch(sceneData, altHeadersListName, ["SceneCmd*", "SCmdBase*"], "alternate header list") - altHeadersList = [value.strip() for value in altHeadersData.split(",") if value.strip() != ""] - - for i in range(len(altHeadersList)): - if not (altHeadersList[i] == "NULL" or altHeadersList[i] == "0"): - parseSceneCommands( - sceneObj.name, sceneObj, roomObjs, altHeadersList[i], sceneData, f3dContext, i + 1, sharedSceneData - ) - - -def parseSceneCommands( - sceneName: str | None, - sceneObj: bpy.types.Object | None, - roomObjs: list[bpy.types.Object] | None, - sceneCommandsName: str, - sceneData: str, - f3dContext: OOTF3DContext, - headerIndex: int, - sharedSceneData: SharedSceneData, -): - if sceneObj is None: - sceneObj = bpy.data.objects.new(sceneCommandsName, None) - bpy.context.scene.collection.objects.link(sceneObj) - sceneObj.empty_display_type = "SPHERE" - sceneObj.ootEmptyType = "Scene" - sceneObj.name = sceneName - - if headerIndex == 0: - sceneHeader = sceneObj.ootSceneHeader - elif headerIndex < 4: - sceneHeader = getattr(sceneObj.ootAlternateSceneHeaders, headerNames[headerIndex]) - sceneHeader.usePreviousHeader = False - else: - cutsceneHeaders = sceneObj.ootAlternateSceneHeaders.cutsceneHeaders - while len(cutsceneHeaders) < headerIndex - 3: - cutsceneHeaders.add() - sceneHeader = cutsceneHeaders[headerIndex - 4] - - commands = getDataMatch(sceneData, sceneCommandsName, ["SceneCmd", "SCmdBase"], "scene commands") - entranceList = None - altHeadersListName = None - 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(",")] - if command == "SCENE_CMD_SOUND_SETTINGS": - setCustomProperty(sceneHeader, "audioSessionPreset", args[0], ootEnumAudioSessionPreset) - setCustomProperty(sceneHeader, "nightSeq", args[1], ootEnumNightSeq) - 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) - - elif command == "SCENE_CMD_MISC_SETTINGS": - setCustomProperty(sceneHeader, "cameraMode", args[0], ootEnumCameraMode) - setCustomProperty(sceneHeader, "mapLocation", args[1], ootEnumMapLocation) - 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 == "SCENE_CMD_ENTRANCE_LIST" and sharedSceneData.includeActors: - 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 == "SCENE_CMD_SPAWN_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], ootEnumSkybox) - setCustomProperty(sceneHeader, "skyboxCloudiness", args[1], ootEnumCloudiness) - 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) - 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]) - - if altHeadersListName is not None: - parseAlternateSceneHeaders(sceneObj, roomObjs, sceneData, altHeadersListName, f3dContext, sharedSceneData) - - return sceneObj diff --git a/fast64_internal/oot/oot_spline.py b/fast64_internal/oot/oot_spline.py deleted file mode 100644 index 05084437c..000000000 --- a/fast64_internal/oot/oot_spline.py +++ /dev/null @@ -1,38 +0,0 @@ -import bpy -from ..utility import PluginError, toAlnum - - -class OOTPath: - def __init__(self, ownerName, objName: str): - self.ownerName = toAlnum(ownerName) - self.objName = objName - self.points = [] - - def pathName(self, headerIndex, index): - return f"{self.ownerName}_pathwayList{index}_header{headerIndex}" - - -def ootConvertPath(name, obj, transformMatrix): - path = OOTPath(name, obj.name) - - spline = obj.data.splines[0] - for point in spline.points: - position = transformMatrix @ point.co.xyz - path.points.append(position) - - return path - - -def onSplineTypeSet(self, context): - self.splines.active.order_u = 1 - - -def assertCurveValid(obj): - curve = obj.data - if not isinstance(curve, bpy.types.Curve) or curve.splines[0].type != "NURBS": - # Curve was likely not intended to be exported - return False - if len(curve.splines) != 1: - # Curve was intended to be exported but has multiple disconnected segments - raise PluginError("Exported curves should have only one single segment, found " + str(len(curve.splines))) - return True diff --git a/fast64_internal/operators.py b/fast64_internal/operators.py index e4a2041cd..335a0a9e2 100644 --- a/fast64_internal/operators.py +++ b/fast64_internal/operators.py @@ -1,8 +1,22 @@ -import bpy, mathutils, math -from bpy.types import Operator, Context, UILayout +from cProfile import Profile +from pstats import SortKey, Stats +from typing import TypeVar, Iterable, Optional + +import bpy, mathutils +from bpy.types import Operator, Context, UILayout, EnumProperty from bpy.utils import register_class, unregister_class -from .utility import * -from .f3d.f3d_material import * +from bpy.props import IntProperty, StringProperty + +from .utility import ( + cleanupTempMeshes, + get_mode_set_from_context_mode, + raisePluginError, + parentObject, + store_original_meshes, + store_original_mtx, + deselectAllObjects, +) +from .f3d.f3d_material import createF3DMat def addMaterialByName(obj, matName, preset): @@ -14,6 +28,9 @@ def addMaterialByName(obj, matName, preset): material.name = matName +PROFILE_ENABLED = False + + class OperatorBase(Operator): """Base class for operators, keeps track of context mode and sets it back after running execute_operator() and catches exceptions for raisePluginError()""" @@ -21,13 +38,19 @@ class OperatorBase(Operator): context_mode: str = "" icon = "NONE" + @classmethod + def is_enabled(cls, context: Context, **op_values): + return True + @classmethod def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, **op_values): """Op args are passed to the operator via setattr()""" icon = icon if icon else cls.icon + layout = layout.column() op = layout.operator(cls.bl_idname, icon=icon, text=text) for key, value in op_values.items(): setattr(op, key, value) + layout.enabled = cls.is_enabled(bpy.context, **op_values) return op def execute_operator(self, context: Context): @@ -40,7 +63,12 @@ def execute(self, context: Context): try: if self.context_mode and self.context_mode != starting_mode_set: bpy.ops.object.mode_set(mode=self.context_mode) - self.execute_operator(context) + if PROFILE_ENABLED: + with Profile() as profile: + self.execute_operator(context) + print(Stats(profile).strip_dirs().sort_stats(SortKey.CUMULATIVE).print_stats()) + else: + self.execute_operator(context) return {"FINISHED"} except Exception as exc: raisePluginError(self, exc) @@ -53,6 +81,138 @@ def execute(self, context: Context): bpy.ops.object.mode_set(mode=starting_mode_set) +CollectionMember = TypeVar("CollectionMember") + + +class CollectionOperatorBase(OperatorBase): + """ + A basic collection operator, implements basic add/remove/move/clear operations, + but can support more by the subclass implementing the .lower equivelent of the op_name. + See some examples in sm64/custom_cmd/operators.py + """ + + # index -1 means no index, so on an add that would mean adding at the end with no copy of the previous element + index: IntProperty(default=-1) + op_name: StringProperty() + copy_on_add: bool = False + object_name: str = "item" # simple name to be used in descriptions + + @classmethod + def description(cls, context: Context, properties: dict) -> str: + op_name: str = properties.get("op_name", "") + description = op_name.capitalize() + index = properties.get("index", -1) + if index != -1: + description += f" (copy of {index})" + + object_name = cls.object_name + if op_name == "CLEAR": + object_name += "s" + description += f" {object_name}" + return description + + @classmethod + def collection(cls, context: Context, op_values: dict) -> Iterable[CollectionMember]: + """Abstract method for getting the collection from the context""" + raise NotImplementedError() + + @classmethod + def is_enabled(cls, context: Context, **op_values) -> bool: + """Checks if the operation being drawn should be enabled in the UI, for example clear requires the collection to not be empty""" + collection = cls.collection(context, op_values) + op_name: str = op_values.get("op_name", "") + match op_name: + case "MOVE_UP": + return op_values.get("index") > 0 + case "MOVE_DOWN": + return op_values.get("index") < len(collection) - 1 + case "CLEAR": + return len(collection) > 0 + case _: + lower = op_name.lower() + "_enabled" + if hasattr(cls, lower): + return getattr(cls, lower)(context, collection) + return True + + @classmethod + def draw_row(cls, row: UILayout, index: int, **op_values): + """Draw add/remove/move/clear operations, clear only draws in a element-less index (-1)""" + + def draw_op(icon: str, op_name: str): + cls.draw_props(row, icon, "", op_name=op_name, index=index, **op_values) + + draw_op("ADD", "ADD") + if index == -1: + draw_op("TRASH", "CLEAR") + else: + draw_op("REMOVE", "REMOVE") + draw_op("TRIA_DOWN", "MOVE_DOWN") + draw_op("TRIA_UP", "MOVE_UP") + + def add( + self, _context: Context, collection: Iterable[CollectionMember] + ) -> tuple[CollectionMember | None, CollectionMember]: + """Returns the previous element and the newly created element""" + collection.add() + old_arg: CollectionMember | None = None + new_arg: CollectionMember = collection[-1] + if self.index != -1: + collection.move(len(collection) - 1, self.index + 1) + old_arg = collection[self.index] + new_arg = collection[self.index + 1] + if self.copy_on_add: + copyPropertyGroup(old_arg, new_arg) + return old_arg, new_arg + + def execute_operator(self, context: Context): + collection = self.__class__.collection(context, self.properties) + match self.op_name: + case "ADD": + self.add(context, collection) + case "REMOVE": + collection.remove(self.index) + case "MOVE_UP": + collection.move(self.index, self.index - 1) + case "MOVE_DOWN": + collection.move(self.index, self.index + 1) + case "CLEAR": + collection.clear() + case _: + lower = self.op_name.lower() + if hasattr(self, lower): + getattr(self, lower)(context, collection) + else: + raise NotImplementedError(f'Unimplemented internal op "{self.op_name}"') + + +class SearchEnumOperatorBase(OperatorBase): + bl_description = "Search Enum" + bl_label = "Search" + bl_property = None + bl_options = {"UNDO"} + + @classmethod + def draw_props(cls, layout: UILayout, data, prop: str, name: str): + row = layout.row() + if name: + row.label(text=name) + row.prop(data, prop, text="") + row.operator(cls.bl_idname, icon="VIEWZOOM", text="") + + def update_enum(self, context: Context): + raise NotImplementedError() + + def execute_operator(self, context: Context): + assert self.bl_property + self.report({"INFO"}, f"Selected: {getattr(self, self.bl_property)}") + self.update_enum(context) + context.region.tag_redraw() + + def invoke(self, context: Context, _): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + class AddWaterBox(OperatorBase): bl_idname = "object.add_water_box" bl_label = "Add Water Box" @@ -68,7 +228,7 @@ def setEmptyType(self, emptyObj): return None def execute_operator(self, context): - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() location = mathutils.Vector(bpy.context.scene.cursor.location) bpy.ops.mesh.primitive_plane_add(size=2 * self.scale, enter_editmode=False, align="WORLD", location=location[:]) diff --git a/fast64_internal/panels.py b/fast64_internal/panels.py index a7c986dac..9b3641f61 100644 --- a/fast64_internal/panels.py +++ b/fast64_internal/panels.py @@ -41,12 +41,12 @@ def poll(cls, context): class OOT_Panel(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" - bl_category = "OOT" + bl_category = "Z64" bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" + return context.scene.gameEditorMode in {"OOT", "MM"} class MK64_Panel(bpy.types.Panel): diff --git a/fast64_internal/repo_settings.py b/fast64_internal/repo_settings.py index 04418e821..d353ff0d2 100644 --- a/fast64_internal/repo_settings.py +++ b/fast64_internal/repo_settings.py @@ -6,7 +6,7 @@ from bpy.props import StringProperty from bpy.path import abspath -from .utility import filepath_checks, prop_split, filepath_ui_warnings, draw_and_check_tab +from .utility import filepath_checks, prop_split, filepath_ui_warnings, draw_and_check_tab, set_prop_if_in_data from .operators import OperatorBase from .f3d.f3d_material import draw_rdp_world_defaults from .sm64.settings.repo_settings import load_sm64_repo_settings, save_sm64_repo_settings @@ -71,18 +71,16 @@ def load_repo_settings(scene: Scene, path: os.PathLike, skip_if_no_auto_load=Fal ) fast64_settings = scene.fast64.settings - fast64_settings.auto_repo_load_settings = data.get("autoLoad", fast64_settings.auto_repo_load_settings) - fast64_settings.auto_pick_texture_format = data.get( - "autoPickTextureFormat", fast64_settings.auto_pick_texture_format - ) - fast64_settings.prefer_rgba_over_ci = data.get("preferRGBAOverCI", fast64_settings.prefer_rgba_over_ci) - scene.f3d_type = data.get("microcode", scene.f3d_type) - scene.saveTextures = data.get("saveTextures", scene.saveTextures) + fast64_settings.from_repo_settings(data) + set_prop_if_in_data(scene, "f3d_type", data, "microcode") + set_prop_if_in_data(scene, "saveTextures", data, "saveTextures") + rdp_defaults: RDPSettings = scene.world.rdp_defaults rdp_defaults.from_dict(data.get("rdpDefaults", {})) if scene.gameEditorMode == "SM64": load_sm64_repo_settings(scene, data.get("sm64", {})) + scene.fast64.settings.glTF.from_dict(data.get("glTF", {})) def save_repo_settings(scene: Scene, path: os.PathLike): @@ -90,20 +88,19 @@ def save_repo_settings(scene: Scene, path: os.PathLike): data = {} data["version"] = CUR_VERSION - data["autoLoad"] = fast64_settings.auto_repo_load_settings + data.update(fast64_settings.to_repo_settings()) data["microcode"] = scene.f3d_type data["saveTextures"] = scene.saveTextures - data["autoPickTextureFormat"] = fast64_settings.auto_pick_texture_format - if fast64_settings.auto_pick_texture_format: - data["preferRGBAOverCI"] = fast64_settings.prefer_rgba_over_ci + rdp_defaults: RDPSettings = scene.world.rdp_defaults data["rdpDefaults"] = rdp_defaults.to_dict() if scene.gameEditorMode == "SM64": data["sm64"] = save_sm64_repo_settings(scene) + data["glTF"] = scene.fast64.settings.glTF.to_dict() with open(abspath(path), "w", encoding="utf-8") as json_file: - json.dump(data, json_file, indent=2) + json.dump(data, json_file, indent="\t") def draw_repo_settings(layout: UILayout, context: Context): @@ -119,7 +116,9 @@ def draw_repo_settings(layout: UILayout, context: Context): SaveRepoSettings.draw_props(col, path=path) col.prop(fast64_settings, "auto_repo_load_settings") - prop_split(col, scene, "f3d_type", "F3D Microcode") + prop_split(col, scene, "f3d_type", "Microcode") + if scene.f3d_type in {"F3DEX3", "T3D"}: + prop_split(col, scene, "packed_normals_algorithm", "Packed normals alg") col.prop(scene, "saveTextures") col.prop(fast64_settings, "auto_pick_texture_format") if fast64_settings.auto_pick_texture_format: diff --git a/fast64_internal/sm64/README.md b/fast64_internal/sm64/README.md index 7daa84a4f..780cbc296 100644 --- a/fast64_internal/sm64/README.md +++ b/fast64_internal/sm64/README.md @@ -52,45 +52,11 @@ For example, for Mario you would rotate the four limb joints around the Y-axis 1 Then after applying the rest pose and skinning, you would apply those operations in reverse order then apply rest pose again. -### Importing/Exporting Binary SM64 Animations (Not Mario) -- Note: SM64 animations only allow for rotations, and translation only on the root bone. - -- Download Quad64, open the desired level, and go to Misc -> Script Dumps. -- Go to the objects header, find the object you want, and view the Behaviour Script tab. -- For most models with animation, you can will see a 27 command, and optionally a 28 command. - -For importing: - -- The last 4 bytes of the 27 command will be the animation list pointer. - - Make sure 'Is DMA Animation' is unchecked, 'Is Anim List' is checked, and 'Is Segmented Pointer' is checked. - - Set the animation importer start address as those 4 bytes. - - If a 28 command exists, then the second byte will be the anim list index. - - Otherwise, the anim list index is usually 0. - -For exporting: - -- Make sure 'Set Anim List Entry' is checked. -- Copy the addresses of the 27 command, which is the first number before the slash on that line. -- Optionally do the same for the 28 command, which may not exist. -- If a 28 command exists, then the second byte will be the anim list index. -- Otherwise, the anim list index is usually 0. - -Select an armature for the animation, and press 'Import/Export animation'. - -### Importing/Exporting Binary Mario Animations -Mario animations use a DMA table, which contains 8 byte entries of (offset from table start, animation size). Documentation about this table is here: -https://dudaw.webs.com/sm64docs/sm64_marios_animation_table.txt -Basically, Mario's DMA table starts at 0x4EC000. There is an 8 byte header, and then the animation entries afterward. Thus the 'climb up ledge' DMA entry is at 0x4EC008. The first 4 bytes at that address indicate the offset from 0x4EC000 at which the actual animation exists. Thus the 'climb up ledge' animation entry address is at 0x4EC690. Using this table you can find animations you want to overwrite. Make sure the 'Is DMA Animation' option is checked and 'Is Segmented Pointer' is unchecked when importing/exporting. Check "Overwrite DMA Entry", set the start address to 4EC000 (for Mario), and set the entry address to the DMA entry obtained previously. - -### Animating Existing Geolayouts -Often times it is hard to rig an existing SM64 geolayout, as there are many intermediate non-deform bones and bones don't point to their children. To make this easier you can use the 'Create Animatable Metarig' operator in the SM64 Armature Tools header. This will generate a metarig which can be used with IK. The metarig bones will be placed on armature layers 3 and 4. +### [Animations](https://fast64.readthedocs.io/en/latest/sm64/animations/index.html) ## Decomp To start, set your base decomp folder in SM64 General Settings. This allows the plugin to automatically add headers/includes to the correct locations. You can always choose to export to a custom location, although headers/includes won't be written. -## Repo settings -Fast64 can save and load repo settings files. By default, they're named fast64.json. These files have RDP defaults, microcode, and more. They also have game-specific settings (OOT will support these in the future). Fast64 will set the path for the settings and auto-load them if auto-load is enabled as soon as the user picks an sm64 decomp path. - ### Decomp Export Types Most exports will let you choose an export type. @@ -165,6 +131,8 @@ Insertable Binary exporting will generate a binary file, with a header containin 1 = Geolayout 2 = Animation 3 = Collision + 4 = Animation Table + 5 = Animation DMA Table 0x04-0x08 : Data Size (size in bytes of Data Section) 0x08-0x0C : Start Address (start address of data, relative to start of Data Section) @@ -183,6 +151,8 @@ To resolve pointer addresses, for each pointer address, # Convert offset to segmented address data[pointer_address] = encode_segmented_address(export_address + current_offset) +### [Custom Commands](https://fast64.readthedocs.io/en/latest/sm64/custom_commands/custom_commands.html) + ### Common Issues Game crashes: Invalid function address for switch/function/held object bones. Animation root translation/rotation not exporting: Make sure you are animating the root bone, not the armature object. diff --git a/fast64_internal/sm64/__init__.py b/fast64_internal/sm64/__init__.py index 81fdbf596..72e65ce71 100644 --- a/fast64_internal/sm64/__init__.py +++ b/fast64_internal/sm64/__init__.py @@ -1,3 +1,7 @@ +from bpy.types import PropertyGroup +from bpy.props import PointerProperty +from bpy.utils import register_class, unregister_class + from .settings import ( settings_props_register, settings_props_unregister, @@ -83,13 +87,24 @@ sm64_dl_writer_unregister, ) -from .sm64_anim import ( - sm64_anim_panel_register, - sm64_anim_panel_unregister, - sm64_anim_register, - sm64_anim_unregister, +from .animation import ( + anim_panel_register, + anim_panel_unregister, + anim_register, + anim_unregister, + SM64_ActionAnimProperty, ) +from .custom_cmd import custom_cmd_register, custom_cmd_unregister + + +class SM64_ActionProperty(PropertyGroup): + """ + Properties in Action.fast64.sm64. + """ + + animation: PointerProperty(type=SM64_ActionAnimProperty, name="SM64 Properties") + def sm64_panel_register(): settings_panels_register() @@ -103,7 +118,7 @@ def sm64_panel_register(): sm64_spline_panel_register() sm64_dl_writer_panel_register() sm64_dl_parser_panel_register() - sm64_anim_panel_register() + anim_panel_register() def sm64_panel_unregister(): @@ -118,12 +133,14 @@ def sm64_panel_unregister(): sm64_spline_panel_unregister() sm64_dl_writer_panel_unregister() sm64_dl_parser_panel_unregister() - sm64_anim_panel_unregister() + anim_panel_unregister() def sm64_register(register_panels: bool): + custom_cmd_register() tools_operators_register() tools_props_register() + anim_register() sm64_col_register() sm64_bone_register() sm64_cam_register() @@ -134,16 +151,18 @@ def sm64_register(register_panels: bool): sm64_spline_register() sm64_dl_writer_register() sm64_dl_parser_register() - sm64_anim_register() settings_props_register() + register_class(SM64_ActionProperty) if register_panels: sm64_panel_register() def sm64_unregister(unregister_panels: bool): + custom_cmd_unregister() tools_operators_unregister() tools_props_unregister() + anim_unregister() sm64_col_unregister() sm64_bone_unregister() sm64_cam_unregister() @@ -154,8 +173,8 @@ def sm64_unregister(unregister_panels: bool): sm64_spline_unregister() sm64_dl_writer_unregister() sm64_dl_parser_unregister() - sm64_anim_unregister() settings_props_unregister() + unregister_class(SM64_ActionProperty) if unregister_panels: sm64_panel_unregister() diff --git a/fast64_internal/sm64/animation/__init__.py b/fast64_internal/sm64/animation/__init__.py new file mode 100644 index 000000000..0aca0cc02 --- /dev/null +++ b/fast64_internal/sm64/animation/__init__.py @@ -0,0 +1,15 @@ +from .operators import anim_ops_register, anim_ops_unregister +from .properties import anim_props_register, anim_props_unregister, SM64_ArmatureAnimProperties, SM64_ActionAnimProperty +from .panels import anim_panel_register, anim_panel_unregister +from .exporting import export_animation, export_animation_table +from .utility import get_anim_obj, is_obj_animatable + + +def anim_register(): + anim_ops_register() + anim_props_register() + + +def anim_unregister(): + anim_ops_unregister() + anim_props_unregister() diff --git a/fast64_internal/sm64/animation/classes.py b/fast64_internal/sm64/animation/classes.py new file mode 100644 index 000000000..d11f612b6 --- /dev/null +++ b/fast64_internal/sm64/animation/classes.py @@ -0,0 +1,1006 @@ +from typing import Optional +from pathlib import Path +from enum import IntFlag +from io import StringIO +from copy import copy +import dataclasses +import numpy as np +import functools +import typing +import re + +from bpy.types import Action + +from ...f3d.f3d_parser import math_eval + +from ...utility import PluginError, cast_integer, encodeSegmentedAddr, intToHex +from ..sm64_constants import MAX_U16, SegmentData +from ..sm64_utility import CommentMatch, adjust_start_end +from ..sm64_classes import RomReader, DMATable, DMATableElement, IntArray + +from .constants import HEADER_STRUCT, HEADER_SIZE, TABLE_ELEMENT_PATTERN +from .utility import get_dma_header_name, get_dma_anim_name + + +@dataclasses.dataclass +class CArrayDeclaration: + name: str = "" + path: Path = Path("") + file_name: str = "" + values: list[str] | dict[str, str] = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class SM64_AnimPair: + values: np.ndarray[typing.Any, np.dtype[np.int16]] = dataclasses.field(compare=False) + + # Importing + address: int = 0 + end_address: int = 0 + + offset: int = 0 # For compressing + + def __post_init__(self): + assert self.values.size > 0, "values cannot be empty" + + def clean_frames(self): + mask = self.values != self.values[-1] + # Reverse the order, find the last element with the same value + index = np.argmax(mask[::-1]) + if index != 1: + self.values = self.values[: 1 if index == 0 else (-index + 1)] + return self + + def get_frame(self, frame: int): + return self.values[min(frame, len(self.values) - 1)] + + +@dataclasses.dataclass +class SM64_AnimData: + pairs: list[SM64_AnimPair] = dataclasses.field(default_factory=list) + indice_reference: str | int = "" + values_reference: str | int = "" + + # Importing + indices_file_name: str = "" + values_file_name: str = "" + value_end_address: int = 0 + indice_end_address: int = 0 + start_address: int = 0 + end_address: int = 0 + + @property + def key(self): + return (self.indice_reference, self.values_reference) + + def create_tables(self, start_address=-1): + indice_tables, value_tables = create_tables([self], start_address=start_address) + assert ( + len(value_tables) == 1 and len(indice_tables) == 1 + ), "Single animation data export should only return 1 of each table." + return indice_tables[0], value_tables[0] + + def to_c(self, dma: bool = False): + text_data = StringIO() + + indice_table, value_table = self.create_tables() + if dma: + indice_table.to_c(text_data, new_lines=2) + value_table.to_c(text_data) + else: + value_table.to_c(text_data, new_lines=2) + indice_table.to_c(text_data) + + return text_data.getvalue() + + def to_binary(self, start_address=-1): + indice_table, value_table = self.create_tables(start_address) + values_offset = len(indice_table.data) * 2 + + data = bytearray() + data.extend(indice_table.to_binary()) + data.extend(value_table.to_binary()) + return data, values_offset + + def read_binary(self, indices_reader: RomReader, values_reader: RomReader, bone_count: int): + print( + f"Reading pairs from indices table at {intToHex(indices_reader.address)}", + f"and values table at {intToHex(values_reader.address)}.", + ) + self.indice_reference = indices_reader.start_address + self.values_reference = values_reader.start_address + + # 3 pairs per bone + 3 for root translation of 2, each 2 bytes + indices_size = (((bone_count + 1) * 3) * 2) * 2 + indices_values = np.frombuffer(indices_reader.read_data(indices_size), dtype=">u2") + for i in range(0, len(indices_values), 2): + max_frame, offset = indices_values[i], indices_values[i + 1] + address, size = values_reader.start_address + (offset * 2), max_frame * 2 + + values = np.frombuffer(values_reader.read_data(size, address), dtype=">i2", count=max_frame) + self.pairs.append(SM64_AnimPair(values, address, address + size, offset).clean_frames()) + self.indice_end_address = indices_reader.address + self.value_end_address = max(pair.end_address for pair in self.pairs) + + self.start_address = min(self.indice_reference, self.values_reference) + self.end_address = max(self.indice_end_address, self.value_end_address) + return self + + def read_c(self, indice_decl: CArrayDeclaration, value_decl: CArrayDeclaration): + print(f'Reading data from "{indice_decl.name}" and "{value_decl.name}" c declarations.') + self.indices_file_name, self.values_file_name = indice_decl.file_name, value_decl.file_name + self.indice_reference, self.values_reference = indice_decl.name, value_decl.name + + indices_values = np.vectorize(lambda x: int(x, 0), otypes=[np.uint16])(indice_decl.values) + values_array = np.vectorize(lambda x: int(x, 0), otypes=[np.int16])(value_decl.values) + + for i in range(0, len(indices_values), 2): + max_frame, offset = indices_values[i], indices_values[i + 1] + self.pairs.append(SM64_AnimPair(values_array[offset : offset + max_frame], -1, -1, offset).clean_frames()) + return self + + +class SM64_AnimFlags(IntFlag): + prop: Optional[str] + + def __new__(cls, value, blender_prop: str | None = None): + obj = int.__new__(cls, value) + obj._value_, obj.prop = 1 << value, blender_prop + return obj + + ANIM_FLAG_NOLOOP = (0, "no_loop") + ANIM_FLAG_FORWARD = (1, "backwards") + ANIM_FLAG_2 = (2, "no_acceleration") + ANIM_FLAG_HOR_TRANS = (3, "only_vertical") + ANIM_FLAG_VERT_TRANS = (4, "only_horizontal") + ANIM_FLAG_5 = (5, "disabled") + ANIM_FLAG_6 = (6, "no_trans") + ANIM_FLAG_7 = 7 + + ANIM_FLAG_BACKWARD = (1, "backwards") # refresh 16 + + # hackersm64 + ANIM_FLAG_NO_ACCEL = (2, "no_acceleration") + ANIM_FLAG_DISABLED = (5, "disabled") + ANIM_FLAG_NO_TRANS = (6, "no_trans") + ANIM_FLAG_UNUSED = 7 + + @classmethod + @functools.cache + def all_flags(cls): + flags = SM64_AnimFlags(0) + for flag in cls.__members__.values(): + flags |= flag + return flags + + @classmethod + @functools.cache + def all_flags_with_prop(cls): + flags = SM64_AnimFlags(0) + for flag in cls.__members__.values(): + if flag.prop is not None: + flags |= flag + return flags + + @classmethod + @functools.cache + def props_to_flags(cls): + return {flag.prop: flag for flag in cls.__members__.values() if flag.prop is not None} + + @classmethod + @functools.cache + def flags_to_names(cls): + names: dict[SM64_AnimFlags, list[str]] = {} + for name, flag in cls.__members__.items(): + if flag in names: + names[flag].append(name) + else: + names[flag] = [name] + return names + + @property + @functools.cache + def names(self): + names: list[str] = [] + for flag, flag_names in SM64_AnimFlags.flags_to_names().items(): + if flag in self: + names.append("/".join(flag_names)) + if self & ~self.__class__.all_flags(): # flag value outside known flags + names.append("unknown bits") + return names + + @classmethod + @functools.cache + def evaluate(cls, value: str | int): + if isinstance(value, cls): # the value was already evaluated + return value + elif isinstance(value, str): + try: + value = cls(math_eval(value, cls)) + except Exception as exc: # pylint: disable=broad-except + print(f"Failed to evaluate flags {value}: {exc}") + if isinstance(value, int): # the value was fully evaluated + if isinstance(value, cls): + value = value.value + # cast to u16 for simplicity + return cls(cast_integer(value, 16, signed=False)) + else: # the value was not evaluated + return value + + +@dataclasses.dataclass +class SM64_AnimHeader: + reference: str | int = "" + flags: SM64_AnimFlags | str = SM64_AnimFlags(0) + trans_divisor: int = 0 + start_frame: int = 0 + loop_start: int = 0 + loop_end: int = 1 + bone_count: int = 0 + length: int = 0 + indice_reference: Optional[str | int] = None + values_reference: Optional[str | int] = None + data: Optional[SM64_AnimData] = None + + enum_name: str = "" + # Imports + file_name: str = "" + end_address: int = 0 + header_variant: int = 0 + table_index: int = 0 + action: Action | None = None + + @property + def data_key(self): + return (self.indice_reference, self.values_reference) + + @property + def flags_comment(self): + if isinstance(self.flags, SM64_AnimFlags): + return ", ".join(self.flags.names) + return "" + + @property + def c_flags(self): + return self.flags if isinstance(self.flags, str) else intToHex(self.flags.value, 2) + + def get_reference(self, override: Optional[str | int], expected_type: type, reference_name: str): + name = reference_name.replace("_", " ") + if override: + reference = override + elif self.data and getattr(self.data, reference_name): + reference = getattr(self.data, reference_name) + elif getattr(self, reference_name): + reference = getattr(self, reference_name) + else: + assert False, f"Unknown {name}" + + assert isinstance( + reference, expected_type + ), f"{name.capitalize()} must be a {expected_type},is instead {type(reference)}." + return reference + + def get_values_reference(self, override: Optional[str | int] = None, expected_type: type = str): + return self.get_reference(override, expected_type, "values_reference") + + def get_indice_reference(self, override: Optional[str | int] = None, expected_type: type = str): + return self.get_reference(override, expected_type, "indice_reference") + + def to_c(self, values_override: Optional[str] = None, indice_override: Optional[str] = None, dma=False): + assert not dma or isinstance( # assert if dma and flags are not SM64_AnimFlags + self.flags, SM64_AnimFlags + ), f"Flags must be SM64_AnimFlags for C DMA, is instead {type(self.flags)}" + return ( + f"static const struct Animation {self.reference}{'[]' if dma else ''} = {{\n" + + f"\t{self.c_flags}, // flags {self.flags_comment}\n" + f"\t{self.trans_divisor}, // animYTransDivisor\n" + f"\t{self.start_frame}, // startFrame\n" + f"\t{self.loop_start}, // loopStart\n" + f"\t{self.loop_end}, // loopEnd\n" + f"\tANIMINDEX_NUMPARTS({self.get_indice_reference(indice_override, str)}), // unusedBoneCount\n" + f"\t{self.get_values_reference(values_override, str)}, // values\n" + f"\t{self.get_indice_reference(indice_override, str)}, // index\n" + "\t0 // length\n" + "};\n" + ) + + def to_binary( + self, + values_override: Optional[int] = None, + indice_override: Optional[int] = None, + segment_data: SegmentData | None = None, + length=0, + ): + assert isinstance( + self.flags, SM64_AnimFlags + ), f"Flags must be SM64_AnimFlags for binary, is instead {type(self.flags)}" + values_address = self.get_values_reference(values_override, int) + indice_address = self.get_indice_reference(indice_override, int) + if segment_data: + values_address = int.from_bytes(encodeSegmentedAddr(values_address, segment_data), "big") + indice_address = int.from_bytes(encodeSegmentedAddr(indice_address, segment_data), "big") + + return HEADER_STRUCT.pack( + self.flags.value, + self.trans_divisor, + self.start_frame, + self.loop_start, + self.loop_end, + self.bone_count, + values_address, + indice_address, + length, + ) + + @staticmethod + def read_binary( + reader: RomReader, + read_headers: dict[str, "SM64_AnimHeader"], + dma: bool = False, + bone_count: Optional[int] = None, + table_index: Optional[int] = None, + ): + if str(reader.start_address) in read_headers: + return read_headers[str(reader.start_address)] + print(f"Reading animation header at {intToHex(reader.start_address)}.") + + header = SM64_AnimHeader() + read_headers[str(reader.start_address)] = header + header.reference = reader.start_address + + header.flags = SM64_AnimFlags.evaluate(reader.read_int(2, True)) # /*0x00*/ s16 flags; + header.trans_divisor = reader.read_int(2, True) # /*0x02*/ s16 animYTransDivisor; + header.start_frame = reader.read_int(2, True) # /*0x04*/ s16 startFrame; + header.loop_start = reader.read_int(2, True) # /*0x06*/ s16 loopStart; + header.loop_end = reader.read_int(2, True) # /*0x08*/ s16 loopEnd; + + # /*0x0A*/ s16 unusedBoneCount; (Unused in engine) + header.bone_count = reader.read_int(2, True) + if header.bone_count <= 0: + if bone_count is None: + raise PluginError( + "No bone count in header and no bone count passed in from target armature, cannot figure out" + ) + header.bone_count = bone_count + print("Old exports lack a defined bone count, invalid armatures won't be detected") + elif bone_count is not None and header.bone_count != bone_count: + raise PluginError( + f"Imported header's bone count is {header.bone_count} but object's is {bone_count}", + ) + + # /*0x0C*/ const s16 *values; + # /*0x10*/ const u16 *index; + if dma: + header.values_reference = reader.start_address + reader.read_int(4) + header.indice_reference = reader.start_address + reader.read_int(4) + else: + header.values_reference, header.indice_reference = reader.read_ptr(), reader.read_ptr() + header.length = reader.read_int(4) + + header.end_address = reader.address + 1 + header.table_index = len(read_headers) if table_index is None else table_index + + data = next( + (other_header.data for other_header in read_headers.values() if header.data_key == other_header.data_key), + None, + ) + if not data: + indices_reader = reader.branch(header.indice_reference) + values_reader = reader.branch(header.values_reference) + if indices_reader and values_reader: + data = SM64_AnimData().read_binary( + indices_reader, + values_reader, + header.bone_count, + ) + header.data = data + + return header + + @staticmethod + def read_c( + header_decl: CArrayDeclaration, + value_decls, + indices_decls, + read_headers: dict[str, "SM64_AnimHeader"], + table_index: Optional[int] = None, + ): + if header_decl.name in read_headers: + return read_headers[header_decl.name] + if len(header_decl.values) != 9: + raise ValueError(f"Header declarion has {len(header_decl.values)} values instead of 9.\n {header_decl}") + print(f'Reading header "{header_decl.name}" c declaration.') + header = SM64_AnimHeader() + read_headers[header_decl.name] = header + header.reference = header_decl.name + header.file_name = header_decl.file_name + + # Place the values into a dictionary, handles designated initialization + if isinstance(header_decl.values, list): + designated = {} + for value, var in zip( + header_decl.values, + [ + "flags", + "animYTransDivisor", + "startFrame", + "loopStart", + "loopEnd", + "unusedBoneCount", + "values", + "index", + "length", + ], + ): + designated[var] = value + else: + designated = header_decl.values + + # Read from the dict + header.flags = SM64_AnimFlags.evaluate(designated["flags"]) + header.trans_divisor = int(designated["animYTransDivisor"], 0) + header.start_frame = int(designated["startFrame"], 0) + header.loop_start = int(designated["loopStart"], 0) + header.loop_end = int(designated["loopEnd"], 0) + # bone_count = designated["unusedBoneCount"] + header.values_reference = designated["values"] + header.indice_reference = designated["index"] + + header.table_index = len(read_headers) if table_index is None else table_index + + data = next( + (other_header.data for other_header in read_headers.values() if header.data_key == other_header.data_key), + None, + ) + if not data: + indices_decl = next((indice for indice in indices_decls if indice.name == header.indice_reference), None) + value_decl = next((value for value in value_decls if value.name == header.values_reference), None) + if indices_decl and value_decl: + data = SM64_AnimData().read_c(indices_decl, value_decl) + header.data = data + + return header + + +@dataclasses.dataclass +class SM64_Anim: + data: SM64_AnimData | None = None + headers: list[SM64_AnimHeader] = dataclasses.field(default_factory=list) + file_name: str = "" + + # Imports + action_name: str = "" # Used for the blender action's name + action: Action | None = None # Used in the table class to prop function + + @property + def names(self) -> tuple[list[str], list[str]]: + names, enums = [], [] + for header in self.headers: + names.append(header.reference) + enums.append(header.enum_name) + return names, enums + + @property + def header_names(self) -> list[str]: + return self.names[0] + + @property + def enum_names(self) -> list[str]: + return self.names[1] + + def to_binary_dma(self): + assert self.data + headers: list[bytes] = [] + + indice_offset = HEADER_SIZE * len(self.headers) + anim_data, values_offset = self.data.to_binary() + for header in self.headers: + header_data = header.to_binary( + indice_offset + values_offset, indice_offset, length=indice_offset + len(anim_data) + ) + headers.append(header_data) + indice_offset -= HEADER_SIZE + return headers, anim_data + + def to_binary(self, start_address: int = 0, segment_data: SegmentData | None = None): + data: bytearray = bytearray() + ptrs: list[int] = [] + if self.data: + anim_data, values_offset = self.data.to_binary() + indice_offset = start_address + (HEADER_SIZE * len(self.headers)) + values_offset = indice_offset + values_offset + else: + anim_data = bytearray() + indice_offset = values_offset = None + for header in self.headers: + if self.data: + ptrs.extend([start_address + len(data) + 12, start_address + len(data) + 16]) + header_data = header.to_binary( + values_offset, + indice_offset, + segment_data, + ) + data.extend(header_data) + + data.extend(anim_data) + return data, ptrs + + def headers_to_c(self, dma: bool) -> str: + text_data = StringIO() + for header in self.headers: + text_data.write(header.to_c(dma=dma)) + text_data.write("\n") + return text_data.getvalue() + + def to_c(self, dma: bool): + text_data = StringIO() + c_headers = self.headers_to_c(dma) + if dma: + text_data.write(c_headers) + text_data.write("\n") + if self.data: + text_data.write(self.data.to_c(dma)) + text_data.write("\n") + if not dma: + text_data.write(c_headers) + return text_data.getvalue() + + +@dataclasses.dataclass +class SM64_AnimTableElement: + reference: str | int | None = None + header: SM64_AnimHeader | None = None + + # C exporting + enum_name: str = "" + reference_start: int = -1 + reference_end: int = -1 + enum_start: int = -1 + enum_end: int = -1 + enum_val: str = "" + + @property + def c_name(self): + if self.reference: + return self.reference + return "" + + @property + def c_reference(self): + if self.reference: + return f"&{self.reference}" + return "NULL" + + @property + def enum_c(self): + if self.enum_val: + return f"{self.enum_name} = {self.enum_val}" + return self.enum_name + + @property + def data(self): + return self.header.data if self.header else None + + def to_c(self, designated: bool): + if designated and self.enum_name: + return f"[{self.enum_name}] = {self.c_reference}," + else: + return f"{self.c_reference}," + + +@dataclasses.dataclass +class SM64_AnimTable: + reference: str | int = "" + enum_list_reference: str = "" + enum_list_delimiter: str = "" + file_name: str = "" + elements: list[SM64_AnimTableElement] = dataclasses.field(default_factory=list) + # Importing + end_address: int = 0 + # C exporting + values_reference: str = "" + start: int = -1 + end: int = -1 + enum_list_start: int = -1 + enum_list_end: int = -1 + + @property + def names(self) -> tuple[list[str], list[str]]: + names, enums = [], [] + for element in self.elements: + names.append(element.c_name) + enums.append(element.enum_name) + return names, enums + + @property + def header_names(self) -> list[str]: + return self.names[0] + + @property + def enum_names(self) -> list[str]: + return self.names[1] + + @property + def header_data_sets(self) -> tuple[list[SM64_AnimHeader], list[SM64_AnimData]]: + # Remove duplicates of data and headers, keep order by using a list + data_set = [] + headers_set = [] + for element in self.elements: + if element.data and not element.data in data_set: + data_set.append(element.data) + if element.header and not element.header in headers_set: + headers_set.append(element.header) + return headers_set, data_set + + @property + def header_set(self) -> list[SM64_AnimHeader]: + return self.header_data_sets[0] + + @property + def has_null_delimiter(self): + return bool(self.elements and self.elements[-1].reference is None) + + def get_seperate_anims(self): + print("Getting seperate animations from table.") + anims: list[SM64_Anim] = [] + headers_set, headers_added = self.header_set, [] + for header in headers_set: + if header in headers_added: + continue + ordered_headers: list[SM64_AnimHeader] = [] + variant = 0 + for other_header in headers_set: + if other_header.data == header.data: + other_header.header_variant = variant + ordered_headers.append(other_header) + headers_added.append(other_header) + variant += 1 + + anims.append(SM64_Anim(header.data, ordered_headers, header.file_name)) + return anims + + def get_seperate_anims_dma(self) -> list[SM64_Anim]: + print("Getting seperate DMA animations from table.") + + anims = [] + header_nums = [] + included_headers: list[SM64_AnimHeader] = [] + data = None + # For creating duplicates + data_already_added = [] + headers_already_added = [] + + for i, element in enumerate(self.elements): + assert element.header, f"Header in table element {i} is not set." + assert element.data, f"Data in table element {i} is not set." + header_nums.append(i) + + header, data = element.header, element.data + if header in headers_already_added: + print(f"Made duplicate of header {i}.") + header = copy(header) + header.reference = get_dma_header_name(i) + headers_already_added.append(header) + + included_headers.append(header) + + # If not at the end of the list and the next element doesn´t have different data + if (i < len(self.elements) - 1) and self.elements[i + 1].data is data: + continue + + name = get_dma_anim_name(header_nums) + file_name = f"{name}.inc.c" + if data in data_already_added: + print(f"Made duplicate of header {i}'s data.") + data = copy(data) + data_already_added.append(data) + + data.indice_reference, data.values_reference = f"{name}_indices", f"{name}_values" + # Normal names are possible (order goes by line and file) but would break convention + for i, included_header in enumerate(included_headers): + included_header.file_name = file_name + included_header.indice_reference = data.indice_reference + included_header.values_reference = data.values_reference + included_header.data = data + included_header.header_variant = i + anims.append(SM64_Anim(data, included_headers, file_name)) + + header_nums.clear() + included_headers = [] + + return anims + + def to_binary_dma(self): + dma_table = DMATable() + for animation in self.get_seperate_anims_dma(): + headers, data = animation.to_binary_dma() + end_offset = len(dma_table.data) + (HEADER_SIZE * len(headers)) + len(data) + for header in headers: + offset = len(dma_table.data) + size = end_offset - offset + dma_table.entries.append(DMATableElement(offset, size)) + dma_table.data.extend(header) + dma_table.data.extend(data) + return dma_table.to_binary() + + def to_combined_binary(self, table_address=0, data_address=-1, segment_data: SegmentData | None = None): + table_data: bytearray = bytearray() + data: bytearray = bytearray() + ptrs: list[int] = [] + headers_set, data_set = self.header_data_sets + + # Pre calculate offsets + table_length = len(self.elements) * 4 + if data_address == -1: + data_address = table_address + table_length + + headers_length = len(headers_set) * HEADER_SIZE + indice_tables, value_tables = create_tables(data_set, self.values_reference, data_address + headers_length) + + # Add the animation table + for i, element in enumerate(self.elements): + if element.header: + ptrs.append(table_address + len(table_data)) + header_offset = data_address + (headers_set.index(element.header) * HEADER_SIZE) + if segment_data: + table_data.extend(encodeSegmentedAddr(header_offset, segment_data)) + else: + table_data.extend(header_offset.to_bytes(4, byteorder="big")) + continue + if element.reference is None: + table_data.extend(0x0.to_bytes(4, byteorder="big")) + continue + assert isinstance(element.reference, int), f"Reference at element {i} is not an int." + table_data.extend(element.reference.to_bytes(4, byteorder="big")) + + for anim_header in headers_set: # Add the headers + if not anim_header.data: + data.extend(anim_header.to_binary()) + continue + ptrs.extend([data_address + len(data) + 12, data_address + len(data) + 16]) + data.extend(anim_header.to_binary(segment_data=segment_data)) + + for table in indice_tables + value_tables: + data.extend(table.to_binary()) + + return table_data, data, ptrs + + def data_and_headers_to_c(self, dma: bool): + files_data: dict[str, str] = {} + animation: SM64_Anim + for animation in self.get_seperate_anims_dma() if dma else self.get_seperate_anims(): + files_data[animation.file_name] = animation.to_c(dma=dma) + return files_data + + def data_and_headers_to_c_combined(self): + text_data = StringIO() + headers_set, data_set = self.header_data_sets + if data_set: + indice_tables, value_tables = create_tables(data_set, self.values_reference) + for table in value_tables + indice_tables: + table.to_c(text_data, new_lines=2) + for anim_header in headers_set: + text_data.write(anim_header.to_c()) + text_data.write("\n") + + return text_data.getvalue() + + def read_binary( + self, + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + size: Optional[int] = None, + ): + print(f"Reading table at address {reader.start_address}.") + self.elements.clear() + self.reference = reader.start_address + + range_size = size or 300 + if table_index is not None: + range_size = min(range_size, table_index + 1) + for i in range(range_size): + ptr = reader.read_ptr() + if size is None and ptr == 0: # If no specified size and ptr is NULL, break + self.elements.append(SM64_AnimTableElement()) + break + elif table_index is not None and i != table_index: + continue # Skip entries until table_index if specified + + header_reader = reader.branch(ptr) + if header_reader is None: + self.elements.append(SM64_AnimTableElement(ptr)) + else: + try: + header = SM64_AnimHeader.read_binary( + header_reader, + read_headers, + False, + bone_count, + i, + ) + except Exception as exc: + raise PluginError(f"Failed to read header in table element {i}: {str(exc)}") from exc + self.elements.append(SM64_AnimTableElement(ptr, header)) + + if table_index is not None: # Break if table_index is specified + break + else: + if table_index is not None: + raise PluginError(f"Table index {table_index} not found in table.") + if size is None: + raise PluginError(f"Iterated through {range_size} elements and no NULL was found.") + self.end_address = reader.address + return self + + def read_dma_binary( + self, + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + ): + dma_table = DMATable() + dma_table.read_binary(reader) + self.reference = reader.start_address + if table_index is not None: + assert table_index >= 0 and table_index < len( + dma_table.entries + ), f"Index {table_index} outside of defined table ({len(dma_table.entries)} entries)." + entrie = dma_table.entries[table_index] + header_reader = reader.branch(entrie.address) + if header_reader is None: + raise PluginError("Failed to branch into DMA entrie's address") + return SM64_AnimHeader.read_binary( + header_reader, + read_headers, + True, + bone_count, + table_index, + ) + + for i, entrie in enumerate(dma_table.entries): + header_reader = reader.branch(entrie.address) + try: + if not header_reader: + raise PluginError("Failed to branch to header's address") + header = SM64_AnimHeader.read_binary(header_reader, read_headers, True, bone_count, i) + except Exception as exc: + raise PluginError(f"Failed to read header in table element {i}: {str(exc)}") from exc + self.elements.append(SM64_AnimTableElement(reader.start_address, header)) + self.end_address = dma_table.end_address + return self + + def read_c( + self, + c_data: str, + start: int, + end: int, + comment_map: list[CommentMatch], + read_headers: dict[str, SM64_AnimHeader], + header_decls: list[CArrayDeclaration], + values_decls: list[CArrayDeclaration], + indices_decls: list[CArrayDeclaration], + ): + table_start, table_end = adjust_start_end(start, end, comment_map) + self.start, self.end = table_start, table_end + + for i, element_match in enumerate(re.finditer(TABLE_ELEMENT_PATTERN, c_data[start:end])): + enum, element, null = ( + element_match.group("enum"), + element_match.group("element"), + element_match.group("null"), + ) + if enum is None and element is None and null is None: # comment + continue + header = None + if element is not None: + header_decl = next((header for header in header_decls if header.name == element), None) + if header_decl: + header = SM64_AnimHeader.read_c( + header_decl, + values_decls, + indices_decls, + read_headers, + i, + ) + element_start, element_end = adjust_start_end( + start + element_match.start(), start + element_match.end(), comment_map + ) + self.elements.append( + SM64_AnimTableElement( + element, + enum_name=enum, + reference_start=element_start - table_start, + reference_end=element_end - table_start, + header=header, + ) + ) + + +def create_tables(anims_data: list[SM64_AnimData], values_name="", start_address=-1): + """ + Can generate multiple indices table with only one value table (or multiple if needed), + which improves compression (this feature is used in table exports). + Update the animation data with the correct references. + Returns: indice_tables, value_tables (in that order) + """ + + def add_data(values_table: IntArray, size: int, anim_data: SM64_AnimData, values_address: int): + data = values_table.data + for pair in anim_data.pairs: + pair_values = pair.values + if len(pair_values) >= MAX_U16: + raise PluginError( + f"Pair frame count ({len(pair_values)}) is higher than the 16 bit max ({MAX_U16}). Too many frames." + ) + + # It's never worth it to find an existing offset for values bigger than 1 frame. + # From my (@Lilaa3) testing, the only improvement in Mario resulted in just 286 bytes saved. + offset = None + if len(pair_values) == 1: + indices = np.isin(data[:size], pair_values[0]).nonzero()[0] + offset = indices[0] if indices.size > 0 else None + + if offset is None: # no existing offset found + offset = size + size = offset + len(pair_values) + if size > MAX_U16: # exceeded limit, but we may be able to recover with a new table + return -1, None + data[offset:size] = pair_values + pair.offset = offset + + # build indice table + indice_values = np.empty((len(anim_data.pairs), 2), np.uint16) + for i, pair in enumerate(anim_data.pairs): + indice_values[i] = [len(pair.values), pair.offset] # Use calculated offsets + indice_values = indice_values.reshape(-1) + indice_table = IntArray(indice_values, str(anim_data.indice_reference), 6, -6) + + if values_address == -1: + anim_data.values_reference = value_table.name + else: + anim_data.values_reference = values_address + return size, indice_table + + indice_tables: list[IntArray] = [] + value_tables: list[IntArray] = [] + + values_name = values_name or str(anims_data[0].values_reference) + indices_address = start_address + if start_address != -1: + for anim_data in anims_data: + anim_data.indice_reference = indices_address + indices_address += len(anim_data.pairs) * 2 * 2 + values_address = indices_address + + print("Generating compressed value table and offsets.") + # opt: this is the max size possible, prevents tons of allocations and only about 65 kb + value_table = IntArray(np.empty(MAX_U16, np.int16), values_name, 8) + size = 0 + value_tables.append(value_table) + i = 0 # we can´t use enumarate, as we may repeat + while i < len(anims_data): + anim_data = anims_data[i] + + size_before_add = size + size, indice_table = add_data(value_table, size, anim_data, values_address) + if size != -1: # sucefully added the data to the value table + assert indice_table is not None + indice_tables.append(indice_table) + i += 1 # do the next animation + else: # Could not add to the value table + if size_before_add == 0: # If the table was empty, it is simply invalid + raise PluginError(f"Index table cannot fit into value table of 16 bit max size ({MAX_U16}).") + else: # try again with a fresh value table + value_table.data.resize(size_before_add, refcheck=False) + if start_address != -1: + values_address += size_before_add * 2 + value_table = IntArray(np.empty(MAX_U16, np.int16), f"{values_name}_{len(value_tables)}", 9) + value_tables.append(value_table) + size = 0 # reset size + # don't increment i, redo + value_table.data.resize(size, refcheck=False) + + return indice_tables, value_tables diff --git a/fast64_internal/sm64/animation/constants.py b/fast64_internal/sm64/animation/constants.py new file mode 100644 index 000000000..7ea4bbdc4 --- /dev/null +++ b/fast64_internal/sm64/animation/constants.py @@ -0,0 +1,88 @@ +import struct +import re + +from ...utility import intToHex +from ..sm64_constants import ACTOR_PRESET_INFO, ActorPresetInfo + +HEADER_STRUCT = struct.Struct(">h h h h h h I I I") +HEADER_SIZE = HEADER_STRUCT.size + +TABLE_ELEMENT_PATTERN = re.compile( # strict but only in the sense that it requires valid c code + r""" + (?:\[\s*(?P\w+)\s*\]\s*=\s*)? # Don´t capture brackets or equal, works with nums + (?:(?:&\s*(?P\w+))|(?PNULL)) # Capture element or null, element requires & + (?:\s*,|) # allow no comma, techinically not correct but no other method works + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_PATTERN = re.compile( + r""" + const\s+struct\s*Animation\s*\*const\s*(?P\w+)\s* + (?:\[.*?\])? # Optional size, don´t capture + \s*=\s*\{ + (?P[\s\S]*) # Capture any character including new lines + (?=\}\s*;) # Look ahead for the end + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_ENUM_PATTERN = re.compile( # strict but only in the sense that it requires valid c code + r""" + (?P\w+)\s* + (?:\s*=\s*(?P\w+)\s*)? + (?=,|) # lookahead, allow no comma, techinically not correct but no other method works + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +TABLE_ENUM_LIST_PATTERN = re.compile( + r""" + enum\s*(?P\w+)\s*\{ + (?P[\s\S]*) # Capture any character including new lines, lazy + (?=\}\s*;) + """, + re.DOTALL | re.VERBOSE | re.MULTILINE, +) + + +enumAnimExportTypes = [ + ("Actor", "Actor Data", "Includes are added to a group in actors/"), + ("Level", "Level Data", "Includes are added to a specific level in levels/"), + ( + "DMA", + "DMA (Mario)", + "No headers or includes are genarated. Mario animation converter order is used (headers, indicies, values)", + ), + ("Custom", "Custom Path", "Exports to a specific path"), +] + +enum_anim_import_types = [ + ("C", "C", "Import a decomp folder or a specific animation"), + ("Binary", "Binary", "Import from ROM"), + ("Insertable Binary", "Insertable Binary", "Import from an insertable binary file"), +] + +enum_anim_binary_import_types = [ + ("DMA", "DMA (Mario)", "Import a DMA animation from a DMA table from a ROM"), + ("Table", "Table", "Import animations from an animation table from a ROM"), + ("Animation", "Animation", "Import one animation from a ROM"), +] + + +enum_animated_behaviours = [("Custom", "Custom Behavior", "Custom"), ("", "Presets", "")] +enum_anim_tables = [("Custom", "Custom", "Custom"), ("", "Presets", "")] +for actor_name, preset_info in ACTOR_PRESET_INFO.items(): + if not preset_info.animation: + continue + behaviours = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.behaviours) + enum_animated_behaviours.extend( + [(intToHex(address), name, intToHex(address)) for name, address in behaviours.items()] + ) + tables = ActorPresetInfo.get_member_as_dict(actor_name, preset_info.animation.address) + enum_anim_tables.extend( + [(name, name, f"{intToHex(address)}, {preset_info.level}") for name, address in tables.items()] + ) diff --git a/fast64_internal/sm64/animation/exporting.py b/fast64_internal/sm64/animation/exporting.py new file mode 100644 index 000000000..4705a4a52 --- /dev/null +++ b/fast64_internal/sm64/animation/exporting.py @@ -0,0 +1,1033 @@ +from typing import TYPE_CHECKING, Optional +from pathlib import Path +import os +import typing +import numpy as np + +import bpy +from bpy.types import Object, Action, PoseBone, Context +from bpy.path import abspath +from mathutils import Euler, Quaternion + +from ...utility import ( + PluginError, + bytesToHex, + encodeSegmentedAddr, + decodeSegmentedAddr, + get64bitAlignedAddr, + getPathAndLevel, + getExportDir, + intToHex, + applyBasicTweaks, + toAlnum, + directory_path_checks, +) +from ...utility_anim import stashActionInArmature + +from ..sm64_constants import BEHAVIOR_COMMANDS, BEHAVIOR_EXITS, defaultExtendSegment4, level_pointers +from ..sm64_utility import ( + ModifyFoundDescriptor, + find_descriptor_in_text, + get_comment_map, + to_include_descriptor, + write_includes, + update_actor_includes, + int_from_str, + write_or_delete_if_found, +) +from ..sm64_classes import BinaryExporter, RomReader, InsertableBinaryData +from ..sm64_level_parser import parseLevelAtPointer +from ..sm64_rom_tweaks import ExtendBank0x04 + +from .classes import ( + SM64_Anim, + SM64_AnimHeader, + SM64_AnimData, + SM64_AnimPair, + SM64_AnimTable, + SM64_AnimTableElement, +) +from .importing import import_enums, import_tables, update_table_with_table_enum +from .utility import ( + get_anim_owners, + get_anim_actor_name, + anim_name_to_enum_name, + get_selected_action, + get_action_props, + duplicate_name, +) +from .constants import HEADER_SIZE + +if TYPE_CHECKING: + from .properties import ( + SM64_ActionAnimProperty, + SM64_AnimHeaderProperties, + SM64_ArmatureAnimProperties, + SM64_AnimTableElementProperties, + ) + from ..settings.properties import SM64_Properties + from ..sm64_objects import SM64_CombinedObjectProperties + + +def trim_duplicates_vectorized(arr2d: np.ndarray) -> list: + """ + Similar to the old removeTrailingFrames(), but using numpy vectorization. + Remove trailing duplicate elements along the last axis of a 2D array. + One dimensional example of this in SM64_AnimPair.clean_frames + """ + # Get the last element of each sub-array along the last axis + last_elements = arr2d[:, -1] + mask = arr2d != last_elements[:, None] + # Reverse the order, find the last element with the same value + trim_indices = np.argmax(mask[:, ::-1], axis=1) + # return list(arr2d) # uncomment to test large sizes + return [ + sub_array if index == 1 else sub_array[: 1 if index == 0 else (-index + 1)] + for sub_array, index in zip(arr2d, trim_indices) + ] + + +def get_entire_fcurve_data( + action: Action, + anim_owner: PoseBone | Object, + prop: str, + max_frame: int, + values: np.ndarray[tuple[typing.Any, typing.Any], np.dtype[np.float32]], +): + data_path = anim_owner.path_from_id(prop) + + default_values = list(getattr(anim_owner, prop)) + populated = [False] * len(default_values) + + for fcurve in action.fcurves: + if fcurve.data_path == data_path: + array_index = fcurve.array_index + for frame in range(max_frame): + values[array_index, frame] = fcurve.evaluate(frame) + populated[array_index] = True + + for i, is_populated in enumerate(populated): + if not is_populated: + values[i] = np.full(values[i].size, default_values[i]) + + return values + + +def read_quick(actions, max_frames, anim_owners, trans_values, rot_values): + def to_xyz(row): + euler = Euler(row, mode) + return [euler.x, euler.y, euler.z] + + for action, max_frame, action_trans, action_rot in zip(actions, max_frames, trans_values, rot_values): + quats = np.empty((4, max_frame), dtype=np.float32) + + get_entire_fcurve_data(action, anim_owners[0], "location", max_frame, action_trans) + + for bone_index, anim_owner in enumerate(anim_owners): + mode = anim_owner.rotation_mode + prop = {"QUATERNION": "rotation_quaternion", "AXIS_ANGLE": "rotation_axis_angle"}.get( + mode, "rotation_euler" + ) + + index = bone_index * 3 + if mode == "QUATERNION": + get_entire_fcurve_data(action, anim_owner, prop, max_frame, quats) + action_rot[index : index + 3] = np.apply_along_axis( + lambda row: Quaternion(row).to_euler(), 1, quats.T + ).T + elif mode == "AXIS_ANGLE": + get_entire_fcurve_data(action, anim_owner, prop, max_frame, quats) + action_rot[index : index + 3] = np.apply_along_axis( + lambda row: list(Quaternion(row[1:], row[0]).to_euler()), 1, quats.T + ).T + else: + get_entire_fcurve_data(action, anim_owner, prop, max_frame, action_rot[index : index + 3]) + if mode != "XYZ": + action_rot[index : index + 3] = np.apply_along_axis(to_xyz, -1, action_rot[index : index + 3].T).T + + +def read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, is_owner_obj): + pre_export_frame = bpy.context.scene.frame_current + pre_export_action = obj.animation_data.action + was_playing = bpy.context.screen.is_animation_playing + + try: + if bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() # if an animation is being played, stop it + for action, action_trans, action_rot, max_frame in zip(actions, trans_values, rot_values, max_frames): + print(f'Reading animation data from action "{action.name}".') + obj.animation_data.action = action + for frame in range(max_frame): + bpy.context.scene.frame_set(frame) + + for bone_index, anim_owner in enumerate(anim_owners): + if is_owner_obj: + local_matrix = anim_owner.matrix_local + else: + local_matrix = obj.convert_space( + pose_bone=anim_owner, matrix=anim_owner.matrix, from_space="POSE", to_space="LOCAL" + ) + if bone_index == 0: + action_trans[0:3, frame] = list(local_matrix.to_translation()) + index = bone_index * 3 + action_rot[index : index + 3, frame] = list(local_matrix.to_euler()) + finally: + obj.animation_data.action = pre_export_action + bpy.context.scene.frame_set(pre_export_frame) + if was_playing != bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() + + +def get_animation_pairs( + sm64_scale: float, actions: list[Action], obj: Object, quick_read=False +) -> dict[Action, list[SM64_AnimPair]]: + anim_owners = get_anim_owners(obj) + is_owner_obj = isinstance(obj.type == "MESH", Object) + + if len(anim_owners) == 0: + raise PluginError(f'No animation bones in armature "{obj.name}"') + + if len(actions) < 1: + return {} + + max_frames = [get_action_props(action).get_max_frame(action) for action in actions] + trans_values = [np.zeros((3, max_frame), dtype=np.float32) for max_frame in max_frames] + rot_values = [np.zeros((len(anim_owners) * 3, max_frame), dtype=np.float32) for max_frame in max_frames] + + if quick_read: + read_quick(actions, max_frames, anim_owners, trans_values, rot_values) + else: + read_full(actions, max_frames, anim_owners, trans_values, rot_values, obj, is_owner_obj) + + action_pairs = {} + for action, action_trans, action_rot in zip(actions, trans_values, rot_values): + action_trans = trim_duplicates_vectorized(np.round(action_trans * sm64_scale).astype(np.int16)) + action_rot = trim_duplicates_vectorized(np.round(np.degrees(action_rot) * (2**16 / 360.0)).astype(np.int16)) + + pairs = [SM64_AnimPair(values) for values in action_trans] + pairs.extend([SM64_AnimPair(values) for values in action_rot]) + action_pairs[action] = pairs + + return action_pairs + + +def to_header_class( + header_props: "SM64_AnimHeaderProperties", + bone_count: int, + data: SM64_AnimData | None, + action: Action, + values_reference: int | str, + indice_reference: int | str, + dma: bool, + export_type: str, + table_index: Optional[int] = None, + actor_name="mario", + gen_enums=False, + file_name="anim_00.inc.c", +): + header = SM64_AnimHeader() + header.reference = header_props.get_name(actor_name, action, dma) + if gen_enums: + header.enum_name = header_props.get_enum(actor_name, action) + + header.flags = header_props.get_flags(not (export_type.endswith("Binary") or dma)) + header.trans_divisor = header_props.trans_divisor + header.start_frame, header.loop_start, header.loop_end = header_props.get_loop_points(action) + header.values_reference = values_reference + header.indice_reference = indice_reference + header.bone_count = bone_count + header.table_index = header_props.table_index if table_index is None else table_index + header.file_name = file_name + header.data = data + return header + + +def to_data_class(pairs: list[SM64_AnimPair], data_name="anim_00", file_name: str = "anim_00.inc.c"): + return SM64_AnimData(pairs, f"{data_name}_indices", f"{data_name}_values", file_name, file_name) + + +def to_animation_class( + action_props: "SM64_ActionAnimProperty", + action: Action, + obj: Object, + blender_to_sm64_scale: float, + quick_read: bool, + export_type: str, + dma: bool, + actor_name="mario", + gen_enums=False, +) -> SM64_Anim: + can_reference = not dma + animation = SM64_Anim() + animation.file_name = action_props.get_file_name(action, export_type, dma) + + if can_reference and action_props.reference_tables: + if export_type.endswith("Binary"): + values_reference, indice_reference = int_from_str(action_props.values_address), int( + action_props.indices_address, 0 + ) + else: + values_reference, indice_reference = action_props.values_table, action_props.indices_table + else: + pairs = get_animation_pairs(blender_to_sm64_scale, [action], obj, quick_read)[action] + animation.data = to_data_class(pairs, action_props.get_name(action, dma), animation.file_name) + values_reference = animation.data.values_reference + indice_reference = animation.data.indice_reference + bone_count = len(get_anim_owners(obj)) + for header_props in action_props.headers: + animation.headers.append( + to_header_class( + header_props=header_props, + bone_count=bone_count, + data=animation.data, + action=action, + values_reference=values_reference, + indice_reference=indice_reference, + dma=dma, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + file_name=animation.file_name, + table_index=None, + ) + ) + + return animation + + +def to_table_element_class( + element_props: "SM64_AnimTableElementProperties", + header_dict: dict["SM64_AnimHeaderProperties", SM64_AnimHeader], + data_dict: dict[Action, SM64_AnimData], + action_pairs: dict[Action, list[SM64_AnimPair]], + bone_count: int, + table_index: int, + dma: bool, + export_type: str, + actor_name="mario", + gen_enums=False, + prev_enums: dict[str, int] | None = None, +): + prev_enums = prev_enums or {} + use_addresses, can_reference = export_type.endswith("Binary"), not dma + element = SM64_AnimTableElement() + + enum = None + if gen_enums: + enum = element_props.get_enum(can_reference, actor_name, prev_enums) + element.enum_name = enum + + if can_reference and element_props.reference: + reference = int_from_str(element_props.header_address) if use_addresses else element_props.header_name + element.reference = reference + if reference == "": + raise PluginError("Header is not set.") + if enum == "": + raise PluginError("Enum name is not set.") + return element + + # Not reference + header_props, action = element_props.get_header(can_reference), element_props.get_action(can_reference) + if not action: + raise PluginError("Action is not set.") + if not header_props: + raise PluginError("Header is not set.") + if enum == "": + raise PluginError("Enum name is not set.") + + action_props = get_action_props(action) + if can_reference and action_props.reference_tables: + data = None + if use_addresses: + values_reference, indice_reference = ( + int_from_str(action_props.values_address), + int_from_str(action_props.indices_address), + ) + else: + values_reference, indice_reference = action_props.values_table, action_props.indices_table + else: + if action in action_pairs and action not in data_dict: + data_dict[action] = to_data_class( + action_pairs[action], + action_props.get_name(action, dma), + action_props.get_file_name(action, export_type, dma), + ) + data = data_dict[action] + values_reference, indice_reference = data.values_reference, data.indice_reference + + if header_props not in header_dict: + header_dict[header_props] = to_header_class( + header_props=header_props, + bone_count=bone_count, + data=data, + action=action, + values_reference=values_reference, + indice_reference=indice_reference, + dma=dma, + export_type=export_type, + table_index=table_index, + actor_name=actor_name, + gen_enums=gen_enums, + file_name=action_props.get_file_name(action, export_type), + ) + + element.header = header_dict[header_props] + element.reference = element.header.reference + return element + + +def to_table_class( + anim_props: "SM64_ArmatureAnimProperties", + obj: Object, + blender_to_sm64_scale: float, + quick_read: bool, + dma: bool, + export_type: str, + actor_name="mario", + gen_enums=False, +) -> SM64_AnimTable: + can_reference = not dma + table = SM64_AnimTable( + anim_props.get_table_name(actor_name), + anim_props.get_enum_name(actor_name), + anim_props.get_enum_end(actor_name), + anim_props.get_table_file_name(actor_name, export_type), + values_reference=toAlnum(f"anim_{actor_name}_values"), + ) + + header_dict: dict[SM64_AnimHeaderProperties, SM64_AnimHeader] = {} + + bone_count = len(get_anim_owners(obj)) + action_pairs = get_animation_pairs( + blender_to_sm64_scale, + [action for action in anim_props.actions if not (can_reference and get_action_props(action).reference_tables)], + obj, + quick_read, + ) + data_dict = {} + + prev_enums = {} + element_props: SM64_AnimTableElementProperties + for i, element_props in enumerate(anim_props.elements): + try: + table.elements.append( + to_table_element_class( + element_props=element_props, + header_dict=header_dict, + data_dict=data_dict, + action_pairs=action_pairs, + bone_count=bone_count, + table_index=i, + dma=dma, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + prev_enums=prev_enums, + ) + ) + except Exception as exc: + raise PluginError(f"Table element {i}: {exc}") from exc + if not dma and anim_props.null_delimiter: + table.elements.append(SM64_AnimTableElement(enum_name=table.enum_list_delimiter)) + return table + + +def update_includes( + combined_props: "SM64_CombinedObjectProperties", + header_dir: Path, + actor_name, + update_table: bool, +): + data_includes = [Path("anims/data.inc.c")] + header_includes = [] + if update_table: + data_includes.append(Path("anims/table.inc.c")) + header_includes.append(Path("anim_header.h")) + update_actor_includes( + combined_props.export_header_type, + combined_props.actor_group_name, + header_dir, + actor_name, + combined_props.export_level_name, + data_includes, + header_includes, + ) + + +def update_anim_header(path: Path, table_name: str, gen_enums: bool, override_files: bool): + to_add = [ + ModifyFoundDescriptor( + f"extern const struct Animation *const {table_name}[];", + rf"extern\h*const\h*struct\h*Animation\h?\*const\h*{table_name}\[.*?\]\h*?;", + ) + ] + if gen_enums: + to_add.append(to_include_descriptor(Path("anims/table_enum.h"))) + if write_or_delete_if_found(path, to_add, create_new=override_files): + print(f"Updated animation header {path}") + + +def update_enum_file(path: Path, override_files: bool, table: SM64_AnimTable): + text, comment_map = "", [] + existing_file = path.exists() and not override_files + if existing_file: + text, comment_map = get_comment_map(path.read_text()) + + if table.enum_list_start == -1 and table.enum_list_end == -1: # create new enum list + if text and text[-1] not in {"\n", "\r"}: + text += "\n" + table.enum_list_start = len(text) + text += f"enum {table.enum_list_reference} {{\n" + table.enum_list_end = len(text) + text += "};\n" + + content = text[table.enum_list_start : table.enum_list_end] + for i, element in enumerate(table.elements): + if element.enum_start == -1 or element.enum_end == -1: + content += f"\t{element.enum_c},\n" + if existing_file: + print(f"Added enum list entrie {element.enum_c}.") + continue + + old_text = content[element.enum_start : element.enum_end] + if old_text != element.enum_c: + content = content[: element.enum_start] + element.enum_c + content[element.enum_end :] + if existing_file: + print(f'Replaced "{old_text}" with "{element.enum_c}".') + # acccount for changed size + size_increase = len(element.enum_c) - len(old_text) + for next_element in table.elements[i + 1 :]: + if next_element.enum_start != -1 and next_element.enum_end != -1: + next_element.enum_start += size_increase + next_element.enum_end += size_increase + if not existing_file: + print(f"Creating enum list file at {path}.") + text = text[: table.enum_list_start] + content + text[table.enum_list_end :] + path.write_text(text) + + +def update_table_file( + table: SM64_AnimTable, + table_path: Path, + add_null_delimiter: bool, + override_files: bool, + gen_enums: bool, + designated: bool, + enum_list_path: Path, +): + assert isinstance(table.reference, str) and table.reference, "Invalid table reference" + + text, enum_text = "", "" + existing_file = table_path.exists() and not override_files + if existing_file: + text = table_path.read_text() + comment_less, comment_map = get_comment_map(text) + + # add include if not already there + descriptor = to_include_descriptor(Path("table_enum.h")) + if gen_enums and len(find_descriptor_in_text(descriptor, comment_less, comment_map)) == 0: + text = '#include "table_enum.h"\n' + text + comment_less, comment_map = get_comment_map(text) + + # First, find existing tables + tables = import_tables(comment_less, table_path, comment_map, table.reference) + enum_tables = [] + if gen_enums: + assert isinstance(table.enum_list_reference, str) and table.enum_list_reference + enum_text, enum_comment_less, enum_comment_map = "", "", [] + if enum_list_path.exists() and not override_files: + enum_text = enum_list_path.read_text() + enum_comment_less, enum_comment_map = get_comment_map(enum_text) + enum_tables = import_enums(enum_comment_less, enum_list_path, enum_comment_map, table.enum_list_reference) + if len(enum_tables) > 1: + raise PluginError(f'Duplicate enum list "{table.enum_list_reference}"') + + if len(tables) > 1: + raise PluginError(f'Duplicate animation table "{table.reference}"') + elif len(tables) == 1: + existing_table = tables[0] + if gen_enums: + if enum_tables: # apply enum table names to existing unset enums + update_table_with_table_enum(existing_table, enum_tables[0]) + table.enum_list_reference, table.enum_list_start, table.enum_list_end = ( + existing_table.enum_list_reference, + existing_table.enum_list_start, + existing_table.enum_list_end, + ) + + # Figure out enums on existing enum-less elements + prev_enums = {name: 0 for name in existing_table.enum_names} + for i, element in enumerate(existing_table.elements): + if element.enum_name: + continue + if not element.reference: + if i == len(existing_table.elements) - 1: + element.enum_name = duplicate_name(table.enum_list_delimiter, prev_enums) + else: + element.enum_name = duplicate_name( + anim_name_to_enum_name(f"{existing_table.reference}_NULL"), prev_enums + ) + continue + element.enum_name = duplicate_name( + next( + (enum for name, enum in zip(*table.names) if enum and name == element.reference), + anim_name_to_enum_name(element.reference), + ), + prev_enums, + ) + + new_elements = existing_table.elements.copy() + has_null_delimiter = existing_table.has_null_delimiter + for element in table.elements: + if element.c_name in existing_table.header_names and ( + not gen_enums or element.enum_name in existing_table.enum_names + ): + continue + if has_null_delimiter: + new_elements[-1].reference = element.reference + new_elements[-1].enum_name = element.enum_name + has_null_delimiter = False + else: + new_elements.append(element) + table.elements = new_elements + table.start, table.end = (existing_table.start, existing_table.end) + else: # create new table + if text and text[-1] not in {"\n", "\r"}: + text += "\n" + table.start = len(text) + text += f"const struct Animation *const {table.reference}[] = {{\n" + table.end = len(text) + text += "};\n" + + if add_null_delimiter and not table.has_null_delimiter: # add null delimiter if not present or replaced + table.elements.append(SM64_AnimTableElement(enum_name=table.enum_list_delimiter)) + + if gen_enums: + update_enum_file(enum_list_path, override_files, table) + + content = text[table.start : table.end] + for i, element in enumerate(table.elements): + element_text = element.to_c(designated and gen_enums) + if element.reference_start == -1 or element.reference_end == -1: + content += f"\t{element_text}\n" + if existing_file: + print(f"Added table entrie {element_text}.") + continue + + # update existing region instead + old_text = content[element.reference_start : element.reference_end] + if old_text != element_text: + content = content[: element.reference_start] + element_text + content[element.reference_end :] + if existing_file: + print(f'Replaced "{old_text}" with "{element_text}".') + + size_increase = len(element_text) - len(old_text) + if size_increase == 0: + continue + for next_element in table.elements[i + 1 :]: # acccount for changed size + if next_element.reference_start != -1 and next_element.reference_end != -1: + next_element.reference_start += size_increase + next_element.reference_end += size_increase + + if not existing_file: + print(f"Creating table file at {table_path}.") + text = text[: table.start] + content + text[table.end :] + table_path.write_text(text) + + +def update_data_file(path: Path, anim_file_names: list[str], override_files: bool = False): + includes = [Path(file_name) for file_name in anim_file_names] + if write_includes(path, includes, create_new=override_files): + print(f"Updating animation data file includes at {path}") + + +def update_behaviour_binary( + binary_exporter: BinaryExporter, address: int, table_address: bytes, beginning_animation: int +): + load_set = False + animate_set = False + exited = False + while not exited and not (load_set and animate_set): + command_index = int.from_bytes(binary_exporter.read(1, address), "big") + name, size = BEHAVIOR_COMMANDS[command_index] + print(name, intToHex(address)) + if name in BEHAVIOR_EXITS: + exited = True + if name == "LOAD_ANIMATIONS": + ptr_address = address + 4 + print( + f"Found LOAD_ANIMATIONS at {intToHex(address)}, " + f"replacing ptr {bytesToHex(binary_exporter.read(4, ptr_address))} " + f"at {intToHex(ptr_address)} with {bytesToHex(table_address)}" + ) + binary_exporter.write(table_address, ptr_address) + load_set = True + elif name == "ANIMATE": + value_address = address + 1 + print( + f"Found ANIMATE at {intToHex(address)}, " + f"replacing value {int.from_bytes(binary_exporter.read(1, value_address), 'big')} " + f"at {intToHex(value_address)} with {beginning_animation}" + ) + binary_exporter.write(beginning_animation.to_bytes(1, "big"), value_address) + animate_set = True + address += 4 * size + if exited: + if not load_set: + raise IndexError("Could not find LOAD_ANIMATIONS command") + if not animate_set: + print("Could not find ANIMATE command") + + +def export_animation_table_binary( + binary_exporter: BinaryExporter, + anim_props: "SM64_ArmatureAnimProperties", + table: SM64_AnimTable, + is_dma: bool, + level_option: str, + extend_bank_4: bool, +): + if is_dma: + data = table.to_binary_dma() + binary_exporter.write_to_range( + get64bitAlignedAddr(int_from_str(anim_props.dma_address)), int_from_str(anim_props.dma_end_address), data + ) + return + + level_parsed = parseLevelAtPointer(binary_exporter.rom_file_output, level_pointers[level_option]) + segment_data = level_parsed.segmentData + if extend_bank_4: + ExtendBank0x04(binary_exporter.rom_file_output, segment_data, defaultExtendSegment4) + + address = get64bitAlignedAddr(int_from_str(anim_props.address)) + end_address = int_from_str(anim_props.end_address) + + if anim_props.write_data_seperately: # Write the data and the table into seperate address range + data_address = get64bitAlignedAddr(int_from_str(anim_props.data_address)) + data_end_address = int_from_str(anim_props.data_end_address) + table_data, data = table.to_combined_binary(address, data_address, segment_data)[:2] + binary_exporter.write_to_range(address, end_address, table_data) + binary_exporter.write_to_range(data_address, data_end_address, data) + else: # Write table then the data in one address range + table_data, data = table.to_combined_binary(address, -1, segment_data)[:2] + binary_exporter.write_to_range(address, end_address, table_data + data) + if anim_props.update_behavior: + update_behaviour_binary( + binary_exporter, + decodeSegmentedAddr(anim_props.behavior_address.to_bytes(4, "big"), segment_data), + encodeSegmentedAddr(address, segment_data), + int_from_str(anim_props.beginning_animation), + ) + + +def export_animation_table_insertable(table: SM64_AnimTable, is_dma: bool, directory: Path): + directory_path_checks(directory, "Empty directory path.") + path = directory / table.file_name + if is_dma: + data = table.to_binary_dma() + InsertableBinaryData("Animation DMA Table", data).write(path) + else: + table_data, data, ptrs = table.to_combined_binary() + InsertableBinaryData("Animation Table", table_data + data, 0, ptrs).write(path) + + +def create_and_get_paths( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + actor_name: str, + decomp: Path, +): + anim_directory = geo_directory = header_directory = None + if anim_props.is_dma: + if combined_props.export_header_type == "Custom": + geo_directory = Path(abspath(combined_props.custom_export_path)) + anim_directory = Path(abspath(combined_props.custom_export_path), anim_props.dma_folder) + else: + anim_directory = Path(decomp, anim_props.dma_folder) + else: + export_path, level_name = getPathAndLevel( + combined_props.is_actor_custom_export, + combined_props.actor_custom_path, + combined_props.export_level_name, + combined_props.level_name, + ) + header_directory, _tex_dir = getExportDir( + combined_props.is_actor_custom_export, + export_path, + combined_props.export_header_type, + level_name, + texDir="", + dirName=actor_name, + ) + header_directory = Path(bpy.path.abspath(header_directory)) + geo_directory = header_directory / actor_name + anim_directory = geo_directory / "anims" + + for path in (anim_directory, geo_directory, header_directory): + if path is not None and not os.path.exists(path): + os.makedirs(path, exist_ok=True) + return (anim_directory, geo_directory, header_directory) + + +def export_animation_table_c( + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + table: SM64_AnimTable, + decomp: Path, + actor_name: str, + designated: bool, +): + if not combined_props.is_actor_custom_export: + applyBasicTweaks(decomp) + anim_directory, geo_directory, header_directory = create_and_get_paths( + anim_props, combined_props, actor_name, decomp + ) + + print("Creating all animation C data") + if anim_props.export_seperately or anim_props.is_dma: + files_data = table.data_and_headers_to_c(anim_props.is_dma) + print("Saving all generated data files") + for file_name, file_data in files_data.items(): + (anim_directory / file_name).write_text(file_data) + print(file_name) + if not anim_props.is_dma: + update_data_file( + anim_directory / "data.inc.c", + list(files_data.keys()), + anim_props.override_files, + ) + else: + result = table.data_and_headers_to_c_combined() + print("Saving generated data file") + (anim_directory / "data.inc.c").write_text(result) + print("All animation data files exported.") + if anim_props.is_dma: # Don´t create an actual table and or update includes for dma exports + return + assert geo_directory and header_directory and isinstance(table.reference, str) + + header_path = geo_directory / "anim_header.h" + update_anim_header(header_path, table.reference, anim_props.gen_enums, anim_props.override_files) + update_table_file( + table=table, + table_path=anim_directory / "table.inc.c", + add_null_delimiter=anim_props.null_delimiter, + gen_enums=anim_props.gen_enums, + designated=designated, + enum_list_path=anim_directory / "table_enum.h", + override_files=anim_props.override_files, + ) + update_includes(combined_props, header_directory, actor_name, True) + + +def export_animation_binary( + binary_exporter: BinaryExporter, + animation: SM64_Anim, + action_props: "SM64_ActionAnimProperty", + anim_props: "SM64_ArmatureAnimProperties", + bone_count: int, + level_option: str, + extend_bank_4: bool, +): + if anim_props.is_dma: + dma_address = int_from_str(anim_props.dma_address) + print("Reading DMA table from ROM") + table = SM64_AnimTable().read_dma_binary( + reader=RomReader(rom_file=binary_exporter.rom_file_output, start_address=dma_address), + read_headers={}, + table_index=None, + bone_count=bone_count, + ) + empty_data = SM64_AnimData() + for header in animation.headers: + while header.table_index >= len(table.elements): + table.elements.append(SM64_AnimTableElement(header=SM64_AnimHeader(data=empty_data))) + table.elements[header.table_index] = SM64_AnimTableElement(header=header) + print("Converting to binary data") + data = table.to_binary_dma() + binary_exporter.write_to_range(dma_address, int_from_str(anim_props.dma_end_address), data) + return + level_parsed = parseLevelAtPointer(binary_exporter.rom_file_output, level_pointers[level_option]) + segment_data = level_parsed.segmentData + if extend_bank_4: + ExtendBank0x04(binary_exporter.rom_file_output, segment_data, defaultExtendSegment4) + + animation_address = get64bitAlignedAddr(int_from_str(action_props.start_address)) + animation_end_address = int_from_str(action_props.end_address) + + data = animation.to_binary(animation_address, segment_data)[0] + binary_exporter.write_to_range( + animation_address, + animation_end_address, + data, + ) + table_address = get64bitAlignedAddr(int_from_str(anim_props.address)) + table_end_address = int_from_str(anim_props.end_address) + if anim_props.update_table: + for i, header in enumerate(animation.headers): + element_address = table_address + (4 * header.table_index) + if element_address > table_end_address: + raise PluginError( + f"Animation header {i + 1} sets table index {header.table_index} which is out of bounds, " + f"table is {table_end_address - table_address} bytes long, " + "update the table start/end addresses in the armature properties" + ) + binary_exporter.seek(element_address) + binary_exporter.write(encodeSegmentedAddr(animation_address + (i * HEADER_SIZE), segment_data)) + if anim_props.update_behavior: + update_behaviour_binary( + binary_exporter, + decodeSegmentedAddr(anim_props.behavior_address.to_bytes(4, "big"), segment_data), + encodeSegmentedAddr(table_address, segment_data), + int_from_str(anim_props.beginning_animation), + ) + + +def export_animation_insertable(animation: SM64_Anim, is_dma: bool, directory: Path): + data, ptrs = animation.to_binary(is_dma) + InsertableBinaryData("Animation", data, 0, ptrs).write(directory / animation.file_name) + + +def export_animation_c( + animation: SM64_Anim, + anim_props: "SM64_ArmatureAnimProperties", + combined_props: "SM64_CombinedObjectProperties", + decomp: Path, + actor_name: str, + designated: bool, +): + if not combined_props.is_actor_custom_export: + applyBasicTweaks(decomp) + anim_directory, geo_directory, header_directory = create_and_get_paths( + anim_props, combined_props, actor_name, decomp + ) + + (anim_directory / animation.file_name).write_text(animation.to_c(anim_props.is_dma)) + + if anim_props.is_dma: # Don´t create an actual table and don´t update includes for dma exports + return + + table_name = anim_props.get_table_name(actor_name) + + if anim_props.update_table: + update_anim_header(geo_directory / "anim_header.h", table_name, anim_props.gen_enums, False) + update_table_file( + table=SM64_AnimTable( + table_name, + enum_list_reference=anim_props.get_enum_name(actor_name), + enum_list_delimiter=anim_props.get_enum_end(actor_name), + elements=[ + SM64_AnimTableElement(header.reference, header, header.enum_name) for header in animation.headers + ], + ), + table_path=anim_directory / "table.inc.c", + add_null_delimiter=anim_props.null_delimiter, + gen_enums=anim_props.gen_enums, + designated=designated, + enum_list_path=anim_directory / "table_enum.h", + override_files=False, + ) + update_data_file(anim_directory / "data.inc.c", [animation.file_name]) + update_includes(combined_props, header_directory, actor_name, anim_props.update_table) + + +def export_animation(context: Context, obj: Object): + scene = context.scene + sm64_props: SM64_Properties = scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + actor_name: str = get_anim_actor_name(context) + + action = get_selected_action(obj) + action_props = get_action_props(action) + stashActionInArmature(obj, action) + bone_count = len(get_anim_owners(obj)) + + try: + animation = to_animation_class( + action_props=action_props, + action=action, + obj=obj, + blender_to_sm64_scale=sm64_props.blender_to_sm64_scale, + quick_read=combined_props.quick_anim_read, + export_type=sm64_props.export_type, + dma=anim_props.is_dma, + actor_name=actor_name, + gen_enums=not sm64_props.binary_export and anim_props.gen_enums, + ) + except Exception as exc: + raise PluginError(f"Failed to generate animation class. {exc}") from exc + if sm64_props.export_type == "C": + export_animation_c( + animation, anim_props, combined_props, sm64_props.abs_decomp_path, actor_name, sm64_props.designated + ) + elif sm64_props.export_type == "Insertable Binary": + export_animation_insertable(animation, anim_props.is_dma, Path(abspath(combined_props.insertable_directory))) + elif sm64_props.export_type == "Binary": + with BinaryExporter( + Path(abspath(sm64_props.export_rom)), Path(abspath(sm64_props.output_rom)) + ) as binary_exporter: + export_animation_binary( + binary_exporter, + animation, + action_props, + anim_props, + bone_count, + combined_props.level_name, + sm64_props.extend_bank_4, + ) + else: + raise NotImplementedError(f"Export type {sm64_props.export_type} is not implemented") + + +def export_animation_table(context: Context, obj: Object): + bpy.ops.object.mode_set(mode="OBJECT") + + scene = context.scene + sm64_props: SM64_Properties = scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + actor_name: str = get_anim_actor_name(context) + + print("Stashing all actions in table") + for action in anim_props.actions: + stashActionInArmature(obj, action) + + if len(anim_props.elements) == 0: + raise PluginError("Empty animation table") + + try: + print("Reading table data from fast64") + table = to_table_class( + anim_props=anim_props, + obj=obj, + blender_to_sm64_scale=sm64_props.blender_to_sm64_scale, + quick_read=combined_props.quick_anim_read, + dma=anim_props.is_dma, + export_type=sm64_props.export_type, + actor_name=actor_name, + gen_enums=not anim_props.is_dma and not sm64_props.binary_export and anim_props.gen_enums, + ) + except Exception as exc: + raise PluginError(f"Failed to generate table class. {exc}") from exc + + print("Exporting table data") + if sm64_props.export_type == "C": + export_animation_table_c( + anim_props, combined_props, table, sm64_props.abs_decomp_path, actor_name, sm64_props.designated + ) + elif sm64_props.export_type == "Insertable Binary": + export_animation_table_insertable(table, anim_props.is_dma, Path(abspath(combined_props.insertable_directory))) + elif sm64_props.export_type == "Binary": + with BinaryExporter( + Path(abspath(sm64_props.export_rom)), Path(abspath(sm64_props.output_rom)) + ) as binary_exporter: + export_animation_table_binary( + binary_exporter, + anim_props, + table, + anim_props.is_dma, + combined_props.level_name, + sm64_props.extend_bank_4, + ) + else: + raise NotImplementedError(f"Export type {sm64_props.export_type} is not implemented") diff --git a/fast64_internal/sm64/animation/importing.py b/fast64_internal/sm64/animation/importing.py new file mode 100644 index 000000000..c9d453546 --- /dev/null +++ b/fast64_internal/sm64/animation/importing.py @@ -0,0 +1,808 @@ +from typing import TYPE_CHECKING, Optional +from pathlib import Path +import dataclasses +import functools +import os +import re +import numpy as np + +import bpy +from bpy.path import abspath +from bpy.types import Object, Action, Context, PoseBone +from mathutils import Quaternion + +from ...f3d.f3d_parser import math_eval +from ...utility import PluginError, decodeSegmentedAddr, filepath_checks, path_checks, intToHex +from ...utility_anim import create_basic_action + +from ..sm64_constants import AnimInfo, level_pointers +from ..sm64_level_parser import parseLevelAtPointer +from ..sm64_utility import CommentMatch, get_comment_map, adjust_start_end, import_rom_checks +from ..sm64_classes import RomReader + +from .utility import ( + animation_operator_checks, + get_action_props, + get_anim_owners, + get_scene_anim_props, + get_anim_actor_name, + anim_name_to_enum_name, + table_name_to_enum, +) +from .classes import ( + SM64_Anim, + CArrayDeclaration, + SM64_AnimHeader, + SM64_AnimTable, + SM64_AnimTableElement, +) +from .constants import ACTOR_PRESET_INFO, TABLE_ENUM_LIST_PATTERN, TABLE_ENUM_PATTERN, TABLE_PATTERN + +if TYPE_CHECKING: + from .properties import ( + SM64_AnimImportProperties, + SM64_ArmatureAnimProperties, + SM64_AnimHeaderProperties, + SM64_ActionAnimProperty, + SM64_AnimTableElementProperties, + ) + from ..settings.properties import SM64_Properties + + +def get_preset_anim_name_list(preset_name: str): + assert preset_name in ACTOR_PRESET_INFO, "Selected preset not in actor presets" + preset = ACTOR_PRESET_INFO[preset_name] + assert preset.animation is not None and isinstance( + preset.animation, AnimInfo + ), "Selected preset's actor has not animation information" + return preset.animation.names + + +def flip_euler(euler: np.ndarray) -> np.ndarray: + euler = euler.copy() + euler[1] = -euler[1] + euler += np.pi + return euler + + +def naive_flip_diff(a1: np.ndarray, a2: np.ndarray) -> np.ndarray: + diff = a1 - a2 + mask = np.abs(diff) > np.pi + return a2 + mask * np.sign(diff) * 2 * np.pi + + +@dataclasses.dataclass +class FramesHolder: + frames: np.ndarray = dataclasses.field(default_factory=list) + + def populate_action(self, action: Action, pose_bone: PoseBone, path: str): + for property_index in range(3): + f_curve = action.fcurves.new( + data_path=pose_bone.path_from_id(path), + index=property_index, + action_group=pose_bone.name, + ) + for time, frame in enumerate(self.frames): + f_curve.keyframe_points.insert(time, frame[property_index], options={"FAST"}) + + +def euler_to_quaternion(euler_angles: np.ndarray): + """ + Fast vectorized euler to quaternion function, euler_angles is an array of shape (-1, 3) + """ + phi = euler_angles[:, 0] + theta = euler_angles[:, 1] + psi = euler_angles[:, 2] + + half_phi = phi / 2.0 + half_theta = theta / 2.0 + half_psi = psi / 2.0 + + cos_half_phi = np.cos(half_phi) + sin_half_phi = np.sin(half_phi) + cos_half_theta = np.cos(half_theta) + sin_half_theta = np.sin(half_theta) + cos_half_psi = np.cos(half_psi) + sin_half_psi = np.sin(half_psi) + + q_w = cos_half_phi * cos_half_theta * cos_half_psi + sin_half_phi * sin_half_theta * sin_half_psi + q_x = sin_half_phi * cos_half_theta * cos_half_psi - cos_half_phi * sin_half_theta * sin_half_psi + q_y = cos_half_phi * sin_half_theta * cos_half_psi + sin_half_phi * cos_half_theta * sin_half_psi + q_z = cos_half_phi * cos_half_theta * sin_half_psi - sin_half_phi * sin_half_theta * cos_half_psi + + quaternions = np.vstack((q_w, q_x, q_y, q_z)).T # shape (-1, 4) + return quaternions + + +@dataclasses.dataclass +class RotationFramesHolder(FramesHolder): + @property + def quaternion(self): + return euler_to_quaternion(self.frames) # We make this code path as optiomal as it can be + + def get_euler(self, order: str): + if order == "XYZ": + return self.frames + return [Quaternion(x).to_euler(order) for x in self.quaternion] + + @property + def axis_angle(self): + result = [] + for x in self.quaternion: + x = Quaternion(x).to_axis_angle() + result.append([x[1]] + list(x[0])) + return result + + def populate_action(self, action: Action, pose_bone: PoseBone, path: str = ""): + rotation_mode = pose_bone.rotation_mode + rotation_mode_name = { + "QUATERNION": "rotation_quaternion", + "AXIS_ANGLE": "rotation_axis_angle", + }.get(rotation_mode, "rotation_euler") + data_path = pose_bone.path_from_id(rotation_mode_name) + + size = 4 + if rotation_mode == "QUATERNION": + rotations = self.quaternion + elif rotation_mode == "AXIS_ANGLE": + rotations = self.axis_angle + else: + rotations = self.get_euler(rotation_mode) + size = 3 + for property_index in range(size): + f_curve = action.fcurves.new( + data_path=data_path, + index=property_index, + action_group=pose_bone.name, + ) + for frame, rotation in enumerate(rotations): + f_curve.keyframe_points.insert(frame, rotation[property_index], options={"FAST"}) + + +@dataclasses.dataclass +class IntermidiateAnimationBone: + translation: FramesHolder = dataclasses.field(default_factory=FramesHolder) + rotation: RotationFramesHolder = dataclasses.field(default_factory=RotationFramesHolder) + + def read_pairs(self, pairs: list["SM64_AnimPair"]): + pair_count = len(pairs) + max_length = max(len(pair.values) for pair in pairs) + result = np.empty((max_length, pair_count), dtype=np.int16) + + for i, pair in enumerate(pairs): + current_length = len(pair.values) + result[:current_length, i] = pair.values + result[current_length:, i] = pair.values[-1] + return result + + def read_translation(self, pairs: list["SM64_AnimPair"], scale: float): + self.translation.frames = self.read_pairs(pairs) / scale + + def continuity_filter(self, frames: np.ndarray) -> np.ndarray: + if len(frames) <= 1: + return frames + + # There is no way to fully vectorize this function + prev = frames[0] + for frame, euler in enumerate(frames): + euler = naive_flip_diff(prev, euler) + flipped_euler = naive_flip_diff(prev, flip_euler(euler)) + if np.all((prev - flipped_euler) ** 2 < (prev - euler) ** 2): + euler = flipped_euler + frames[frame] = prev = euler + + return frames + + def read_rotation(self, pairs: list["SM64_AnimPair"], continuity_filter: bool): + frames = self.read_pairs(pairs).astype(np.uint16).astype(np.float32) + frames *= 360.0 / (2**16) + frames = np.radians(frames) + if continuity_filter: + frames = self.continuity_filter(frames) + self.rotation.frames = frames + + def populate_action(self, action: Action, pose_bone: PoseBone): + self.translation.populate_action(action, pose_bone, "location") + self.rotation.populate_action(action, pose_bone, "") + + +def from_header_class( + header_props: "SM64_AnimHeaderProperties", + header: SM64_AnimHeader, + action: Action, + actor_name: str, + use_custom_name: bool, +): + if isinstance(header.reference, str) and header.reference != header_props.get_name(actor_name, action): + header_props.custom_name = header.reference + if use_custom_name: + header_props.use_custom_name = True + if header.enum_name and header.enum_name != header_props.get_enum(actor_name, action): + header_props.custom_enum = header.enum_name + header_props.use_custom_enum = True + + correct_loop_points = header.start_frame, header.loop_start, header.loop_end + header_props.start_frame, header_props.loop_start, header_props.loop_end = correct_loop_points + if correct_loop_points != header_props.get_loop_points(action): # check if auto loop points don´t match + header_props.use_manual_loop = True + + header_props.trans_divisor = header.trans_divisor + header_props.set_flags(header.flags) + + header_props.table_index = header.table_index + + +def from_anim_class( + action_props: "SM64_ActionAnimProperty", + action: Action, + animation: SM64_Anim, + actor_name: str, + use_custom_name: bool, + import_type: str, +): + main_header = animation.headers[0] + is_from_binary = import_type.endswith("Binary") + + if animation.action_name: + action_name = animation.action_name + elif main_header.file_name: + action_name = main_header.file_name.removesuffix(".c").removesuffix(".inc") + elif is_from_binary: + action_name = intToHex(main_header.reference) + + action.name = action_name.removeprefix("anim_") + print(f'Populating action "{action.name}" properties.') + + indice_reference, values_reference = main_header.indice_reference, main_header.values_reference + if is_from_binary: + action_props.indices_address, action_props.values_address = intToHex(indice_reference), intToHex( + values_reference + ) + else: + action_props.indices_table, action_props.values_table = indice_reference, values_reference + + if animation.data: + file_name = animation.data.indices_file_name + action_props.custom_max_frame = max([1] + [len(x.values) for x in animation.data.pairs]) + if action_props.get_max_frame(action) != action_props.custom_max_frame: + action_props.use_custom_max_frame = True + else: + file_name = main_header.file_name + action_props.reference_tables = True + if file_name: + action_props.custom_file_name = file_name + if use_custom_name and action_props.get_file_name(action, import_type) != action_props.custom_file_name: + action_props.use_custom_file_name = True + if is_from_binary: + start_addresses = [x.reference for x in animation.headers] + end_addresses = [x.end_address for x in animation.headers] + if animation.data: + start_addresses.append(animation.data.start_address) + end_addresses.append(animation.data.end_address) + + action_props.start_address = intToHex(min(start_addresses)) + action_props.end_address = intToHex(max(end_addresses)) + + print("Populating header properties.") + for i, header in enumerate(animation.headers): + if i: + action_props.header_variants.add() + header_props = action_props.headers[-1] + header.action = action # Used in table class to prop + from_header_class(header_props, header, action, actor_name, use_custom_name) + + action_props.update_variant_numbers() + + +def from_table_element_class( + element_props: "SM64_AnimTableElementProperties", + element: SM64_AnimTableElement, + use_custom_name: bool, + actor_name: str, + prev_enums: dict[str, int], +): + if element.header: + assert element.header.action + element_props.set_variant(element.header.action, element.header.header_variant) + else: + element_props.reference = True + + if isinstance(element.reference, int): + element_props.header_address = intToHex(element.reference) + else: + element_props.header_name = element.c_name + element_props.header_address = intToHex(0) + + if element.enum_name: + element_props.custom_enum = element.enum_name + if use_custom_name and element.enum_name != element_props.get_enum(True, actor_name, prev_enums): + element_props.use_custom_enum = True + + +def from_anim_table_class( + anim_props: "SM64_ArmatureAnimProperties", + table: SM64_AnimTable, + clear_table: bool, + use_custom_name: bool, + actor_name: str, +): + if clear_table: + anim_props.elements.clear() + anim_props.null_delimiter = table.has_null_delimiter + + prev_enums: dict[str, int] = {} + for i, element in enumerate(table.elements): + if anim_props.null_delimiter and i == len(table.elements) - 1: + break + anim_props.elements.add() + from_table_element_class(anim_props.elements[-1], element, use_custom_name, actor_name, prev_enums) + + if isinstance(table.reference, int): # Binary + anim_props.dma_address = intToHex(table.reference) + anim_props.dma_end_address = intToHex(table.end_address) + anim_props.address = intToHex(table.reference) + anim_props.end_address = intToHex(table.end_address) + + # Data + start_addresses = [] + end_addresses = [] + for element in table.elements: + if element.header and element.header.data: + start_addresses.append(element.header.data.start_address) + end_addresses.append(element.header.data.end_address) + if start_addresses and end_addresses: + anim_props.write_data_seperately = True + anim_props.data_address = intToHex(min(start_addresses)) + anim_props.data_end_address = intToHex(max(end_addresses)) + elif isinstance(table.reference, str) and table.reference: # C + if use_custom_name: + anim_props.custom_table_name = table.reference + if anim_props.get_table_name(actor_name) != anim_props.custom_table_name: + anim_props.use_custom_table_name = True + + +def animation_import_to_blender( + obj: Object, + blender_to_sm64_scale: float, + anim_import: SM64_Anim, + actor_name: str, + use_custom_name: bool, + import_type: str, + force_quaternion: bool, + continuity_filter: bool, +): + action = create_basic_action(obj, "") + try: + if anim_import.data: + print("Converting pairs to intermidiate data.") + bones = get_anim_owners(obj) + bones_data: list[IntermidiateAnimationBone] = [] + pairs = anim_import.data.pairs + for pair_num in range(3, len(pairs), 3): + bone = IntermidiateAnimationBone() + if pair_num == 3: + bone.read_translation(pairs[0:3], blender_to_sm64_scale) + bone.read_rotation(pairs[pair_num : pair_num + 3], continuity_filter) + bones_data.append(bone) + print("Populating action keyframes.") + for pose_bone, bone_data in zip(bones, bones_data): + if force_quaternion: + pose_bone.rotation_mode = "QUATERNION" + bone_data.populate_action(action, pose_bone) + + from_anim_class(get_action_props(action), action, anim_import, actor_name, use_custom_name, import_type) + return action + except PluginError as exc: + bpy.data.actions.remove(action) + raise exc + + +def update_table_with_table_enum(table: SM64_AnimTable, enum_table: SM64_AnimTable): + for element, enum_element in zip(table.elements, enum_table.elements): + if element.enum_name: + enum_element = next( + ( + other_enum_element + for other_enum_element in enum_table.elements + if element.enum_name == other_enum_element.enum_name + ), + enum_element, + ) + element.enum_name = enum_element.enum_name + element.enum_val = enum_element.enum_val + element.enum_start = enum_element.enum_start + element.enum_end = enum_element.enum_end + table.enum_list_reference = enum_table.enum_list_reference + table.enum_list_start = enum_table.enum_list_start + table.enum_list_end = enum_table.enum_list_end + + +def import_enums(c_data: str, path: Path, comment_map: list[CommentMatch], specific_name=""): + tables = [] + for list_match in re.finditer(TABLE_ENUM_LIST_PATTERN, c_data): + name, content = list_match.group("name"), list_match.group("content") + if name is None and content is None: # comment + continue + if specific_name and name != specific_name: + continue + list_start, list_end = adjust_start_end(c_data.find(content, list_match.start()), list_match.end(), comment_map) + content = c_data[list_start:list_end] + table = SM64_AnimTable( + file_name=path.name, + enum_list_reference=name, + enum_list_start=list_start, + enum_list_end=list_end, + ) + for element_match in re.finditer(TABLE_ENUM_PATTERN, content): + name, num = (element_match.group("name"), element_match.group("num")) + if name is None and num is None: # comment + continue + enum_start, enum_end = adjust_start_end( + list_start + element_match.start(), list_start + element_match.end(), comment_map + ) + table.elements.append( + SM64_AnimTableElement( + enum_name=name, enum_val=num, enum_start=enum_start - list_start, enum_end=enum_end - list_start + ) + ) + tables.append(table) + return tables + + +def import_tables( + c_data: str, + path: Path, + comment_map: list[CommentMatch], + specific_name="", + header_decls: Optional[list[CArrayDeclaration]] = None, + values_decls: Optional[list[CArrayDeclaration]] = None, + indices_decls: Optional[list[CArrayDeclaration]] = None, +): + read_headers = {} + header_decls, values_decls, indices_decls = ( + header_decls or [], + values_decls or [], + indices_decls or [], + ) + tables: list[SM64_AnimTable] = [] + for table_match in re.finditer(TABLE_PATTERN, c_data): + table_elements = [] + name, content = table_match.group("name"), table_match.group("content") + if name is None and content is None: # comment + continue + if specific_name and name != specific_name: + continue + + table = SM64_AnimTable(name, file_name=path.name, elements=table_elements) + table.read_c( + c_data, + c_data.find(content, table_match.start()), + table_match.end(), + comment_map, + read_headers, + header_decls, + values_decls, + indices_decls, + ) + tables.append(table) + return tables + + +DECL_PATTERN = re.compile( + r"(static\s+const\s+struct\s+Animation|static\s+const\s+u16|static\s+const\s+s16)\s+" + r"(\w+)\s*?(?:\[.*?\])?\s*?=\s*?\{(.*?)\s*?\};", + re.DOTALL, +) +VALUE_SPLIT_PATTERN = re.compile(r"\s*(?:(?:\.(?P\w+)|\[\s*(?P.*?)\s*\])\s*=\s*)?(?P.+?)(?:,|\Z)") + + +def find_decls(c_data: str, path: Path, decl_list: dict[str, list[CArrayDeclaration]]): + """At this point a generilized c parser would be better""" + matches = DECL_PATTERN.findall(c_data) + for decl_type, name, value_text in matches: + values = [] + for match in VALUE_SPLIT_PATTERN.finditer(value_text): + var, designator, val = match.group("var"), match.group("designator"), match.group("val") + assert val is not None + if designator is not None: + designator = math_eval(designator, object()) + if isinstance(designator, int): + if isinstance(values, dict): + raise PluginError("Invalid mix of designated initializers") + first_val = values[0] if values else "0" + values.extend([first_val] * (designator + 1 - len(values))) + else: + if not values: + values = {} + elif isinstance(values, list): + raise PluginError("Invalid mix of designated initializers") + values[designator] = val + elif var is not None: + if not values: + values = {} + elif isinstance(values, list): + raise PluginError("Mix of designated and positional variable assignment") + values[var] = val + else: + if isinstance(values, dict): + raise PluginError("Mix of designated and positional variable assignment") + values.append(val) + decl_list[decl_type].append(CArrayDeclaration(name, path, path.name, values)) + + +def import_c_animations(path: Path) -> tuple[SM64_AnimTable | None, dict[str, SM64_AnimHeader]]: + path_checks(path) + if path.is_file(): + file_paths = [path] + elif path.is_dir(): + file_paths = sorted([f for f in path.rglob("*") if f.suffix in {".c", ".h"}]) + else: + raise PluginError("Path is neither a file or a folder but it exists, somehow.") + + print("Reading from:\n" + "\n".join([f.name for f in file_paths])) + c_files = {file_path: get_comment_map(file_path.read_text()) for file_path in file_paths} + + decl_lists = {"static const struct Animation": [], "static const u16": [], "static const s16": []} + header_decls, indices_decls, value_decls = ( + decl_lists["static const struct Animation"], + decl_lists["static const u16"], + decl_lists["static const s16"], + ) + tables: list[SM64_AnimTable] = [] + enum_lists: list[SM64_AnimTable] = [] + for file_path, (comment_less, _comment_map) in c_files.items(): + find_decls(comment_less, file_path, decl_lists) + for file_path, (comment_less, comment_map) in c_files.items(): + tables.extend(import_tables(comment_less, file_path, comment_map, "", header_decls, value_decls, indices_decls)) + enum_lists.extend(import_enums(comment_less, file_path, comment_map)) + + if len(tables) > 1: + raise ValueError("More than 1 table declaration") + elif len(tables) == 1: + table: SM64_AnimTable = tables[0] + if enum_lists: + enum_table = next( # find enum with the same name or use the first + ( + enum_table + for enum_table in enum_lists + if enum_table.reference == table_name_to_enum(table.reference) + ), + enum_lists[0], + ) + update_table_with_table_enum(table, enum_table) + read_headers = {header.reference: header for header in table.header_set} + return table, read_headers + else: + read_headers: dict[str, SM64_AnimHeader] = {} + for table_index, header_decl in enumerate(sorted(header_decls, key=lambda h: h.name)): + SM64_AnimHeader().read_c(header_decl, value_decls, indices_decls, read_headers, table_index) + return None, read_headers + + +def import_binary_animations( + data_reader: RomReader, + import_type: str, + read_headers: dict[str, SM64_AnimHeader], + table: SM64_AnimTable, + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + table_size: Optional[int] = None, +): + if import_type == "Table": + table.read_binary(data_reader, read_headers, table_index, bone_count, table_size) + elif import_type == "DMA": + table.read_dma_binary(data_reader, read_headers, table_index, bone_count) + elif import_type == "Animation": + SM64_AnimHeader.read_binary( + data_reader, + read_headers, + False, + bone_count, + table_size, + ) + else: + raise PluginError("Unimplemented binary import type.") + + +def import_insertable_binary_animations( + reader: RomReader, + read_headers: dict[str, SM64_AnimHeader], + table: SM64_AnimTable, + table_index: Optional[int] = None, + bone_count: Optional[int] = None, + table_size: Optional[int] = None, +): + if reader.insertable.data_type == "Animation": + SM64_AnimHeader.read_binary( + reader, + read_headers, + False, + bone_count, + ) + elif reader.insertable.data_type == "Animation Table": + table.read_binary(reader, read_headers, table_index, bone_count, table_size) + elif reader.insertable.data_type == "Animation DMA Table": + table.read_dma_binary(reader, read_headers, table_index, bone_count) + + +def import_animations(context: Context): + animation_operator_checks(context, False) + + scene = context.scene + obj: Object = context.object + sm64_props: SM64_Properties = scene.fast64.sm64 + import_props: SM64_AnimImportProperties = sm64_props.animation.importing + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + + update_table_preset(import_props, context) + + read_headers: dict[str, SM64_AnimHeader] = {} + table = SM64_AnimTable() + + print("Reading animation data.") + + if import_props.binary: + rom_path = Path(abspath(import_props.rom if import_props.rom else sm64_props.import_rom)) + binary_args = ( + read_headers, + table, + import_props.table_index, + None if import_props.ignore_bone_count else len(get_anim_owners(obj)), + import_props.table_size, + ) + if import_props.import_type == "Binary": + import_rom_checks(rom_path) + address = import_props.address + with rom_path.open("rb") as rom_file: + if import_props.binary_import_type == "DMA": + segment_data = None + else: + segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData + if import_props.is_segmented_address: + address = decodeSegmentedAddr(address.to_bytes(4, "big"), segment_data) + import_binary_animations( + RomReader(rom_file, start_address=address, segment_data=segment_data), + import_props.binary_import_type, + *binary_args, + ) + elif import_props.import_type == "Insertable Binary": + insertable_path = Path(abspath(import_props.path)) + filepath_checks(insertable_path) + with insertable_path.open("rb") as insertable_file: + if import_props.read_from_rom: + import_rom_checks(rom_path) + with rom_path.open("rb") as rom_file: + segment_data = parseLevelAtPointer(rom_file, level_pointers[import_props.level]).segmentData + import_insertable_binary_animations( + RomReader(rom_file, insertable_file=insertable_file, segment_data=segment_data), + *binary_args, + ) + else: + import_insertable_binary_animations(RomReader(insertable_file=insertable_file), *binary_args) + elif import_props.import_type == "C": + table, read_headers = import_c_animations(Path(abspath(import_props.path))) + table = table or SM64_AnimTable() + else: + raise NotImplementedError(f"Unimplemented animation import type {import_props.import_type}") + + if not table.elements: + print("No table was read. Automatically creating table.") + table.elements = [SM64_AnimTableElement(header=header) for header in read_headers.values()] + seperate_anims = table.get_seperate_anims() + + actor_name: str = get_anim_actor_name(context) + if import_props.use_preset and import_props.preset in ACTOR_PRESET_INFO: + preset_animation_names = get_preset_anim_name_list(import_props.preset) + for animation in seperate_anims: + if len(animation.headers) == 0: + continue + names, indexes = [], [] + for header in animation.headers: + if header.table_index >= len(preset_animation_names): + continue + name = preset_animation_names[header.table_index] + header.enum_name = header.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}") + names.append(name) + indexes.append(str(header.table_index)) + animation.action_name = f"{'/'.join(indexes)} - {'/'.join(names)}" + for i, element in enumerate(table.elements[: len(preset_animation_names)]): + name = preset_animation_names[i] + element.enum_name = element.enum_name or anim_name_to_enum_name(f"{actor_name}_anim_{name}") + + print("Importing animations into blender.") + actions = [] + for animation in seperate_anims: + actions.append( + animation_import_to_blender( + obj, + sm64_props.blender_to_sm64_scale, + animation, + actor_name, + import_props.use_custom_name, + import_props.import_type, + import_props.force_quaternion, + import_props.continuity_filter if not import_props.force_quaternion else True, + ) + ) + + if import_props.run_decimate: + print("Decimating imported actions's fcurves") + old_area = bpy.context.area.type + old_action = obj.animation_data.action + try: + if obj.type == "ARMATURE": + bpy.ops.object.posemode_toggle() # Select all bones + bpy.ops.pose.select_all(action="SELECT") + + bpy.context.area.type = "GRAPH_EDITOR" + for action in actions: + print(f"Decimating {action.name}.") + obj.animation_data.action = action + bpy.ops.graph.select_all(action="SELECT") + bpy.ops.graph.decimate(mode="ERROR", factor=1, remove_error_margin=import_props.decimate_margin) + finally: + bpy.context.area.type = old_area + obj.animation_data.action = old_action + + if import_props.binary: + anim_props.is_dma = import_props.binary_import_type == "DMA" + if table: + print("Importing animation table into properties.") + from_anim_table_class(anim_props, table, import_props.clear_table, import_props.use_custom_name, actor_name) + + +@functools.cache +def cached_enum_from_import_preset(preset: str): + animation_names = get_preset_anim_name_list(preset) + enum_items: list[tuple[str, str, str, int]] = [] + enum_items.append(("Custom", "Custom", "Pick your own animation index", 0)) + if animation_names: + enum_items.append(("", "Presets", "", 1)) + for i, name in enumerate(animation_names): + enum_items.append((str(i), f"{i} - {name}", f'"{preset}" Animation {i}', i + 2)) + return enum_items + + +def get_enum_from_import_preset(_import_props: "SM64_AnimImportProperties", context): + try: + return cached_enum_from_import_preset(get_scene_anim_props(context).importing.preset) + except Exception as exc: # pylint: disable=broad-except + print(str(exc)) + return [("Custom", "Custom", "Pick your own animation index", 0)] + + +def update_table_preset(import_props: "SM64_AnimImportProperties", context): + if not import_props.use_preset: + return + + preset = ACTOR_PRESET_INFO[import_props.preset] + assert preset.animation is not None and isinstance( + preset.animation, AnimInfo + ), "Selected preset's actor has not animation information" + + if import_props.preset_animation == "": + # If the previously selected animation isn't in this preset, select animation 0 + import_props.preset_animation = "0" + + # C + decomp_path = import_props.decomp_path if import_props.decomp_path else context.scene.fast64.sm64.decomp_path + directory = preset.animation.directory if preset.animation.directory else f"{preset.decomp_path}/anims" + import_props.path = os.path.join(decomp_path, directory) + + # Binary + import_props.ignore_bone_count = preset.animation.ignore_bone_count + import_props.level = preset.level + if preset.animation.dma: + import_props.dma_table_address = intToHex(preset.animation.address) + import_props.binary_import_type = "DMA" + import_props.is_segmented_address_prop = False + else: + import_props.table_address = intToHex(preset.animation.address) + import_props.binary_import_type = "Table" + import_props.is_segmented_address_prop = True + + if preset.animation.size is None: + import_props.check_null = True + else: + import_props.check_null = False + import_props.table_size_prop = preset.animation.size diff --git a/fast64_internal/sm64/animation/operators.py b/fast64_internal/sm64/animation/operators.py new file mode 100644 index 000000000..7f7ea1437 --- /dev/null +++ b/fast64_internal/sm64/animation/operators.py @@ -0,0 +1,347 @@ +from typing import TYPE_CHECKING + +import bpy +from bpy.utils import register_class, unregister_class +from bpy.types import Context, Scene, Action +from bpy.props import EnumProperty, StringProperty, IntProperty +from bpy.app.handlers import persistent + +from ...operators import OperatorBase, SearchEnumOperatorBase +from ...utility import copyPropertyGroup +from ...utility_anim import get_action + +from .importing import import_animations, get_enum_from_import_preset +from .exporting import export_animation, export_animation_table +from .utility import ( + animation_operator_checks, + get_action_props, + get_anim_obj, + get_scene_anim_props, + get_anim_props, + get_anim_actor_name, +) +from .constants import enum_anim_tables, enum_animated_behaviours + +if TYPE_CHECKING: + from .properties import SM64_AnimProperties, SM64_AnimHeaderProperties + + +@persistent +def emulate_no_loop(scene: Scene): + if scene.gameEditorMode != "SM64": + return + anim_props: SM64_AnimProperties = scene.fast64.sm64.animation + played_action: Action = anim_props.played_action + if not played_action: + return + if not bpy.context.screen.is_animation_playing or anim_props.played_header >= len( + get_action_props(played_action).headers + ): + anim_props.played_action = None + return + + frame = scene.frame_current + header_props = get_action_props(played_action).headers[anim_props.played_header] + _start, loop_start, end = header_props.get_loop_points(played_action) + if header_props.backwards: + if frame < loop_start: + if header_props.no_loop: + scene.frame_set(loop_start) + else: + scene.frame_set(end - 1) + elif frame >= end: + if header_props.no_loop: + scene.frame_set(end - 1) + else: + scene.frame_set(loop_start) + + +class SM64_PreviewAnim(OperatorBase): + bl_idname = "scene.sm64_preview_animation" + bl_label = "Preview Animation" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "PLAY" + + played_header: IntProperty(name="Header", min=0, default=0) + played_action: StringProperty(name="Action") + + def execute_operator(self, context): + animation_operator_checks(context) + played_action = get_action(self.played_action) + scene = context.scene + anim_props = scene.fast64.sm64.animation + + context.object.animation_data.action = played_action + action_props = get_action_props(played_action) + + if self.played_header >= len(action_props.headers): + raise ValueError("Invalid Header Index") + header_props: SM64_AnimHeaderProperties = action_props.headers[self.played_header] + start_frame = header_props.get_loop_points(played_action)[0] + scene.frame_set(start_frame) + scene.render.fps = 30 + + if bpy.context.screen.is_animation_playing: + bpy.ops.screen.animation_play() # in case it was already playing, stop it + bpy.ops.screen.animation_play() + + anim_props.played_header = self.played_header + anim_props.played_action = played_action + + +# TODO: update these to use CollectionOperatorBase +class SM64_AnimTableOps(OperatorBase): + bl_idname = "scene.sm64_table_operations" + bl_label = "Table Operations" + bl_description = "Move, remove, clear or add table elements" + bl_options = {"UNDO"} + + index: IntProperty() + op_name: StringProperty() + action_name: StringProperty() + header_variant: IntProperty() + + @classmethod + def is_enabled(cls, context: Context, op_name: str, index: int, **_kwargs): + table_elements = get_anim_props(context).elements + if op_name == "MOVE_UP" and index == 0: + return False + elif op_name == "MOVE_DOWN" and index >= len(table_elements) - 1: + return False + elif op_name == "CLEAR" and len(table_elements) == 0: + return False + return True + + def execute_operator(self, context): + table_elements = get_anim_props(context).elements + if self.op_name == "MOVE_UP": + table_elements.move(self.index, self.index - 1) + elif self.op_name == "MOVE_DOWN": + table_elements.move(self.index, self.index + 1) + elif self.op_name == "ADD": + if self.index != -1: + table_element = table_elements[self.index] + table_elements.add() + if self.action_name: # set based on action variant + table_elements[-1].set_variant(bpy.data.actions[self.action_name], self.header_variant) + elif self.index != -1: # copy from table + copyPropertyGroup(table_element, table_elements[-1]) + if self.index != -1: + table_elements.move(len(table_elements) - 1, self.index + 1) + elif self.op_name == "ADD_ALL": + action = bpy.data.actions[self.action_name] + for header_variant in range(len(get_action_props(action).headers)): + table_elements.add() + table_elements[-1].set_variant(action, header_variant) + elif self.op_name == "REMOVE": + table_elements.remove(self.index) + elif self.op_name == "CLEAR": + table_elements.clear() + else: + raise NotImplementedError(f"Unimplemented table op {self.op_name}") + + +class SM64_AnimVariantOps(OperatorBase): + bl_idname = "scene.sm64_header_variant_operations" + bl_label = "Header Variant Operations" + bl_description = "Move, remove, clear or add variants" + bl_options = {"UNDO"} + + index: IntProperty() + op_name: StringProperty() + action_name: StringProperty() + + @classmethod + def is_enabled(cls, context: Context, action_name: str, op_name: str, index: int, **_kwargs): + action_props = get_action_props(get_action(action_name)) + headers = action_props.headers + if op_name == "REMOVE" and index == 0: + return False + elif op_name == "MOVE_UP" and index <= 0: + return False + elif op_name == "MOVE_DOWN" and index >= len(headers) - 1: + return False + elif op_name == "CLEAR" and len(headers) <= 1: + return False + return True + + def execute_operator(self, context): + action = get_action(self.action_name) + action_props = get_action_props(action) + headers = action_props.headers + variants = action_props.header_variants + variant_position = self.index - 1 + if self.op_name == "MOVE_UP": + if self.index - 1 == 0: + variants.add() + copyPropertyGroup(headers[0], variants[-1]) + copyPropertyGroup(headers[self.index], headers[0]) + copyPropertyGroup(variants[-1], headers[self.index]) + variants.remove(len(variants) - 1) + else: + variants.move(variant_position, variant_position - 1) + elif self.op_name == "MOVE_DOWN": + if self.index == 0: + variants.add() + copyPropertyGroup(headers[0], variants[-1]) + copyPropertyGroup(headers[1], headers[0]) + copyPropertyGroup(variants[-1], headers[1]) + variants.remove(len(variants) - 1) + else: + variants.move(variant_position, variant_position + 1) + elif self.op_name == "ADD": + variants.add() + added_variant = variants[-1] + + copyPropertyGroup(action_props.headers[self.index], added_variant) + variants.move(len(variants) - 1, variant_position + 1) + action_props.update_variant_numbers() + added_variant.action = action + added_variant.expand_tab = True + added_variant.use_custom_name = False + added_variant.use_custom_enum = False + added_variant.custom_name = added_variant.get_name(get_anim_actor_name(context), action) + elif self.op_name == "REMOVE": + variants.remove(variant_position) + elif self.op_name == "CLEAR": + variants.clear() + else: + raise NotImplementedError(f"Unimplemented table op {self.op_name}") + action_props.update_variant_numbers() + + +class SM64_AddNLATracksToTable(OperatorBase): + bl_idname = "scene.sm64_add_nla_tracks_to_table" + bl_label = "Add Existing NLA Tracks To Animation Table" + bl_description = "Adds all NLA tracks in the selected armature to the animation table" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "NLA" + + @classmethod + def poll(cls, context): + if get_anim_obj(context) is None or get_anim_obj(context).animation_data is None: + return False + actions = get_anim_props(context).actions + for track in context.object.animation_data.nla_tracks: + for strip in track.strips: + if strip.action is not None and strip.action not in actions: + return True + return False + + def execute_operator(self, context): + assert self.__class__.poll(context) + anim_props = get_anim_props(context) + for track in context.object.animation_data.nla_tracks: + for strip in track.strips: + action = strip.action + if action is None or action in anim_props.actions: + continue + for header_variant in range(len(get_action_props(action).headers)): + anim_props.elements.add() + anim_props.elements[-1].set_variant(action, header_variant) + + +class SM64_ExportAnimTable(OperatorBase): + bl_idname = "scene.sm64_export_anim_table" + bl_label = "Export Animation Table" + bl_description = "Exports the animation table of the selected armature" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "EXPORT" + + @classmethod + def poll(cls, context): + return get_anim_obj(context) is not None + + def execute_operator(self, context): + animation_operator_checks(context) + export_animation_table(context, context.object) + self.report({"INFO"}, "Exported animation table successfully!") + + +class SM64_ExportAnim(OperatorBase): + bl_idname = "scene.sm64_export_anim" + bl_label = "Export Individual Animation" + bl_description = "Exports the select action of the selected armature" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION" + + @classmethod + def poll(cls, context): + return get_anim_obj(context) is not None + + def execute_operator(self, context): + animation_operator_checks(context) + export_animation(context, context.object) + self.report({"INFO"}, "Exported animation successfully!") + + +class SM64_ImportAnim(OperatorBase): + bl_idname = "scene.sm64_import_anim" + bl_label = "Import Animation(s)" + bl_description = "Imports animations into the call context's animation propreties, scene or object" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "IMPORT" + + def execute_operator(self, context): + import_animations(context) + + +class SM64_SearchAnimPresets(SearchEnumOperatorBase): + bl_idname = "scene.search_mario_anim_enum_operator" + bl_property = "preset_animation" + + preset_animation: EnumProperty(items=get_enum_from_import_preset) + + def update_enum(self, context: Context): + get_scene_anim_props(context).importing.preset_animation = self.preset_animation + + +class SM64_SearchAnimTablePresets(SearchEnumOperatorBase): + bl_idname = "scene.search_anim_table_enum_operator" + bl_property = "preset" + + preset: EnumProperty(items=enum_anim_tables) + + def update_enum(self, context: Context): + get_scene_anim_props(context).importing.preset = self.preset + + +class SM64_SearchAnimatedBhvs(SearchEnumOperatorBase): + bl_idname = "scene.search_animated_behavior_enum_operator" + bl_property = "behaviour" + + behaviour: EnumProperty(items=enum_animated_behaviours) + + def update_enum(self, context: Context): + get_anim_props(context).behaviour = self.behaviour + + +classes = ( + SM64_ExportAnimTable, + SM64_ExportAnim, + SM64_PreviewAnim, + SM64_AnimTableOps, + SM64_AnimVariantOps, + SM64_AddNLATracksToTable, + SM64_ImportAnim, + SM64_SearchAnimPresets, + SM64_SearchAnimatedBhvs, + SM64_SearchAnimTablePresets, +) + + +def anim_ops_register(): + for cls in classes: + register_class(cls) + + bpy.app.handlers.frame_change_pre.append(emulate_no_loop) + + +def anim_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/panels.py b/fast64_internal/sm64/animation/panels.py new file mode 100644 index 000000000..563e9219d --- /dev/null +++ b/fast64_internal/sm64/animation/panels.py @@ -0,0 +1,200 @@ +from typing import TYPE_CHECKING + +from bpy.utils import register_class, unregister_class +from bpy.types import Context + +from ...utility_anim import is_action_stashed, CreateAnimData, AddBasicAction, StashAction +from ...panels import SM64_Panel + +from .utility import ( + get_action_props, + get_anim_actor_name, + get_anim_props, + get_selected_action, + dma_structure_context, + get_anim_obj, +) +from .operators import SM64_ExportAnim, SM64_ExportAnimTable, SM64_AddNLATracksToTable + +if TYPE_CHECKING: + from ..settings.properties import SM64_Properties + from ..sm64_objects import SM64_CombinedObjectProperties + from .properties import SM64_AnimImportProperties + + +# Base +class AnimationPanel(SM64_Panel): + bl_label = "SM64 Animation Inspector" + goal = "Object/Actor/Anim" + + +# Base panels +class SceneAnimPanel(AnimationPanel): + bl_idname = "SM64_PT_anim" + bl_parent_id = bl_idname + + +class ObjAnimPanel(AnimationPanel): + bl_idname = "OBJECT_PT_SM64_anim" + bl_context = "object" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_parent_id = bl_idname + + +# Main tab +class SceneAnimPanelMain(SceneAnimPanel): + bl_parent_id = "" + + def draw(self, context): + col = self.layout.column() + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + + if sm64_props.export_type == "C": + if not sm64_props.hackersm64: + col.prop(sm64_props, "designated_prop", text="Designated Initialization for Tables") + else: + combined_props.draw_anim_props(col, sm64_props.export_type, dma_structure_context(context)) + SM64_ExportAnimTable.draw_props(col) + anim_obj = get_anim_obj(context) + if anim_obj is None: + col.box().label(text="No selected armature/animated object") + else: + col.box().label(text=f'Armature "{anim_obj.name}"') + + +class ObjAnimPanelMain(ObjAnimPanel): + bl_parent_id = "OBJECT_PT_context_object" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None + + def draw(self, context): + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + get_anim_props(context).draw_props( + self.layout, + sm64_props.export_type, + combined_props.export_header_type, + get_anim_actor_name(context), + combined_props.export_bhv, + ) + + +# Action tab + + +class AnimationPanelAction(AnimationPanel): + bl_label = "Action Inspector" + + def draw(self, context): + col = self.layout.column() + + if context.object.animation_data is None: + col.box().label(text="Select object has no animation data") + CreateAnimData.draw_props(col) + action = None + else: + col.prop(context.object.animation_data, "action", text="Selected Action") + action = get_selected_action(context.object, False) + if action is None: + AddBasicAction.draw_props(col) + return + + if not is_action_stashed(context.object, action): + warn_col = col.column() + StashAction.draw_props(warn_col, action=action.name) + warn_col.alert = True + + sm64_props: SM64_Properties = context.scene.fast64.sm64 + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export + if sm64_props.export_type != "C": + SM64_ExportAnim.draw_props(col) + + export_seperately = get_anim_props(context).export_seperately + if sm64_props.export_type == "C": + export_seperately = export_seperately or combined_props.export_single_action + elif sm64_props.export_type == "Insertable Binary": + export_seperately = True + get_action_props(action).draw_props( + layout=col, + action=action, + specific_variant=None, + in_table=False, + updates_table=get_anim_props(context).update_table, + export_seperately=export_seperately, + export_type=sm64_props.export_type, + actor_name=get_anim_actor_name(context), + gen_enums=get_anim_props(context).gen_enums, + dma=dma_structure_context(context), + ) + + +class SceneAnimPanelAction(AnimationPanelAction, SceneAnimPanel): + bl_idname = "SM64_PT_anim_panel_action" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None and SceneAnimPanel.poll(context) + + +class ObjAnimPanelAction(AnimationPanelAction, ObjAnimPanel): + bl_idname = "OBJECT_PT_SM64_anim_action" + + +class ObjAnimPanelTable(ObjAnimPanel): + bl_label = "Table" + bl_idname = "OBJECT_PT_SM64_anim_table" + + def draw(self, context): + if SM64_AddNLATracksToTable.poll(context): + SM64_AddNLATracksToTable.draw_props(self.layout) + sm64_props: SM64_Properties = context.scene.fast64.sm64 + get_anim_props(context).draw_table(self.layout, sm64_props.export_type, get_anim_actor_name(context)) + + +# Importing tab + + +class AnimationPanelImport(AnimationPanel): + bl_label = "Importing" + import_panel = True + + def draw(self, context): + sm64_props: SM64_Properties = context.scene.fast64.sm64 + importing: SM64_AnimImportProperties = sm64_props.animation.importing + importing.draw_props(self.layout, sm64_props.import_rom, sm64_props.decomp_path) + + +class SceneAnimPanelImport(SceneAnimPanel, AnimationPanelImport): + bl_idname = "SM64_PT_anim_panel_import" + + @classmethod + def poll(cls, context: Context): + return get_anim_obj(context) is not None and AnimationPanelImport.poll(context) + + +class ObjAnimPanelImport(ObjAnimPanel, AnimationPanelImport): + bl_idname = "OBJECT_PT_SM64_anim_panel_import" + + +classes = ( + ObjAnimPanelMain, + ObjAnimPanelTable, + ObjAnimPanelAction, + SceneAnimPanelMain, + SceneAnimPanelAction, + SceneAnimPanelImport, +) + + +def anim_panel_register(): + for cls in classes: + register_class(cls) + + +def anim_panel_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/properties.py b/fast64_internal/sm64/animation/properties.py new file mode 100644 index 000000000..a11ce61f1 --- /dev/null +++ b/fast64_internal/sm64/animation/properties.py @@ -0,0 +1,1202 @@ +import os + +import bpy +from bpy.types import PropertyGroup, Action, UILayout, Scene, Context +from bpy.utils import register_class, unregister_class +from bpy.props import ( + BoolProperty, + StringProperty, + EnumProperty, + IntProperty, + FloatProperty, + CollectionProperty, + PointerProperty, +) +from bpy.path import abspath, clean_name + +from ...utility import ( + decompFolderMessage, + directory_ui_warnings, + run_and_draw_errors, + path_ui_warnings, + draw_and_check_tab, + multilineLabel, + prop_split, + intToHex, + upgrade_old_prop, + toAlnum, +) +from ...utility_anim import getFrameInterval + +from ..sm64_utility import import_rom_ui_warnings, int_from_str, string_int_prop, string_int_warning +from ..sm64_constants import MAX_U16, MIN_S16, MAX_S16, enumLevelNames + +from .operators import ( + OperatorBase, + SM64_PreviewAnim, + SM64_AnimTableOps, + SM64_AnimVariantOps, + SM64_ImportAnim, + SM64_SearchAnimPresets, + SM64_SearchAnimatedBhvs, + SM64_SearchAnimTablePresets, +) +from .constants import enum_anim_import_types, enum_anim_binary_import_types, enum_animated_behaviours, enum_anim_tables +from .classes import SM64_AnimFlags +from .utility import ( + dma_structure_context, + get_action_props, + get_dma_anim_name, + get_dma_header_name, + is_obj_animatable, + anim_name_to_enum_name, + action_name_to_anim_name, + duplicate_name, + table_name_to_enum, +) +from .importing import get_enum_from_import_preset, update_table_preset + + +def draw_custom_or_auto(holder, layout: UILayout, prop: str, default: str, factor=0.5, **kwargs): + use_custom_prop = "use_custom_" + prop + name_split = layout.split(factor=factor) + name_split.prop(holder, use_custom_prop, **kwargs) + if getattr(holder, use_custom_prop): + name_split.prop(holder, "custom_" + prop, text="") + else: + prop_size_label(name_split, text=default, icon="LOCKED") + + +def draw_forced(layout: UILayout, holder, prop: str, forced: bool): + row = layout.row(align=True) if forced else layout.column() + if forced: + prop_size_label(row, text="", icon="LOCKED") + row.alignment = "LEFT" + row.enabled = not forced + row.prop(holder, prop, invert_checkbox=not getattr(holder, prop) if forced else False) + + +def prop_size_label(layout: UILayout, **label_args): + box = layout.box() + box.scale_y = 0.5 + box.label(**label_args) + return box + + +def draw_list_op(layout: UILayout, op_cls: OperatorBase, op_name: str, index=-1, text="", icon="", **op_args): + col = layout.column() + icon = icon or {"MOVE_UP": "TRIA_UP", "MOVE_DOWN": "TRIA_DOWN", "CLEAR": "TRASH"}.get(op_name) or op_name + return op_cls.draw_props(col, icon, text, index=index, op_name=op_name, **op_args) + + +def draw_list_ops(layout: UILayout, op_cls: OperatorBase, index: int, **op_args): + layout.label(text=str(index)) + ops = ("MOVE_UP", "MOVE_DOWN", "ADD", "REMOVE") + for op_name in ops: + draw_list_op(layout, op_cls, op_name, index, **op_args) + + +def set_if_different(owner, prop: str, value): + if getattr(owner, prop) != value: + setattr(owner, prop, value) + + +def on_flag_update(self: "SM64_AnimHeaderProperties", context: Context): + use_int = context.scene.fast64.sm64.binary_export or dma_structure_context(context) + self.set_flags(self.get_flags(not use_int), set_custom=not self.use_custom_flags) + + +class SM64_AnimHeaderProperties(PropertyGroup): + expand_tab_in_action: BoolProperty(name="Header Properties", default=True) + header_variant: IntProperty(name="Header Variant Number", min=0) + + use_custom_name: BoolProperty(name="Name") + custom_name: StringProperty(name="Name", default="anim_00") + use_custom_enum: BoolProperty(name="Enum") + custom_enum: StringProperty(name="Enum", default="ANIM_00") + use_manual_loop: BoolProperty(name="Manual Loop Points") + start_frame: IntProperty(name="Start", min=0, max=MAX_S16) + loop_start: IntProperty(name="Loop Start", min=0, max=MAX_S16) + loop_end: IntProperty(name="End", min=0, max=MAX_S16) + trans_divisor: IntProperty( + name="Translation Divisor", + description="(animYTransDivisor)\n" + "If set to 0, the translation multiplier will be 1. " + "Otherwise, the translation multiplier is determined by " + "dividing the object's translation dividend (animYTrans) by this divisor", + min=MIN_S16, + max=MAX_S16, + ) + use_custom_flags: BoolProperty(name="Set Custom Flags") + custom_flags: StringProperty(name="Flags", default="ANIM_NO_LOOP", update=on_flag_update) + # Some flags are inverted in the ui for readability, descriptions match ui behavior + no_loop: BoolProperty( + name="No Loop", + description="(ANIM_FLAG_NOLOOP)\n" + "When disabled, the animation will not repeat from the loop start after reaching the loop " + "end frame", + update=on_flag_update, + ) + backwards: BoolProperty( + name="Loop Backwards", + description="(ANIM_FLAG_FORWARD/ANIM_FLAG_BACKWARD)\n" + "When enabled, the animation will loop (or stop if looping is disabled) after reaching " + "the loop start frame.\n" + "Tipically used with animations which use acceleration to play an animation backwards", + update=on_flag_update, + ) + no_acceleration: BoolProperty( + name="No Acceleration", + description="(ANIM_FLAG_NO_ACCEL/ANIM_FLAG_2)\n" + "When disabled, acceleration will not be used when calculating which animation frame is " + "next", + update=on_flag_update, + ) + disabled: BoolProperty( + name="No Shadow Translation", + description="(ANIM_FLAG_DISABLED/ANIM_FLAG_5)\n" + "When disabled, the animation translation will not be applied to shadows", + update=on_flag_update, + ) + only_vertical: BoolProperty( + name="Only Vertical Translation", + description="(ANIM_FLAG_HOR_TRANS)\n" + "When enabled, only the animation vertical translation will be applied during rendering (takes priority over no translation and only horizontal)\n" + "(shadows included), the horizontal translation will still be exported and included", + update=on_flag_update, + ) + only_horizontal: BoolProperty( + name="Only Horizontal Translation", + description="(ANIM_FLAG_VERT_TRANS)\n" + "When enabled, only the animation horizontal translation will be applied during rendering (takes priority over no translation)\n" + "(shadows included) the vertical translation will still be exported and included", + update=on_flag_update, + ) + no_trans: BoolProperty( + name="No Translation", + description="(ANIM_FLAG_NO_TRANS/ANIM_FLAG_6)\n" + "When disabled, the animation translation will not be used during rendering\n" + "(shadows included), the translation will still be exported and included", + update=on_flag_update, + ) + # Binary + table_index: IntProperty(name="Table Index", min=0) + + def get_flags(self, allow_str: bool) -> SM64_AnimFlags | str: + if self.use_custom_flags: + result = SM64_AnimFlags.evaluate(self.custom_flags) + if not allow_str and isinstance(result, str): + raise ValueError("Failed to evaluate custom flags") + return result + value = SM64_AnimFlags(0) + for prop, flag in SM64_AnimFlags.props_to_flags().items(): + if getattr(self, prop, False): + value |= flag + return value + + @property + def int_flags(self): + return self.get_flags(allow_str=False) + + def set_flags(self, value: SM64_AnimFlags | str, set_custom=True): + if isinstance(value, SM64_AnimFlags): # the value was fully evaluated + for prop, flag in SM64_AnimFlags.props_to_flags().items(): # set prop flags + set_if_different(self, prop, flag in value) + if set_custom: + if value not in SM64_AnimFlags.all_flags_with_prop(): # if a flag does not have a prop + set_if_different(self, "use_custom_flags", True) + set_if_different(self, "custom_flags", intToHex(value, 2)) + elif isinstance(value, str): + if set_custom: + set_if_different(self, "custom_flags", value) + set_if_different(self, "use_custom_flags", True) + else: # invalid + raise ValueError(f"Invalid type: {value}") + + @property + def manual_loop_range(self) -> tuple[int, int, int]: + if self.use_manual_loop: + return (self.start_frame, self.loop_start, self.loop_end) + + def get_loop_points(self, action: Action): + if self.use_manual_loop: + return self.manual_loop_range + loop_start, loop_end = getFrameInterval(action) + return (0, loop_start, loop_end + 1) + + def get_name(self, actor_name: str, action: Action, dma=False) -> str: + if dma: + return get_dma_header_name(self.table_index) + elif self.use_custom_name: + return self.custom_name + elif self.header_variant == 0: + return toAlnum(f"{actor_name}_anim_{action.name}") + else: + main_header_name = get_action_props(action).headers[0].get_name(actor_name, action, dma) + return toAlnum(f"{main_header_name}_{self.header_variant}") + + def get_enum(self, actor_name: str, action: Action) -> str: + if self.use_custom_enum: + return self.custom_enum + elif self.use_custom_name: + return anim_name_to_enum_name(self.get_name(actor_name, action)) + elif self.header_variant == 0: + anim_name = action_name_to_anim_name(action.name) + return anim_name_to_enum_name(f"{actor_name}_anim_{anim_name}") + else: + main_enum = get_action_props(action).headers[0].get_enum(actor_name, action) + return f"{main_enum}_{self.header_variant}" + + def draw_flag_props(self, layout: UILayout, use_int_flags: bool = False): + col = layout.column() + custom_split = col.split() + custom_split.prop(self, "use_custom_flags") + if self.use_custom_flags: + custom_split.prop(self, "custom_flags", text="") + if use_int_flags: + run_and_draw_errors(col, self.get_flags, False) + return + else: + prop_size_label(custom_split, text=intToHex(self.int_flags, 2), icon="LOCKED") + # Draw flag toggles + row = col.row(align=True) + row.prop(self, "no_loop", invert_checkbox=True, text="Loop", toggle=1) + row.prop(self, "backwards", toggle=1) + row.prop(self, "no_acceleration", invert_checkbox=True, text="Acceleration", toggle=1) + if self.no_acceleration and self.backwards: + col.label(text="Backwards has no porpuse without acceleration.", icon="INFO") + + trans_row = col.row(align=True) + no_row = trans_row.row() + no_row.enabled = not self.only_vertical and not self.only_horizontal + no_row.prop(self, "no_trans", invert_checkbox=True, text="Translate", toggle=1) + + vert_row = trans_row.row() + vert_row.prop(self, "only_vertical", text="Only Vertical", toggle=1) + + hor_row = trans_row.row() + hor_row.enabled = not self.only_vertical + hor_row.prop(self, "only_horizontal", text="Only Horizontal", toggle=1) + if self.only_vertical and self.only_horizontal: + multilineLabel( + layout=col, + text='"Only Vertical" takes priority, only vertical\n translation will be used.', + icon="INFO", + ) + if (self.only_vertical or self.only_horizontal) and self.no_trans: + multilineLabel( + layout=col, + text='"Only Horizontal" and "Only Vertical" take\n priority over no translation.', + icon="INFO", + ) + + disabled_row = trans_row.row() + disabled_row.enabled = not self.no_trans and not self.only_vertical + disabled_row.prop(self, "disabled", invert_checkbox=True, text="Shadow", toggle=1) + + def draw_frame_range(self, layout: UILayout, action: Action): + split = layout.split() + split.prop(self, "use_manual_loop") + if self.use_manual_loop: + split = layout.split() + split.prop(self, "start_frame") + split.prop(self, "loop_start") + split.prop(self, "loop_end") + else: + start, loop_start, end = self.get_loop_points(action) + prop_size_label(split, text=f"Start {start}, Loop Start {loop_start}, End {end}", icon="LOCKED") + + def draw_names(self, layout: UILayout, action: Action, actor_name: str, gen_enums: bool, dma: bool): + col = layout.column() + if gen_enums: + draw_custom_or_auto(self, col, "enum", self.get_enum(actor_name, action)) + draw_custom_or_auto(self, col, "name", self.get_name(actor_name, action, dma)) + + def draw_props( + self, + layout: UILayout, + action: Action, + in_table: bool, + updates_table: bool, + dma: bool, + export_type: str, + actor_name: str, + gen_enums: bool, + ): + col = layout.column() + split = col.split() + preview_op = SM64_PreviewAnim.draw_props(split) + preview_op.played_header = self.header_variant + preview_op.played_action = action.name + if not in_table: # Don´t show index or name in table props + draw_list_op( + split, + SM64_AnimTableOps, + "ADD", + text="Add To Table", + icon="LINKED", + action_name=action.name, + header_variant=self.header_variant, + ) + if (export_type == "C" and dma) or (export_type == "Binary" and updates_table): + prop_split(col, self, "table_index", "Table Index") + if not dma and export_type == "C": + self.draw_names(col, action, actor_name, gen_enums, dma) + col.separator() + + prop_split(col, self, "trans_divisor", "Translation Divisor") + self.draw_frame_range(col, action) + self.draw_flag_props(col, use_int_flags=dma or export_type.endswith("Binary")) + + +class SM64_ActionAnimProperty(PropertyGroup): + """Properties in Action.fast64.sm64.animation""" + + header: PointerProperty(type=SM64_AnimHeaderProperties) + variants_tab: BoolProperty(name="Header Variants") + header_variants: CollectionProperty(type=SM64_AnimHeaderProperties) + use_custom_file_name: BoolProperty(name="File Name") + custom_file_name: StringProperty(name="File Name", default="anim_00.inc.c") + use_custom_max_frame: BoolProperty(name="Max Frame") + custom_max_frame: IntProperty(name="Max Frame", min=1, max=MAX_U16, default=1) + reference_tables: BoolProperty(name="Reference Tables") + indices_table: StringProperty(name="Indices Table", default="anim_00_indices") + values_table: StringProperty(name="Value Table", default="anim_00_values") + # Binary, toad anim 0 for defaults + indices_address: StringProperty(name="Indices Table", default=intToHex(0x00A42150)) + values_address: StringProperty(name="Value Table", default=intToHex(0x00A40CC8)) + start_address: StringProperty(name="Start Address", default=intToHex(0x00A40CC8)) + end_address: StringProperty(name="End Address", default=intToHex(0x00A42265)) + + @property + def headers(self) -> list[SM64_AnimHeaderProperties]: + return [self.header] + list(self.header_variants) + + @property + def dma_name(self): + return get_dma_anim_name([header.table_index for header in self.headers]) + + def get_name(self, action: Action, dma=False) -> str: + if dma: + return self.dma_name + return toAlnum(f"anim_{action.name}") + + def get_file_name(self, action: Action, export_type: str, dma=False) -> str: + if not export_type in {"C", "Insertable Binary"}: + return "" + if export_type == "C" and dma: + return f"{self.dma_name}.inc.c" + elif self.use_custom_file_name: + return self.custom_file_name + else: + name = clean_name(f"anim_{action.name}", replace=" ") + return name + (".inc.c" if export_type == "C" else ".insertable") + + def get_max_frame(self, action: Action) -> int: + if self.use_custom_max_frame: + return self.custom_max_frame + loop_ends: list[int] = [getFrameInterval(action)[1]] + header_props: SM64_AnimHeaderProperties + for header_props in self.headers: + loop_ends.append(header_props.get_loop_points(action)[2]) + + return max(loop_ends) + + def update_variant_numbers(self): + for i, variant in enumerate(self.headers): + variant.header_variant = i + + def draw_variants( + self, + layout: UILayout, + action: Action, + dma: bool, + actor_name: str, + header_args: list, + ): + col = layout.column() + op_row = col.row() + op_row.label(text=f"Header Variants ({len(self.headers)})", icon="NLA") + draw_list_op(op_row, SM64_AnimVariantOps, "CLEAR", action_name=action.name) + + for i, header_props in enumerate(self.headers): + if i != 0: + col.separator() + + row = col.row() + if draw_and_check_tab( + row, + header_props, + "expand_tab_in_action", + header_props.get_name(actor_name, action, dma), + ): + header_props.draw_props(col, *header_args) + op_row = row.row() + op_row.alignment = "RIGHT" + draw_list_ops(op_row, SM64_AnimVariantOps, i, action_name=action.name) + + def draw_references(self, layout: UILayout, is_binary: bool = False): + col = layout.column() + col.prop(self, "reference_tables") + if not self.reference_tables: + return + if is_binary: + string_int_prop(col, self, "indices_address", "Indices Table") + string_int_prop(col, self, "values_address", "Value Table") + else: + prop_split(col, self, "indices_table", "Indices Table") + prop_split(col, self, "values_table", "Value Table") + + def draw_props( + self, + layout: UILayout, + action: Action, + specific_variant: int | None, + in_table: bool, + updates_table: bool, + export_seperately: bool, + export_type: str, + actor_name: str, + gen_enums: bool, + dma: bool, + ): + # Args to pass to the headers + header_args = (action, in_table, updates_table, dma, export_type, actor_name, gen_enums) + + col = layout.column() + if specific_variant is not None: + col.label(text="Action Properties", icon="ACTION") + if not in_table: + draw_list_op( + col, + SM64_AnimTableOps, + "ADD_ALL", + text="Add All Variants To Table", + icon="LINKED", + action_name=action.name, + ) + col.separator() + + if export_type == "Binary" and not dma: + string_int_prop(col, self, "start_address", "Start Address") + string_int_prop(col, self, "end_address", "End Address") + if export_type != "Binary" and (export_seperately or not in_table): + if not dma or export_type == "Insertable Binary": # not c dma or insertable + text = "File Name" + if not in_table and not export_seperately: + text = "File Name (individual action export)" + draw_custom_or_auto(self, col, "file_name", self.get_file_name(action, export_type), text=text) + elif not in_table: # C DMA forced auto name + split = col.split(factor=0.5) + split.label(text="File Name") + file_name = self.get_file_name(action, export_type, dma) + prop_size_label(split, text=file_name, icon="LOCKED") + if dma or not self.reference_tables: # DMA tables don´t allow references + draw_custom_or_auto(self, col, "max_frame", str(self.get_max_frame(action))) + if not dma: + self.draw_references(col, is_binary=export_type.endswith("Binary")) + col.separator() + + if specific_variant is not None: + if specific_variant < 0 or specific_variant >= len(self.headers): + col.box().label(text="Header variant does not exist.", icon="ERROR") + else: + col.label(text="Variant Properties", icon="NLA") + self.headers[specific_variant].draw_props(col, *header_args) + else: + self.draw_variants(col, action, dma, actor_name, header_args) + + +class SM64_AnimTableElementProperties(PropertyGroup): + expand_tab: BoolProperty() + action_prop: PointerProperty(name="Action", type=Action) + variant: IntProperty(name="Variant", min=0) + reference: BoolProperty(name="Reference") + # Toad example + header_name: StringProperty(name="Header Reference", default="toad_seg6_anim_0600B66C") + header_address: StringProperty(name="Header Reference", default=intToHex(0x0600B75C)) + use_custom_enum: BoolProperty(name="Enum") + custom_enum: StringProperty(name="Enum Name") + + def get_enum(self, can_reference: bool, actor_name: str, prev_enums: dict[str, int]): + """Updates prev_enums""" + enum = "" + if self.use_custom_enum: + self.custom_enum: str + enum = self.custom_enum + elif can_reference and self.reference: + enum = duplicate_name(anim_name_to_enum_name(self.header_name), prev_enums) + else: + action, header = self.get_action_header(can_reference) + if header and action: + enum = duplicate_name(header.get_enum(actor_name, action), prev_enums) + return enum + + def get_action_header(self, can_reference: bool): + self.variant: int + self.action_prop: Action + if (not can_reference or not self.reference) and self.action_prop: + headers = get_action_props(self.action_prop).headers + if self.variant < len(headers): + return (self.action_prop, headers[self.variant]) + return (None, None) + + def get_action(self, can_reference: bool) -> Action | None: + return self.get_action_header(can_reference)[0] + + def get_header(self, can_reference: bool) -> SM64_AnimHeaderProperties | None: + return self.get_action_header(can_reference)[1] + + def set_variant(self, action: Action, variant: int): + self.action_prop = action + self.variant = variant + + def draw_reference( + self, layout: UILayout, export_type: str = "C", gen_enums: bool = False, prev_enums: dict[str, int] = None + ): + if export_type.endswith("Binary"): + string_int_prop(layout, self, "header_address", "Header Address") + return + split = layout.split() + if gen_enums: + draw_custom_or_auto(self, split, "enum", self.get_enum(True, "", prev_enums), factor=0.3) + split.prop(self, "header_name", text="") + + def draw_props( + self, + row: UILayout, # left side of the row for table ops + prop_layout: UILayout, + index: int, + dma: bool, + updates_table: bool, + export_seperately: bool, + export_type: str, + gen_enums: bool, + actor_name: str, + prev_enums: dict[str, int], + ): + can_reference = not dma + col = prop_layout.column() + if can_reference: + reference_row = row.row() + reference_row.alignment = "LEFT" + reference_row.prop(self, "reference") + if self.reference: + self.draw_reference(col, export_type, gen_enums, prev_enums) + return + action_row = row.row() + action_row.alignment = "EXPAND" + action_row.prop(self, "action_prop", text="") + + if not self.action_prop: + col.box().label(text="Header´s action does not exist.", icon="ERROR") + return + action = self.action_prop + action_props = get_action_props(action) + + variant_split = col.split(factor=0.3) + variant_split.prop(self, "variant") + + if 0 <= self.variant < len(action_props.headers): + header_props = self.get_header(can_reference) + if dma: + name = get_dma_header_name(index) + else: + name = header_props.get_name(actor_name, action, dma) + if gen_enums: + draw_custom_or_auto( + self, + variant_split, + "enum", + self.get_enum(can_reference, actor_name, prev_enums), + factor=0.3, + ) + tab_name = name + (f" (Variant {self.variant})" if self.variant > 0 else "") + if not draw_and_check_tab(col, self, "expand_tab", tab_name): + return + + action_props.draw_props( + layout=col, + action=action, + specific_variant=self.variant, + in_table=True, + updates_table=updates_table, + export_seperately=export_seperately, + export_type=export_type, + actor_name=actor_name, + gen_enums=gen_enums, + dma=dma, + ) + + +class SM64_AnimImportProperties(PropertyGroup): + run_decimate: BoolProperty(name="Run Decimate (Allowed Change)", default=True) + decimate_margin: FloatProperty( + name="Error Margin", + default=0.025, + min=0.0, + max=0.025, + description="Use blender's builtin decimate (allowed change) operator to clean up all the " + "keyframes, generally the better option compared to clean keyframes but can be slow", + ) + + continuity_filter: BoolProperty(name="Continuity Filter", default=True) + force_quaternion: BoolProperty( + name="Force Quaternions", + description="Changes bones to quaternion rotation mode, can break existing actions", + ) + + clear_table: BoolProperty(name="Clear Table On Import", default=True) + import_type: EnumProperty(items=enum_anim_import_types, name="Import Type", default="C") + preset: bpy.props.EnumProperty( + items=enum_anim_tables, + name="Preset", + update=update_table_preset, + default="Mario", + ) + decomp_path: StringProperty(name="Decomp Path", subtype="FILE_PATH", default="") + binary_import_type: EnumProperty( + items=enum_anim_binary_import_types, + name="Binary Import Type", + default="Table", + ) + read_entire_table: BoolProperty(name="Read Entire Table", default=True) + check_null: BoolProperty(name="Check NULL Delimiter", default=True) + table_size_prop: IntProperty(name="Size", min=1) + table_index_prop: IntProperty(name="Index", min=0) + ignore_bone_count: BoolProperty( + name="Ignore bone count", + description="The armature bone count won´t be used when importing, a safety check will be skipped and old " + "fast64 animations won´t import, needed to import bowser's broken animation", + ) + preset_animation: EnumProperty(name="Preset Animation", items=get_enum_from_import_preset) + + rom: StringProperty(name="Import ROM", subtype="FILE_PATH") + table_address: StringProperty(name="Address", default=intToHex(0x0600FC48)) # Toad + animation_address: StringProperty(name="Address", default=intToHex(0x0600B75C)) + is_segmented_address_prop: BoolProperty(name="Is Segmented Address", default=True) + level: EnumProperty(items=enumLevelNames, name="Level", default="castle_inside") + dma_table_address: StringProperty(name="DMA Table Address", default="0x4EC000") + + read_from_rom: BoolProperty( + name="Read From Import ROM", + description="When enabled, the importer will read from the import ROM given an " + "address not included in the insertable file's defined pointers", + ) + + path: StringProperty(name="Path", subtype="FILE_PATH", default="anims/") + use_custom_name: BoolProperty(name="Use Custom Name", default=True) + + @property + def binary(self) -> bool: + return self.import_type.endswith("Binary") + + @property + def table_index(self): + if self.read_entire_table: + return + elif self.preset_animation == "Custom" or not self.use_preset: + return self.table_index_prop + else: + return int_from_str(self.preset_animation) + + @property + def address(self): + if self.import_type != "Binary": + return + elif self.binary_import_type == "DMA": + return int_from_str(self.dma_table_address) + elif self.binary_import_type == "Table": + return int_from_str(self.table_address) + else: + return int_from_str(self.animation_address) + + @property + def is_segmented_address(self): + if self.import_type != "Binary": + return + return ( + self.is_segmented_address_prop + if self.import_type == "Binary" and self.binary_import_type in {"Table", "Animation"} + else False + ) + + @property + def table_size(self): + return None if self.check_null else self.table_size_prop + + @property + def use_preset(self): + return self.import_type != "Insertable Binary" and self.preset != "Custom" + + def upgrade_old_props(self, scene: Scene): + upgrade_old_prop( + self, + "animation_address", + scene, + "animStartImport", + fix_forced_base_16=True, + ) + upgrade_old_prop(self, "is_segmented_address_prop", scene, "animIsSegPtr") + upgrade_old_prop(self, "level", scene, "levelAnimImport") + upgrade_old_prop(self, "table_index_prop", scene, "animListIndexImport") + if scene.pop("isDMAImport", False): + self.binary_import_type = "DMA" + elif scene.pop("animIsAnimList", True): + self.binary_import_type = "Table" + + def draw_clean_up(self, layout: UILayout): + col = layout.column() + col.prop(self, "run_decimate") + if self.run_decimate: + prop_split(col, self, "decimate_margin", "Error Margin") + col.box().label(text="While very useful and stable, it can be very slow", icon="INFO") + col.separator() + + row = col.row() + row.prop(self, "force_quaternion") + continuity_row = row.row() + continuity_row.enabled = not self.force_quaternion + continuity_row.prop( + self, + "continuity_filter", + text="Continuity Filter" + (" (Always on)" if self.force_quaternion else ""), + invert_checkbox=not self.continuity_filter if self.force_quaternion else False, + ) + + def draw_path(self, layout: UILayout): + prop_split(layout, self, "path", "Directory or File Path") + path_ui_warnings(layout, abspath(self.path)) + + def draw_c(self, layout: UILayout, decomp: os.PathLike = ""): + col = layout.column() + if self.preset == "Custom": + self.draw_path(col) + else: + col.label(text="Uses scene decomp path by default", icon="INFO") + prop_split(col, self, "decomp_path", "Decomp Path") + directory_ui_warnings(col, abspath(self.decomp_path or decomp)) + col.prop(self, "use_custom_name") + + def draw_import_rom(self, layout: UILayout, import_rom: os.PathLike = ""): + col = layout.column() + col.label(text="Uses scene import ROM by default", icon="INFO") + prop_split(col, self, "rom", "Import ROM") + return import_rom_ui_warnings(col, abspath(self.rom or import_rom)) + + def draw_table_settings(self, layout: UILayout): + row = layout.row(align=True) + left_row = row.row(align=True) + left_row.alignment = "LEFT" + left_row.prop(self, "read_entire_table") + left_row.prop(self, "check_null") + right_row = row.row(align=True) + right_row.alignment = "EXPAND" + if not self.read_entire_table: + right_row.prop(self, "table_index_prop", text="Index") + elif not self.check_null: + right_row.prop(self, "table_size_prop") + + def draw_binary(self, layout: UILayout, import_rom: os.PathLike): + col = layout.column() + self.draw_import_rom(col, import_rom) + col.separator() + + if self.preset != "Custom": + split = col.split() + split.prop(self, "read_entire_table") + if not self.read_entire_table: + SM64_SearchAnimPresets.draw_props(split, self, "preset_animation", "") + if self.preset_animation == "Custom": + split.prop(self, "table_index_prop", text="Index") + return + col.prop(self, "ignore_bone_count") + prop_split(col, self, "binary_import_type", "Animation Type") + if self.binary_import_type == "DMA": + string_int_prop(col, self, "dma_table_address", "DMA Table Address") + split = col.split() + split.prop(self, "read_entire_table") + if not self.read_entire_table: + split.prop(self, "table_index_prop", text="Index") + return + + split = col.split() + split.prop(self, "is_segmented_address_prop") + if self.binary_import_type == "Table": + split.prop(self, "table_address", text="") + string_int_warning(col, self.table_address) + elif self.binary_import_type == "Animation": + split.prop(self, "animation_address", text="") + string_int_warning(col, self.animation_address) + prop_split(col, self, "level", "Level") + if self.binary_import_type == "Table": # Draw settings after level + self.draw_table_settings(col) + + def draw_insertable_binary(self, layout: UILayout, import_rom: os.PathLike): + col = layout.column() + self.draw_path(col) + col.separator() + + col.label(text="Animation type will be read from the files", icon="INFO") + + table_box = col.column() + table_box.label(text="Table Imports", icon="ANIM") + self.draw_table_settings(table_box) + col.separator() + + col.prop(self, "read_from_rom") + if self.read_from_rom: + self.draw_import_rom(col, import_rom) + prop_split(col, self, "level", "Level") + + col.prop(self, "ignore_bone_count") + + def draw_props(self, layout: UILayout, import_rom: os.PathLike = "", decomp: os.PathLike = ""): + col = layout.column() + + prop_split(col, self, "import_type", "Type") + + if self.import_type in {"C", "Binary"}: + SM64_SearchAnimTablePresets.draw_props(col, self, "preset", "Preset") + col.separator() + + if self.import_type == "C": + self.draw_c(col, decomp) + elif self.binary: + if self.import_type == "Binary": + self.draw_binary(col, import_rom) + elif self.import_type == "Insertable Binary": + self.draw_insertable_binary(col, import_rom) + col.separator() + + self.draw_clean_up(col) + col.prop(self, "clear_table") + SM64_ImportAnim.draw_props(col) + + +class SM64_AnimProperties(PropertyGroup): + version: IntProperty(name="SM64_AnimProperties Version", default=0) + cur_version = 1 # version after property migration + + played_header: IntProperty(min=0) + played_action: PointerProperty(name="Action", type=Action) + + importing: PointerProperty(type=SM64_AnimImportProperties) + + def upgrade_old_props(self, scene: Scene): + self.importing.upgrade_old_props(scene) + + # Export + loop = scene.pop("loopAnimation", None) + start_address = scene.pop("animExportStart", None) + end_address = scene.pop("animExportEnd", None) + + for action in bpy.data.actions: + action_props: SM64_ActionAnimProperty = get_action_props(action) + action_props.header: SM64_AnimHeaderProperties + if loop is not None: + action_props.header.set_flags(SM64_AnimFlags(0) if loop else SM64_AnimFlags.ANIM_FLAG_NOLOOP) + if start_address is not None: + action_props.start_address = intToHex(int(start_address, 16)) + if end_address is not None: + action_props.end_address = intToHex(int(end_address, 16)) + + insertable_path = scene.pop("animInsertableBinaryPath", "") + is_dma = scene.pop("loopAnimation", None) + update_table = scene.pop("animExportStart", None) + update_behavior = scene.pop("animExportEnd", None) + beginning_animation = scene.pop("animListIndexExport", None) + for obj in bpy.data.objects: + if not is_obj_animatable(obj): + continue + anim_props: SM64_ArmatureAnimProperties = obj.fast64.sm64.animation + if is_dma is not None: + anim_props.is_dma = is_dma + if update_table is not None: + anim_props.update_table = update_table + if update_behavior is not None: + anim_props.update_behavior = update_behavior + if beginning_animation is not None: + anim_props.beginning_animation = beginning_animation + if insertable_path is not None: # Ignores directory + anim_props.use_custom_file_name = True + anim_props.custom_file_name = os.path.split(insertable_path)[0] + + # Deprecated: + # - addr 0x27 was a pointer to a load anim cmd that would be used to update table pointers + # the actual table pointer is used instead + # - addr 0x28 was a pointer to a animate cmd that would be updated to the beggining + # animation a behavior script pointer is used instead so both load an animate can be updated + # easily without much thought + + self.version = 1 + + def upgrade_changed_props(self, scene): + if self.version != self.cur_version: + self.upgrade_old_props(scene) + self.version = SM64_AnimProperties.cur_version + + +class SM64_ArmatureAnimProperties(PropertyGroup): + version: IntProperty(name="SM64_AnimProperties Version", default=0) + cur_version = 1 # version after property migration + + is_dma: BoolProperty(name="Is DMA Export") + dma_folder: StringProperty(name="DMA Folder", default="assets/anims/") + update_table: BoolProperty( + name="Update Table On Action Export", + description="Update table outside of table exports", + default=True, + ) + + # Table + elements: CollectionProperty(type=SM64_AnimTableElementProperties) + + export_seperately_prop: BoolProperty(name="Export All Seperately") + write_data_seperately: BoolProperty(name="Write Data Seperately") + null_delimiter: BoolProperty(name="Add Null Delimiter") + override_files_prop: BoolProperty(name="Override Table and Data Files", default=True) + gen_enums: BoolProperty(name="Generate Enums", default=True) + use_custom_table_name: BoolProperty(name="Table Name") + custom_table_name: StringProperty(name="Table Name", default="mario_anims") + # Binary, Toad animation table example + data_address: StringProperty( + name="Data Address", + default=intToHex(0x00A3F7E0), + ) + data_end_address: StringProperty( + name="Data End", + default=intToHex(0x00A466C0), + ) + address: StringProperty(name="Table Address", default=intToHex(0x00A46738)) + end_address: StringProperty(name="Table End", default=intToHex(0x00A4675C)) + update_behavior: BoolProperty(name="Update Behavior", default=True) + behaviour: bpy.props.EnumProperty(items=enum_animated_behaviours, default=intToHex(0x13002EF8)) + behavior_address_prop: StringProperty(name="Behavior Address", default=intToHex(0x13002EF8)) + beginning_animation: StringProperty(name="Begining Animation", default="0x00") + # Mario animation table + dma_address: StringProperty(name="DMA Table Address", default=intToHex(0x4EC000)) + dma_end_address: StringProperty(name="DMA Table End", default=intToHex(0x4EC000 + 0x8DC20)) + + use_custom_file_name: BoolProperty(name="File Name") + custom_file_name: StringProperty(name="File Name", default="toad.insertable") + + @property + def behavior_address(self) -> int: + if self.behaviour == "Custom": + return int_from_str(self.behavior_address_prop) + return int_from_str(self.behaviour) + + @property + def export_seperately(self): + return self.is_dma or self.export_seperately_prop + + @property + def override_files(self) -> bool: + return not self.export_seperately or self.override_files_prop + + @property + def actions(self) -> list[Action]: + actions = [] + for element_props in self.elements: + action = element_props.get_action(not self.is_dma) + if action and action not in actions: + actions.append(action) + return actions + + def get_table_name(self, actor_name: str) -> str: + if self.use_custom_table_name: + return self.custom_table_name + return f"{actor_name}_anims" + + def get_enum_name(self, actor_name: str): + return table_name_to_enum(self.get_table_name(actor_name)) + + def get_enum_end(self, actor_name: str): + table_name = self.get_table_name(actor_name) + return f"{table_name.upper()}_END" + + def get_table_file_name(self, actor_name: str, export_type: str) -> str: + if not export_type in {"C", "Insertable Binary"}: + return "" + elif export_type == "Insertable Binary": + if self.use_custom_file_name: + return self.custom_file_name + return clean_name(actor_name + ("_dma_table" if self.is_dma else "_table")) + ".insertable" + else: + return "table.inc.c" + + def draw_element( + self, + layout: UILayout, + index: int, + table_element: SM64_AnimTableElementProperties, + export_type: str, + actor_name: str, + prev_enums: dict[str, int], + ): + col = layout.column() + row = col.row() + left_row = row.row() + left_row.alignment = "EXPAND" + op_row = row.row() + op_row.alignment = "RIGHT" + draw_list_ops(op_row, SM64_AnimTableOps, index) + + table_element.draw_props( + left_row, + col, + index, + self.is_dma, + self.update_table, + self.export_seperately, + export_type, + export_type == "C" and self.gen_enums and not self.is_dma, + actor_name, + prev_enums, + ) + + def draw_table(self, layout: UILayout, export_type: str, actor_name: str): + col = layout.column() + + op_row = col.row() + op_row.label( + text="Headers " + (f"({len(self.elements)})" if self.elements else "(Empty)"), + icon="NLA", + ) + draw_list_op(op_row, SM64_AnimTableOps, "ADD") + draw_list_op(op_row, SM64_AnimTableOps, "CLEAR") + + if not self.elements: + return + + box = col.box().column() + actions_dups: dict[Action, list[int]] = {} + if self.is_dma: + actions_repeats: dict[Action, list[int]] = {} # possible dups + last_action = None + for i, element_props in enumerate(self.elements): + action: Action = element_props.get_action(can_reference=False) + if action != last_action: + if action in actions_repeats: + actions_repeats[action].append(i) + if action not in actions_dups: + actions_dups[action] = actions_repeats[action] + else: + actions_repeats[action] = [i] + last_action = action + + if actions_dups: + lines = [f'Action "{a.name}", Headers: {i}' for a, i in actions_dups.items()] + warn_box = box.box() + warn_box.alert = True + multilineLabel( + warn_box, + "In DMA tables, headers for each action must be \n" + "in one sequence or the data will be duplicated.\n" + "This will be handeled automatically but is undesirable.\n" + f'Data duplicate{"s" if len(actions_dups) > 1 else ""} in:\n' + "\n".join(lines), + "INFO", + ) + + prev_enums = {} + element_props: SM64_AnimTableElementProperties + for i, element_props in enumerate(self.elements): + if i != 0: + box.separator() + element_box = box.column() + action = element_props.get_action(not self.is_dma) + if action in actions_dups: + other_actions = [j for j in actions_dups[action] if j != i] + element_box.box().label(text=f"Action duplicates at {other_actions}") + self.draw_element(element_box, i, element_props, export_type, actor_name, prev_enums) + + def draw_c_settings(self, layout: UILayout, header_type: str): + col = layout.column() + if self.is_dma: + prop_split(col, self, "dma_folder", "Folder", icon="FILE_FOLDER") + if header_type == "Custom": + col.label(text="This folder will be relative to your custom path") + else: + decompFolderMessage(col) + return + + def draw_props(self, layout: UILayout, export_type: str, header_type: str, actor_name: str, bhv_export: bool): + col = layout.column() + col.prop(self, "is_dma") + if export_type == "C": + self.draw_c_settings(col, header_type) + if export_type != "Insertable Binary" and not self.is_dma: + col.prop(self, "update_table") + + if self.is_dma: + if export_type == "Binary": + string_int_prop(col, self, "dma_address", "Table Address") + string_int_prop(col, self, "dma_end_address", "Table End") + elif export_type == "C": + multilineLabel( + col, + "The export will follow the vanilla DMA naming\n" + "conventions (anim_xx.inc.c, anim_xx, anim_xx_values, etc).", + icon="INFO", + ) + else: + if export_type == "C": + draw_custom_or_auto(self, col, "table_name", self.get_table_name(actor_name)) + col.prop(self, "gen_enums") + if self.gen_enums: + multilineLabel( + col.box(), + f"Enum List Name: {self.get_enum_name(actor_name)}\n" + f"End Enum: {self.get_enum_end(actor_name)}", + ) + col.separator() + col.prop(self, "export_seperately_prop") + draw_forced(col, self, "override_files_prop", not self.export_seperately) + if bhv_export: + prop_split(col, self, "beginning_animation", "Beginning Animation") + elif export_type == "Binary": + string_int_prop(col, self, "address", "Table Address") + string_int_prop(col, self, "end_address", "Table End") + + box = col.box().column() + box.prop(self, "update_behavior") + if self.update_behavior: + multilineLabel( + box, + "Will update the LOAD_ANIMATIONS and ANIMATE commands.\n" + "Does not raise an error if there is no ANIMATE command", + "INFO", + ) + SM64_SearchAnimatedBhvs.draw_props(box, self, "behaviour", "Behaviour") + if self.behaviour == "Custom": + prop_split(box, self, "behavior_address_prop", "Behavior Address") + prop_split(box, self, "beginning_animation", "Beginning Animation") + + col.prop(self, "write_data_seperately") + if self.write_data_seperately: + string_int_prop(col, self, "data_address", "Data Address") + string_int_prop(col, self, "data_end_address", "Data End") + col.prop(self, "null_delimiter") + if export_type == "Insertable Binary": + draw_custom_or_auto(self, col, "file_name", self.get_table_file_name(actor_name, export_type)) + + +classes = ( + SM64_AnimHeaderProperties, + SM64_AnimTableElementProperties, + SM64_ActionAnimProperty, + SM64_AnimImportProperties, + SM64_AnimProperties, + SM64_ArmatureAnimProperties, +) + + +def anim_props_register(): + for cls in classes: + register_class(cls) + + +def anim_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/animation/utility.py b/fast64_internal/sm64/animation/utility.py new file mode 100644 index 000000000..276cb40e9 --- /dev/null +++ b/fast64_internal/sm64/animation/utility.py @@ -0,0 +1,165 @@ +from typing import TYPE_CHECKING +import functools +import re + +from bpy.types import Context, Object, Action, PoseBone + +from ...utility import findStartBones, PluginError, toAlnum +from ..sm64_geolayout_utility import is_bone_animatable + +if TYPE_CHECKING: + from .properties import SM64_ActionAnimProperty, SM64_AnimProperties, SM64_ArmatureAnimProperties + + +def is_obj_animatable(obj: Object) -> bool: + if obj.type == "ARMATURE" or (obj.type == "MESH" and obj.geo_cmd_static == "DisplayListWithOffset"): + return True + return False + + +def get_anim_obj(context: Context) -> Object | None: + obj = context.object + if obj is None and len(context.selected_objects) > 0: + obj = context.selected_objects[0] + if obj is not None and is_obj_animatable(obj): + return obj + + +def animation_operator_checks(context: Context, requires_animation=True, specific_obj: Object | None = None): + if specific_obj is None: + if len(context.selected_objects) > 1: + raise PluginError("Multiple objects selected at once.") + obj = get_anim_obj(context) + else: + obj = specific_obj + if is_obj_animatable(obj): + raise PluginError(f'Selected object "{obj.name}" is not an armature.') + if requires_animation and obj.animation_data is None: + raise PluginError(f'Armature "{obj.name}" has no animation data.') + + +def get_selected_action(obj: Object, raise_exc=True) -> Action: + assert obj is not None + if not is_obj_animatable(obj): + if raise_exc: + raise ValueError(f'Object "{obj.name}" is not animatable in SM64.') + elif obj.animation_data is not None and obj.animation_data.action is not None: + return obj.animation_data.action + if raise_exc: + raise ValueError(f'No action selected in object "{obj.name}".') + + +def get_anim_owners(obj: Object): + """Get SM64 animation bones from an armature or return the obj if it's an animated cmd mesh""" + + def check_children(children: list[Object] | None): + if children is None: + return + for child in children: + if child.geo_cmd_static == "DisplayListWithOffset": + raise PluginError("Cannot have child mesh with animation, use an armature") + check_children(child.children) + + if obj.type == "MESH": # Object will be treated as a bone + if obj.geo_cmd_static == "DisplayListWithOffset": + check_children(obj.children) + return [obj] + else: + raise PluginError("Mesh is not animatable") + + assert obj.type == "ARMATURE", "Obj is neither mesh or armature" + + bones_to_process: list[str] = findStartBones(obj) + current_bone = obj.data.bones[bones_to_process[0]] + anim_bones: list[PoseBone] = [] + + # Get animation bones in order + while len(bones_to_process) > 0: + bone_name = bones_to_process[0] + current_bone = obj.data.bones[bone_name] + current_pose_bone = obj.pose.bones[bone_name] + bones_to_process = bones_to_process[1:] + + # Only handle 0x13 bones for animation + if is_bone_animatable(current_bone): + anim_bones.append(current_pose_bone) + + # Traverse children in alphabetical order. + children_names = sorted([bone.name for bone in current_bone.children]) + bones_to_process = children_names + bones_to_process + + return anim_bones + + +def num_to_padded_hex(num: int): + hex_str = hex(num)[2:].upper() # remove the '0x' prefix + return hex_str.zfill(2) + + +@functools.cache +def get_dma_header_name(index: int): + return f"anim_{num_to_padded_hex(index)}" + + +def get_dma_anim_name(header_indices: list[int]): + return f'anim_{"_".join([f"{num_to_padded_hex(num)}" for num in header_indices])}' + + +@functools.cache +def action_name_to_anim_name(action_name: str) -> str: + return re.sub(r"^_(\d+_)+(?=\w)", "", toAlnum(action_name), flags=re.MULTILINE) + + +@functools.cache +def anim_name_to_enum_name(anim_name: str) -> str: + enum_name = anim_name.upper() + enum_name: str = re.sub(r"(?<=_)_|_$", "", toAlnum(enum_name), flags=re.MULTILINE) + if anim_name == enum_name: + enum_name = f"{enum_name}_ENUM" + return enum_name + + +def duplicate_name(name: str, existing_names: dict[str, int]) -> str: + """Updates existing_names""" + current_num = existing_names.get(name) + if current_num is None: + existing_names[name] = 0 + elif name != "": + current_num += 1 + existing_names[name] = current_num + return f"{name}_{current_num}" + return name + + +def table_name_to_enum(name: str): + return name.title().replace("_", "") + + +def get_action_props(action: Action) -> "SM64_ActionAnimProperty": + return action.fast64.sm64.animation + + +def get_scene_anim_props(context: Context) -> "SM64_AnimProperties": + return context.scene.fast64.sm64.animation + + +def get_anim_props(context: Context) -> "SM64_ArmatureAnimProperties": + obj = get_anim_obj(context) + assert obj is not None + return obj.fast64.sm64.animation + + +def get_anim_actor_name(context: Context) -> str | None: + sm64_props = context.scene.fast64.sm64 + if sm64_props.export_type == "C" and sm64_props.combined_export.export_anim: + return toAlnum(sm64_props.combined_export.obj_name_anim) + elif context.object: + return sm64_props.combined_export.filter_name(toAlnum(context.object.name), True) + else: + return None + + +def dma_structure_context(context: Context) -> bool: + if get_anim_obj(context) is None: + return False + return get_anim_props(context).is_dma diff --git a/fast64_internal/sm64/c_templates/tile_scroll.py b/fast64_internal/sm64/c_templates/tile_scroll.py index 6fa1adb69..d85c58532 100644 --- a/fast64_internal/sm64/c_templates/tile_scroll.py +++ b/fast64_internal/sm64/c_templates/tile_scroll.py @@ -13,28 +13,28 @@ */ void shift_s(Gfx *dl, u32 cmd, u16 s) { - SetTileSize *tile = dl; + SetTileSize *tile = (SetTileSize *) dl; tile += cmd; tile->s += s; tile->u += s; } void shift_t(Gfx *dl, u32 cmd, u16 t) { - SetTileSize *tile = dl; + SetTileSize *tile = (SetTileSize *) dl; tile += cmd; tile->t += t; tile->v += t; } void shift_s_down(Gfx *dl, u32 cmd, u16 s) { - SetTileSize *tile = dl; + SetTileSize *tile = (SetTileSize *) dl; tile += cmd; tile->s -= s; tile->u += s; } void shift_t_down(Gfx *dl, u32 cmd, u16 t) { - SetTileSize *tile = dl; + SetTileSize *tile = (SetTileSize *) dl; tile += cmd; tile->t -= t; tile->v += t; diff --git a/fast64_internal/sm64/custom_cmd/__init__.py b/fast64_internal/sm64/custom_cmd/__init__.py new file mode 100644 index 000000000..88d01330e --- /dev/null +++ b/fast64_internal/sm64/custom_cmd/__init__.py @@ -0,0 +1,12 @@ +from .properties import props_register, props_unregister +from .operators import operators_register, operators_unregister + + +def custom_cmd_register(): + props_register() + operators_register() + + +def custom_cmd_unregister(): + props_unregister() + operators_unregister() diff --git a/fast64_internal/sm64/custom_cmd/exporting.py b/fast64_internal/sm64/custom_cmd/exporting.py new file mode 100644 index 000000000..2d2bed17f --- /dev/null +++ b/fast64_internal/sm64/custom_cmd/exporting.py @@ -0,0 +1,436 @@ +import dataclasses +import math +import operator +import struct +import ast +from io import StringIO +from typing import Iterable, NamedTuple, Optional, TypeVar, Union + +from ...utility import ( + PluginError, + get_clean_color, + quantize_color, + cast_integer, + to_s16, + cast_integer, + encodeSegmentedAddr, +) + +from ..sm64_constants import SegmentData +from ..sm64_geolayout_utility import BaseDisplayListNode + +from .utility import getDrawLayerName + +BIT_COUNTS = {"CHAR": 8, "SHORT": 16, "INT": 32, "LONG": 64, "FLOAT": 32, "DOUBLE": 64} + +T = TypeVar("T") + + +def flatten(iterable: Iterable[T]) -> tuple[T]: + if not isinstance(iterable, Iterable) or isinstance(iterable, str): + return (iterable,) + flat = [] + for x in iterable: + if isinstance(x, Iterable): + flat.extend(flatten(x)) + else: + flat.append(x) + return tuple(flat) + + +bin_ops = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.Mod: operator.mod, + ast.LShift: operator.lshift, + ast.RShift: operator.rshift, + ast.BitOr: operator.or_, + ast.BitAnd: operator.and_, + ast.BitXor: operator.xor, + ast.Pow: operator.pow, + ast.FloorDiv: operator.floordiv, + ast.USub: operator.neg, + ast.UAdd: lambda a: a, + ast.Not: operator.not_, + ast.NotEq: operator.ne, + ast.And: operator.and_, + ast.Or: operator.or_, + ast.In: operator.contains, + ast.NotIn: lambda a, b: not operator.contains(a, b), + ast.Is: operator.is_, + ast.IsNot: operator.is_not, + ast.Eq: operator.eq, + ast.Lt: operator.lt, + ast.LtE: operator.le, + ast.Gt: operator.gt, + ast.GtE: operator.ge, + ast.Invert: operator.invert, +} + +builtins_map = { + "round": round, + "abs": abs, + "tuple": tuple, + "list": list, + "set": set, + "dict": dict, + "len": len, + "range": range, + "min": min, + "max": max, + "sum": sum, + "sorted": sorted, + "all": all, + "any": any, + "enumerate": enumerate, + "flatten": flatten, + "cast_integer": cast_integer, +} +collection_constructors = {ast.List: list, ast.Tuple: tuple, ast.Set: set} + + +def math_eval(s, start_scope: dict[str, object] | None = None): + if start_scope is None: + start_scope = {} + if isinstance(s, int): + return s + + s = s.strip() + node = ast.parse(s, mode="eval") + + def _eval(node: ast.expr, scope: dict[str, object]): + scope = scope.copy() + + def eval_comprehension(elt_node: ast.expr, generators: list[ast.comprehension], scope: dict[str, object]): + if not generators: + result = [_eval(elt_node, scope)] + else: + result = [] + first_comp, rest_comps = generators[0], generators[1:] + for value in _eval(first_comp.iter, scope): + new_scope = scope.copy() + if isinstance(first_comp.target, ast.Name): + new_scope[first_comp.target.id] = value + elif isinstance(first_comp.target, (ast.Tuple, ast.List, ast.Set)): + for i, elt in enumerate(first_comp.target.elts): + new_scope[elt.id] = value[i] + if all(_eval(if_node, new_scope) for if_node in first_comp.ifs): + sub_results = eval_comprehension(elt_node, rest_comps, new_scope) + result.extend(sub_results) + return result + + if isinstance(node, ast.Name): + if node.id in scope: + return scope[node.id] + elif hasattr(math, node.id): + return getattr(math, node.id) + else: + return builtins_map.get(node.id, node.id) + elif isinstance(node, ast.Constant): + return node.value + elif isinstance(node, ast.UnaryOp): + return bin_ops[type(node.op)](_eval(node.operand, scope)) + elif isinstance(node, ast.BinOp): + return bin_ops[type(node.op)](_eval(node.left, scope), _eval(node.right, scope)) + elif isinstance(node, ast.Call): + args = [_eval(x, scope) for x in node.args] + funcName = _eval(node.func, scope) + return funcName(*args) + elif isinstance(node, ast.ListComp): + return eval_comprehension(node.elt, node.generators, scope) + elif isinstance(node, ast.SetComp): + return set(eval_comprehension(node.elt, node.generators, scope)) + elif isinstance(node, ast.GeneratorExp): + return eval_comprehension(node.elt, node.generators, scope) + elif isinstance(node, tuple(collection_constructors.keys())): + return collection_constructors[type(node)](_eval(x, scope) for x in node.elts) + elif isinstance(node, ast.Expression): + return _eval(node.body, scope) + elif isinstance(node, ast.Subscript): + return _eval(node.value, scope)[_eval(node.slice, scope)] + elif isinstance(node, ast.Slice): + lower, upper, step = 0, None, None + if node.lower is not None: + lower = _eval(node.lower, scope) + if node.upper is not None: + upper = _eval(node.upper, scope) + if node.step is not None: + step = _eval(node.step, scope) + return slice(lower, upper, step) + elif isinstance(node, ast.IfExp): + if _eval(node.test, scope): + return _eval(node.body, scope) + else: + return _eval(node.orelse, scope) + elif isinstance(node, ast.Compare): + left = _eval(node.left, scope) + for op, right in zip(node.ops, node.comparators): + right = _eval(right, scope) + if not bin_ops[type(op)](left, right): + return False + left = right + return True + else: + raise Exception(f"Unsupported AST node: {ast.dump(node)}") + + return _eval(node.body, start_scope) + + +class ArgExport(NamedTuple): + value: float | int | bool | str + bit_count: int = 32 + signed: bool = True + + +@dataclasses.dataclass +class ExportCombinedColor: + rgba: tuple[float, float, float, float] + bit_counts: tuple[int, int, int, int] + + def to_c(self): + bit_count = sum(c for c in self.bit_counts) + elements = [] + for c, v in zip(self.bit_counts, self.rgba): + if c == 0: + continue + bit_count -= c + rounded_value = round(v * (2**c - 1)) + if bit_count == 0: + elements.append(f"{rounded_value}") + else: + elements.append(f"({rounded_value} << {bit_count})") + return f"({' | '.join(elements)})" + + def to_binary(self): + return quantize_color(self.rgba, self.bit_counts) + + +@dataclasses.dataclass +class CustomCmd(BaseDisplayListNode): + data: dict + draw_layer: int | str | None = 0 + hasDL: bool = False + dlRef: str = None + name: str = "" + bleed_independently: bool = False + fMesh: "FMesh" = None + DLmicrocode: Union["GfxList", None] = None + # exists to get the override DL from an fMesh + override_hash: tuple | None = None + + def __post_init__(self): + self.hasDL &= self.data.get("dl_option") != "NONE" + self.group_children = self.data.get("group_children", True) + + @property + def drawLayer(self): + """HACK: drawLayer's default is usually per bone/object, but in the custom cmd system defaults are per argument. + We instead store a layer that can be none, and set it to a real value if the setter is called. + """ + if self.draw_layer is None: + return 0 + return self.draw_layer + + @drawLayer.setter + def drawLayer(self, value): + self.draw_layer = value + + @property + def args(self): + yield from self.data["args"] + if self.hasDL and "dl_command" in self.data: + yield {"name": "Displaylist", "arg_type": "DL"} + + def do_export_checks(self, children_count: int): + name = "" or self.data.get("name") or self.data.get("str_cmd") + name = f" ({name})" if name else "" + children_requirements = self.data.get("children_requirements", "ANY") + if children_requirements == "MUST" and children_count == 0: + raise PluginError(f"Command{name} must have at least one child node") + elif children_requirements == "NONE" and children_count > 0: + raise PluginError(f"Command{name} must have no children") + if self.data.get("dl_option") == "REQUIRED": + if self.DLmicrocode is None: + raise PluginError(f"Command{name} requires a displaylist") + + def to_arg(self, data: dict, binary=False) -> Iterable[ArgExport]: + def run_eval(value, bit_count=32, signed=True): + if ( + (not self.data["skip_eval"] or binary) + and isinstance(value, (int, float, complex, tuple, list, ExportCombinedColor)) + and (not isinstance(value, bool) or binary) + and "eval_expression" in data + ): + if isinstance(value, ExportCombinedColor): + value = value.to_binary() + evaluated = math_eval(data["eval_expression"], {"x": value}) + yield from tuple(ArgExport(x, bit_count, signed) for x in flatten(evaluated)) + else: + yield from tuple(ArgExport(x, bit_count, signed) for x in flatten(value)) + + arg_type = data.get("arg_type") + round_to_sm64 = data.get("round_to_sm64", True) + match arg_type: + case "COLOR": + if round_to_sm64: + bit_counts = data.get("color_bits", (8, 8, 8, 8)) + color = get_clean_color(data["color"], True, False, True) + combined_color = ExportCombinedColor(color, bit_counts) + yield from run_eval(combined_color, sum(bit_counts), False) + else: + yield from run_eval(get_clean_color(data["color"], True, True, True), 32, False) + case "PARAMETER": + if binary: + value = math_eval(data["parameter"], {}) + if isinstance(value, str): + raise PluginError("Strings not supported in binary") + yield from run_eval(value) + else: + yield from run_eval(data["parameter"]) + case "ENUM": + if data["enum"] >= len(data["enum_options"]): + option = {"int_value": 0, "str_value": "INVALID"} + else: + option = data["enum_options"][data["enum"]] + if binary: + yield from run_eval(option["int_value"]) + else: + yield from run_eval(option["str_value"]) + case "LAYER": + layer = data["layer"] if self.draw_layer is None or not data.get("inherit", True) else self.draw_layer + if binary: + layer = int(data["layer"]) + if "dl_command" in self.data: + layer = (1 << 7) | layer + yield from run_eval(layer, 8, False) + else: + yield from run_eval(getDrawLayerName(layer)) + case "BOOLEAN": + yield from run_eval(data["boolean"], 8) + case "NUMBER": + yield from run_eval(data["value"], 32) + case "TRANSLATION": + translation = data["translation"] + if round_to_sm64: + yield from run_eval(tuple(round(x) for x in translation), 16) + else: + yield from run_eval(tuple(x for x in translation), 32) + case "SCALE" | "MATRIX": + scale_matrix = data.get(arg_type.lower()) + if round_to_sm64 and arg_type == "SCALE": + yield from run_eval(round(scale_matrix * 0x10000)) + yield from run_eval(scale_matrix) + case "ROTATION": + rot_type = data["rot_type"] + rot = data.get(rot_type.lower()) + if round_to_sm64 and rot_type == "EULER": + yield from run_eval(tuple(to_s16(round(x)) for x in rot), 16) + else: + yield from run_eval(rot, 32) + case "DL": + has_dl, dl_ref = self.hasDL, self.dlRef + self.hasDL, self.dlRef = True, (data.get("dl") or None) + if binary: + yield from run_eval(self.get_dl_address(), 32) + else: + yield from run_eval(self.get_dl_name(), 32) + self.hasDL, self.dlRef = has_dl, dl_ref + case _: + raise PluginError(f"Unknown arg type {arg_type}") + + def to_c(self, depth: int = 0, max_length: int = 150) -> str: + data = StringIO() + dl_command = self.data.get("dl_command") + data.write(dl_command if dl_command is not None and self.hasDL else self.data["str_cmd"]) + data.write("(") + groups = [] + for i, arg_data in enumerate(self.args): + group = [] + try: + for value, _, _ in self.to_arg(arg_data): + if value is None: + value = "NULL" + elif isinstance(value, bool): + value = str(value).upper() + elif hasattr(value, "to_c"): + value = value.to_c() + group.append(str(value)) + group_str = ", ".join(group) + if "name" in arg_data and arg_data["name"]: + group_str = f"/*{arg_data['name']}*/ {group_str}" + groups.append(group_str) + except Exception as exc: + raise PluginError(f'Failed to export arg "{arg_data.get("name", f"Arg {i}")}": {exc}') from exc + + if len("".join(groups)) > max_length: + separator = ",\n" + ("\t" * (depth + 1)) + data.write(separator.join(groups)) + else: + data.write(", ".join(groups)) + + data.write(")") + return data.getvalue() + + def to_binary_groups(self, segment_data: Optional[SegmentData] = None): + groups = [] + groups.append(("Command Index (𝗔𝘂𝘁𝗼𝗺𝗮𝘁𝗶𝗰)", self.data["int_cmd"].to_bytes(1, "big"))) + for i, arg_data in enumerate(self.args): + name = arg_data.get("name", f"Arg {i}") + try: + group = bytearray(0) + for value, bit_count, signed in self.to_arg(arg_data, True): + if value is None: + value = 0 + signed = arg_data.get("signed", signed) + if "value_type" in arg_data: + bit_count = BIT_COUNTS[arg_data["value_type"]] + if arg_data["value_type"] in {"FLOAT", "DOUBLE"}: + value = float(value) + else: + value = int(value) + if arg_data.get("seg_addr", False) and segment_data is not None: + value = encodeSegmentedAddr(value, segment_data) + if isinstance(value, bytes): + group += value + elif isinstance(value, float): + group += struct.pack("f" if bit_count == 32 else "d", value) + elif isinstance(value, int): + value = cast_integer(value, bit_count, signed) + group += value.to_bytes(math.ceil(bit_count / 8), "big", signed=signed) + elif hasattr(value, "to_binary"): + group += value.to_binary() + else: + raise PluginError(f"{type(value)} not supported in binary") + groups.append((name, group)) + except Exception as exc: + raise PluginError(f'Failed to export arg "{name}": \n{exc}') from exc + + size = sum(len(data) for _, data in groups) + padding = size % 4 + if padding != 0: + groups.append(("Trailing Padding (𝗔𝘂𝘁𝗼𝗺𝗮𝘁𝗶𝗰)", bytes(4 - padding))) + return groups + + def to_binary(self, segment_data: Optional[SegmentData] = None): + return bytearray(b for _, data in self.to_binary_groups(segment_data) for b in data) + + def size(self, segment_data: Optional[SegmentData] = None): + return sum(len(data) for _, data in self.to_binary_groups(segment_data)) + + def get_ptr_offsets(self): + return [] + + def to_text_dump(self, segment_data: Optional[SegmentData] = None): + data = StringIO() + data.write(f"Size: {self.size(segment_data)} bytes.") + if segment_data is None: + data.write("\nNo segment range provided, won't encode to a respective segment") + for name, bytes in self.to_binary_groups(segment_data): + bytes_str = ", ".join(f"0x{byte:02x}" for byte in bytes) + if name: + data.write(f'\n\t"{name}": {bytes_str}') + else: + data.write(f"\n\t{bytes_str}") + return data.getvalue() diff --git a/fast64_internal/sm64/custom_cmd/operators.py b/fast64_internal/sm64/custom_cmd/operators.py new file mode 100644 index 000000000..254096b21 --- /dev/null +++ b/fast64_internal/sm64/custom_cmd/operators.py @@ -0,0 +1,182 @@ +from typing import TYPE_CHECKING, Iterable + +from bpy.utils import register_class, unregister_class +from bpy.props import StringProperty, IntProperty, EnumProperty +from bpy.types import Context, Scene + +from ...operators import OperatorBase, CollectionOperatorBase, SearchEnumOperatorBase +from ...utility import PluginError + +from .utility import custom_cmd_preset_update, duplicate_name, get_custom_cmd_preset_enum, get_custom_prop + +if TYPE_CHECKING: + from .properties import SM64_CustomCmdProperties, SM64_CustomArgProperties, SM64_CustomEnumProperties + + +def get_conf_type(context: Context): + custom = get_custom_prop(context).custom + return "PRESET_EDIT" if custom is None or custom.preset != "NONE" else "NO_PRESET" + + +class SM64_CustomCmdOps(CollectionOperatorBase): + bl_idname = "scene.sm64_custom_cmd_ops" + bl_label = "" + bl_options = {"UNDO"} + object_name = "custom command preset" + + index: IntProperty(default=-1) + op_name: StringProperty() + example_name: StringProperty(default="") + + @classmethod + def description(cls, context: Context, properties: dict) -> str: + op_name: str = properties.get("op_name", "") + if op_name == "COPY_EXAMPLE": + return "Copy example defines" + return super().description(context, properties) + + @classmethod + def collection(cls, context: Context, op_values: dict) -> Iterable["SM64_CustomCmdProperties"]: + return context.scene.fast64.sm64.custom_cmds + + def execute_operator(self, context): + presets = context.scene.fast64.sm64.custom_cmds + custom, owner = get_custom_prop(context) + conf_type = get_conf_type(context) + match self.op_name: + case "ADD": + presets.add() + new_preset: "SM64_CustomCmdProperties" = presets[-1] + old_preset: "SM64_CustomCmdProperties" | None = None + if self.index == -1: + if custom is not None: + old_preset = custom + else: + old_preset = presets[self.index] + + if old_preset is not None: + new_preset.from_dict(old_preset.to_dict(conf_type, owner, include_defaults=True), set_defaults=True) + old_name = old_preset.name + else: + old_name = None + existing_names = {preset.name for preset in presets if preset != new_preset} + new_preset.name = duplicate_name(new_preset.name, existing_names, old_name) + new_preset.tab = True + if self.index != -1: + presets.move(len(presets) - 1, self.index + 1) + if custom is not None: + custom.preset = new_preset.name + for area in context.screen.areas: # HACK: redraw everything + area.tag_redraw() + case "REMOVE": + presets.remove(self.index) + case "COPY_EXAMPLE": + preset = presets[self.index] if custom is None else custom + context.window_manager.clipboard = preset.get_examples(owner, conf_type)[self.example_name][1] + case _: + raise NotImplementedError(f'Unimplemented internal custom command preset op "{self.op_name}"') + custom_cmd_preset_update(self, context) + + +def get_args(context: Context, command_index: int) -> Iterable["SM64_CustomArgProperties"]: + owner = get_custom_prop(context).owner + if isinstance(owner, Scene): + return context.scene.fast64.sm64.custom_cmds[command_index].args + elif owner is not None: + return owner.fast64.sm64.custom.args + else: + raise PluginError("Invalid context") + + +class SM64_CustomArgsOps(CollectionOperatorBase): + bl_idname = "scene.sm64_custom_args_ops" + bl_label = "" + bl_options = {"UNDO"} + object_name = "arg" + + command_index: IntProperty(default=0) # for scene command presets + + @classmethod + def collection(cls, context: Context, op_values: dict): + return get_args(context, op_values.get("command_index", 0)) + + @classmethod + def description(cls, context: Context, properties: dict) -> str: + op_name: str = properties.get("op_name", "") + if op_name == "COPY_EXAMPLE": + return "Copy example enum list" + return super().description(context, properties) + + def add(self, context: Context, collection: Iterable["SM64_CustomArgProperties"]): + old, new = super().add(context, collection) + old_name = None + if old is not None: + old_name = old.name + new.from_dict( + old.to_dict(get_conf_type(context), owner=get_custom_prop(context).owner, include_defaults=True), + set_defaults=True, + ) + existing_names = {arg.name for arg in collection if arg != new} + new.name = duplicate_name(new.name, existing_names, old_name) + + def copy_example(self, context: Context, collection: Iterable["SM64_CustomArgProperties"]): + """Copy example of enum list to clipboard""" + arg: "SM64_CustomArgProperties" = collection[self.index] + context.window_manager.clipboard = arg.get_enum_list_example() + + def execute_operator(self, context: Context): + super().execute_operator(context) + custom_cmd_preset_update(self, context) + + +class SM64_CustomEnumOps(CollectionOperatorBase): + bl_idname = "scene.sm64_custom_enum_ops" + bl_label = "" + bl_options = {"UNDO"} + object_name = "enum option" + + command_index: IntProperty(default=0) # for scene command presets + arg_index: IntProperty(default=0) + + @classmethod + def collection(cls, context: Context, op_values: dict) -> Iterable["SM64_CustomEnumProperties"]: + args = get_args(context, op_values.get("command_index", 0)) + return args[op_values.get("arg_index", 0)].enum_options + + def add(self, context: Context, collection: Iterable["SM64_CustomArgProperties"]): + old, new = CollectionOperatorBase.add(self, context, collection) + old_name = None + if old is not None: + old_name = old.name + new.from_dict(old.to_dict()) + existing_names = {enum.name for enum in collection if enum != new} + new.name = duplicate_name(new.name, existing_names, old_name) + + +class SM64_SearchCustomCmds(SearchEnumOperatorBase): + bl_idname = "scene.sm64_search_custom_cmds" + bl_label = "Search Custom Commands" + bl_options = {"REGISTER", "UNDO"} + bl_property = "preset" + preset: EnumProperty(items=get_custom_cmd_preset_enum) + + def update_enum(self, context): + context.object.fast64.sm64.custom.preset = self.preset + + +classes = ( + SM64_CustomEnumOps, + SM64_CustomArgsOps, + SM64_CustomCmdOps, + SM64_SearchCustomCmds, +) + + +def operators_register(): + for cls in classes: + register_class(cls) + + +def operators_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/custom_cmd/properties.py b/fast64_internal/sm64/custom_cmd/properties.py new file mode 100644 index 000000000..bc8916278 --- /dev/null +++ b/fast64_internal/sm64/custom_cmd/properties.py @@ -0,0 +1,1133 @@ +import math +from typing import TYPE_CHECKING, Optional +from io import StringIO +import mathutils + +from bpy.utils import register_class, unregister_class +from bpy.props import ( + StringProperty, + IntProperty, + BoolProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntVectorProperty, + CollectionProperty, + PointerProperty, +) +from bpy.types import Object, Bone, UILayout, Context, PropertyGroup + +from ...utility import ( + Matrix4x4Property, + PluginError, + draw_and_check_tab, + get_first_set_prop, + multilineLabel, + prop_split, + toAlnum, + upgrade_old_prop, +) +from ...f3d.f3d_material import sm64EnumDrawLayers + +from ..sm64_constants import MIN_S32, MAX_S32 + +from .exporting import CustomCmd +from .operators import SM64_CustomArgsOps, SM64_CustomCmdOps, SM64_CustomEnumOps, SM64_SearchCustomCmds +from .utility import ( + AvailableOwners, + CustomCmdConf, + better_round, + custom_cmd_preset_update, + duplicate_name, + get_custom_cmd_preset, + get_custom_cmd_preset_enum, + get_custom_prop, + get_transforms, +) + +if TYPE_CHECKING: + from ..settings.properties import SM64_Properties + + +def update_internal_number(self: "SM64_CustomNumberProperties", context: Context): + use_limits = True + custom, owner = get_custom_prop(context) + if owner is None: + return + if custom is not None: + use_limits = custom.preset != "NONE" + if not math.isclose(self.floating, self.get_new_number(use_limits), rel_tol=1e-7): + self.floating = self.get_new_number(use_limits) + if self.integer != better_round(self.get_new_number(use_limits)): + self.integer = better_round(self.get_new_number(use_limits)) + self.set_step_min_max(*self.step_min_max) + + +def update_internal_number_and_check_preset(self: "SM64_CustomArgProperties", context: Context): + update_internal_number(self, context) + custom_cmd_preset_update(self, context) + + +class SM64_CustomNumberProperties(PropertyGroup): + is_integer: BoolProperty(name="Is Integer", default=False, update=update_internal_number_and_check_preset) + floating: FloatProperty(name="Float", default=0.0, precision=5, update=update_internal_number) + integer: IntProperty(name="Integer", default=0, update=update_internal_number) + floating_step: FloatProperty(name="Step", default=0.0, update=update_internal_number_and_check_preset) + floating_min: FloatProperty( + name="Min", default=-math.inf, min=-math.inf, max=math.inf, update=update_internal_number_and_check_preset + ) + floating_max: FloatProperty( + name="Max", default=math.inf, min=-math.inf, max=math.inf, update=update_internal_number_and_check_preset + ) + integer_step: IntProperty(name="Step", default=1, update=update_internal_number_and_check_preset) + integer_min: IntProperty( + name="Min", + default=MIN_S32, + min=MIN_S32, + max=MAX_S32, + update=update_internal_number_and_check_preset, + ) + integer_max: IntProperty( + name="Max", + default=MAX_S32, + min=MIN_S32, + max=MAX_S32, + update=update_internal_number_and_check_preset, + ) + + @property + def step_min_max(self): + if self.is_integer: + return self.integer_step, self.integer_min, self.integer_max + else: + return self.floating_step, self.floating_min, self.floating_max + + def set_step_min_max(self, step: float, min_value: float, max_value: float): + for name, value in zip(("step", "min", "max"), (step, min_value, max_value)): + if getattr(self, f"integer_{name}") != better_round(value): + setattr(self, f"integer_{name}", better_round(value)) + if not math.isclose(getattr(self, f"floating_{name}"), value, rel_tol=1e-7): + setattr(self, f"floating_{name}", value) + + def get_new_number(self, skip_limits=False): + new_value = self.integer if self.is_integer else self.floating + if skip_limits: + step, min_value, max_value = self.step_min_max + if step == 0: + new_value = max(min_value, min(new_value, max_value)) + else: + if min_value > -math.inf: + new_value -= min_value # start value from min + step_count = new_value // step # number of steps for the closest value + new_value = step_count * step + if min_value > -math.inf: + new_value += min_value + new_value = max(min_value, min(new_value, max_value)) + if self.is_integer: + return int(new_value) + return new_value + + def to_dict(self, conf_type: CustomCmdConf = "PRESET_EDIT"): + data = {"is_integer": self.is_integer} + if conf_type == "PRESET_EDIT": + if self.is_integer: + data.update({"step": self.integer_step, "min": self.integer_min, "max": self.integer_max}) + else: + data.update({"step": self.floating_step, "min": self.floating_min, "max": self.floating_max}) + return data, {"value": self.get_new_number()} + + def from_dict(self, data: dict, defaults: dict, set_defaults=True): + self.is_integer = data.get("is_integer", False) + if set_defaults: + value = defaults.get("value", 0) + self.floating = value + self.integer = better_round(value) + self.set_step_min_max( + data.get("step", 1.0 if self.is_integer else 0), data.get("min", -math.inf), data.get("max", math.inf) + ) + + def draw_props(self, name_split: UILayout, layout: UILayout, conf_type: CustomCmdConf): + col = layout.column() + if conf_type != "PRESET": + col.prop(self, "is_integer") + name_split.prop(self, "integer" if self.is_integer else "floating", text="") + usual_steps = {0, 1} if self.is_integer else {0} + if conf_type != "PRESET_EDIT" and self.step_min_max[0] not in usual_steps: + col.label(text=f"Increments of {self.step_min_max[0]}") + col.separator(factor=0.5) + if conf_type == "PRESET_EDIT": + typ = "integer" if self.is_integer else "floating" + prop_split(col, self, f"{typ}_min", "Min") + prop_split(col, self, f"{typ}_max", "Max") + prop_split(col, self, f"{typ}_step", "Step") + + +class SM64_CustomEnumProperties(PropertyGroup): + name: StringProperty(name="Name", default="Enum Name", update=custom_cmd_preset_update) + description: StringProperty(name="Description", default="Description", update=custom_cmd_preset_update) + str_value: StringProperty(name="Value", default="ENUM_NAME", update=custom_cmd_preset_update) + int_value: IntProperty(name="Value", default=0, update=custom_cmd_preset_update) + + def enum_tuple(self, i: int): + return (str(i), self.name, self.description.replace("\\n", "\n")) + + def to_dict(self): + return { + "name": self.name, + "description": self.description.replace("\\n", "\n"), + "str_value": self.str_value, + "int_value": self.int_value, + } + + def from_dict(self, data: dict): + self.name, self.description = data.get("name", "Name"), data.get("description", "Description") + self.str_value, self.int_value = data.get("str_value", "ENUM_NAME"), data.get("int_value", 0) + + def draw_props(self, layout: UILayout, op_row: UILayout, is_binary=False): + op_row.prop(self, "name", text="") + layout.prop(self, "description") + prop_split(layout, self, "int_value" if is_binary else "str_value", "Value") + + +def can_have_mesh(owner: Optional[AvailableOwners]): + return (isinstance(owner, Object) and owner.type == "MESH") or isinstance(owner, Bone) or owner is None + + +class SM64_CustomArgProperties(PropertyGroup): + name: StringProperty(name="Name", default="Argument Name", update=custom_cmd_preset_update) + show_as_preset: BoolProperty( + default=True, description="Show argument when used as a preset", update=custom_cmd_preset_update + ) + arg_type: EnumProperty( + name="Type", + items=[ + ("PARAMETER", "Parameter", "Parameter"), + ("BOOLEAN", "Boolean", "Boolean"), + ("NUMBER", "Number", "Number"), + ("COLOR", "Color", "Color"), + ("ENUM", "Enum", "Enum"), + ("", "Transforms", ""), + ("TRANSLATION", "Translation", "Translation"), + ("ROTATION", "Rotation", "Rotation"), + ("SCALE", "Scale", "Scale"), + ("MATRIX", "Matrix", "3x3 Matrix"), + ("", "", ""), + ("LAYER", "Layer", "Layer"), + ("DL", "Displaylist", "Displaylist"), + ], + update=custom_cmd_preset_update, + ) + inherit: BoolProperty( + name="Inherit", description="Inherit arg from owner", default=True, update=custom_cmd_preset_update + ) + apply_scale: BoolProperty(name="Blender to SM64 Scale", default=True, update=custom_cmd_preset_update) + round_to_sm64: BoolProperty(name="Round to Conventional Units", update=custom_cmd_preset_update) + seg_addr: BoolProperty(name="Encode To Segmented Address", default=True, update=custom_cmd_preset_update) + value_type: EnumProperty( + items=[ + ("AUTO", "Auto Type", "Auto"), + ("", "", ""), + ("CHAR", "Char", "Char"), + ("SHORT", "Short", "Short"), + ("INT", "Int", "Int"), + ("LONG", "Long", "Long"), + ("", "", ""), + ("FLOAT", "Float", "Float"), + ("DOUBLE", "Double", "Double"), + ], + default="AUTO", + ) + signed: BoolProperty(name="Signed", default=True) + + color: FloatVectorProperty( + name="Color", + size=4, + min=0.0, + max=1.0, + subtype="COLOR", + default=(1.0, 1.0, 1.0, 1.0), + update=custom_cmd_preset_update, + ) + color_bits: IntVectorProperty( + name="Color Bits", + description="Bits per channel. RGBA", + size=4, + default=(5, 5, 5, 1), + min=0, + max=8, + update=custom_cmd_preset_update, + ) + parameter: StringProperty(name="Parameter", default="0") + boolean: BoolProperty(name="Boolean", default=True) + number: PointerProperty(type=SM64_CustomNumberProperties) + layer: EnumProperty(items=sm64EnumDrawLayers, default="1") + relative: BoolProperty(name="Use Relative Transformation", default=True, update=custom_cmd_preset_update) + rot_type: EnumProperty( + name="Rotation", + items=[ + ("EULER", "Euler (XYZ deg)", "Euler XYZ order, degrees"), + ("QUATERNION", "Quaternion", "Quaternion"), + ("AXIS_ANGLE", "Axis Angle", "Axis angle"), + ], + update=custom_cmd_preset_update, + ) + translation_scale: FloatVectorProperty(name="Translation", size=3, default=(0.0, 0.0, 0.0), subtype="XYZ") + euler: FloatVectorProperty(name="Rotation", size=3, default=(0.0, 0.0, 0.0), subtype="EULER") + order: EnumProperty( + items=[ + ("XYZ", "XYZ", "XYZ"), + ("XZY", "XZY", "XZY"), + ("YXZ", "YXZ", "YXZ"), + ("YZX", "YZX", "YZX"), + ("ZXY", "ZXY", "ZXY"), + ("ZYX", "ZYX", "ZYX"), + ], + update=custom_cmd_preset_update, + ) + quaternion: FloatVectorProperty(name="Quaternion", size=4, default=(1.0, 0.0, 0.0, 0.0), subtype="QUATERNION") + axis_angle: FloatVectorProperty(name="Axis Angle", size=4, default=((1.0), 0.0, 0.0, 0.0), subtype="AXISANGLE") + matrix: PointerProperty(type=Matrix4x4Property) + dl: StringProperty(name="Displaylist", default="breakable_box_seg8_dl_cork_box") + + enum_tab: BoolProperty(name="Enum Options", default=False) + enum_options: CollectionProperty(type=SM64_CustomEnumProperties, name="Options") + enum_option: EnumProperty( + name="Enum Option", + items=lambda self, _context: [e.enum_tuple(i) for i, e in enumerate(self.enum_options)] + or [("0", "Invalid", "Invalid")], + ) + + eval_expression: StringProperty( + name="Eval Expression", + default="", + description="Apply a limited math expression to the values of this argument group, as seen in scale nodes.\nLeave empty to skip this step", + update=custom_cmd_preset_update, + ) + + @property + def is_transform(self): + return self.arg_type in {"MATRIX", "TRANSLATION", "ROTATION", "SCALE"} + + @property + def modifable_value_type(self): + return self.arg_type not in {"LAYER", "DL"} + + @property + def can_be_signed(self): + return self.modifable_value_type and self.value_type not in {"FLOAT", "DOUBLE", "AUTO"} + + @property + def can_round_to_sm64(self): + return self.arg_type in {"TRANSLATION", "COLOR", "SCALE"} or ( + self.arg_type == "ROTATION" and self.rot_type == "EULER" + ) + + @property + def has_order(self): + return self.arg_type in {"TRANSLATION", "SCALE"} or (self.arg_type == "ROTATION" and self.rot_type == "EULER") + + def show_eval_expression(self, custom_cmd: "SM64_CustomCmdProperties", is_binary: bool): + if is_binary: + return True + if custom_cmd.skips_eval(is_binary): + return False + return self.arg_type not in {"PARAMETER", "BOOLEAN", "ENUM", "LAYER", "DL"} + + def can_inherit(self, owner: Optional[AvailableOwners]): + """Scene still includes all, the inherented property will be defaults, like identity matrix""" + valid_types = {"MATRIX", "TRANSLATION", "ROTATION"} + is_mesh = isinstance(owner, Object) and owner.type == "MESH" + if not isinstance(owner, Bone): + valid_types.add("SCALE") + if is_mesh or owner is None: + valid_types.add("LAYER") + if can_have_mesh(owner): + valid_types.add("DL") + return self.arg_type in valid_types + + def inherits(self, owner: Optional[AvailableOwners]): + return self.can_inherit(owner) and self.inherit + + def inherits_without_default(self, owner: Optional[AvailableOwners]): + """Inherits without a default, layers for example inherit but have a default in case of no geometry""" + return self.inherits(owner) and self.arg_type not in {"LAYER"} + + def modifable_inherit(self, owner: Optional[AvailableOwners]): + """Can be modified in presets, inherit becomes a default value therefor ignored by the hashing""" + return self.can_inherit(owner) and self.arg_type in {"DL"} + + def show_inherit_toggle(self, owner: Optional[AvailableOwners], conf_type: CustomCmdConf): + return (self.can_inherit(owner) and conf_type != "PRESET") or self.modifable_inherit(owner) + + def show_segmented_toggle(self, owner: Optional[AvailableOwners], conf_type: CustomCmdConf): + return ( + conf_type != "PRESET" + and ((not self.inherits(owner) or conf_type == "PRESET_EDIT") and self.arg_type in {"DL"}) + or (self.arg_type in {"PARAMETER"} and self.value_type in {"INT", "LONG"} and not self.signed) + ) + + def shows_name(self, owner: Optional[AvailableOwners]): + return not self.inherits_without_default(owner) or self.show_inherit_toggle(owner, "PRESET") + + def will_draw(self, owner: Optional[AvailableOwners], conf_type: CustomCmdConf): + return (self.shows_name(owner) and self.show_as_preset) or conf_type != "PRESET" + + def get_transform( + self, + owner: Optional[AvailableOwners], + world_matrix: Optional[mathutils.Matrix], + local_matrix: Optional[mathutils.Matrix], + blender_scale=1.0, + ): + inherit = self.inherits(owner) + if inherit: + world_matrix, local_matrix = world_matrix or mathutils.Matrix.Identity( + 4 + ), local_matrix or mathutils.Matrix.Identity(4) + matrix = local_matrix if self.relative else world_matrix + if not self.apply_scale: + blender_scale = 1.0 + match self.arg_type: + case "MATRIX": + matrix = matrix if inherit else self.matrix.to_matrix() + if blender_scale != 1.0: + trans, rot, scale = matrix.decompose() + matrix = ( + mathutils.Matrix.Translation(trans * blender_scale).to_4x4() + @ rot.to_matrix().to_4x4() + @ mathutils.Matrix.Diagonal(scale).to_4x4() + ) + return tuple(tuple(y for y in x) for x in matrix) + case "TRANSLATION": + return tuple( + getattr( + (matrix.to_translation() if inherit else mathutils.Vector(self.translation_scale)) + * blender_scale, + self.order.lower(), + ) + ) + case "ROTATION": + match self.rot_type: + case "EULER": + euler = matrix.to_euler(self.order) if inherit else mathutils.Euler(self.euler, self.order) + return tuple(math.degrees(x) for x in euler) + case "QUATERNION": + return tuple((matrix.to_quaternion() if inherit else self.quaternion)) + case "AXIS_ANGLE": + quat = ( + matrix.to_quaternion() + if inherit + else mathutils.Quaternion(self.axis_angle[:3], self.axis_angle[3]) + ) + axis, angle = quat.to_axis_angle() + return tuple((tuple(axis), math.degrees(angle))) + case "SCALE": + scale = getattr( + matrix.to_scale() if inherit else mathutils.Vector(self.translation_scale), self.order.lower() + ) + if self.round_to_sm64: + return sum(x for x in scale) / 3 + return tuple(scale) + + def to_dict( + self, + conf_type: CustomCmdConf, + owner: Optional[AvailableOwners] = None, + world_matrix: Optional[mathutils.Matrix] = None, + local_matrix: Optional[mathutils.Matrix] = None, + blender_scale=1.0, + include_defaults=True, + is_export=False, + ): + data = {} + defaults = {} + if conf_type != "PRESET" or is_export: + if conf_type != "NO_PRESET": + data["name"] = self.name + data["show_as_preset"] = self.show_as_preset + data["arg_type"] = self.arg_type + if self.modifable_inherit(owner): + defaults["inherit"] = self.inherit + elif self.can_inherit(owner): + data["inherit"] = self.inherit + if self.eval_expression: + data["eval_expression"] = self.eval_expression + if self.modifable_value_type and self.value_type != "AUTO": + data["value_type"] = self.value_type + if self.can_be_signed: + data["signed"] = self.signed + if self.show_segmented_toggle(owner, conf_type): + data["seg_addr"] = self.seg_addr + if self.can_round_to_sm64: + data["round_to_sm64"] = self.round_to_sm64 + if self.has_order: + data["order"] = self.order + match self.arg_type: + case "NUMBER": + number_data, number_defaults = self.number.to_dict(conf_type) + defaults.update(number_defaults) + data.update(number_data) + case "ENUM": + defaults["enum"] = int(self.enum_option) + data["enum_options"] = tuple(option.to_dict() for option in self.enum_options) + case "COLOR": + defaults["color"] = tuple(self.color) + if self.round_to_sm64: + data["color_bits"] = tuple(self.color_bits) + case _: + name = self.arg_type.lower() + if self.is_transform: + data["relative"] = self.relative + data["apply_scale"] = self.apply_scale and self.arg_type in {"MATRIX", "TRANSLATION"} + if self.arg_type == "ROTATION": + data["rot_type"] = self.rot_type + if self.arg_type == "ROTATION": + name = self.rot_type.lower() + defaults[name] = self.get_transform(owner, world_matrix, local_matrix, blender_scale=blender_scale) + elif (not self.inherits_without_default(owner) or conf_type == "PRESET_EDIT") and hasattr(self, name): + defaults[name] = getattr(self, name) + if defaults and include_defaults: + if conf_type == "PRESET_EDIT" and not is_export: + data["defaults"] = defaults + else: + data.update(defaults) + return data + + def from_dict(self, data: dict, index=0, set_defaults=False): + defaults = data.get("defaults") + if not defaults: + defaults = data + self.name = data.get("name", f"Arg {index}") + self.show_as_preset = data.get("show_as_preset", True) + self.arg_type = data.get("arg_type", "PARAMETER") + self.inherit = data.get("inherit", True) + self.eval_expression = data.get("eval_expression", "") + self.value_type = data.get("value_type", "AUTO") + self.signed = data.get("signed", True) + self.seg_addr = data.get("seg_addr", True) + self.relative = data.get("relative", True) + self.apply_scale = data.get("apply_scale", True) + self.round_to_sm64 = data.get("round_to_sm64", False) + self.rot_type = data.get("rot_type", "EULER") + self.order = data.get("order", "XYZ") + self.enum_options.clear() + for option in data.get("enum_options", []): + self.enum_options.add() + self.enum_options[-1].from_dict(option) + self.color_bits = data.get("color_bits", (8, 8, 8, 8)) + self.number.from_dict(data, defaults, set_defaults) + if not set_defaults: + return + self.enum_option = str(defaults.get("enum", 0)) + if "scale" in defaults and self.round_to_sm64: + self.translation_scale = [defaults.get("scale", 0)] * 3 + else: + self.translation_scale = defaults.get("translation", None) or defaults.get("scale", None) or [0, 0, 0] + self.euler = [math.radians(x) for x in defaults.get("euler", [0, 0, 0])] + self.quaternion = defaults.get("quaternion", [1, 0, 0, 0]) + axis_angle = defaults.get("axis_angle", [[0, 0, 0], 0]) + self.axis_angle = (*axis_angle[0], math.radians(axis_angle[1])) + if "matrix" in defaults: + self.matrix.from_matrix(defaults.get("matrix")) + else: + self.matrix.from_matrix(mathutils.Matrix.Identity(4)) + for prop in ["color", "parameter", "layer", "boolean", "dl"]: + setattr(self, prop, defaults.get(prop, getattr(self, prop))) + + def example_macro_args( + self, cmd_prop: "SM64_CustomCmdProperties", previous_arg_names: set[str], conf_type: CustomCmdConf = "NO_PRESET" + ): + def add_name(args: list[str]): + name = self.name + if not name or (cmd_prop.preset == "NONE" and conf_type == "NO_PRESET"): + name = self.arg_type.lower() + name = duplicate_name(name, previous_arg_names) + previous_arg_names.add(name) + return ", ".join(toAlnum(name + arg).lower() for arg in args) + + if self.has_order: + if self.arg_type == "SCALE" and self.round_to_sm64: + return add_name(("",)) + return add_name(tuple(f"_{x}" for x in self.order.lower())) + elif self.arg_type == "ROTATION": + return add_name( + {"QUATERNION": ("_w", "_x", "_y", "_z"), "AXIS_ANGLE": ("_x", "_y", "_z", "_a")}[self.rot_type] + ) + + match self.arg_type: + case "MATRIX": + return add_name(tuple(f"_{x}_{y}" for x in range(4) for y in range(4))) + case "COLOR": + if self.round_to_sm64: + return add_name(("",)) + return add_name(("_r", "_g", "_b", "_a")) + case "PARAMETER" | "LAYER" | "BOOLEAN" | "NUMBER" | "DL" | "ENUM": + return add_name(("",)) + case _: + raise PluginError(f"Unknown arg type {self.arg_type}") + + def draw_transforms( + self, + name_split: UILayout, + inherit_info: UILayout, + layout: UILayout, + owner: Optional[AvailableOwners], + conf_type: CustomCmdConf = "NO_PRESET", + ): + col = layout.column() + inherit = self.inherits(owner) + if conf_type != "PRESET": + if inherit: + col.prop(self, "relative") + if self.arg_type == "ROTATION": + prop_split(col, self, "rot_type", "Rotation Type") + if self.arg_type in {"TRANSLATION", "MATRIX"}: + col.prop(self, "apply_scale") + if self.has_order: + prop_split(col, self, "order", "Order") + force_scale = conf_type == "PRESET_EDIT" and self.arg_type == "SCALE" + if inherit and force_scale: + inherit_info.label(text="Not supported in bones.", icon="INFO") + if not inherit or force_scale: + if self.arg_type in {"TRANSLATION", "SCALE"}: + name_split.prop(self, "translation_scale", text="") + elif self.arg_type == "ROTATION": + name_split.prop(self, self.rot_type.lower(), text="") + elif self.arg_type == "MATRIX": + self.matrix.draw_props(col) + + def get_enum_list_example(self): + macro_define = StringIO() + fixed_name = toAlnum(self.name) + if self.name: + macro_define.write(f"// {self.name}'s enums\n") + macro_define.write(f"enum {fixed_name.replace('_', '')} {{\n") + else: + macro_define.write("enum {{\n") + for option in self.enum_options: + macro_define.write(f"\t{option.str_value} = {option.int_value},\n") + if self.name: + macro_define.write(f"\t{fixed_name.upper()}_COUNT\n") + else: + macro_define.write("\tCOUNT\n") + macro_define.write("};") + return macro_define.getvalue() + + def draw_enum( + self, + name_split: UILayout, + layout: UILayout, + command_index: int, + arg_index: int, + conf_type: CustomCmdConf = "NO_PRESET", + is_binary=False, + ): + name_split.prop(self, "enum_option", text="") + if conf_type == "PRESET": + return + col = layout.column() + options_box = col.box().column() + if not draw_and_check_tab(options_box, self, "enum_tab"): + return + SM64_CustomEnumOps.draw_row(options_box.row(), -1, command_index=command_index, arg_index=arg_index) + option: SM64_CustomEnumProperties + for i, option in enumerate(self.enum_options): + op_row = options_box.row() + option.draw_props(options_box, op_row, is_binary) + SM64_CustomEnumOps.draw_row(op_row, i, command_index=command_index, arg_index=arg_index) + + col.separator() + box = col.box().column() + multilineLabel(box, self.get_enum_list_example().replace("\t", " " * 5)) + SM64_CustomArgsOps.draw_props( + box, + "COPYDOWN", + "Copy Enum List Example", + op_name="COPY_EXAMPLE", + command_index=command_index, + ) + + def draw_props( + self, + arg_row: UILayout, + layout: UILayout, + owner: Optional[AvailableOwners], + custom_cmd: "SM64_CustomCmdProperties", + command_index: int, + arg_index: int, + conf_type: CustomCmdConf = "NO_PRESET", + is_binary=False, + ): + inherit = self.inherits(owner) + col = layout.column() + + if conf_type != "NO_PRESET": + name_split = col.split(factor=0.5) + if conf_type == "PRESET" and self.shows_name(owner) and self.name != "": + name_split.label(text=self.name) + elif conf_type == "PRESET_EDIT": + name_row = name_split.row() + name_row.prop(self, "show_as_preset", text="") + name_row.prop(self, "name", text="") + else: + name_split = col + + if conf_type != "PRESET": + arg_row.prop(self, "arg_type", text="") + if self.can_round_to_sm64: + col.prop(self, "round_to_sm64") + + inherit_info = col + if self.show_inherit_toggle(owner, conf_type): + if conf_type == "PRESET": + inherit_info = name_split + name_split = col + inherit_info = inherit_info.row() + inherit_info.alignment = "LEFT" + inherit_info.prop(self, "inherit") + + match self.arg_type: + case "NUMBER": + self.number.draw_props(name_split, col, conf_type) + case "ENUM": + self.draw_enum(name_split, col, command_index, arg_index, conf_type, is_binary) + case "LAYER" | "DL": + if inherit and conf_type == "PRESET_EDIT": + inherit_info.label(text="Not supported in object empties.", icon="INFO") + if not inherit or conf_type == "PRESET_EDIT": + name_split.prop(self, self.arg_type.lower(), text="") + case "COLOR": + name_split.prop(self, "color", text="") + quantize_split = col.row() + if self.round_to_sm64: + quantize_split.prop(self, "color_bits", text="") + case _: + if self.is_transform: + self.draw_transforms(name_split, inherit_info, col, owner, conf_type) + elif hasattr(self, self.arg_type.lower()): + name_split.prop(self, self.arg_type.lower(), text="") + + if conf_type != "PRESET": + if self.show_eval_expression(custom_cmd, is_binary): + prop_split(col, self, "eval_expression", "Expression") + if is_binary: + if self.modifable_value_type: + type_split = col.row() + type_split.prop(self, "value_type", text="") + if self.can_be_signed: + type_split.prop(self, "signed") + if self.show_segmented_toggle(owner, conf_type): + col.prop(self, "seg_addr") + + +def custom_cmd_change_preset(self: "SM64_CustomCmdProperties", context: Context): + if self.preset == "NONE": + return + preset_cmd = get_custom_cmd_preset(self, context) + if preset_cmd is None: + self.preset = "NONE" + return + self.saved_hash = "" + self.from_dict(preset_cmd.to_dict("PRESET_EDIT", None, include_defaults=True), set_defaults=True) + self.saved_hash = self.preset_hash + custom_cmd_preset_update(self, context) + + +class SM64_CustomCmdProperties(PropertyGroup): + version: IntProperty(name="SM64_CustomCmdProperties Version", default=0) + + tab: BoolProperty(default=False) + preset: EnumProperty(items=get_custom_cmd_preset_enum, update=custom_cmd_change_preset) + name: StringProperty(name="Name", default="Custom Command Name", update=custom_cmd_preset_update) + cmd_type: EnumProperty( + name="Type", + items=[ + ("Level", "Level", "Level script Command"), + ("Geo", "Geo", "Geolayout Command"), + ("Collision", "Collision", "Collision Command"), + ], + update=custom_cmd_preset_update, + ) + str_cmd: StringProperty(name="Command", default="CUSTOM_CMD", update=custom_cmd_preset_update) + int_cmd: IntProperty(name="Command", default=0, update=custom_cmd_preset_update) + skip_eval: BoolProperty( + name="Skip Eval", + description="Skip evaluating values outside binary", + default=True, + update=custom_cmd_preset_update, + ) + + # Geo + children_requirements: EnumProperty( + name="Children Requirements", + items=[ + ("ANY", "None", "No requirements"), + ("", "", ""), + ("MUST", "Must Have Children", "Must have at least one child node"), + ("NONE", "No Children", "Must have no children nodeS"), + ], + update=custom_cmd_preset_update, + ) + group_children: BoolProperty( + name="Group Children", + description="Use GEO_OPEN/CLOSE_NODE to group the node's children", + default=True, + update=custom_cmd_preset_update, + ) + dl_option: EnumProperty( + name="DL Option", + items=[ + ("NONE", "None", "No geometry will be inherited, deform will be off in bones"), + ("", "", ""), + ( + "OPTIONAL", + "Optional", + "Can inherit geometry, or will use a NULL value, also allows the use of dl ext commands like GEO_TRANSLATE/GEO_TRANSLATE_WITH_DL", + ), + ("REQUIRED", "Required", "Must inherit geometry, otherwise an error will occur"), + ], + default="OPTIONAL", + update=custom_cmd_preset_update, + ) + use_dl_cmd: BoolProperty( + name="Displaylist Command", + description="Add a displaylist arg at the end of the command if there is geometry. In c, use this macro, in binary OR the first layer with 0x80", + update=custom_cmd_preset_update, + ) + dl_command: StringProperty( + name="Displaylist Command", default="GEO_CUSTOM_CMD_WITH_DL", update=custom_cmd_preset_update + ) + is_animated: BoolProperty(name="Is Animated", update=custom_cmd_preset_update) + + # Level + section: EnumProperty( + name="Section", + items=[ + ( + "HIEARCHY", + "Hierarchy", + "If parented to level, add it before the areas, otherwise in the respective area", + ), + ("AREA", "Area", "Add it to an area, errors if parented to the level"), + ("LEVEL", "Level", "Add it to the level, errors if parented to an area"), + ("FORCE_LEVEL", "Force to Level", "Add it to the level, even if parented to an area"), + ], + update=custom_cmd_preset_update, + ) + + args_tab: BoolProperty(default=True) + args: CollectionProperty(type=SM64_CustomArgProperties) + examples_tab: BoolProperty(default=False) + + saved_hash: StringProperty() + locked: BoolProperty() + + @property + def preset_hash(self): + return str(hash(str(self.to_dict("PRESET_EDIT", include_defaults=False).items()))) + + def upgrade_object(self, obj: Object): + if self.version != 0: + return + found_cmd, arg = upgrade_old_prop(self, "str_cmd", obj, "customGeoCommand"), get_first_set_prop( + obj, "customGeoCommandArgs" + ) + if found_cmd: + self.cmd_type = "Geo" + if arg is not None: + self.args.add() + self.args[-1].arg_type = "PARAMETER" + self.args[-1].parameter = arg + + def upgrade_bone(self, bone: Bone): + if self.version != 0: + return + upgrade_old_prop(self, "str_cmd", self, "custom_geo_cmd_macro") + args = get_first_set_prop(self, "custom_geo_cmd_args") + if args is not None: + self.args.clear() + self.args.add() + self.args[-1].arg_type = "PARAMETER" + self.args[0].parameter = args + old_cmd = bone.get("geo_cmd") + if old_cmd is not None: + if old_cmd in {15, 16}: # custom animated / custom non-animated + bone.geo_cmd = "Custom" + if old_cmd == 15: + self.is_animated = True + self.version = 1 + + def get_cmd_type(self, owner: Optional[AvailableOwners] = None): + if isinstance(owner, Bone): + return "Geo" + return self.cmd_type + + def skips_eval(self, is_binary: bool): + if is_binary: + return False + return self.skip_eval + + def can_animate(self, owner: Optional[AvailableOwners] = None): + return self.get_cmd_type(owner) == "Geo" and (isinstance(owner, Bone) or owner is None) + + def can_have_mesh(self, owner: Optional[AvailableOwners] = None): + return self.get_cmd_type(owner) == "Geo" and can_have_mesh(owner) + + def adds_dl_ext(self, owner: Optional[AvailableOwners] = None): + return self.can_have_mesh(owner) and self.dl_option == "OPTIONAL" and self.use_dl_cmd + + def to_dict( + self, + conf_type: CustomCmdConf, + owner: Optional[AvailableOwners] = None, + world_matrix: Optional[mathutils.Matrix] = None, + local_matrix: Optional[mathutils.Matrix] = None, + blender_scale=1.0, + include_defaults=True, + is_export=False, + ): + preset_export = conf_type == "PRESET" and is_export + data = {} + if conf_type == "PRESET_EDIT" or preset_export: + data["name"] = self.name + if conf_type != "PRESET" or is_export: + data.update( + { + "cmd_type": self.get_cmd_type(owner), + "str_cmd": self.str_cmd, + "int_cmd": self.int_cmd, + "skip_eval": self.skip_eval, + } + ) + if can_have_mesh(owner): + if conf_type == "PRESET_EDIT" or preset_export: + data["children_requirements"] = self.children_requirements + if data.get("children_requirements") != "NONE": + data["group_children"] = self.group_children + data["dl_option"] = self.dl_option + if self.adds_dl_ext(owner): + data["dl_command"] = self.dl_command + if self.can_animate(owner): + data["is_animated"] = self.is_animated + if self.get_cmd_type(owner) == "Level": + data["section"] = self.section + self.args: list[SM64_CustomArgProperties] + data["args"] = [ + arg.to_dict(conf_type, owner, world_matrix, local_matrix, blender_scale, include_defaults, is_export) + for arg in self.args + ] + return data + + def from_dict(self, data: dict, set_defaults=True): + try: + self.locked = True # dont check preset hashes while setting values + self.name = data.get("name", "My Custom Command") + self.cmd_type = data.get("cmd_type", "Level") + self.str_cmd = data.get("str_cmd", "CUSTOM_COMMAND") + self.int_cmd = data.get("int_cmd", 0) + self.skip_eval = data.get("skip_eval", True) + self.children_requirements = data.get("children_requirements", "ANY") + self.group_children = data.get("group_children", True) + self.dl_option = data.get("dl_option", "NONE") + self.is_animated = data.get("is_animated", False) + self.use_dl_cmd = "dl_command" in data + self.dl_command = data.get("dl_command", "GEO_CUSTOM_CMD_WITH_DL") + self.section = data.get("section", "HIEARCHY") + args = data.get("args", []) + if set_defaults: + self.args.clear() + else: + for i in range(len(args), len(self.args)): + self.args.remove(i) + for i, arg in enumerate(args): + if i >= len(self.args): + self.args.add() + self.args[i].from_dict(arg, i, set_defaults) + finally: + self.locked = False + + def get_final_cmd( + self, + owner: Optional[AvailableOwners], + blender_scale: float, + world_matrix: mathutils.Matrix, + local_matrix: mathutils.Matrix, + layer: Optional[str | int] = None, + has_dl=False, + dl_ref: Optional[str] = None, + name="", + conf_type: Optional[CustomCmdConf] = None, + ): + if conf_type is None: + conf_type = "NO_PRESET" if self.preset == "NONE" else "PRESET" + return CustomCmd( + self.to_dict(conf_type, owner, world_matrix, local_matrix, blender_scale, is_export=True), + layer, + has_dl, + dl_ref, + name, + ) + + def example_macro_define(self, conf_type: CustomCmdConf = "NO_PRESET", use_dl_cmd=False, max_len=100): + macro_define = StringIO() + macro_define.write(f"// {self.name}\n") + macro_define.write("#define ") + macro_define.write(self.dl_command if use_dl_cmd else self.str_cmd) + macro_define.write("(") + previous_arg_names = set() + macro_args = [arg.example_macro_args(self, previous_arg_names, conf_type) for arg in self.args] + if use_dl_cmd: + macro_args.append(f'/*Displaylist*/ {duplicate_name("displaylist", previous_arg_names)}') + joined_args = ", ".join(macro_args) + if len(joined_args) > max_len: + joined_args = ", \\\n\t\t".join(macro_args) + macro_define.write("\\\n\t\t") + macro_define.write(f"{joined_args}) \\\n") + macro_define.write("\t(/* Your code goes here */)") + return macro_define.getvalue() + + def get_examples(self, owner: Optional[AvailableOwners], conf_type: CustomCmdConf, blender_scale=100.0): + cmd_examples = { + "Without DL": ( + self.get_final_cmd(owner, blender_scale, *get_transforms(owner), has_dl=False, conf_type=conf_type), + self.example_macro_define(conf_type, False, 25), + ) + } + if self.adds_dl_ext(owner): + cmd_examples["With DL"] = ( + self.get_final_cmd(owner, blender_scale, *get_transforms(owner), has_dl=True, conf_type=conf_type), + self.example_macro_define(conf_type, True, 25), + ) + return cmd_examples + + def draw_examples( + self, + layout: UILayout, + owner: Optional[AvailableOwners], + conf_type: CustomCmdConf, + blender_scale: float, + is_binary=False, + command_index=0, + ): + col = layout.column() + cmd_examples = self.get_examples(owner, conf_type, blender_scale) + try: + for name, (cmd, macro_example) in cmd_examples.items(): + box = col.box().column() + if len(cmd_examples) > 1: + box.label(text=name) + if is_binary: + multilineLabel(box, cmd.to_text_dump()) + continue + multilineLabel(box, cmd.to_c(max_length=25).replace("\t", " " * 5)) + SM64_CustomCmdOps.draw_props( + box, + "COPYDOWN", + "Copy example to clipboard", + op_name="COPY_EXAMPLE", + index=command_index, + example_name=name, + ) + multilineLabel(box, macro_example.replace("\t", " " * 5)) + except Exception as exc: + multilineLabel(box, f"Error: {exc}") + + def draw_props( + self, + layout: UILayout, + is_binary: bool, + owner: Optional[AvailableOwners] = None, + conf_type: CustomCmdConf = "NO_PRESET", + blender_scale=100.0, + command_index=-1, + ): + cmd_type = self.get_cmd_type(owner) + col = layout.column() + if self.preset != "NONE": + conf_type = "PRESET" + if conf_type != "PRESET_EDIT": + preset_row = col.row() + label_row = preset_row.row() + label_row.alignment = "LEFT" + label_row.label(text="Preset") + SM64_SearchCustomCmds.draw_props(preset_row, self, "preset", "") + SM64_CustomCmdOps.draw_props(preset_row, "PRESET_NEW", "", op_name="ADD", index=-1) + if conf_type != "PRESET": + if conf_type == "PRESET_EDIT": + prop_split(col, self, "name", "Preset Name") + if not isinstance(owner, Bone): # bone is always Geo + prop_split(col, self, "cmd_type", "Type") + prop_split(col, self, "int_cmd" if is_binary else "str_cmd", "Command") + if not is_binary and conf_type != "NO_PRESET": + col.prop(self, "skip_eval") + col.separator() + + if self.can_have_mesh(owner): + if conf_type == "PRESET_EDIT": + prop_split(col, self, "children_requirements", "Children Requirements") + if conf_type != "PRESET_EDIT" or self.children_requirements != "NONE": + col.prop(self, "group_children") + prop_split(col, self, "dl_option", "Displaylist Option") + if self.dl_option == "OPTIONAL": + row = col.row() + row.prop(self, "use_dl_cmd") + if self.use_dl_cmd: + row.prop(self, "dl_command", text="") + if self.can_animate(owner): + col.prop(self, "is_animated") + if conf_type == "PRESET_EDIT" and cmd_type == "Level": + col.prop(self, "section") + + if conf_type != "PRESET" and draw_and_check_tab(col, self, "args_tab", text=f"Arguments ({len(self.args)})"): + SM64_CustomArgsOps.draw_row(col.row(), -1, command_index=command_index) + + if self.args_tab or conf_type == "PRESET": + arg: SM64_CustomArgProperties + for i, arg in enumerate(self.args): + if not arg.will_draw(owner, conf_type): + continue + ops_row = col.row() + if conf_type != "PRESET": + num_row = ops_row.row() + num_row.alignment = "LEFT" + num_row.label(text=str(i)) + SM64_CustomArgsOps.draw_row(ops_row, i, command_index=command_index) + arg.draw_props(ops_row, col, owner, self, command_index, i, conf_type, is_binary) + if conf_type != "PRESET": + col.separator(factor=1.0) + + if conf_type != "PRESET" and draw_and_check_tab(col, self, "examples_tab", text="Examples"): + self.draw_examples(col, owner, conf_type, blender_scale, is_binary, command_index) + + +def draw_custom_cmd_presets(sm64_props: "SM64_Properties", layout: UILayout): + col = layout.column() + if not draw_and_check_tab(col, sm64_props, "custom_cmds_tab", icon="SETTINGS"): + return + basic_op_row = col.row() + SM64_CustomCmdOps.draw_props(basic_op_row, "ADD", "", op_name="ADD") + preset: SM64_CustomCmdProperties + for i, preset in enumerate(sm64_props.custom_cmds): + op_row = col.row() + if draw_and_check_tab(op_row, preset, "tab", preset.name): + preset.draw_props(col, sm64_props.binary_export, conf_type="PRESET_EDIT", command_index=i) + SM64_CustomCmdOps.draw_props(op_row, "ADD", "", op_name="ADD", index=i) + SM64_CustomCmdOps.draw_props(op_row, "REMOVE", "", op_name="REMOVE", index=i) + + +classes = ( + SM64_CustomNumberProperties, + SM64_CustomEnumProperties, + SM64_CustomArgProperties, + SM64_CustomCmdProperties, +) + + +def props_register(): + for cls in classes: + register_class(cls) + + +def props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/sm64/custom_cmd/utility.py b/fast64_internal/sm64/custom_cmd/utility.py new file mode 100644 index 000000000..8768d8499 --- /dev/null +++ b/fast64_internal/sm64/custom_cmd/utility.py @@ -0,0 +1,126 @@ +from typing import Literal, NamedTuple, Optional, TYPE_CHECKING +from re import fullmatch + +import mathutils +from bpy.types import Object, Bone, Context, SpaceView3D, Scene + +from ...utility import z_up_to_y_up_matrix +from ..sm64_geolayout_utility import updateBone + +if TYPE_CHECKING: + from .properties import SM64_CustomCmdProperties + +AvailableOwners = Object | Bone | Scene +CustomCmdConf = Literal["PRESET", "PRESET_EDIT", "NO_PRESET"] # type of configuration + + +def getDrawLayerName(drawLayer): + from ..sm64_geolayout_classes import getDrawLayerName + + return getDrawLayerName(drawLayer) + + +def duplicate_name(name, existing_names: set, old_name: Optional[str] = None): + if not name in existing_names: + return name + num = 0 + if old_name is not None: + number_match = fullmatch(r"(.*?)\.(\d+)$", old_name) + if number_match is not None: # if name already a duplicate/copy, add number + name, num = number_match.group(1), int(number_match.group(2)) + else: + name, num = old_name, 0 + + for i in range(1, len(existing_names) + 1): + new_name = f"{name}.{num+i:03}" + if new_name not in existing_names: # only use name if it's unique + return new_name + assert False, "Failed to generate unique name" + + +def get_custom_prop(context: Context): + """If owner is a scene, custom is always None""" + + class CustomContext(NamedTuple): + custom: Optional["SM64_CustomCmdProperties"] + owner: Optional[AvailableOwners] + + if isinstance(context.space_data, SpaceView3D): + return CustomContext(None, context.scene) + else: + if hasattr(context, "bone") and context.bone is not None: + return CustomContext(context.bone.fast64.sm64.custom, context.bone) + if hasattr(context, "object") and context.object is not None: + return CustomContext(context.object.fast64.sm64.custom, context.object) + return CustomContext(None, None) + + +def get_custom_cmd_preset( + custom_cmd: "SM64_CustomCmdProperties", context: Context +) -> Optional["SM64_CustomCmdProperties"]: + if custom_cmd.preset == "": + return None + presets: dict["SM64_CustomCmdProperties"] = { + custom.name: custom for custom in context.scene.fast64.sm64.custom_cmds + } + return presets[custom_cmd.preset] + + +def check_preset_hashes(owner: AvailableOwners, context: Context): + if owner.fast64.sm64.custom.locked: + return + custom_cmd: "SM64_CustomCmdProperties" = owner.fast64.sm64.custom + if custom_cmd.preset == "NONE": + return + preset_cmd = get_custom_cmd_preset(custom_cmd, context) + if preset_cmd is None: + custom_cmd.preset = "NONE" + elif custom_cmd.saved_hash != preset_cmd.preset_hash: + custom_cmd.from_dict( + preset_cmd.to_dict("PRESET_EDIT", owner, *get_transforms(owner), include_defaults=False), set_defaults=False + ) + custom_cmd.saved_hash = preset_cmd.preset_hash + + +def custom_cmd_preset_update(_self, context: Context): + owner = get_custom_prop(context).owner + if isinstance(owner, Scene): # current context is scene, check all + for obj in context.scene.objects: + check_preset_hashes(obj, context) + if obj.type == "ARMATURE": + for bone in obj.data.bones: + check_preset_hashes(bone, context) + elif owner is not None: + check_preset_hashes(owner, context) + if isinstance(owner, Bone): + updateBone(owner, context) + + +def get_custom_cmd_preset_enum(_self, context: Context): + if isinstance(get_custom_prop(context)[1], Bone): + allowed_types = {"Geo"} + else: + allowed_types = {"Level", "Geo", "Special"} + return [("NONE", "No Preset", "No preset selected")] + [ + (preset.name, preset.name, f"{preset.name} ({preset.cmd_type})") + for preset in (context.scene.fast64.sm64.custom_cmds) + if preset.cmd_type in allowed_types + ] + + +def better_round(value: float): # round, but handle inf + return round(max(-(2**31), min(2**31 - 1, value))) + + +def get_transforms(owner: Optional[AvailableOwners] = None): + if isinstance(owner, Object): + return tuple( + z_up_to_y_up_matrix @ x @ z_up_to_y_up_matrix.inverted() for x in [owner.matrix_world, owner.matrix_local] + ) + elif isinstance(owner, Bone): + relative = owner.matrix_local + if owner.parent is not None: + relative = owner.parent.matrix_local.inverted() @ relative + return tuple(z_up_to_y_up_matrix @ x @ z_up_to_y_up_matrix.inverted() for x in [owner.matrix_local, relative]) + else: + return (z_up_to_y_up_matrix @ mathutils.Matrix.Identity(4) @ z_up_to_y_up_matrix.inverted(),) * 2 diff --git a/fast64_internal/sm64/settings/panels.py b/fast64_internal/sm64/settings/panels.py index 97fe73040..ba82351a7 100644 --- a/fast64_internal/sm64/settings/panels.py +++ b/fast64_internal/sm64/settings/panels.py @@ -2,8 +2,7 @@ from bpy.types import Context from ...panels import SM64_Panel - -from .repo_settings import draw_repo_settings +from ...utility import draw_and_check_tab class SM64_GeneralSettingsPanel(SM64_Panel): @@ -18,10 +17,12 @@ def draw(self, context: Context): if sm64_props.export_type == "C": # If the repo settings tab is open, we pass show_repo_settings as False # because we want to draw those specfic properties in the repo settings box - draw_repo_settings(scene, col.box()) - col.separator() + box = col.box().column() + if draw_and_check_tab(box, sm64_props, "sm64_repo_settings_tab", icon="PROPERTIES"): + sm64_props.draw_repo_settings(box) + col.separator() - sm64_props.draw_props(col, not sm64_props.sm64_repo_settings_tab) + sm64_props.draw_props(col, not sm64_props.sm64_repo_settings_tab or sm64_props.binary_export) else: sm64_props.draw_props(col, True) diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 471ad617d..cb9a57c5d 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -1,16 +1,34 @@ import os +from pathlib import Path import bpy from bpy.types import PropertyGroup, UILayout, Context -from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty, PointerProperty +from bpy.props import ( + BoolProperty, + StringProperty, + EnumProperty, + IntProperty, + FloatProperty, + PointerProperty, + CollectionProperty, +) from bpy.path import abspath from bpy.utils import register_class, unregister_class from ...render_settings import on_update_render_settings -from ...utility import directory_path_checks, directory_ui_warnings, prop_split, upgrade_old_prop -from ..sm64_constants import defaultExtendSegment4 +from ...utility import ( + directory_path_checks, + directory_ui_warnings, + prop_split, + set_prop_if_in_data, + upgrade_old_prop, + get_first_set_prop, +) +from ..sm64_constants import defaultExtendSegment4, OLD_BINARY_LEVEL_ENUMS from ..sm64_objects import SM64_CombinedObjectProperties +from ..custom_cmd.properties import SM64_CustomCmdProperties, draw_custom_cmd_presets from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings from ..tools import SM64_AddrConvProperties +from ..animation.properties import SM64_AnimProperties from .constants import ( enum_refresh_versions, @@ -22,23 +40,27 @@ def decomp_path_update(self, context: Context): fast64_settings = context.scene.fast64.settings - if fast64_settings.repo_settings_path: + if fast64_settings.repo_settings_path and Path(abspath(fast64_settings.repo_settings_path)).exists(): return - directory_path_checks(abspath(self.decomp_path)) - fast64_settings.repo_settings_path = os.path.join(abspath(self.decomp_path), "fast64.json") + directory_path_checks(self.abs_decomp_path) + fast64_settings.repo_settings_path = str(self.abs_decomp_path / "fast64.json") class SM64_Properties(PropertyGroup): """Global SM64 Scene Properties found under scene.fast64.sm64""" version: IntProperty(name="SM64_Properties Version", default=0) - cur_version = 3 # version after property migration + cur_version = 6 # version after property migration # UI Selection show_importing_menus: BoolProperty(name="Show Importing Menus", default=False) export_type: EnumProperty(items=enum_export_type, name="Export Type", default="C") goal: EnumProperty(items=enum_sm64_goal_type, name="Goal", default="All") combined_export: bpy.props.PointerProperty(type=SM64_CombinedObjectProperties) + animation: PointerProperty(type=SM64_AnimProperties) + custom_cmds: CollectionProperty(type=SM64_CustomCmdProperties) + custom_cmds_tab: BoolProperty(default=True, name="Custom Commands") + address_converter: PointerProperty(type=SM64_AddrConvProperties) blender_to_sm64_scale: FloatProperty( name="Blender To SM64 Scale", @@ -47,6 +69,7 @@ class SM64_Properties(PropertyGroup): ) import_rom: StringProperty(name="Import ROM", subtype="FILE_PATH") + # binary export_rom: StringProperty(name="Export ROM", subtype="FILE_PATH") output_rom: StringProperty(name="Output ROM", subtype="FILE_PATH") extend_bank_4: BoolProperty( @@ -56,7 +79,6 @@ class SM64_Properties(PropertyGroup): f"{hex(defaultExtendSegment4[1])}) and copies data from old bank", ) - address_converter: PointerProperty(type=SM64_AddrConvProperties) # C decomp_path: StringProperty( name="Decomp Folder", @@ -80,10 +102,38 @@ class SM64_Properties(PropertyGroup): name="Matstack Fix", description="Exports account for matstack fix requirements", ) + lighting_engine_presets: BoolProperty(name="Lighting Engine Presets") + write_all: BoolProperty( + name="Write All", + description="Write single load geo and set othermode commands instead of writting the difference to defaults. Can result in smaller displaylists but may introduce issues", + ) + # could be used for other properties outside animation + designated_prop: BoolProperty( + name="Designated Initialization for Animation Tables", + description="Extremely recommended but must be off when compiling with IDO. Included in Repo Setting file", + ) @property def binary_export(self): - return self.export_type in ["Binary", "Insertable Binary"] + return self.export_type in {"Binary", "Insertable Binary"} + + @property + def abs_decomp_path(self) -> Path: + return Path(abspath(self.decomp_path)) + + @property + def hackersm64(self) -> bool: + return self.refresh_version.startswith("HackerSM64") + + @property + def designated(self) -> bool: + return self.designated_prop or self.hackersm64 + + @property + def gfx_write_method(self): + from ...f3d.f3d_gbi import GfxMatWriteMethod + + return GfxMatWriteMethod.WriteAll if self.write_all else GfxMatWriteMethod.WriteDifferingAndRevert @staticmethod def upgrade_changed_props(): @@ -99,18 +149,23 @@ def upgrade_changed_props(): "exportType": "export_type", } old_export_props_to_new = { - "custom_group_name": {"geoLevelName", "colLevelName", "animLevelName"}, - "custom_export_path": {"geoExportPath", "colExportPath", "animExportPath"}, + "custom_level_name": {"levelName", "geoLevelName", "colLevelName", "animLevelName", "DLLevelName"}, + "custom_export_path": {"geoExportPath", "colExportPath", "animExportPath", "DLExportPath"}, "object_name": {"geoName", "colName", "animName"}, - "group_name": {"geoGroupName", "colGroupName", "animGroupName"}, - "level_name": {"levelOption", "geoLevelOption", "colLevelOption", "animLevelOption"}, - "custom_level_name": {"levelName", "geoLevelName", "colLevelName", "animLevelName"}, + "group_name": {"geoGroupName", "colGroupName", "animGroupName", "DLGroupName"}, + "level_name": {"levelOption", "geoLevelOption", "colLevelOption", "animLevelOption", "DLLevelOption"}, "non_decomp_level": {"levelCustomExport"}, "export_header_type": {"geoExportHeaderType", "colExportHeaderType", "animExportHeaderType"}, + "custom_include_directory": {"geoTexDir"}, + "binary_level": {"levelAnimExport"}, + # as the others binary props get carried over to here we need to update the cur_version again } + binary_level_names = {"levelAnimExport", "colExportLevel", "levelDLExport", "levelGeoExport"} + old_custom_props = {"animCustomExport", "colCustomExport", "geoCustomExport", "DLCustomExport"} for scene in bpy.data.scenes: sm64_props: SM64_Properties = scene.fast64.sm64 sm64_props.address_converter.upgrade_changed_props(scene) + sm64_props.animation.upgrade_changed_props(scene) if sm64_props.version == SM64_Properties.cur_version: continue upgrade_old_prop( @@ -128,11 +183,61 @@ def upgrade_changed_props(): upgrade_old_prop(sm64_props, new, scene, old) upgrade_old_prop(sm64_props, "show_importing_menus", sm64_props, "showImportingMenus") - combined_props = scene.fast64.sm64.combined_export + combined_props: SM64_CombinedObjectProperties = sm64_props.combined_export for new, old in old_export_props_to_new.items(): upgrade_old_prop(combined_props, new, scene, old) + + insertable_directory = get_first_set_prop(scene, "animInsertableBinaryPath") + if insertable_directory is not None: # Ignores file name + combined_props.insertable_directory = os.path.split(insertable_directory)[1] + + if get_first_set_prop(combined_props, old_custom_props): + combined_props.export_header_type = "Custom" + upgrade_old_prop(combined_props, "level_name", scene, binary_level_names, old_enum=OLD_BINARY_LEVEL_ENUMS) sm64_props.version = SM64_Properties.cur_version + def to_repo_settings(self): + data = {} + data["refresh_version"] = self.refresh_version + data["compression_format"] = self.compression_format + data["force_extended_ram"] = self.force_extended_ram + data["matstack_fix"] = self.matstack_fix + if self.matstack_fix: + data["lighting_engine_presets"] = self.lighting_engine_presets + data["write_all"] = self.write_all + if not self.hackersm64: + data["designated"] = self.designated_prop + if self.custom_cmds: + data["custom_cmds"] = [preset.to_dict("PRESET_EDIT") for preset in self.custom_cmds] + return data + + def from_repo_settings(self, data: dict): + set_prop_if_in_data(self, "refresh_version", data, "refresh_version") + set_prop_if_in_data(self, "compression_format", data, "compression_format") + set_prop_if_in_data(self, "force_extended_ram", data, "force_extended_ram") + set_prop_if_in_data(self, "matstack_fix", data, "matstack_fix") + set_prop_if_in_data(self, "lighting_engine_presets", data, "lighting_engine_presets") + set_prop_if_in_data(self, "write_all", data, "write_all") + set_prop_if_in_data(self, "designated_prop", data, "designated") + if "custom_cmds" in data: + self.custom_cmds.clear() + for preset_data in data.get("custom_cmds", []): + self.custom_cmds.add() + self.custom_cmds[-1].from_dict(preset_data) + + def draw_repo_settings(self, layout: UILayout): + col = layout.column() + if not self.binary_export: + col.prop(self, "disable_scroll") + prop_split(col, self, "compression_format", "Compression Format") + prop_split(col, self, "refresh_version", "Refresh (Function Map)") + col.prop(self, "force_extended_ram") + col.prop(self, "matstack_fix") + if self.matstack_fix: + col.prop(self, "lighting_engine_presets") + col.prop(self, "write_all") + draw_custom_cmd_presets(self, col.box()) + def draw_props(self, layout: UILayout, show_repo_settings: bool = True): col = layout.column() @@ -149,17 +254,12 @@ def draw_props(self, layout: UILayout, show_repo_settings: bool = True): col.prop(self, "extend_bank_4") elif not self.binary_export: prop_split(col, self, "decomp_path", "Decomp Path") - directory_ui_warnings(col, abspath(self.decomp_path)) + directory_ui_warnings(col, self.abs_decomp_path) col.separator() - if not self.binary_export: - col.prop(self, "disable_scroll") - if show_repo_settings: - prop_split(col, self, "compression_format", "Compression Format") - prop_split(col, self, "refresh_version", "Refresh (Function Map)") - col.prop(self, "force_extended_ram") - col.prop(self, "matstack_fix") - col.separator() + if show_repo_settings: + self.draw_repo_settings(col) + col.separator() col.prop(self, "show_importing_menus") if self.show_importing_menus: diff --git a/fast64_internal/sm64/settings/repo_settings.py b/fast64_internal/sm64/settings/repo_settings.py index f74b7eb3d..a32df8621 100644 --- a/fast64_internal/sm64/settings/repo_settings.py +++ b/fast64_internal/sm64/settings/repo_settings.py @@ -2,7 +2,7 @@ from bpy.types import Scene, UILayout -from ...utility import draw_and_check_tab, prop_split +from ...utility import draw_and_check_tab, prop_split, set_prop_if_in_data def save_sm64_repo_settings(scene: Scene): @@ -18,11 +18,7 @@ def save_sm64_repo_settings(scene: Scene): } sm64_props = scene.fast64.sm64 - data["refresh_version"] = sm64_props.refresh_version - data["compression_format"] = sm64_props.compression_format - data["force_extended_ram"] = sm64_props.force_extended_ram - data["matstack_fix"] = sm64_props.matstack_fix - + data.update(sm64_props.to_repo_settings()) return data @@ -33,26 +29,9 @@ def load_sm64_repo_settings(scene: Scene, data: dict[str, Any]): for layer in range(8): draw_layer = draw_layers.get(str(layer), {}) if "cycle_1" in draw_layer: - setattr(world, f"draw_layer_{layer}_cycle_1", draw_layer["cycle_1"]) + set_prop_if_in_data(world, f"draw_layer_{layer}_cycle_1", draw_layer, "cycle_1") if "cycle_2" in draw_layer: - setattr(world, f"draw_layer_{layer}_cycle_2", draw_layer["cycle_2"]) - - sm64_props = scene.fast64.sm64 - sm64_props.refresh_version = data.get("refresh_version", sm64_props.refresh_version) - sm64_props.compression_format = data.get("compression_format", sm64_props.compression_format) - sm64_props.force_extended_ram = data.get("force_extended_ram", sm64_props.force_extended_ram) - sm64_props.matstack_fix = data.get("matstack_fix", sm64_props.matstack_fix) + set_prop_if_in_data(world, f"draw_layer_{layer}_cycle_2", draw_layer, "cycle_2") - -def draw_repo_settings(scene: Scene, layout: UILayout): - col = layout.column() sm64_props = scene.fast64.sm64 - if not draw_and_check_tab(col, sm64_props, "sm64_repo_settings_tab", icon="PROPERTIES"): - return - - prop_split(col, sm64_props, "compression_format", "Compression Format") - prop_split(col, sm64_props, "refresh_version", "Refresh (Function Map)") - col.prop(sm64_props, "force_extended_ram") - col.prop(sm64_props, "matstack_fix") - - col.label(text="See Fast64 repo settings for general settings", icon="INFO") + sm64_props.from_repo_settings(data) diff --git a/fast64_internal/sm64/sm64_anim.py b/fast64_internal/sm64/sm64_anim.py deleted file mode 100644 index 0aa570cb0..000000000 --- a/fast64_internal/sm64/sm64_anim.py +++ /dev/null @@ -1,1119 +0,0 @@ -import bpy, os, copy, shutil, mathutils, math -from bpy.utils import register_class, unregister_class -from ..panels import SM64_Panel -from .sm64_level_parser import parseLevelAtPointer -from .sm64_rom_tweaks import ExtendBank0x04 -from .sm64_geolayout_bone import animatableBoneTypes - -from ..utility import ( - CData, - PluginError, - ValueFrameData, - raisePluginError, - encodeSegmentedAddr, - decodeSegmentedAddr, - getExportDir, - toAlnum, - writeIfNotFound, - get64bitAlignedAddr, - writeInsertableFile, - getFrameInterval, - findStartBones, - saveTranslationFrame, - saveQuaternionFrame, - removeTrailingFrames, - applyRotation, - getPathAndLevel, - applyBasicTweaks, - tempName, - bytesToHex, - prop_split, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, - writeBoxExportType, - stashActionInArmature, - enumExportHeaderType, -) - -from .sm64_constants import ( - bank0Segment, - insertableBinaryTypes, - level_pointers, - defaultExtendSegment4, - level_enums, - enumLevelNames, - marioAnimations, -) - -from .sm64_utility import export_rom_checks, import_rom_checks - -sm64_anim_types = {"ROTATE", "TRANSLATE"} - - -class SM64_Animation: - def __init__(self, name): - self.name = name - self.header = None - self.indices = SM64_ShortArray(name + "_indices", False) - self.values = SM64_ShortArray(name + "_values", True) - - def get_ptr_offsets(self, isDMA): - return [12, 16] if not isDMA else [] - - def to_binary(self, segmentData, isDMA, startAddress): - return ( - self.header.to_binary(segmentData, isDMA, startAddress) + self.indices.to_binary() + self.values.to_binary() - ) - - def to_c(self): - data = CData() - data.header = "extern const struct Animation *const " + self.name + "[];\n" - data.source = self.values.to_c() + "\n" + self.indices.to_c() + "\n" + self.header.to_c() + "\n" - return data - - -class SM64_ShortArray: - def __init__(self, name, signed): - self.name = name - self.shortData = [] - self.signed = signed - - def to_binary(self): - data = bytearray(0) - for short in self.shortData: - # All euler values have been pre-converted to positive values, so don't care about signed. - data += short.to_bytes(2, "big", signed=False) - return data - - def to_c(self): - data = "static const " + ("s" if self.signed else "u") + "16 " + self.name + "[] = {\n\t" - wrapCounter = 0 - for short in self.shortData: - data += "0x" + format(short, "04X") + ", " - wrapCounter += 1 - if wrapCounter > 8: - data += "\n\t" - wrapCounter = 0 - data += "\n};\n" - return data - - -class SM64_AnimationHeader: - def __init__( - self, - name, - repetitions, - marioYOffset, - frameInterval, - nodeCount, - transformValuesStart, - transformIndicesStart, - animSize, - ): - self.name = name - self.repetitions = repetitions - self.marioYOffset = marioYOffset - self.frameInterval = frameInterval - self.nodeCount = nodeCount - self.transformValuesStart = transformValuesStart - self.transformIndicesStart = transformIndicesStart - self.animSize = animSize # DMA animations only - - self.transformIndices = [] - - # presence of segmentData indicates DMA. - def to_binary(self, segmentData, isDMA, startAddress): - if isDMA: - transformValuesStart = self.transformValuesStart - transformIndicesStart = self.transformIndicesStart - else: - transformValuesStart = self.transformValuesStart + startAddress - transformIndicesStart = self.transformIndicesStart + startAddress - - data = bytearray(0) - data.extend(self.repetitions.to_bytes(2, byteorder="big")) - data.extend(self.marioYOffset.to_bytes(2, byteorder="big")) # y offset, only used for mario - data.extend([0x00, 0x00]) # unknown, common with secondary anims, variable length animations? - data.extend(int(round(self.frameInterval[0])).to_bytes(2, byteorder="big")) - data.extend(int(round(self.frameInterval[1] - 1)).to_bytes(2, byteorder="big")) - data.extend(self.nodeCount.to_bytes(2, byteorder="big")) - if not isDMA: - data.extend(encodeSegmentedAddr(transformValuesStart, segmentData)) - data.extend(encodeSegmentedAddr(transformIndicesStart, segmentData)) - data.extend(bytearray([0x00] * 6)) - else: - data.extend(transformValuesStart.to_bytes(4, byteorder="big")) - data.extend(transformIndicesStart.to_bytes(4, byteorder="big")) - data.extend(self.animSize.to_bytes(4, byteorder="big")) - data.extend(bytearray([0x00] * 2)) - return data - - def to_c(self): - data = ( - "static const struct Animation " - + self.name - + " = {\n" - + "\t" - + str(self.repetitions) - + ",\n" - + "\t" - + str(self.marioYOffset) - + ",\n" - + "\t0,\n" - + "\t" - + str(int(round(self.frameInterval[0]))) - + ",\n" - + "\t" - + str(int(round(self.frameInterval[1] - 1))) - + ",\n" - + "\tANIMINDEX_NUMPARTS(" - + self.name - + "_indices),\n" - + "\t" - + self.name - + "_values,\n" - + "\t" - + self.name - + "_indices,\n" - + "\t0,\n" - + "};\n" - ) - return data - - -class SM64_AnimIndexNode: - def __init__(self, x, y, z): - self.x = x - self.y = y - self.z = z - - -class SM64_AnimIndex: - def __init__(self, numFrames, startOffset): - self.startOffset = startOffset - self.numFrames = numFrames - - -def getLastKeyframeTime(keyframes): - last = keyframes[0].co[0] - for keyframe in keyframes: - if keyframe.co[0] > last: - last = keyframe.co[0] - return last - - -# add definition to groupN.h -# add data/table includes to groupN.c (bin_id?) -# add data/table files -def exportAnimationC(armatureObj, loopAnim, dirPath, dirName, groupName, customExport, headerType, levelName): - dirPath, texDir = getExportDir(customExport, dirPath, headerType, levelName, "", dirName) - - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, dirName + "_anim") - animName = armatureObj.animation_data.action.name - - geoDirPath = os.path.join(dirPath, toAlnum(dirName)) - if not os.path.exists(geoDirPath): - os.mkdir(geoDirPath) - - animDirPath = os.path.join(geoDirPath, "anims") - if not os.path.exists(animDirPath): - os.mkdir(animDirPath) - - animsName = dirName + "_anims" - animFileName = "anim_" + toAlnum(animName) + ".inc.c" - animPath = os.path.join(animDirPath, animFileName) - - data = sm64_anim.to_c() - outFile = open(animPath, "w", newline="\n") - outFile.write(data.source) - outFile.close() - - headerPath = os.path.join(geoDirPath, "anim_header.h") - headerFile = open(headerPath, "w", newline="\n") - headerFile.write("extern const struct Animation *const " + animsName + "[];\n") - headerFile.close() - - # write to data.inc.c - dataFilePath = os.path.join(animDirPath, "data.inc.c") - if not os.path.exists(dataFilePath): - dataFile = open(dataFilePath, "w", newline="\n") - dataFile.close() - writeIfNotFound(dataFilePath, '#include "' + animFileName + '"\n', "") - - # write to table.inc.c - tableFilePath = os.path.join(animDirPath, "table.inc.c") - - # if table doesn´t exist, create one - if not os.path.exists(tableFilePath): - tableFile = open(tableFilePath, "w", newline="\n") - tableFile.write("const struct Animation *const " + animsName + "[] = {\n\tNULL,\n};\n") - tableFile.close() - - stringData = "" - with open(tableFilePath, "r") as f: - stringData = f.read() - - # if animation header isn´t already in the table then add it. - if sm64_anim.header.name not in stringData: - # search for the NULL value which represents the end of the table - # (this value is not present in vanilla animation tables) - footerIndex = stringData.rfind("\tNULL,\n") - - # if the null value cant be found, look for the end of the array - if footerIndex == -1: - footerIndex = stringData.rfind("};") - - # if that can´t be found then throw an error. - if footerIndex == -1: - raise PluginError("Animation table´s footer does not seem to exist.") - - stringData = stringData[:footerIndex] + "\tNULL,\n" + stringData[footerIndex:] - - stringData = stringData[:footerIndex] + f"\t&{sm64_anim.header.name},\n" + stringData[footerIndex:] - - with open(tableFilePath, "w") as f: - f.write(stringData) - - if not customExport: - if headerType == "Actor": - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/anims/data.inc.c"', "") - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/anims/table.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + dirName + '/anim_header.h"', "#endif") - elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/anims/data.inc.c"', "") - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/anims/table.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + dirName + '/anim_header.h"', "\n#endif" - ) - - -def exportAnimationBinary(romfile, exportRange, armatureObj, DMAAddresses, segmentData, isDMA, loopAnim): - startAddress = get64bitAlignedAddr(exportRange[0]) - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, armatureObj.name) - - animData = sm64_anim.to_binary(segmentData, isDMA, startAddress) - - if startAddress + len(animData) > exportRange[1]: - raise PluginError( - "Size too big: Data ends at " - + hex(startAddress + len(animData)) - + ", which is larger than the specified range." - ) - - romfile.seek(startAddress) - romfile.write(animData) - - addrRange = (startAddress, startAddress + len(animData)) - - if not isDMA: - animTablePointer = get64bitAlignedAddr(startAddress + len(animData)) - romfile.seek(animTablePointer) - romfile.write(encodeSegmentedAddr(startAddress, segmentData)) - return addrRange, animTablePointer - else: - if DMAAddresses is not None: - romfile.seek(DMAAddresses["entry"]) - romfile.write((startAddress - DMAAddresses["start"]).to_bytes(4, byteorder="big")) - romfile.seek(DMAAddresses["entry"] + 4) - romfile.write(len(animData).to_bytes(4, byteorder="big")) - return addrRange, None - - -def exportAnimationInsertableBinary(filepath, armatureObj, isDMA, loopAnim): - startAddress = get64bitAlignedAddr(0) - sm64_anim = exportAnimationCommon(armatureObj, loopAnim, armatureObj.name) - segmentData = copy.copy(bank0Segment) - - animData = sm64_anim.to_binary(segmentData, isDMA, startAddress) - - if startAddress + len(animData) > 0xFFFFFF: - raise PluginError( - "Size too big: Data ends at " - + hex(startAddress + len(animData)) - + ", which is larger than the specified range." - ) - - writeInsertableFile( - filepath, insertableBinaryTypes["Animation"], sm64_anim.get_ptr_offsets(isDMA), startAddress, animData - ) - - -def exportAnimationCommon(armatureObj, loopAnim, name): - if armatureObj.animation_data is None or armatureObj.animation_data.action is None: - raise PluginError("No active animation selected.") - - anim = armatureObj.animation_data.action - stashActionInArmature(armatureObj, anim) - - sm64_anim = SM64_Animation(toAlnum(name + "_" + anim.name)) - - nodeCount = len(armatureObj.data.bones) - - frame_start, frame_last = getFrameInterval(anim) - - translationData, armatureFrameData = convertAnimationData( - anim, - armatureObj, - frame_start=frame_start, - frame_count=(frame_last - frame_start + 1), - ) - - repetitions = 0 if loopAnim else 1 - marioYOffset = 0x00 # ??? Seems to be this value for most animations - - transformValuesOffset = 0 - headerSize = 0x1A - transformIndicesStart = headerSize # 0x18 if including animSize? - - # all node rotations + root translation - # *3 for each property (xyz) and *4 for entry size - # each keyframe stored as 2 bytes - # transformValuesStart = transformIndicesStart + (nodeCount + 1) * 3 * 4 - transformValuesStart = transformIndicesStart - - for translationFrameProperty in translationData: - frameCount = len(translationFrameProperty.frames) - sm64_anim.indices.shortData.append(frameCount) - sm64_anim.indices.shortData.append(transformValuesOffset) - if (transformValuesOffset) > 2**16 - 1: - raise PluginError("Animation is too large.") - transformValuesOffset += frameCount - transformValuesStart += 4 - for value in translationFrameProperty.frames: - sm64_anim.values.shortData.append( - int.from_bytes(value.to_bytes(2, "big", signed=True), byteorder="big", signed=False) - ) - - for boneFrameData in armatureFrameData: - for boneFrameDataProperty in boneFrameData: - frameCount = len(boneFrameDataProperty.frames) - sm64_anim.indices.shortData.append(frameCount) - sm64_anim.indices.shortData.append(transformValuesOffset) - if (transformValuesOffset) > 2**16 - 1: - raise PluginError("Animation is too large.") - transformValuesOffset += frameCount - transformValuesStart += 4 - for value in boneFrameDataProperty.frames: - sm64_anim.values.shortData.append(value) - - animSize = headerSize + len(sm64_anim.indices.shortData) * 2 + len(sm64_anim.values.shortData) * 2 - - sm64_anim.header = SM64_AnimationHeader( - sm64_anim.name, - repetitions, - marioYOffset, - [frame_start, frame_last + 1], - nodeCount, - transformValuesStart, - transformIndicesStart, - animSize, - ) - - return sm64_anim - - -def convertAnimationData(anim, armatureObj, *, frame_start, frame_count): - bonesToProcess = findStartBones(armatureObj) - currentBone = armatureObj.data.bones[bonesToProcess[0]] - animBones = [] - - # Get animation bones in order - while len(bonesToProcess) > 0: - boneName = bonesToProcess[0] - currentBone = armatureObj.data.bones[boneName] - currentPoseBone = armatureObj.pose.bones[boneName] - bonesToProcess = bonesToProcess[1:] - - # Only handle 0x13 bones for animation - if currentBone.geo_cmd in animatableBoneTypes: - animBones.append(boneName) - - # Traverse children in alphabetical order. - childrenNames = sorted([bone.name for bone in currentBone.children]) - bonesToProcess = childrenNames + bonesToProcess - - # list of boneFrameData, which is [[x frames], [y frames], [z frames]] - translationData = [ValueFrameData(0, i, []) for i in range(3)] - armatureFrameData = [ - [ValueFrameData(i, 0, []), ValueFrameData(i, 1, []), ValueFrameData(i, 2, [])] for i in range(len(animBones)) - ] - - currentFrame = bpy.context.scene.frame_current - for frame in range(frame_start, frame_start + frame_count): - bpy.context.scene.frame_set(frame) - rootPoseBone = armatureObj.pose.bones[animBones[0]] - - translation = ( - mathutils.Matrix.Scale(bpy.context.scene.fast64.sm64.blender_to_sm64_scale, 4) @ rootPoseBone.matrix_basis - ).decompose()[0] - saveTranslationFrame(translationData, translation) - - for boneIndex in range(len(animBones)): - boneName = animBones[boneIndex] - currentBone = armatureObj.data.bones[boneName] - currentPoseBone = armatureObj.pose.bones[boneName] - - rotationValue = (currentBone.matrix.to_4x4().inverted() @ currentPoseBone.matrix).to_quaternion() - if currentBone.parent is not None: - rotationValue = ( - currentBone.matrix.to_4x4().inverted() - @ currentPoseBone.parent.matrix.inverted() - @ currentPoseBone.matrix - ).to_quaternion() - - # rest pose local, compared to current pose local - - saveQuaternionFrame(armatureFrameData[boneIndex], rotationValue) - - bpy.context.scene.frame_set(currentFrame) - removeTrailingFrames(translationData) - for frameData in armatureFrameData: - removeTrailingFrames(frameData) - - return translationData, armatureFrameData - - -def getNextBone(boneStack, armatureObj): - if len(boneStack) == 0: - raise PluginError("More bones in animation than on armature.") - bone = armatureObj.data.bones[boneStack[0]] - boneStack = boneStack[1:] - boneStack = sorted([child.name for child in bone.children]) + boneStack - - # Only return 0x13 bone - while armatureObj.data.bones[bone.name].geo_cmd not in animatableBoneTypes: - if len(boneStack) == 0: - raise PluginError("More bones in animation than on armature.") - bone = armatureObj.data.bones[boneStack[0]] - boneStack = boneStack[1:] - boneStack = sorted([child.name for child in bone.children]) + boneStack - - return bone, boneStack - - -def importAnimationToBlender(romfile, startAddress, armatureObj, segmentData, isDMA, animName): - boneStack = findStartBones(armatureObj) - startBoneName = boneStack[0] - if armatureObj.data.bones[startBoneName].geo_cmd not in animatableBoneTypes: - startBone, boneStack = getNextBone(boneStack, armatureObj) - startBoneName = startBone.name - boneStack = [startBoneName] + boneStack - - animationHeader, armatureFrameData = readAnimation(animName, romfile, startAddress, segmentData, isDMA) - - if len(armatureFrameData) > len(armatureObj.data.bones) + 1: - raise PluginError("More bones in animation than on armature.") - - # bpy.context.scene.render.fps = 30 - bpy.context.scene.frame_end = animationHeader.frameInterval[1] - anim = bpy.data.actions.new(animName) - - isRootTranslation = True - # boneFrameData = [[x keyframes], [y keyframes], [z keyframes]] - # len(armatureFrameData) should be = number of bones - # property index = 0,1,2 (aka x,y,z) - for boneFrameData in armatureFrameData: - if isRootTranslation: - for propertyIndex in range(3): - fcurve = anim.fcurves.new( - data_path='pose.bones["' + startBoneName + '"].location', - index=propertyIndex, - action_group=startBoneName, - ) - for frame in range(len(boneFrameData[propertyIndex])): - fcurve.keyframe_points.insert(frame, boneFrameData[propertyIndex][frame]) - isRootTranslation = False - else: - bone, boneStack = getNextBone(boneStack, armatureObj) - for propertyIndex in range(3): - fcurve = anim.fcurves.new( - data_path='pose.bones["' + bone.name + '"].rotation_euler', - index=propertyIndex, - action_group=bone.name, - ) - for frame in range(len(boneFrameData[propertyIndex])): - fcurve.keyframe_points.insert(frame, boneFrameData[propertyIndex][frame]) - - if armatureObj.animation_data is None: - armatureObj.animation_data_create() - - stashActionInArmature(armatureObj, anim) - armatureObj.animation_data.action = anim - - -def readAnimation(name, romfile, startAddress, segmentData, isDMA): - animationHeader = readAnimHeader(name, romfile, startAddress, segmentData, isDMA) - - print("Frames: " + str(animationHeader.frameInterval[1]) + " / Nodes: " + str(animationHeader.nodeCount)) - - animationHeader.transformIndices = readAnimIndices( - romfile, animationHeader.transformIndicesStart, animationHeader.nodeCount - ) - - armatureFrameData = [] # list of list of frames - - # sm64 space -> blender space -> pose space - # BlenderToSM64: YZX (set rotation mode of bones) - # SM64toBlender: ZXY (set anim keyframes and model armature) - # new bones should extrude in +Y direction - - # handle root translation - boneFrameData = [[], [], []] - rootIndexNode = animationHeader.transformIndices[0] - boneFrameData[0] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.x) - ] - boneFrameData[1] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.y) - ] - boneFrameData[2] = [ - n for n in getKeyFramesTranslation(romfile, animationHeader.transformValuesStart, rootIndexNode.z) - ] - armatureFrameData.append(boneFrameData) - - # handle rotations - for boneIndexNode in animationHeader.transformIndices[1:]: - boneFrameData = [[], [], []] - - # Transforming SM64 space to Blender space - boneFrameData[0] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.x) - ] - boneFrameData[1] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.y) - ] - boneFrameData[2] = [ - n for n in getKeyFramesRotation(romfile, animationHeader.transformValuesStart, boneIndexNode.z) - ] - - armatureFrameData.append(boneFrameData) - - return (animationHeader, armatureFrameData) - - -def getKeyFramesRotation(romfile, transformValuesStart, boneIndex): - ptrToValue = transformValuesStart + boneIndex.startOffset - romfile.seek(ptrToValue) - - keyframes = [] - for frame in range(boneIndex.numFrames): - romfile.seek(ptrToValue + frame * 2) - value = int.from_bytes(romfile.read(2), "big") * 360 / (2**16) - keyframes.append(math.radians(value)) - - return keyframes - - -def getKeyFramesTranslation(romfile, transformValuesStart, boneIndex): - ptrToValue = transformValuesStart + boneIndex.startOffset - romfile.seek(ptrToValue) - - keyframes = [] - for frame in range(boneIndex.numFrames): - romfile.seek(ptrToValue + frame * 2) - keyframes.append( - int.from_bytes(romfile.read(2), "big", signed=True) / bpy.context.scene.fast64.sm64.blender_to_sm64_scale - ) - - return keyframes - - -def readAnimHeader(name, romfile, startAddress, segmentData, isDMA): - frameInterval = [0, 0] - - romfile.seek(startAddress + 0x00) - numRepeats = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x02) - marioYOffset = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x06) - frameInterval[0] = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x08) - frameInterval[1] = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x0A) - numNodes = int.from_bytes(romfile.read(2), "big") - - romfile.seek(startAddress + 0x0C) - transformValuesOffset = int.from_bytes(romfile.read(4), "big") - if isDMA: - transformValuesStart = startAddress + transformValuesOffset - else: - transformValuesStart = decodeSegmentedAddr(transformValuesOffset.to_bytes(4, byteorder="big"), segmentData) - - romfile.seek(startAddress + 0x10) - transformIndicesOffset = int.from_bytes(romfile.read(4), "big") - if isDMA: - transformIndicesStart = startAddress + transformIndicesOffset - else: - transformIndicesStart = decodeSegmentedAddr(transformIndicesOffset.to_bytes(4, byteorder="big"), segmentData) - - romfile.seek(startAddress + 0x14) - animSize = int.from_bytes(romfile.read(4), "big") - - return SM64_AnimationHeader( - name, numRepeats, marioYOffset, frameInterval, numNodes, transformValuesStart, transformIndicesStart, animSize - ) - - -def readAnimIndices(romfile, ptrAddress, nodeCount): - indices = [] - - # Handle root transform - rootPosIndex = readTransformIndex(romfile, ptrAddress) - indices.append(rootPosIndex) - - # Handle rotations - for i in range(nodeCount): - rotationIndex = readTransformIndex(romfile, ptrAddress + (i + 1) * 12) - indices.append(rotationIndex) - - return indices - - -def readTransformIndex(romfile, startAddress): - x = readValueIndex(romfile, startAddress + 0) - y = readValueIndex(romfile, startAddress + 4) - z = readValueIndex(romfile, startAddress + 8) - - return SM64_AnimIndexNode(x, y, z) - - -def readValueIndex(romfile, startAddress): - romfile.seek(startAddress) - numFrames = int.from_bytes(romfile.read(2), "big") - romfile.seek(startAddress + 2) - - # multiply 2 because value is the index in array of shorts (???) - startOffset = int.from_bytes(romfile.read(2), "big") * 2 - # print(str(hex(startAddress)) + ": " + str(numFrames) + " " + str(startOffset)) - return SM64_AnimIndex(numFrames, startOffset) - - -def writeAnimation(romfile, startAddress, segmentData): - pass - - -def writeAnimHeader(romfile, startAddress, segmentData): - pass - - -class SM64_ExportAnimMario(bpy.types.Operator): - bl_idname = "object.sm64_export_anim" - bl_label = "Export Animation" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileOutput = None - tempROM = None - try: - if len(context.selected_objects) == 0 or not isinstance( - context.selected_objects[0].data, bpy.types.Armature - ): - raise PluginError("Armature not selected.") - if len(context.selected_objects) > 1: - raise PluginError("Multiple objects selected, make sure to select only one.") - armatureObj = context.selected_objects[0] - if context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - try: - # Rotate all armatures 90 degrees - applyRotation([armatureObj], math.radians(90), "X") - - if context.scene.fast64.sm64.export_type == "C": - exportPath, levelName = getPathAndLevel( - context.scene.animCustomExport, - context.scene.animExportPath, - context.scene.animLevelName, - context.scene.animLevelOption, - ) - if not context.scene.animCustomExport: - applyBasicTweaks(exportPath) - exportAnimationC( - armatureObj, - context.scene.loopAnimation, - exportPath, - bpy.context.scene.animName, - bpy.context.scene.animGroupName, - context.scene.animCustomExport, - context.scene.animExportHeaderType, - levelName, - ) - self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.export_type == "Insertable Binary": - exportAnimationInsertableBinary( - bpy.path.abspath(context.scene.animInsertableBinaryPath), - armatureObj, - context.scene.isDMAExport, - context.scene.loopAnimation, - ) - self.report({"INFO"}, "Success! Animation at " + context.scene.animInsertableBinaryPath) - else: - export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) - tempROM = tempName(context.scene.fast64.sm64.output_rom) - romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") - shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) - romfileExport.close() - romfileOutput = open(bpy.path.abspath(tempROM), "rb+") - - # Note actual level doesn't matter for Mario, since he is in all of them - levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelAnimExport]) - segmentData = levelParsed.segmentData - if context.scene.fast64.sm64.extend_bank_4: - ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) - - DMAAddresses = None - if context.scene.animOverwriteDMAEntry: - DMAAddresses = {} - DMAAddresses["start"] = int(context.scene.DMAStartAddress, 16) - DMAAddresses["entry"] = int(context.scene.DMAEntryAddress, 16) - - addrRange, nonDMAListPtr = exportAnimationBinary( - romfileOutput, - [int(context.scene.animExportStart, 16), int(context.scene.animExportEnd, 16)], - bpy.context.active_object, - DMAAddresses, - segmentData, - context.scene.isDMAExport, - context.scene.loopAnimation, - ) - - if not context.scene.isDMAExport: - segmentedPtr = encodeSegmentedAddr(addrRange[0], segmentData) - if context.scene.setAnimListIndex: - romfileOutput.seek(int(context.scene.addr_0x27, 16) + 4) - segAnimPtr = romfileOutput.read(4) - virtAnimPtr = decodeSegmentedAddr(segAnimPtr, segmentData) - romfileOutput.seek(virtAnimPtr + 4 * context.scene.animListIndexExport) - romfileOutput.write(segmentedPtr) - if context.scene.overwrite_0x28: - romfileOutput.seek(int(context.scene.addr_0x28, 16) + 1) - romfileOutput.write(bytearray([context.scene.animListIndexExport])) - else: - segmentedPtr = None - - romfileOutput.close() - if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): - os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) - - if not context.scene.isDMAExport: - if context.scene.setAnimListIndex: - self.report( - {"INFO"}, - "Sucess! Animation table at " - + hex(virtAnimPtr) - + ", animation at (" - + hex(addrRange[0]) - + ", " - + hex(addrRange[1]) - + ") " - + "(Seg. " - + bytesToHex(segmentedPtr) - + ").", - ) - else: - self.report( - {"INFO"}, - "Sucess! Animation at (" - + hex(addrRange[0]) - + ", " - + hex(addrRange[1]) - + ") " - + "(Seg. " - + bytesToHex(segmentedPtr) - + ").", - ) - else: - self.report( - {"INFO"}, "Success! Animation at (" + hex(addrRange[0]) + ", " + hex(addrRange[1]) + ")." - ) - - applyRotation([armatureObj], math.radians(-90), "X") - except Exception as e: - applyRotation([armatureObj], math.radians(-90), "X") - - if romfileOutput is not None: - romfileOutput.close() - if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): - os.remove(bpy.path.abspath(tempROM)) - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ExportAnimPanel(SM64_Panel): - bl_idname = "SM64_PT_export_anim" - bl_label = "SM64 Animation Exporter" - goal = "Object/Actor/Anim" - - # called every frame - def draw(self, context): - col = self.layout.column() - propsAnimExport = col.operator(SM64_ExportAnimMario.bl_idname) - - col.prop(context.scene, "loopAnimation") - - if context.scene.fast64.sm64.export_type == "C": - col.prop(context.scene, "animCustomExport") - if context.scene.animCustomExport: - col.prop(context.scene, "animExportPath") - prop_split(col, context.scene, "animName", "Name") - customExportWarning(col) - else: - prop_split(col, context.scene, "animExportHeaderType", "Export Type") - prop_split(col, context.scene, "animName", "Name") - if context.scene.animExportHeaderType == "Actor": - prop_split(col, context.scene, "animGroupName", "Group Name") - elif context.scene.animExportHeaderType == "Level": - prop_split(col, context.scene, "animLevelOption", "Level") - if context.scene.animLevelOption == "Custom": - prop_split(col, context.scene, "animLevelName", "Level Name") - - decompFolderMessage(col) - writeBox = makeWriteInfoBox(col) - writeBoxExportType( - writeBox, - context.scene.animExportHeaderType, - context.scene.animName, - context.scene.animLevelName, - context.scene.animLevelOption, - ) - - elif context.scene.fast64.sm64.export_type == "Insertable Binary": - col.prop(context.scene, "isDMAExport") - col.prop(context.scene, "animInsertableBinaryPath") - else: - col.prop(context.scene, "isDMAExport") - if context.scene.isDMAExport: - col.prop(context.scene, "animOverwriteDMAEntry") - if context.scene.animOverwriteDMAEntry: - prop_split(col, context.scene, "DMAStartAddress", "DMA Start Address") - prop_split(col, context.scene, "DMAEntryAddress", "DMA Entry Address") - else: - col.prop(context.scene, "setAnimListIndex") - if context.scene.setAnimListIndex: - prop_split(col, context.scene, "addr_0x27", "27 Command Address") - prop_split(col, context.scene, "animListIndexExport", "Anim List Index") - col.prop(context.scene, "overwrite_0x28") - if context.scene.overwrite_0x28: - prop_split(col, context.scene, "addr_0x28", "28 Command Address") - col.prop(context.scene, "levelAnimExport") - col.separator() - prop_split(col, context.scene, "animExportStart", "Start Address") - prop_split(col, context.scene, "animExportEnd", "End Address") - - -class SM64_ImportAnimMario(bpy.types.Operator): - bl_idname = "object.sm64_import_anim" - bl_label = "Import Animation" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileSrc = None - try: - import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) - romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - try: - levelParsed = parseLevelAtPointer(romfileSrc, level_pointers[context.scene.levelAnimImport]) - segmentData = levelParsed.segmentData - - animStart = int(context.scene.animStartImport, 16) - if context.scene.animIsSegPtr: - animStart = decodeSegmentedAddr(animStart.to_bytes(4, "big"), segmentData) - - if not context.scene.isDMAImport and context.scene.animIsAnimList: - romfileSrc.seek(animStart + 4 * context.scene.animListIndexImport) - actualPtr = romfileSrc.read(4) - animStart = decodeSegmentedAddr(actualPtr, segmentData) - - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - armatureObj = context.active_object - if armatureObj.type != "ARMATURE": - raise PluginError("Armature not selected.") - - importAnimationToBlender( - romfileSrc, animStart, armatureObj, segmentData, context.scene.isDMAImport, "sm64_anim" - ) - romfileSrc.close() - self.report({"INFO"}, "Success!") - except Exception as e: - if romfileSrc is not None: - romfileSrc.close() - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ImportAllMarioAnims(bpy.types.Operator): - bl_idname = "object.sm64_import_mario_anims" - bl_label = "Import All Mario Animations" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - romfileSrc = None - try: - import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) - romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - try: - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - armatureObj = context.active_object - if armatureObj.type != "ARMATURE": - raise PluginError("Armature not selected.") - - for adress, animName in marioAnimations: - importAnimationToBlender(romfileSrc, adress, armatureObj, {}, context.scene.isDMAImport, animName) - - romfileSrc.close() - self.report({"INFO"}, "Success!") - except Exception as e: - if romfileSrc is not None: - romfileSrc.close() - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - return {"FINISHED"} # must return a set - - -class SM64_ImportAnimPanel(SM64_Panel): - bl_idname = "SM64_PT_import_anim" - bl_label = "SM64 Animation Importer" - goal = "Object/Actor/Anim" - import_panel = True - - # called every frame - def draw(self, context): - col = self.layout.column() - propsAnimImport = col.operator(SM64_ImportAnimMario.bl_idname) - propsMarioAnimsImport = col.operator(SM64_ImportAllMarioAnims.bl_idname) - - col.prop(context.scene, "isDMAImport") - if not context.scene.isDMAImport: - col.prop(context.scene, "animIsAnimList") - if context.scene.animIsAnimList: - prop_split(col, context.scene, "animListIndexImport", "Anim List Index") - - prop_split(col, context.scene, "animStartImport", "Start Address") - col.prop(context.scene, "animIsSegPtr") - col.prop(context.scene, "levelAnimImport") - - -sm64_anim_classes = ( - SM64_ExportAnimMario, - SM64_ImportAnimMario, - SM64_ImportAllMarioAnims, -) - -sm64_anim_panels = ( - SM64_ImportAnimPanel, - SM64_ExportAnimPanel, -) - - -def sm64_anim_panel_register(): - for cls in sm64_anim_panels: - register_class(cls) - - -def sm64_anim_panel_unregister(): - for cls in sm64_anim_panels: - unregister_class(cls) - - -def sm64_anim_register(): - for cls in sm64_anim_classes: - register_class(cls) - - bpy.types.Scene.animStartImport = bpy.props.StringProperty(name="Import Start", default="4EC690") - bpy.types.Scene.animExportStart = bpy.props.StringProperty(name="Start", default="11D8930") - bpy.types.Scene.animExportEnd = bpy.props.StringProperty(name="End", default="11FFF00") - bpy.types.Scene.isDMAImport = bpy.props.BoolProperty(name="Is DMA Animation", default=True) - bpy.types.Scene.isDMAExport = bpy.props.BoolProperty(name="Is DMA Animation") - bpy.types.Scene.DMAEntryAddress = bpy.props.StringProperty(name="DMA Entry Address", default="4EC008") - bpy.types.Scene.DMAStartAddress = bpy.props.StringProperty(name="DMA Start Address", default="4EC000") - bpy.types.Scene.levelAnimImport = bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") - bpy.types.Scene.levelAnimExport = bpy.props.EnumProperty(items=level_enums, name="Level", default="IC") - bpy.types.Scene.loopAnimation = bpy.props.BoolProperty(name="Loop Animation", default=True) - bpy.types.Scene.setAnimListIndex = bpy.props.BoolProperty(name="Set Anim List Entry", default=True) - bpy.types.Scene.overwrite_0x28 = bpy.props.BoolProperty(name="Overwrite 0x28 behaviour command", default=True) - bpy.types.Scene.addr_0x27 = bpy.props.StringProperty(name="0x27 Command Address", default="21CD00") - bpy.types.Scene.addr_0x28 = bpy.props.StringProperty(name="0x28 Command Address", default="21CD08") - bpy.types.Scene.animExportPath = bpy.props.StringProperty(name="Directory", subtype="FILE_PATH") - bpy.types.Scene.animOverwriteDMAEntry = bpy.props.BoolProperty(name="Overwrite DMA Entry") - bpy.types.Scene.animInsertableBinaryPath = bpy.props.StringProperty(name="Filepath", subtype="FILE_PATH") - bpy.types.Scene.animIsSegPtr = bpy.props.BoolProperty(name="Is Segmented Address", default=False) - bpy.types.Scene.animIsAnimList = bpy.props.BoolProperty(name="Is Anim List", default=True) - bpy.types.Scene.animListIndexImport = bpy.props.IntProperty(name="Anim List Index", min=0, max=255) - bpy.types.Scene.animListIndexExport = bpy.props.IntProperty(name="Anim List Index", min=0, max=255) - bpy.types.Scene.animName = bpy.props.StringProperty(name="Name", default="mario") - bpy.types.Scene.animGroupName = bpy.props.StringProperty(name="Group Name", default="group0") - bpy.types.Scene.animWriteHeaders = bpy.props.BoolProperty(name="Write Headers For Actor", default=True) - bpy.types.Scene.animCustomExport = bpy.props.BoolProperty(name="Custom Export Path") - bpy.types.Scene.animExportHeaderType = bpy.props.EnumProperty( - items=enumExportHeaderType, name="Header Export", default="Actor" - ) - bpy.types.Scene.animLevelName = bpy.props.StringProperty(name="Level", default="bob") - bpy.types.Scene.animLevelOption = bpy.props.EnumProperty(items=enumLevelNames, name="Level", default="bob") - - -def sm64_anim_unregister(): - for cls in reversed(sm64_anim_classes): - unregister_class(cls) - - del bpy.types.Scene.animStartImport - del bpy.types.Scene.animExportStart - del bpy.types.Scene.animExportEnd - del bpy.types.Scene.levelAnimImport - del bpy.types.Scene.levelAnimExport - del bpy.types.Scene.isDMAImport - del bpy.types.Scene.isDMAExport - del bpy.types.Scene.DMAStartAddress - del bpy.types.Scene.DMAEntryAddress - del bpy.types.Scene.loopAnimation - del bpy.types.Scene.setAnimListIndex - del bpy.types.Scene.overwrite_0x28 - del bpy.types.Scene.addr_0x27 - del bpy.types.Scene.addr_0x28 - del bpy.types.Scene.animExportPath - del bpy.types.Scene.animOverwriteDMAEntry - del bpy.types.Scene.animInsertableBinaryPath - del bpy.types.Scene.animIsSegPtr - del bpy.types.Scene.animIsAnimList - del bpy.types.Scene.animListIndexImport - del bpy.types.Scene.animListIndexExport - del bpy.types.Scene.animName - del bpy.types.Scene.animGroupName - del bpy.types.Scene.animWriteHeaders - del bpy.types.Scene.animCustomExport - del bpy.types.Scene.animExportHeaderType - del bpy.types.Scene.animLevelName - del bpy.types.Scene.animLevelOption diff --git a/fast64_internal/sm64/sm64_classes.py b/fast64_internal/sm64/sm64_classes.py new file mode 100644 index 000000000..48c165b50 --- /dev/null +++ b/fast64_internal/sm64/sm64_classes.py @@ -0,0 +1,290 @@ +from io import BufferedReader, StringIO +from typing import BinaryIO +from pathlib import Path +import dataclasses +import shutil +import struct +import os +import numpy as np + +from ..utility import intToHex, decodeSegmentedAddr, PluginError, toAlnum +from .sm64_constants import insertableBinaryTypes, SegmentData +from .sm64_utility import export_rom_checks, temp_file_path + + +@dataclasses.dataclass +class InsertableBinaryData: + data_type: str = "" + data: bytearray = dataclasses.field(default_factory=bytearray) + start_address: int = 0 + ptrs: list[int] = dataclasses.field(default_factory=list) + + def write(self, path: Path): + path.write_bytes(self.to_binary()) + + def to_binary(self): + data = bytearray() + data.extend(insertableBinaryTypes[self.data_type].to_bytes(4, "big")) # 0-4 + data.extend(len(self.data).to_bytes(4, "big")) # 4-8 + data.extend(self.start_address.to_bytes(4, "big")) # 8-12 + data.extend(len(self.ptrs).to_bytes(4, "big")) # 12-16 + for ptr in self.ptrs: # 16-(16 + len(ptr) * 4) + data.extend(ptr.to_bytes(4, "big")) + data.extend(self.data) + return data + + def read(self, file: BufferedReader, expected_type: list = None): + print(f"Reading insertable binary data from {file.name}") + reader = RomReader(file) + type_num = reader.read_int(4) + if type_num not in insertableBinaryTypes.values(): + raise ValueError(f"Unknown data type: {intToHex(type_num)}") + self.data_type = next(k for k, v in insertableBinaryTypes.items() if v == type_num) + if expected_type and self.data_type not in expected_type: + raise ValueError(f"Unexpected data type: {self.data_type}") + + data_size = reader.read_int(4) + self.start_address = reader.read_int(4) + pointer_count = reader.read_int(4) + self.ptrs = [] + for _ in range(pointer_count): + self.ptrs.append(reader.read_int(4)) + + actual_start = reader.address + self.start_address + self.data = reader.read_data(data_size, actual_start) + return self + + +@dataclasses.dataclass +class RomReader: + """ + Helper class that simplifies reading data continously from a starting address. + Can read insertable binary files, in which it can also read data from ROM if provided. + """ + + rom_file: BufferedReader = None + insertable_file: BufferedReader = None + start_address: int = 0 + segment_data: SegmentData = dataclasses.field(default_factory=dict) + insertable: InsertableBinaryData = None + address: int = dataclasses.field(init=False) + + def __post_init__(self): + self.address = self.start_address + if self.insertable_file and not self.insertable: + self.insertable = InsertableBinaryData().read(self.insertable_file) + assert self.insertable or self.rom_file + + def branch(self, start_address=-1): + start_address = self.address if start_address == -1 else start_address + if self.read_int(1, specific_address=start_address) is None: + if self.insertable and self.rom_file: + return RomReader(self.rom_file, start_address=start_address, segment_data=self.segment_data) + return None + return RomReader( + self.rom_file, + self.insertable_file, + start_address, + self.segment_data, + self.insertable, + ) + + def skip(self, size: int): + self.address += size + + def read_data(self, size=-1, specific_address=-1): + if specific_address == -1: + address = self.address + self.skip(size) + else: + address = specific_address + + if self.insertable: + data = self.insertable.data[address : address + size] + else: + self.rom_file.seek(address) + data = self.rom_file.read(size) + if size > 0 and not data: + raise IndexError(f"Value at {intToHex(address)} not present in data.") + return data + + def read_ptr(self, specific_address=-1): + address = self.address if specific_address == -1 else specific_address + ptr = self.read_int(4, specific_address=specific_address) + if self.insertable and address in self.insertable.ptrs: + return ptr + if ptr and self.segment_data: + return decodeSegmentedAddr(ptr.to_bytes(4, "big"), self.segment_data) + return ptr + + def read_int(self, size=4, signed=False, specific_address=-1): + return int.from_bytes(self.read_data(size, specific_address), "big", signed=signed) + + def read_float(self, size=4, specific_address=-1): + return struct.unpack(">f", self.read_data(size, specific_address))[0] + + def read_str(self, specific_address=-1): + ptr = self.read_ptr() if specific_address == -1 else specific_address + if not ptr: + return None + branch = self.branch(ptr) + text_data = bytearray() + while True: + byte = branch.read_data(1) + if byte == b"\x00" or not byte: + break + text_data.append(ord(byte)) + text = text_data.decode("utf-8") + return text + + +@dataclasses.dataclass +class BinaryExporter: + export_rom: Path + output_rom: Path + rom_file_output: BinaryIO = dataclasses.field(init=False) + temp_rom: Path = dataclasses.field(init=False) + + @property + def tell(self): + return self.rom_file_output.tell() + + def __enter__(self): + export_rom_checks(self.export_rom) + print(f"Binary export started, exporting to {self.output_rom}") + self.temp_rom = temp_file_path(self.output_rom) + print(f'Copying "{self.export_rom}" to temporary file "{self.temp_rom}".') + shutil.copy(self.export_rom, self.temp_rom) + self.rom_file_output = self.temp_rom.open("rb+") + return self + + def write_to_range(self, start_address: int, end_address: int, data: bytes | bytearray): + address_range_str = f"[{intToHex(start_address)}, {intToHex(end_address)}]" + if end_address < start_address: + raise PluginError(f"Start address is higher than the end address: {address_range_str}") + if start_address + len(data) > end_address: + raise PluginError( + f"Data ({len(data) / 1000.0} kb) does not fit in range {address_range_str} " + f"({(end_address - start_address) / 1000.0} kb).", + ) + print(f"Writing {len(data) / 1000.0} kb to {address_range_str} ({(end_address - start_address) / 1000.0} kb))") + self.write(data, start_address) + + def seek(self, offset: int, whence: int = 0): + self.rom_file_output.seek(offset, whence) + + def read(self, n=-1, offset=-1): + if offset != -1: + self.seek(offset) + return self.rom_file_output.read(n) + + def write(self, s: bytes, offset=-1): + if offset != -1: + self.seek(offset) + return self.rom_file_output.write(s) + + def __exit__(self, exc_type, exc_value, traceback): + if self.temp_rom.exists(): + print(f"Closing temporary file {self.temp_rom}.") + self.rom_file_output.close() + else: + raise FileNotFoundError(f"Temporary file {self.temp_rom} does not exist?") + if exc_value: + print("Deleting temporary file because of exception.") + os.remove(self.temp_rom) + print("Type:", exc_type, "\nValue:", exc_value, "\nTraceback:", traceback) + else: + print(f"Moving temporary file to {self.output_rom}.") + if os.path.exists(self.output_rom): + os.remove(self.output_rom) + self.temp_rom.rename(self.output_rom) + + +@dataclasses.dataclass +class DMATableElement: + offset: int = 0 + size: int = 0 + address: int = 0 + end_address: int = 0 + + +@dataclasses.dataclass +class DMATable: + address_place_holder: int = 0 + entries: list[DMATableElement] = dataclasses.field(default_factory=list) + data: bytearray = dataclasses.field(default_factory=bytearray) + address: int = 0 + end_address: int = 0 + + def to_binary(self): + print( + f"Generating DMA table with {len(self.entries)} entries", + f"and {len(self.data)} bytes of data", + ) + data = bytearray() + data.extend(len(self.entries).to_bytes(4, "big", signed=False)) + data.extend(self.address_place_holder.to_bytes(4, "big", signed=False)) + + entries_offset = 8 + entries_length = len(self.entries) * 8 + entrie_data_offset = entries_offset + entries_length + + for entrie in self.entries: + offset = entrie_data_offset + entrie.offset + data.extend(offset.to_bytes(4, "big", signed=False)) + data.extend(entrie.size.to_bytes(4, "big", signed=False)) + data.extend(self.data) + + return data + + def read_binary(self, reader: RomReader): + print("Reading DMA table at", intToHex(reader.start_address)) + self.address = reader.start_address + + num_entries = reader.read_int(4) # numEntries + self.address_place_holder = reader.read_int(4) # addrPlaceholder + + table_size = 0 + for _ in range(num_entries): + offset = reader.read_int(4) + size = reader.read_int(4) + address = self.address + offset + self.entries.append(DMATableElement(offset, size, address, address + size)) + end_of_entry = offset + size + if end_of_entry > table_size: + table_size = end_of_entry + self.end_address = self.address + table_size + print(f"Found {len(self.entries)} DMA entries") + return self + + +@dataclasses.dataclass +class IntArray: + data: np.ndarray + name: str = "" + wrap: int = 6 + wrap_start: int = 0 # -6 To replicate decomp animation index table formatting + + def to_binary(self): + return self.data.astype(">i2").tobytes() + + def to_c(self, c_data: StringIO | None = None, new_lines=1): + assert self.name, "Array must have a name" + data = self.data + byte_count = data.itemsize + data_type = f"{'s' if data.dtype == np.int16 else 'u'}{byte_count * 8}" + print(f'Generating {data_type} array "{self.name}" with {len(self.data)} elements') + + c_data = c_data or StringIO() + c_data.write(f"// {len(self.data)}\n") + c_data.write(f"static const {data_type} {toAlnum(self.name)}[] = {{\n\t") + i = self.wrap_start + for value in self.data: + c_data.write(f"{intToHex(value, byte_count, False)}, ") + i += 1 + if i >= self.wrap: + c_data.write("\n\t") + i = 0 + + c_data.write("\n};" + ("\n" * new_lines)) + return c_data diff --git a/fast64_internal/sm64/sm64_collision.py b/fast64_internal/sm64/sm64_collision.py index 6e2907f0c..d3982cc49 100644 --- a/fast64_internal/sm64/sm64_collision.py +++ b/fast64_internal/sm64/sm64_collision.py @@ -1,16 +1,11 @@ +from pathlib import Path import bpy, shutil, os, math, mathutils from bpy.utils import register_class, unregister_class from io import BytesIO -from .sm64_constants import ( - level_enums, - level_pointers, - enumLevelNames, - insertableBinaryTypes, - defaultExtendSegment4, -) -from .sm64_utility import export_rom_checks +from .sm64_constants import insertableBinaryTypes, defaultExtendSegment4 +from .sm64_utility import export_rom_checks, to_include_descriptor, update_actor_includes, write_or_delete_if_found from .sm64_objects import SM64_Area, start_process_sm64_objects -from .sm64_level_parser import parseLevelAtPointer +from .sm64_level_parser import parse_level_binary from .sm64_rom_tweaks import ExtendBank0x04 from ..panels import SM64_Panel @@ -23,8 +18,6 @@ get64bitAlignedAddr, prop_split, getExportDir, - writeIfNotFound, - deleteIfFound, duplicateHierarchy, cleanupDuplicatedObjects, writeInsertableFile, @@ -34,11 +27,7 @@ tempName, bytesToHex, applyRotation, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, - writeBoxExportType, - enumExportHeaderType, + selectSingleObject, ) @@ -146,11 +135,11 @@ def to_c(self): if len(self.specials) > 0: data.source += "\tCOL_SPECIAL_INIT(" + str(len(self.specials)) + "),\n" for special in self.specials: - data.source += "\t" + special.to_c() + data.source += "\t" + special.to_c(1) + ",\n" if len(self.water_boxes) > 0: data.source += "\tCOL_WATER_BOX_INIT(" + str(len(self.water_boxes)) + "),\n" for waterBox in self.water_boxes: - data.source += "\t" + waterBox.to_c() + data.source += "\t" + waterBox.to_c(1) + ",\n" data.source += "\tCOL_END()\n" + "};\n" return data @@ -331,31 +320,24 @@ def exportCollisionC( cDefFile.write(cDefine) cDefFile.close() - if headerType == "Actor": - # Write to group files - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"') - writeIfNotFound(groupPathH, '\n#include "' + name + '/collision_header.h"', "\n#endif") - - elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"') - writeIfNotFound(groupPathH, '\n#include "levels/' + levelName + "/" + name + '/collision_header.h"', "\n#endif") + data_includes = [Path("collision.inc.c")] + if writeRoomsFile: + data_includes.append(Path("rooms.inc.c")) + update_actor_includes( + headerType, groupName, Path(dirPath), name, levelName, data_includes, [Path("collision_header.h")] + ) + if not writeRoomsFile: # TODO: Could be done better + if headerType == "Actor": + group_path_c = Path(dirPath, f"{groupName}.c") + write_or_delete_if_found(group_path_c, to_remove=[to_include_descriptor(Path(name, "rooms.inc.c"))]) + elif headerType == "Level": + group_path_c = Path(dirPath, "leveldata.c") + write_or_delete_if_found( + group_path_c, + to_remove=[ + to_include_descriptor(Path(name, "rooms.inc.c"), Path("levels", levelName, name, "rooms.inc.c")), + ], + ) return cDefine @@ -377,8 +359,7 @@ def exportCollisionInsertableBinary(obj, transformMatrix, filepath, includeSpeci def exportCollisionCommon(obj, transformMatrix, includeSpecials, includeChildren, name, areaIndex): - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) + selectSingleObject(obj) # dict of collisionType : faces collisionDict = {} @@ -387,7 +368,7 @@ def exportCollisionCommon(obj, transformMatrix, includeSpecials, includeChildren try: addCollisionTriangles(tempObj, collisionDict, includeChildren, transformMatrix, areaIndex) if not collisionDict: - raise PluginError("No collision data to export") + raise PluginError("No collision data to export", PluginError.exc_warn) cleanupDuplicatedObjects(allObjs) obj.select_set(True) bpy.context.view_layer.objects.active = obj @@ -537,7 +518,7 @@ def execute(self, context): romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") - levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.colExportLevel]) + levelParsed = parse_level_binary(romfileOutput, props.level_name) segmentData = levelParsed.segmentData if context.scene.fast64.sm64.extend_bank_4: @@ -606,6 +587,7 @@ class SM64_ExportCollisionPanel(SM64_Panel): def draw(self, context): col = self.layout.column() propsColE = col.operator(SM64_ExportCollision.bl_idname) + props = context.scene.fast64.sm64.combined_export col.prop(context.scene, "colIncludeChildren") if context.scene.fast64.sm64.export_type == "Insertable Binary": @@ -613,7 +595,7 @@ def draw(self, context): else: prop_split(col, context.scene, "colStartAddr", "Start Address") prop_split(col, context.scene, "colEndAddr", "End Address") - prop_split(col, context.scene, "colExportLevel", "Level Used By Collision") + prop_split(col, props, "level_name", "Level") col.prop(context.scene, "set_addr_0x2A") if context.scene.set_addr_0x2A: prop_split(col, context.scene, "addr_0x2A", "0x2A Behaviour Command Address") @@ -642,9 +624,6 @@ def sm64_col_register(): register_class(cls) # Collision - bpy.types.Scene.colExportLevel = bpy.props.EnumProperty( - items=level_enums, name="Level Used By Collision", default="WF" - ) bpy.types.Scene.addr_0x2A = bpy.props.StringProperty(name="0x2A Behaviour Command Address", default="21A9CC") bpy.types.Scene.set_addr_0x2A = bpy.props.BoolProperty(name="Overwrite 0x2A Behaviour Command") bpy.types.Scene.colStartAddr = bpy.props.StringProperty(name="Start Address", default="11D8930") @@ -673,7 +652,6 @@ def sm64_col_register(): def sm64_col_unregister(): # Collision - del bpy.types.Scene.colExportLevel del bpy.types.Scene.addr_0x2A del bpy.types.Scene.set_addr_0x2A del bpy.types.Scene.colStartAddr diff --git a/fast64_internal/sm64/sm64_constants.py b/fast64_internal/sm64/sm64_constants.py index fd1893770..11053ba82 100644 --- a/fast64_internal/sm64/sm64_constants.py +++ b/fast64_internal/sm64/sm64_constants.py @@ -1,3 +1,6 @@ +import dataclasses +from typing import Any, Iterable, TypeVar + # RAM address used in evaluating switch for hatless Mario marioHatSwitch = 0x80277740 marioLowPolySwitch = 0x80277150 @@ -27,6 +30,28 @@ "metal": 0x9EC, } +NULL = 0x00000000 + +MIN_U8 = 0 +MAX_U8 = (2**8) - 1 + +MIN_S8 = -(2**7) +MAX_S8 = (2**7) - 1 + +MIN_S16 = -(2**15) +MAX_S16 = (2**15) - 1 + +MIN_U16 = 0 +MAX_U16 = 2**16 - 1 + +MIN_S32 = -(2**31) +MAX_S32 = 2**31 - 1 + +MIN_U32 = 0 +MAX_U32 = 2**32 - 1 + +SegmentData = dict[int, tuple[int, int]] + commonGeolayoutPointers = { "Dorrie": [2039136, "HMC"], "Bowser": [1809204, "BFB"], @@ -35,75 +60,78 @@ } -level_enums = [ - ("HH", "Big Boo's Haunt", "HH"), # Originally Haunted House - ("CCM", "Cool Cool Mountain", "CCM"), - ("IC", "Inside Castle", "IC"), - ("HMC", "Hazy Maze Cave", "HMC"), - ("SSL", "Shifting Sand Land", "SSL"), - ("BOB", "Bob-Omb's Battlefield", "BOB"), - ("SML", "Snow Man's land", "SML"), - ("WDW", "Wet Dry World", "WDW"), - ("JRB", "Jolly Roger Bay", "JRB"), - ("THI", "Tiny Huge Island", "THI"), - ("TTC", "Tick Tock Clock", "TTC"), - ("RR", "Rainbow Ride", "RR"), - ("CG", "Castle Grounds", "CG"), - ("BFC", "Bowser First Course", "BFC"), - ("VC", "Vanish Cap", "VC"), - ("BFS", "Bowser's Fire Sea", "BFS"), - ("SA", "Secret Aquarium", "SA"), - ("BTC", "Bowser Third Course", "BTC"), - ("LLL", "Lethal Lava Land", "LLL"), - ("DDD", "Dire Dire Docks", "DDD"), - ("WF", "Whomp's Fortress", "WF"), - ("PIC", "Picture at the end", "PIC"), - ("CC", "Castle Courtyard", "CC"), - ("PSS", "Peach's Secret Slide", "PSS"), - ("MC", "Metal Cap", "MC"), - ("WC", "Wing Cap", "WC"), - ("BFB", "Bowser First Battle", "BFB"), - ("RC", "Rainbow Clouds", "RC"), - ("BSB", "Bowser Second Battle", "BSB"), - ("BTB", "Bowser Third Battle", "BTB"), - ("TTM", "Tall Tall Mountain", "TTM"), +OLD_BINARY_LEVEL_ENUMS = [ + "bbh", # Big Boo's Haunt + "ccm", # Cool Cool Mountain + "castle_inside", # Inside Castle + "hmc", # Hazy Maze Cave + "ssl", # Shifting Sand Land + "bob", # Bob-Omb's Battlefield + "sl", # Snow Man's Land + "wdw", # Wet Dry World + "jrb", # Jolly Roger Bay + "thi", # Tiny Huge Island + "ttc", # Tick Tock Clock + "rr", # Rainbow Ride + "castle_grounds", # Castle Grounds + "bitdw", # Bowser In The Dark World + "vcutm", # Vanish Cap + "bitfs", # Bowser's Fire Sea + "sa", # Secret Aquarium + "bits", # Bowser In The Sky + "lll", # Lethal Lava Land + "ddd", # Dire Dire Docks + "wf", # Whomp's Fortress + "ending", # Picture at the end + "castle_courtyard", # Castle Courtyard + "pss", # Peach's Secret Slide + "cotmc", # Cavern Of The Metal Cap + "totwc", # Tower Of The Wing Cap + "bowser_1", # Bowser Battle 1 + "wmotr", # Wing Mario Over The Rainbow + "bowser_2", # Bowser Battle 2 + "bowser_3", # Bowser Battle 3 + "ttm", # Tall Tall Mountain ] enumLevelNames = [ - ("Custom", "Custom", "Custom"), - ("bbh", "Big Boo's Haunt", "Big Boo's Haunt"), - ("bitdw", "Bowser In The Dark World", "Bowser In The Dark World"), - ("bitfs", "Bowser In The Fire Sea", "Bowser In The Fire Sea"), - ("bits", "Bowser In The Sky", "Bowser In The Sky"), - ("bob", "Bob-omb Battlefield", "Bob-omb Battlefield"), - ("bowser_1", "Bowser 1", "Bowser 1"), - ("bowser_2", "Bowser 2", "Bowser 2"), - ("bowser_3", "Bowser 3", "Bowser 3"), - ("castle_courtyard", "Castle Courtyard", "Castle Courtyard"), - ("castle_grounds", "Castle Grounds", "Castle Grounds"), - ("castle_inside", "Castle Inside", "Castle Inside"), - ("ccm", "Cool Cool Mountain", "Cool Cool Mountain"), - ("cotmc", "Cavern Of The Metal Cap", "Cavern Of The Metal Cap"), - ("ddd", "Dire Dire Docks", "Dire Dire Docks"), - ("ending", "Ending", "Ending"), - ("hmc", "Hazy Maze Cave", "Hazy Maze Cave"), - ("intro", "Intro", "Intro"), - ("jrb", "Jolly Roger Bay", "Jolly Roger Bay"), - ("lll", "Lethal Lava Land", "Lethal Lava Land"), - ("menu", "Menu", "Menu"), - ("pss", "Peach's Secret Slide", "Peach's Secret Slide"), - ("rr", "Rainbow Ride", "Rainbow Ride"), - ("sa", "Secret Aquarium", "Secret Aquarium"), - ("sl", "Snowman's Land", "Snowman's Land"), - ("ssl", "Shifting Sand Land", "Shifting Sand Land"), - ("thi", "Tiny Huge Island", "Tiny Huge Island"), - ("totwc", "Tower Of The Wing Cap", "Tower Of The Wing Cap"), - ("ttc", "Tick Tock Clock", "Tick Tock Clock"), - ("ttm", "Tall Tall Mountain", "Tall Tall Mountain"), - ("vcutm", "Vanish Cap Under The Moat", "Vanish Cap Under The Moat"), - ("wdw", "Wet Dry World", "Wet Dry World"), - ("wf", "Whomp's Fortress", "Whomp's Fortress"), - ("wmotr", "Wing Mario Over The Rainbow", "Wing Mario Over The Rainbow"), + ("bob", "Bob-omb Battlefield", "Bob-omb Battlefield", 5), + ("wf", "Whomp's Fortress", "Whomp's Fortress", 32), + ("jrb", "Jolly Roger Bay", "Jolly Roger Bay", 18), + ("ccm", "Cool Cool Mountain", "Cool Cool Mountain", 12), + ("bbh", "Big Boo's Haunt", "Big Boo's Haunt", 1), + ("hmc", "Hazy Maze Cave", "Hazy Maze Cave", 16), + ("lll", "Lethal Lava Land", "Lethal Lava Land", 19), + ("ssl", "Shifting Sand Land", "Shifting Sand Land", 25), + ("ddd", "Dire Dire Docks", "Dire Dire Docks", 14), + ("sl", "Snowman's Land", "Snowman's Land", 24), + ("wdw", "Wet Dry World", "Wet Dry World", 31), + ("ttm", "Tall Tall Mountain", "Tall Tall Mountain", 29), + ("thi", "Tiny Huge Island", "Tiny Huge Island", 26), + ("ttc", "Tick Tock Clock", "Tick Tock Clock", 28), + ("rr", "Rainbow Ride", "Rainbow Ride", 22), + ("", "Castle", "", 34), + ("castle_courtyard", "Castle Courtyard", "Castle Courtyard", 9), + ("castle_grounds", "Castle Grounds", "Castle Grounds", 10), + ("castle_inside", "Castle Inside", "Castle Inside", 11), + ("", "Extra Courses", "", 35), + ("totwc", "Tower Of The Wing Cap", "Tower Of The Wing Cap", 27), + ("cotmc", "Cavern Of The Metal Cap", "Cavern Of The Metal Cap", 13), + ("vcutm", "Vanish Cap Under The Moat", "Vanish Cap Under The Moat", 30), + ("pss", "Peach's Secret Slide", "Peach's Secret Slide", 21), + ("sa", "Secret Aquarium", "Secret Aquarium", 23), + ("wmotr", "Wing Mario Over The Rainbow", "Wing Mario Over The Rainbow", 33), + ("bitdw", "Bowser In The Dark World", "Bowser In The Dark World", 2), + ("bowser_1", "Bowser Battle 1", "Bowser Battle 1", 6), + ("bitfs", "Bowser In The Fire Sea", "Bowser In The Fire Sea", 3), + ("bowser_2", "Bowser Battle 2", "Bowser Battle 2", 7), + ("bits", "Bowser In The Sky", "Bowser In The Sky", 4), + ("bowser_3", "Bowser Battle 3", "Bowser Battle 3", 8), + ("", "Special", "", 36), + ("Custom", "Custom", "Custom", 0), + ("intro", "Intro", "Intro", 17), + ("menu", "Menu", "Menu", 20), + ("ending", "Picture at the end", "Cake screen", 15), ] levelIDNames = { @@ -261,40 +289,49 @@ def __init__(self, geoAddr, level, switchDict): } level_pointers = { - "HH": 0x2AC094, - "CCM": 0x2AC0A8, - "IC": 0x2AC0BC, - "HMC": 0x2AC0D0, - "SSL": 0x2AC0E4, - "BOB": 0x2AC0F8, - "SML": 0x2AC10C, - "WDW": 0x2AC120, - "JRB": 0x2AC134, - "THI": 0x2AC148, - "TTC": 0x2AC15C, - "RR": 0x2AC170, - "CG": 0x2AC184, - "BFC": 0x2AC198, - "VC": 0x2AC1AC, - "BFS": 0x2AC1C0, - "SA": 0x2AC1D4, - "BTC": 0x2AC1E8, - "LLL": 0x2AC1FC, - "DDD": 0x2AC210, - "WF": 0x2AC224, - "PIC": 0x2AC238, - "CC": 0x2AC24C, - "PSS": 0x2AC260, - "MC": 0x2AC274, - "WC": 0x2AC288, - "BFB": 0x2AC29C, - "RC": 0x2AC2B0, - "BSB": 0x2AC2C4, - "BTB": 0x2AC2D8, - "TTM": 0x2AC2EC, + "bbh": 0x2AC094, + "ccm": 0x2AC0A8, + "castle_inside": 0x2AC0BC, + "hmc": 0x2AC0D0, + "ssl": 0x2AC0E4, + "bob": 0x2AC0F8, + "sl": 0x2AC10C, + "wdw": 0x2AC120, + "jrb": 0x2AC134, + "thi": 0x2AC148, + "ttc": 0x2AC15C, + "rr": 0x2AC170, + "castle_grounds": 0x2AC184, + "bitdw": 0x2AC198, + "vcutm": 0x2AC1AC, + "bitfs": 0x2AC1C0, + "sa": 0x2AC1D4, + "bits": 0x2AC1E8, + "lll": 0x2AC1FC, + "ddd": 0x2AC210, + "wf": 0x2AC224, + "ending": 0x2AC238, + "castle_courtyard": 0x2AC24C, + "pss": 0x2AC260, + "cotmc": 0x2AC274, + "totwc": 0x2AC288, + "bowser_1": 0x2AC29C, + "wmotr": 0x2AC2B0, + "bowser_2": 0x2AC2C4, + "bowser_3": 0x2AC2D8, + "ttm": 0x2AC2EC, + "menu": 0x2A6130, + "intro": 0x269EB0, } -insertableBinaryTypes = {"Display List": 0, "Geolayout": 1, "Animation": 2, "Collision": 3} +insertableBinaryTypes = { + "Display List": 0, + "Geolayout": 1, + "Animation": 2, + "Collision": 3, + "Animation Table": 4, + "Animation DMA Table": 5, +} enumBehaviourPresets = [ ("Custom", "Custom", "Custom"), ("1300407c", "1 Up", "1 Up"), @@ -2098,7 +2135,7 @@ def __init__(self, geoAddr, level, switchDict): ("group9", "group9", "Haunted Objects (Boo, Mad Piano etc.)"), ("group10", "group10", "Peach/Yoshi"), ("group11", "group11", "THI Ojbects (Lakitu, Wiggler, Bubba)"), - ("Do Not Write", "Do Not Write", "Do Not Write"), + ("None", "None", "None"), ("Custom", "Custom", "Custom"), ] @@ -2110,10 +2147,17 @@ def __init__(self, geoAddr, level, switchDict): ("group15", "group15", "Castle Objects (MIPS, Toad etc.)"), ("group16", "group16", "Ice Objects (Chill Bully, Moneybags)"), ("group17", "group17", "Cave Objects (Swoop, Scuttlebug, Dorrie etc.)"), - ("Do Not Write", "Do Not Write", "Do Not Write"), + ("None", "None", "None"), + ("Custom", "Custom", "Custom"), +] + +groups_seg8 = [ + ("common0", "common0", "Generic course objects (Goomba, Bob-ombs, Cannon etc.)"), + ("None", "None", "None"), ("Custom", "Custom", "Custom"), ] + # groups you can use for the combined object export groups_obj_export = [ ("common0", "common0", "chuckya, boxes, blue coin switch"), @@ -2139,219 +2183,1642 @@ def __init__(self, geoAddr, level, switchDict): ("Custom", "Custom", "Custom"), ] -marioAnimations = [ - # ( Adress, "Animation name" ), - (5162640, "0 - Slow ledge climb up"), - (5165520, "1 - Fall over backwards"), - (5165544, "2 - Backward air kb"), - (5172396, "3 - Dying on back"), - (5177044, "4 - Backflip"), - (5179584, "5 - Climbing up pole"), - (5185656, "6 - Grab pole short"), - (5186824, "7 - Grab pole swing part 1"), - (5186848, "8 - Grab pole swing part 2"), - (5191920, "9 - Handstand idle"), - (5194740, "10 - Handstand jump"), - (5194764, "11 - Start handstand"), - (5188592, "12 - Return from handstand"), - (5196388, "13 - Idle on pole"), - (5197436, "14 - A pose"), - (5197792, "15 - Skid on ground"), - (5197816, "16 - Stop skid"), - (5199596, "17 - Crouch from fast longjump"), - (5201048, "18 - Crouch from a slow longjump"), - (5202644, "19 - Fast longjump"), - (5204600, "20 - Slow longjump"), - (5205980, "21 - Airborne on stomach"), - (5207188, "22 - Walk with light object"), - (5211916, "23 - Run with light object"), - (5215136, "24 - Slow walk with light object"), - (5219864, "25 - Shivering and warming hands"), - (5225496, "26 - Shivering return to idle "), - (5226920, "27 - Shivering"), - (5230056, "28 - Climb down on ledge"), - (5231112, "29 - Credits - Waving"), - (5232768, "30 - Credits - Look up"), - (5234576, "31 - Credits - Return from look up"), - (5235700, "32 - Credits - Raising hand"), - (5243100, "33 - Credits - Lowering hand"), - (5245988, "34 - Credits - Taking off cap"), - (5248016, "35 - Credits - Start walking and look up"), - (5256508, "36 - Credits - Look back then run"), - (5266160, "37 - Final Bowser - Raise hand and spin"), - (5274456, "38 - Final Bowser - Wing cap take off"), - (5282084, "39 - Credits - Peach sign"), - (5291340, "40 - Stand up from lava boost"), - (5292628, "41 - Fire/Lava burn"), - (5293488, "42 - Wing cap flying"), - (5295016, "43 - Hang on owl"), - (5296876, "44 - Land on stomach"), - (5296900, "45 - Air forward kb"), - (5302796, "46 - Dying on stomach"), - (5306100, "47 - Suffocating"), - (5313796, "48 - Coughing"), - (5319500, "49 - Throw catch key"), - (5330436, "50 - Dying fall over"), - (5338604, "51 - Idle on ledge"), - (5341720, "52 - Fast ledge grab"), - (5343296, "53 - Hang on ceiling"), - (5347276, "54 - Put cap on"), - (5351252, "55 - Take cap off then on"), - (5358356, "56 - Quickly put cap on"), - (5359476, "57 - Head stuck in ground"), - (5372172, "58 - Ground pound landing"), - (5372824, "59 - Triple jump ground-pound"), - (5374304, "60 - Start ground-pound"), - (5374328, "61 - Ground-pound"), - (5375380, "62 - Bottom stuck in ground"), - (5387148, "63 - Idle with light object"), - (5390520, "64 - Jump land with light object"), - (5391892, "65 - Jump with light object"), - (5392704, "66 - Fall land with light object"), - (5393936, "67 - Fall with light object"), - (5394296, "68 - Fall from sliding with light object"), - (5395224, "69 - Sliding on bottom with light object"), - (5395248, "70 - Stand up from sliding with light object"), - (5396716, "71 - Riding shell"), - (5397832, "72 - Walking"), - (5403208, "73 - Forward flip"), - (5404784, "74 - Jump riding shell"), - (5405676, "75 - Land from double jump"), - (5407340, "76 - Double jump fall"), - (5408288, "77 - Single jump"), - (5408312, "78 - Land from single jump"), - (5411044, "79 - Air kick"), - (5412900, "80 - Double jump rise"), - (5413596, "81 - Start forward spinning"), - (5414876, "82 - Throw light object"), - (5416032, "83 - Fall from slide kick"), - (5418280, "84 - Bend kness riding shell"), - (5419872, "85 - Legs stuck in ground"), - (5431416, "86 - General fall"), - (5431440, "87 - General land"), - (5433276, "88 - Being grabbed"), - (5434636, "89 - Grab heavy object"), - (5437964, "90 - Slow land from dive"), - (5441520, "91 - Fly from cannon"), - (5442516, "92 - Moving right while hanging"), - (5444052, "93 - Moving left while hanging"), - (5445472, "94 - Missing cap"), - (5457860, "95 - Pull door walk in"), - (5463196, "96 - Push door walk in"), - (5467492, "97 - Unlock door"), - (5480428, "98 - Start reach pocket"), - (5481448, "99 - Reach pocket"), - (5483352, "100 - Stop reach pocket"), - (5484876, "101 - Ground throw"), - (5486852, "102 - Ground kick"), - (5489076, "103 - First punch"), - (5489740, "104 - Second punch"), - (5490356, "105 - First punch fast"), - (5491396, "106 - Second punch fast"), - (5492732, "107 - Pick up light object"), - (5493948, "108 - Pushing"), - (5495508, "109 - Start riding shell"), - (5497072, "110 - Place light object"), - (5498484, "111 - Forward spinning"), - (5498508, "112 - Backward spinning"), - (5498884, "113 - Breakdance"), - (5501240, "114 - Running"), - (5501264, "115 - Running (unused)"), - (5505884, "116 - Soft back kb"), - (5508004, "117 - Soft front kb"), - (5510172, "118 - Dying in quicksand"), - (5515096, "119 - Idle in quicksand"), - (5517836, "120 - Move in quicksand"), - (5528568, "121 - Electrocution"), - (5532480, "122 - Shocked"), - (5533160, "123 - Backward kb"), - (5535796, "124 - Forward kb"), - (5538372, "125 - Idle heavy object"), - (5539764, "126 - Stand against wall"), - (5544580, "127 - Side step left"), - (5548480, "128 - Side step right"), - (5553004, "129 - Start sleep idle"), - (5557588, "130 - Start sleep scratch"), - (5563636, "131 - Start sleep yawn"), - (5568648, "132 - Start sleep sitting"), - (5573680, "133 - Sleep idle"), - (5574280, "134 - Sleep start laying"), - (5577460, "135 - Sleep laying"), - (5579300, "136 - Dive"), - (5579324, "137 - Slide dive"), - (5580860, "138 - Ground bonk"), - (5584116, "139 - Stop slide light object"), - (5587364, "140 - Slide kick"), - (5588288, "141 - Crouch from slide kick"), - (5589652, "142 - Slide motionless"), - (5589676, "143 - Stop slide"), - (5591572, "144 - Fall from slide"), - (5592860, "145 - Slide"), - (5593404, "146 - Tiptoe"), - (5599280, "147 - Twirl land"), - (5600160, "148 - Twirl"), - (5600516, "149 - Start twirl"), - (5601072, "150 - Stop crouching"), - (5602028, "151 - Start crouching"), - (5602720, "152 - Crouching"), - (5605756, "153 - Crawling"), - (5613048, "154 - Stop crawling"), - (5613968, "155 - Start crawling"), - (5614876, "156 - Summon star"), - (5620036, "157 - Return star approach door"), - (5622256, "158 - Backwards water kb"), - (5626540, "159 - Swim with object part 1"), - (5627592, "160 - Swim with object part 2"), - (5628260, "161 - Flutter kick with object"), - (5629456, "162 - Action end with object in water"), - (5631180, "163 - Stop holding object in water"), - (5634048, "164 - Holding object in water"), - (5635976, "165 - Drowning part 1"), - (5641400, "166 - Drowning part 2"), - (5646324, "167 - Dying in water"), - (5649660, "168 - Forward kb in water"), - (5653848, "169 - Falling from water"), - (5655852, "170 - Swimming part 1"), - (5657100, "171 - Swimming part 2"), - (5658128, "172 - Flutter kick"), - (5660112, "173 - Action end in water"), - (5662248, "174 - Pick up object in water"), - (5663480, "175 - Grab object in water part 2"), - (5665916, "176 - Grab object in water part 1"), - (5666632, "177 - Throw object in water"), - (5669328, "178 - Idle in water"), - (5671428, "179 - Star dance in water"), - (5678200, "180 - Return from in water star dance"), - (5680324, "181 - Grab bowser"), - (5680348, "182 - Swing bowser"), - (5682008, "183 - Release bowser"), - (5685264, "184 - Holding bowser"), - (5686316, "185 - Heavy throw"), - (5688660, "186 - Walk panting"), - (5689924, "187 - Walk with heavy object"), - (5694332, "188 - Turning part 1"), - (5694356, "189 - Turning part 2"), - (5696160, "190 - Side flip land"), - (5697196, "191 - Side flip"), - (5699408, "192 - Triple jump land"), - (5702136, "193 - Triple jump"), - (5704880, "194 - First person"), - (5710580, "195 - Idle head left"), - (5712800, "196 - Idle head right"), - (5715020, "197 - Idle head center"), - (5717240, "198 - Handstand left"), - (5719184, "199 - Handstand right"), - (5722304, "200 - Wake up from sleeping"), - (5724228, "201 - Wake up from laying"), - (5726444, "202 - Start tiptoeing"), - (5728720, "203 - Slide jump"), - (5728744, "204 - Start wallkick"), - (5730404, "205 - Star dance"), - (5735864, "206 - Return from star dance"), - (5737600, "207 - Forwards spinning flip"), - (5740584, "208 - Triple jump fly"), +BEHAVIOR_EXITS = [ + "RETURN", + "GOTO", + "END_LOOP", + "BREAK", + "BREAK_UNUSED", + "DEACTIVATE", +] + +BEHAVIOR_COMMANDS = [ + # Name, Size + ("BEGIN", 1), # bhv_cmd_begin + ("DELAY", 1), # bhv_cmd_delay + ("CALL", 1), # bhv_cmd_call + ("RETURN", 1), # bhv_cmd_return + ("GOTO", 1), # bhv_cmd_goto + ("BEGIN_REPEAT", 1), # bhv_cmd_begin_repeat + ("END_REPEAT", 1), # bhv_cmd_end_repeat + ("END_REPEAT_CONTINUE", 1), # bhv_cmd_end_repeat_continue + ("BEGIN_LOOP", 1), # bhv_cmd_begin_loop + ("END_LOOP", 1), # bhv_cmd_end_loop + ("BREAK", 1), # bhv_cmd_break + ("BREAK_UNUSED", 1), # bhv_cmd_break_unused + ("CALL_NATIVE", 2), # bhv_cmd_call_native + ("ADD_FLOAT", 1), # bhv_cmd_add_float + ("SET_FLOAT", 1), # bhv_cmd_set_float + ("ADD_INT", 1), # bhv_cmd_add_int + ("SET_INT", 1), # bhv_cmd_set_int + ("OR_INT", 1), # bhv_cmd_or_int + ("BIT_CLEAR", 1), # bhv_cmd_bit_clear + ("SET_INT_RAND_RSHIFT", 2), # bhv_cmd_set_int_rand_rshift + ("SET_RANDOM_FLOAT", 2), # bhv_cmd_set_random_float + ("SET_RANDOM_INT", 2), # bhv_cmd_set_random_int + ("ADD_RANDOM_FLOAT", 2), # bhv_cmd_add_random_float + ("ADD_INT_RAND_RSHIFT", 2), # bhv_cmd_add_int_rand_rshift + ("NOP_1", 1), # bhv_cmd_nop_1 + ("NOP_2", 1), # bhv_cmd_nop_2 + ("NOP_3", 1), # bhv_cmd_nop_3 + ("SET_MODEL", 1), # bhv_cmd_set_model + ("SPAWN_CHILD", 3), # bhv_cmd_spawn_child + ("DEACTIVATE", 1), # bhv_cmd_deactivate + ("DROP_TO_FLOOR", 1), # bhv_cmd_drop_to_floor + ("SUM_FLOAT", 1), # bhv_cmd_sum_float + ("SUM_INT", 1), # bhv_cmd_sum_int + ("BILLBOARD", 1), # bhv_cmd_billboard + ("HIDE", 1), # bhv_cmd_hide + ("SET_HITBOX", 2), # bhv_cmd_set_hitbox + ("NOP_4", 1), # bhv_cmd_nop_4 + ("DELAY_VAR", 1), # bhv_cmd_delay_var + ("BEGIN_REPEAT_UNUSED", 1), # bhv_cmd_begin_repeat_unused + ("LOAD_ANIMATIONS", 2), # bhv_cmd_load_animations + ("ANIMATE", 1), # bhv_cmd_animate + ("SPAWN_CHILD_WITH_PARAM", 3), # bhv_cmd_spawn_child_with_param + ("LOAD_COLLISION_DATA", 2), # bhv_cmd_load_collision_data + ("SET_HITBOX_WITH_OFFSET", 3), # bhv_cmd_set_hitbox_with_offset + ("SPAWN_OBJ", 3), # bhv_cmd_spawn_obj + ("SET_HOME", 1), # bhv_cmd_set_home + ("SET_HURTBOX", 2), # bhv_cmd_set_hurtbox + ("SET_INTERACT_TYPE", 2), # bhv_cmd_set_interact_type + ("SET_OBJ_PHYSICS", 5), # bhv_cmd_set_obj_physics + ("SET_INTERACT_SUBTYPE", 2), # bhv_cmd_set_interact_subtype + ("SCALE", 1), # bhv_cmd_scale + ("PARENT_BIT_CLEAR", 2), # bhv_cmd_parent_bit_clear + ("ANIMATE_TEXTURE", 1), # bhv_cmd_animate_texture + ("DISABLE_RENDERING", 1), # bhv_cmd_disable_rendering + ("SET_INT_UNUSED", 2), # bhv_cmd_set_int_unused + ("SPAWN_WATER_DROPLET", 2), # bhv_cmd_spawn_water_droplet ] +T = TypeVar("T") +DictOrVal = T | dict[str, T] | None +ListOrVal = T | list[T] | None + + +def as_list(val: ListOrVal[Any]) -> list[T]: + if isinstance(val, Iterable): + return list(val) + if val is None: + return [] + return [val] + + +def as_dict(val: DictOrVal[T], name: str = "") -> dict[str, T]: + """If val is a dict, returns it, otherwise returns {name: member}""" + if isinstance(val, dict): + return val + elif val is not None: + return {name: val} + return {} + + +def validate_dict(val: DictOrVal, val_type: type): + return all(isinstance(k, str) and isinstance(v, val_type) for k, v in as_dict(val).items()) + + +def validate_list(val: ListOrVal, val_type: type): + return all(isinstance(v, val_type) for v in as_list(val)) + + +@dataclasses.dataclass +class AnimInfo: + address: int + behaviours: DictOrVal[int] = dataclasses.field(default_factory=dict) + size: int | None = None # None means the size can be determined from the NULL delimiter + ignore_bone_count: bool = False + dma: bool = False + directory: str | None = None + names: list[str] = dataclasses.field(default_factory=list) + + def __post_init__(self): + assert isinstance(self.address, int) + assert validate_dict(self.behaviours, int) + assert self.size is None or isinstance(self.size, int) + assert isinstance(self.ignore_bone_count, bool) + assert isinstance(self.dma, bool) + assert self.directory is None or isinstance(self.directory, str) + assert validate_list(self.names, str) + + +@dataclasses.dataclass +class ModelIDInfo: + number: int + enum: str + + def __post_init__(self): + assert isinstance(self.number, int) + assert isinstance(self.enum, str) + + +@dataclasses.dataclass +class DisplaylistInfo: + address: int + # Displaylists are compressed, so their c name can´t be fetched from func_map like geolayouts + c_name: str + + def __post_init__(self): + assert isinstance(self.address, int) + assert isinstance(self.c_name, str) + + +@dataclasses.dataclass +class ModelInfo: + model_id: ListOrVal[ModelIDInfo] = dataclasses.field(default_factory=list) + geolayout: int | None = None + displaylist: DisplaylistInfo | None = None + + def __post_init__(self): + self.model_id = as_list(self.model_id) + assert validate_list(self.model_id, ModelIDInfo) + assert validate_list(self.geolayout, int) + assert validate_list(self.displaylist, DisplaylistInfo) + + +@dataclasses.dataclass +class CollisionInfo: + address: int + c_name: str + + def __post_init__(self): + assert isinstance(self.address, int) + assert isinstance(self.c_name, str) + + +@dataclasses.dataclass +class ActorPresetInfo: + decomp_path: str = None + level: str | None = None + group: str | None = None + animation: DictOrVal[AnimInfo] = dataclasses.field(default_factory=dict) + models: DictOrVal[ModelInfo] = dataclasses.field(default_factory=dict) + collision: DictOrVal[CollisionInfo] = dataclasses.field(default_factory=dict) + + def __post_init__(self): + assert self.decomp_path is not None and isinstance(self.decomp_path, str) + assert self.group is None or isinstance(self.group, str) + assert validate_dict(self.animation, AnimInfo) + assert validate_dict(self.models, ModelInfo) + assert validate_dict(self.collision, CollisionInfo) + group_to_level = { + "common0": "bbh", + "common1": "bbh", + "group0": "bbh", + "group1": "wf", + "group2": "lll", + "group3": "bob", + "group4": "jrb", + "group5": "ssl", + "group6": "ttm", + "group7": "ccm", + "group8": "vcutm", + "group9": "bbh", + "group10": "castle_grounds", + "group11": "thi", + "group12": "bowser_1", + "group13": "wdw", + "group14": "bob", + "group15": "castle_inside", + "group16": "ccm", + "group17": "hmc", + } + if self.level is None and self.group is not None: + self.level = group_to_level[self.group] + assert isinstance(self.level, str) + + @staticmethod + def get_member_as_dict(name: str, member: DictOrVal[T]): + return as_dict(member, name) + + +ACTOR_PRESET_INFO = { + "Amp": ActorPresetInfo( + decomp_path="actors/amp", + group="common0", + animation=AnimInfo( + address=0x8004034, + behaviours={"Circling Amp": 0x13003388, "Homing Amp": 0x13003354}, + names=["Moving"], + ignore_bone_count=True, + ), + models=ModelInfo(model_id=ModelIDInfo(0xC2, "MODEL_AMP"), geolayout=0xF000028), + ), + "Bird": ActorPresetInfo( + decomp_path="actors/bird", + group="group10", + animation=AnimInfo( + address=0x50009E8, + behaviours={"Bird": 0x13005354, "End Birds 1": 0x1300565C, "End Birds 2": 0x13005680}, + names=["Flying", "Gliding"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BIRDS"), geolayout=0xC000000), + ), + "Blargg": ActorPresetInfo( + decomp_path="actors/blargg", + group="group2", + animation=AnimInfo(address=0x500616C, names=["Idle", "Bite"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BLARGG"), geolayout=0xC000240), + ), + "Blue Coin Switch": ActorPresetInfo( + decomp_path="actors/blue_coin_switch", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x8C, "MODEL_BLUE_COIN_SWITCH"), geolayout=0xF000000), + collision=CollisionInfo(address=0x8000E98, c_name="blue_coin_switch_seg8_collision_08000E98"), + ), + "Blue Fish": ActorPresetInfo( + decomp_path="actors/blue_fish", + group="common1", + animation=AnimInfo(address=0x301C2B0, behaviours=0x13001B2C, names=["Swimming", "Diving"]), + models={ + "Fish": ModelInfo(model_id=ModelIDInfo(0xB9, "MODEL_FISH"), geolayout=0x16000C44), + "Fish (With Shadow)": ModelInfo(model_id=ModelIDInfo(0xBA, "MODEL_FISH_SHADOW"), geolayout=0x16000BEC), + }, + ), + "Bobomb": ActorPresetInfo( + decomp_path="actors/bobomb", + group="common0", + animation=AnimInfo( + address=0x802396C, + behaviours={"Bobomb": 0x13003174, "Bobomb Buddy": 0x130031DC, "Bobomb Buddy (Opens Cannon)": 0x13003228}, + names=["Walking", "Strugling"], + ), + models={ + "Bobomb": ModelInfo(model_id=ModelIDInfo(0xBC, "MODEL_BLACK_BOBOMB"), geolayout=0xF0007B8), + "Bobomb Buddy": ModelInfo(model_id=ModelIDInfo(0xC3, "MODEL_BOBOMB_BUDDY"), geolayout=0xF0008F4), + }, + ), + "Bowser Bomb": ActorPresetInfo( + decomp_path="actors/bomb", + group="group12", + models=ModelInfo( + model_id=[ModelIDInfo(0x65, "MODEL_BOWSER_BOMB_CHILD_OBJ"), ModelIDInfo(0xB3, "MODEL_BOWSER_BOMB")], + geolayout=0xD000BBC, + ), + ), + "Boo": ActorPresetInfo( + decomp_path="actors/boo", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BOO"), geolayout=0xC000224), + ), + "Boo (Inside Castle)": ActorPresetInfo( + decomp_path="actors/boo_castle", + group="group15", + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_BOO_CASTLE"), geolayout=0xD0005B0), + ), + "Bookend": ActorPresetInfo( + decomp_path="actors/book", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_BOOKEND"), geolayout=0xC0000C0), + ), + "Bookend Part": ActorPresetInfo( + decomp_path="actors/bookend", + group="group9", + animation=AnimInfo(address=0x5002540, behaviours=0x1300506C, names=["Opening Mouth", "Bite", "Closed"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_BOOKEND_PART"), geolayout=0xC000000), + ), + "Metal Ball": ActorPresetInfo( + decomp_path="actors/bowling_ball", + group="common0", + models={ + "Bowling Ball": ModelInfo(model_id=ModelIDInfo(0xB4, "MODEL_BOWLING_BALL"), geolayout=0xF000640), + "Trajectory Marker Ball": ModelInfo( + model_id=ModelIDInfo(0xE1, "MODEL_TRAJECTORY_MARKER_BALL"), geolayout=0xF00066C + ), + }, + ), + "Bowser": ActorPresetInfo( + decomp_path="actors/bowser", + group="group12", + animation=AnimInfo( + address=0x60577E0, + behaviours=0x13001850, + size=27, + ignore_bone_count=True, + names=[ + "Stand Up", + "Stand Up (Unused)", + "Shaking", + "Grabbed", + "Broken Animation (Unused)", + "Fall Down", + "Fire Breath", + "Jump", + "Jump Stop", + "Jump Start", + "Dance", + "Fire Breath Up", + "Idle", + "Slow Gait", + "Look Down Stop Walk", + "Look Up Start Walk", + "Flip Down", + "Lay Down", + "Run Start", + "Run", + "Run Stop", + "Run Slip", + "Fire Breath Quick", + "Edge Move", + "Edge Stop", + "Flip", + "Stand Up From Flip", + ], + ), + models={ + "Bowser": ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_BOWSER"), geolayout=0xD000AC4), + "Bowser (No Shadow)": ModelInfo(model_id=ModelIDInfo(0x69, "MODEL_BOWSER_NO_SHADOW"), geolayout=0xD000B40), + }, + ), + "Bowser Flame": ActorPresetInfo( + decomp_path="actors/bowser_flame", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_BOWSER_FLAMES"), geolayout=0xD000000), + ), + "Bowser Key": ActorPresetInfo( + decomp_path="actors/bowser_key", + group="common1", + animation=AnimInfo( + address=0x30172D0, + behaviours={"Bowser Key": 0x13001BB4, "Bowser Key (Cutscene)": 0x13001BD4}, + size=2, + names=["Unlock Door", "Course Exit"], + ), + models={ + "Bowser Key (Cutscene)": ModelInfo( + model_id=ModelIDInfo(0xC8, "MODEL_BOWSER_KEY_CUTSCENE"), geolayout=0x16000AB0 + ), + "Bowser Key": ModelInfo(model_id=ModelIDInfo(0xCC, "MODEL_BOWSER_KEY"), geolayout=0x16000A84), + }, + ), + "Breakable Box": ActorPresetInfo( + decomp_path="actors/breakable_box", + group="common0", + models={ + "Breakable Box": ModelInfo(model_id=ModelIDInfo(0x81, "MODEL_BREAKABLE_BOX"), geolayout=0xF0005D0), + "Breakable Box (Small)": ModelInfo( + model_id=ModelIDInfo(0x82, "MODEL_BREAKABLE_BOX_SMALL"), geolayout=0xF000610 + ), + }, + collision=CollisionInfo(address=0x8012D70, c_name="breakable_box_seg8_collision_08012D70"), + ), + "Bub": ActorPresetInfo( + decomp_path="actors/bub", + group="group13", + animation=AnimInfo(address=0x6012354, behaviours=0x1300220C, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_BUB"), geolayout=0xD00038C), + ), + "Bubba": ActorPresetInfo( + decomp_path="actors/bubba", + group="group11", + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_BUBBA"), geolayout=0xC000000), + ), + "Bubble": ActorPresetInfo( + decomp_path="actors/bubble", + group="group0", + models={ + "Bubble": ModelInfo(model_id=ModelIDInfo(0xA8, "MODEL_BUBBLE"), geolayout=0x17000000), + "Bubble Marble": ModelInfo(model_id=ModelIDInfo(0xAA, "MODEL_PURPLE_MARBLE"), geolayout=0x1700001C), + }, + ), + "Bullet Bill": ActorPresetInfo( + decomp_path="actors/bullet_bill", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_BULLET_BILL"), geolayout=0xC000264), + ), + "Bully": ActorPresetInfo( + decomp_path="actors/bully", + group="group2", + animation=AnimInfo( + address=0x500470C, + behaviours={"Bully": 0x13003660, "Bully (With Minions)": 0x13003694, "Bully (Small)": 0x1300362C}, + names=["Patrol", "Chase", "Falling over (Unused)", "Knockback"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_BULLY"), geolayout=0xC000000), + ), + "Burn Smoke": ActorPresetInfo( + decomp_path="actors/burn_smoke", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x94, "MODEL_BURN_SMOKE"), geolayout=0x17000084), + ), + "Butterfly": ActorPresetInfo( + decomp_path="actors/butterfly", + group="common1", + animation=AnimInfo( + address=0x30056B0, + behaviours={"Butterfly": 0x130033BC, "Triplet Butterfly": 0x13005598}, + size=2, + names=["Flying", "Resting"], + ), + models=ModelInfo(model_id=ModelIDInfo(0xBB, "MODEL_BUTTERFLY"), geolayout=0x160000A8), + ), + "Cannon Barrel": ActorPresetInfo( + decomp_path="actors/cannon_barrel", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x7F, "MODEL_CANNON_BARREL"), geolayout=0xF0001C0), + ), + "Cannon Base": ActorPresetInfo( + decomp_path="actors/cannon_base", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x80, "MODEL_CANNON_BASE"), geolayout=0xF0001A8), + ), + "Cannon Lid": ActorPresetInfo( + decomp_path="actors/cannon_lid", + group="common0", + collision=CollisionInfo(address=0x8004950, c_name="cannon_lid_seg8_collision_08004950"), + models=ModelInfo( + model_id=ModelIDInfo(0xC9, "MODEL_DL_CANNON_LID"), + displaylist=DisplaylistInfo(0x80048E0, "cannon_lid_seg8_dl_080048E0"), + ), + ), + "Cap Switch": ActorPresetInfo( + decomp_path="actors/capswitch", + group="group8", + models={ + "Cap Switch": ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_CAP_SWITCH"), geolayout=0xC000048), + "Cap Switch (Exclamation)": ModelInfo( + model_id=ModelIDInfo(0x54, "MODEL_CAP_SWITCH_EXCLAMATION"), + displaylist=DisplaylistInfo(0x5002E00, "cap_switch_exclamation_seg5_dl_05002E00"), + ), + "Cap Switch (Base)": ModelInfo( + model_id=ModelIDInfo(0x56, "MODEL_CAP_SWITCH_BASE"), + displaylist=DisplaylistInfo(0x5003120, "cap_switch_base_seg5_dl_05003120"), + ), + }, + collision={ + "Cap Switch (Base)": CollisionInfo(address=0x50033D0, c_name="capswitch_collision_050033D0"), + "Cap Switch (Top)": CollisionInfo(address=0x5003448, c_name="capswitch_collision_05003448"), + }, + ), + "Chain Ball": ActorPresetInfo( # also known as metallic ball + decomp_path="actors/chain_ball", + group="group14", + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_METALLIC_BALL"), geolayout=0xD0005D0), + ), + "Chain Chomp": ActorPresetInfo( + decomp_path="actors/chain_chomp", + group="group14", + animation=AnimInfo(address=0x6025178, behaviours=0x1300478C, names=["Chomping"]), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_CHAIN_CHOMP"), geolayout=0xD0005EC), + ), + "Haunted Chair": ActorPresetInfo( + decomp_path="actors/chair", + group="group9", + animation=AnimInfo(address=0x5005784, behaviours=0x13004FD4, names=["Default Pose"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_HAUNTED_CHAIR"), geolayout=0xC0000D8), + ), + "Checkerboard Platform": ActorPresetInfo( + decomp_path="actors/checkerboard_platform", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0xCA, "MODEL_CHECKERBOARD_PLATFORM"), geolayout=0xF0004E4), + collision=CollisionInfo(address=0x800D710, c_name="checkerboard_platform_seg8_collision_0800D710"), + ), + "Chilly Chief": ActorPresetInfo( + decomp_path="actors/chilly_chief", + group="group16", + animation=AnimInfo( + address=0x6003994, + behaviours={"Chilly Chief (Small)": 0x130036C8, "Chilly Chief (Big)": 0x13003700}, + names=["Patrol", "Chase", "Falling over (Unused)", "Knockback"], + ), + models={ + "Chilly Chief (Small)": ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_CHILL_BULLY"), geolayout=0x6003754), + "Chilly Chief (Big)": ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_BIG_CHILL_BULLY"), geolayout=0x6003874), + }, + ), + "Chuckya": ActorPresetInfo( + decomp_path="actors/chuckya", + group="common0", + animation=AnimInfo( + address=0x800C070, + behaviours=0x13000528, + names=["Grab Mario", "Holding Mario", "Being Held", "Throwing", "Moving", "Balancing/Idle (Unused)"], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDF, "MODEL_CHUCKYA"), geolayout=0xF0001D8), + ), + "Clam Shell": ActorPresetInfo( + decomp_path="actors/clam", + group="group4", + animation=AnimInfo(address=0x5001744, behaviours=0x13005440, names=["Close", "Open"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_CLAM_SHELL"), geolayout=0xC000000), + ), + "Coin": ActorPresetInfo( + decomp_path="actors/coin", + group="common1", + models={ + "Yellow Coin": ModelInfo(model_id=ModelIDInfo(0x74, "MODEL_YELLOW_COIN"), geolayout=0x1600013C), + "Yellow Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0x75, "MODEL_YELLOW_COIN_NO_SHADOW"), geolayout=0x160001A0 + ), + "Blue Coin": ModelInfo(model_id=ModelIDInfo(0x76, "MODEL_BLUE_COIN"), geolayout=0x16000200), + "Blue Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0x77, "MODEL_BLUE_COIN_NO_SHADOW"), geolayout=0x16000264 + ), + "Red Coin": ModelInfo(model_id=ModelIDInfo(0xD7, "MODEL_RED_COIN"), geolayout=0x160002C4), + "Red Coin (No Shadow)": ModelInfo( + model_id=ModelIDInfo(0xD8, "MODEL_RED_COIN_NO_SHADOW"), geolayout=0x16000328 + ), + }, + ), + "Cyan Fish": ActorPresetInfo( + decomp_path="actors/cyan_fish", + group="group13", + animation=AnimInfo(address=0x600E264, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_CYAN_FISH"), geolayout=0xD000324), + ), + "Dirt": ActorPresetInfo( + decomp_path="actors/dirt", + group="common1", + models={ + "Dirt": ModelInfo(model_id=ModelIDInfo(0x8A, "MODEL_DIRT_ANIMATION"), geolayout=0x16000ED4), + "(Unused) Cartoon Start": ModelInfo(model_id=ModelIDInfo(0x8B, "MODEL_CARTOON_STAR"), geolayout=0x16000F24), + }, + ), + "Door": ActorPresetInfo( + decomp_path="actors/door", + group="common1", + animation=AnimInfo( + address=0x30156C0, + behaviours=0x13000B0C, + names=[ + "Closed", + "Open and Close", + "Open and Close (Slower?)", + "Open and Close (Slower? Last 10 frames)", + "Open and Close (Last 10 frames)", + ], + ignore_bone_count=True, + ), + models={ + "Castle Door": ModelInfo( + model_id=[ + ModelIDInfo(0x26, "MODEL_CASTLE_GROUNDS_CASTLE_DOOR"), + ModelIDInfo(0x26, "MODEL_CASTLE_CASTLE_DOOR"), + ModelIDInfo(0x1C, "MODEL_CASTLE_CASTLE_DOOR_UNUSED"), + ], + geolayout=0x160003A8, + ), + "Cabin Door": ModelInfo(model_id=ModelIDInfo(0x27, "MODEL_CCM_CABIN_DOOR"), geolayout=0x1600043C), + "Wooden Door": ModelInfo( + model_id=[ + ModelIDInfo(0x1D, "MODEL_CASTLE_WOODEN_DOOR_UNUSED"), + ModelIDInfo(0x1D, "MODEL_HMC_WOODEN_DOOR"), + ModelIDInfo(0x27, "MODEL_CASTLE_WOODEN_DOOR"), + ModelIDInfo(0x27, "MODEL_COURTYARD_WOODEN_DOOR"), + ], + geolayout=0x160004D0, + ), + "Wooden Door 2": ModelInfo(geolayout=0x16000564), + "Metal Door": ModelInfo( + model_id=[ + ModelIDInfo(0x1F, "MODEL_HMC_METAL_DOOR"), + ModelIDInfo(0x29, "MODEL_CASTLE_METAL_DOOR"), + ModelIDInfo(0x29, "MODEL_CASTLE_GROUNDS_METAL_DOOR"), + ], + geolayout=0x16000618, + ), + "Hazy Maze Door": ModelInfo(model_id=ModelIDInfo(0x20, "MODEL_HMC_HAZY_MAZE_DOOR"), geolayout=0x1600068C), + "Haunted Door": ModelInfo(model_id=ModelIDInfo(0x1D, "MODEL_BBH_HAUNTED_DOOR"), geolayout=0x16000720), + "Castle Door (0 Star)": ModelInfo( + model_id=ModelIDInfo(0x22, "MODEL_CASTLE_DOOR_0_STARS"), geolayout=0x160007B4 + ), + "Castle Door (1 Star)": ModelInfo( + model_id=ModelIDInfo(0x23, "MODEL_CASTLE_DOOR_1_STAR"), geolayout=0x16000868 + ), + "Castle Door (3 Star)": ModelInfo( + model_id=ModelIDInfo(0x24, "MODEL_CASTLE_DOOR_3_STARS"), geolayout=0x1600091C + ), + "Key Door": ModelInfo(model_id=ModelIDInfo(0x25, "MODEL_CASTLE_KEY_DOOR"), geolayout=0x160009D0), + }, + ), + "Dorrie": ActorPresetInfo( + decomp_path="actors/dorrie", + group="group17", + animation=AnimInfo( + address=0x600F638, behaviours=0x13004F90, size=3, names=["Idle", "Moving", "Lower and Raise Head"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_DORRIE"), geolayout=0xD000230), + ), + "Exclamation Box": ActorPresetInfo( + decomp_path="actors/exclamation_box", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x89, "MODEL_EXCLAMATION_BOX"), geolayout=0xF000694), + ), + "Exclamation Box Outline": ActorPresetInfo( + decomp_path="actors/exclamation_box_outline", + group="common0", + models={ + "Exclamation Box Outline": ModelInfo( + model_id=ModelIDInfo(0x83, "MODEL_EXCLAMATION_BOX_OUTLINE"), geolayout=0xF000A5A + ), + "Exclamation Point": ModelInfo( + model_id=ModelIDInfo(0x84, "MODEL_EXCLAMATION_POINT"), + displaylist=DisplaylistInfo(0x8025F08, "exclamation_box_outline_seg8_dl_08025F08"), + ), + }, + collision=CollisionInfo(address=0x8025F78, c_name="exclamation_box_outline_seg8_collision_08025F78"), + ), + "Explosion": ActorPresetInfo( + decomp_path="actors/explosion", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xCD, "MODEL_EXPLOSION"), geolayout=0x16000040), + ), + "Eyerok": ActorPresetInfo( + decomp_path="actors/eyerok", + group="group5", + animation=AnimInfo( + address=0x50116E4, + behaviours=0x130052B4, + names=["Recovering", "Death", "Idle", "Attacked", "Open", "Show Eye", "Sleep", "Close"], + ), + models={ + "Eyerok Left Hand": ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_EYEROK_LEFT_HAND"), geolayout=0xC0005A8), + "Eyerok Right Hand": ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_EYEROK_RIGHT_HAND"), geolayout=0xC0005E4), + }, + ), + "Flame": ActorPresetInfo( + decomp_path="actors/flame", + group="common1", + models={ + "Red Flame (With Shadow)": ModelInfo( + model_id=ModelIDInfo(0xCB, "MODEL_RED_FLAME_SHADOW"), geolayout=0x16000B10 + ), + "Red Flame": ModelInfo(model_id=ModelIDInfo(0x90, "MODEL_RED_FLAME"), geolayout=0x16000B2C), + "Blue Flame": ModelInfo(model_id=ModelIDInfo(0x91, "MODEL_BLUE_FLAME"), geolayout=0x16000B8C), + }, + ), + "Fly Guy": ActorPresetInfo( + decomp_path="actors/flyguy", + group="common0", + animation=AnimInfo(address=0x8011A64, behaviours=0x130046DC, names=["Flying"]), + models=ModelInfo(model_id=ModelIDInfo(0xDC, "MODEL_FLYGUY"), geolayout=0xF000518), + ), + "Fwoosh": ActorPresetInfo( + decomp_path="actors/fwoosh", + group="group6", + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_FWOOSH"), geolayout=0xC00036C), + ), + "Goomba": ActorPresetInfo( + decomp_path="actors/goomba", + group="common0", + animation=AnimInfo(address=0x801DA4C, behaviours=0x1300472C, names=["Walking"]), + models=ModelInfo(model_id=ModelIDInfo(0xC0, "MODEL_GOOMBA"), geolayout=0xF0006E4), + ), + "Haunted Cage": ActorPresetInfo( + decomp_path="actors/haunted_cage", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x5A, "MODEL_HAUNTED_CAGE"), geolayout=0xC000274), + ), + "Heart": ActorPresetInfo( + decomp_path="actors/heart", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0x78, "MODEL_HEART"), geolayout=0xF0004FC), + ), + "Heave-Ho": ActorPresetInfo( + decomp_path="actors/heave_ho", + group="group1", + animation=AnimInfo(address=0x501534C, behaviours=0x13001548, names=["Moving", "Throwing", "Stop"]), + models=ModelInfo(model_id=ModelIDInfo(0x59, "MODEL_HEAVE_HO"), geolayout=0xC00028C), + ), + "Hoot": ActorPresetInfo( + decomp_path="actors/hoot", + group="group1", + animation=AnimInfo(address=0x5005768, behaviours=0x130033EC, names=["Flying", "Flying Fast"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_HOOT"), geolayout=0xC000018), + ), + "Bowser Impact Ring": ActorPresetInfo( + decomp_path="actors/impact_ring", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_BOWSER_WAVE"), geolayout=0xD000090), + ), + "Bowser Impact Smoke": ActorPresetInfo( + decomp_path="actors/impact_smoke", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_BOWSER_SMOKE"), geolayout=0xD000BFC), + ), + "King Bobomb": ActorPresetInfo( + decomp_path="actors/bobomb", + group="group3", + animation=AnimInfo( + address=0x500FE30, + behaviours=0x130001F4, + size=12, + names=[ + "Grab Mario", + "Holding Mario", + "Hit Ground", + "Unkwnown (Unused)", + "Stomp", + "Idle", + "Being Held", + "Landing", + "Jump", + "Throw Mario", + "Stand Up", + "Walking", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_KING_BOBOMB"), geolayout=0xC000000), + ), + "Klepto": ActorPresetInfo( + decomp_path="actors/klepto", + group="group5", + animation=AnimInfo( + address=0x5008CFC, + behaviours=0x13005310, + names=[ + "Dive", + "Struck By Mario", + "Dive at Mario", + "Dive at Mario 2", + "Dive at Mario 3", + "Dive at Mario 4", + "Dive Flap", + "Dive Flap 2", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_KLEPTO"), geolayout=0xC000000), + ), + "Koopa": ActorPresetInfo( + decomp_path="actors/koopa", + group="group14", + animation=AnimInfo( + address=0x6011364, + behaviours=0x13004580, + names=[ + "Falling Over (Unused Shelled Act 3)", + "Run Away", + "Laying (Unshelled)", + "Running", + "Run (Unused)", + "Laying (Shelled)", + "Stand Up", + "Stopped", + "Wake Up (Unused)", + "Walk", + "Walk Stop", + "Walk Start", + "Jump", + "Land", + ], + ), + models={ + "Koopa (Without Shell)": ModelInfo( + model_id=ModelIDInfo(0xBF, "MODEL_KOOPA_WITHOUT_SHELL"), geolayout=0xD0000D0 + ), + "Koopa (With Shell)": ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_KOOPA_WITH_SHELL"), geolayout=0xD000214), + }, + ), + "Koopa Flag": ActorPresetInfo( + decomp_path="actors/koopa_flag", + group="group14", + animation=AnimInfo(address=0x6001028, behaviours=0x130045F8, names=["Waving"]), + models=ModelInfo(model_id=ModelIDInfo(0x6A, "MODEL_KOOPA_FLAG"), geolayout=0xD000000), + ), + "Koopa Shell": ActorPresetInfo( + decomp_path="actors/koopa_shell", + group="common0", + models={ + "Koopa Shell": ModelInfo(model_id=ModelIDInfo(0xBE, "MODEL_KOOPA_SHELL"), geolayout=0xF000AB0), + "(Unused) Koopa Shell 1": ModelInfo(geolayout=0xF000ADC), + "(Unused) Koopa Shell 2": ModelInfo(geolayout=0xF000B08), + }, + ), + "Lakitu (Cameraman)": ActorPresetInfo( + decomp_path="actors/lakitu_cameraman", + group="group15", + animation=AnimInfo( + address=0x60058F8, + behaviours={"Lakitu (Beginning)": 0x13005610, "Lakitu (Cameraman)": 0x13004954}, + names=["Flying"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_LAKITU"), geolayout=0xD000000), + ), + "Lakitu (Enemy)": ActorPresetInfo( + decomp_path="actors/lakitu_enemy", + group="group11", + animation=AnimInfo( + address=0x50144D4, behaviours=0x13004918, names=["Flying", "No Spiny", "Throw Spiny", "Hold Spiny"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_ENEMY_LAKITU"), geolayout=0xC0001BC), + ), + "Leaves": ActorPresetInfo( + decomp_path="actors/leaves", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xA2, "MODEL_LEAVES"), geolayout=0x16000C8C), + ), + "Mad Piano": ActorPresetInfo( + decomp_path="actors/mad_piano", + group="group9", + animation=AnimInfo(address=0x5009B14, behaviours=0x13005024, names=["Sleeping", "Chomping"]), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_MAD_PIANO"), geolayout=0xC0001B4), + ), + "Manta Ray": ActorPresetInfo( + decomp_path="actors/manta", + group="group4", + animation=AnimInfo(address=0x5008EB4, behaviours=0x13004370, names=["Swimming"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_MANTA_RAY"), geolayout=0x5008D14), + ), + "Mario": ActorPresetInfo( + decomp_path="actors/mario", + group="group0", + animation=AnimInfo( + address=0x4EC000, + dma=True, + directory="assets/anims", + names=[ + "Slow ledge climb up", + "Fall over backwards", + "Backward air kb", + "Dying on back", + "Backflip", + "Climbing up pole", + "Grab pole short", + "Grab pole swing part 1", + "Grab pole swing part 2", + "Handstand idle", + "Handstand jump", + "Start handstand", + "Return from handstand", + "Idle on pole", + "A pose", + "Skid on ground", + "Stop skid", + "Crouch from fast longjump", + "Crouch from a slow longjump", + "Fast longjump", + "Slow longjump", + "Airborne on stomach", + "Walk with light object", + "Run with light object", + "Slow walk with light object", + "Shivering and warming hands", + "Shivering return to idle ", + "Shivering", + "Climb down on ledge", + "Waving (Credits)", + "Look up (Credits)", + "Return from look up (Credits)", + "Raising hand (Credits)", + "Lowering hand (Credits)", + "Taking off cap (Credits)", + "Start walking and look up (Credits)", + "Look back then run (Credits)", + "Final Bowser - Raise hand and spin", + "Final Bowser - Wing cap take off", + "Peach sign (Credits)", + "Stand up from lava boost", + "Fire/Lava burn", + "Wing cap flying", + "Hang on owl", + "Land on stomach", + "Air forward kb", + "Dying on stomach", + "Suffocating", + "Coughing", + "Throw catch key", + "Dying fall over", + "Idle on ledge", + "Fast ledge grab", + "Hang on ceiling", + "Put cap on", + "Take cap off then on", + "Quickly put cap on", + "Head stuck in ground", + "Ground pound landing", + "Triple jump ground-pound", + "Start ground-pound", + "Ground-pound", + "Bottom stuck in ground", + "Idle with light object", + "Jump land with light object", + "Jump with light object", + "Fall land with light object", + "Fall with light object", + "Fall from sliding with light object", + "Sliding on bottom with light object", + "Stand up from sliding with light object", + "Riding shell", + "Walking", + "Forward flip", + "Jump riding shell", + "Land from double jump", + "Double jump fall", + "Single jump", + "Land from single jump", + "Air kick", + "Double jump rise", + "Start forward spinning", + "Throw light object", + "Fall from slide kick", + "Bend kness riding shell", + "Legs stuck in ground", + "General fall", + "General land", + "Being grabbed", + "Grab heavy object", + "Slow land from dive", + "Fly from cannon", + "Moving right while hanging", + "Moving left while hanging", + "Missing cap", + "Pull door walk in", + "Push door walk in", + "Unlock door", + "Start reach pocket", + "Reach pocket", + "Stop reach pocket", + "Ground throw", + "Ground kick", + "First punch", + "Second punch", + "First punch fast", + "Second punch fast", + "Pick up light object", + "Pushing", + "Start riding shell", + "Place light object", + "Forward spinning", + "Backward spinning", + "Breakdance", + "Running", + "Running (unused)", + "Soft back kb", + "Soft front kb", + "Dying in quicksand", + "Idle in quicksand", + "Move in quicksand", + "Electrocution", + "Shocked", + "Backward kb", + "Forward kb", + "Idle heavy object", + "Stand against wall", + "Side step left", + "Side step right", + "Start sleep idle", + "Start sleep scratch", + "Start sleep yawn", + "Start sleep sitting", + "Sleep idle", + "Sleep start laying", + "Sleep laying", + "Dive", + "Slide dive", + "Ground bonk", + "Stop slide light object", + "Slide kick", + "Crouch from slide kick", + "Slide motionless", + "Stop slide", + "Fall from slide", + "Slide", + "Tiptoe", + "Twirl land", + "Twirl", + "Start twirl", + "Stop crouching", + "Start crouching", + "Crouching", + "Crawling", + "Stop crawling", + "Start crawling", + "Summon star", + "Return star approach door", + "Backwards water kb", + "Swim with object part 1", + "Swim with object part 2", + "Flutter kick with object", + "Action end with object in water", + "Stop holding object in water", + "Holding object in water", + "Drowning part 1", + "Drowning part 2", + "Dying in water", + "Forward kb in water", + "Falling from water", + "Swimming part 1", + "Swimming part 2", + "Flutter kick", + "Action end in water", + "Pick up object in water", + "Grab object in water part 2", + "Grab object in water part 1", + "Throw object in water", + "Idle in water", + "Star dance in water", + "Return from in water star dance", + "Grab bowser", + "Swing bowser", + "Release bowser", + "Holding bowser", + "Heavy throw", + "Walk panting", + "Walk with heavy object", + "Turning part 1", + "Turning part 2", + "Side flip land", + "Side flip", + "Triple jump land", + "Triple jump", + "First person", + "Idle head left", + "Idle head right", + "Idle head center", + "Handstand left", + "Handstand right", + "Wake up from sleeping", + "Wake up from laying", + "Start tiptoeing", + "Slide jump", + "Start wallkick", + "Star dance", + "Return from star dance", + "Forwards spinning flip", + "Triple jump fly", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x1, "MODEL_MARIO"), geolayout=0x17002DD4), + ), + "Mario's Cap": ActorPresetInfo( + decomp_path="actors/mario_cap", + group="common1", + models={ + "Mario's Cap": ModelInfo(model_id=ModelIDInfo(0x88, "MODEL_MARIOS_CAP"), geolayout=0x16000CA4), + "Mario's Metal Cap": ModelInfo(model_id=ModelIDInfo(0x86, "MODEL_MARIOS_METAL_CAP"), geolayout=0x16000CF0), + "Mario's Wing Cap": ModelInfo(model_id=ModelIDInfo(0x87, "MODEL_MARIOS_WING_CAP"), geolayout=0x16000D3C), + "Mario's Winged Metal Cap": ModelInfo( + model_id=ModelIDInfo(0x85, "MODEL_MARIOS_WINGED_METAL_CAP"), geolayout=0x16000DA8 + ), + }, + ), + "Metal Box": ActorPresetInfo( + decomp_path="actors/metal_box", + group="common0", + models={ + "Metal Box": ModelInfo(model_id=ModelIDInfo(0xD9, "MODEL_METAL_BOX"), geolayout=0xF000A30), + "Metal Box (DL)": ModelInfo( + model_id=ModelIDInfo(0xDA, "MODEL_METAL_BOX_DL"), displaylist=DisplaylistInfo(0x8024BB8, "metal_box_dl") + ), + }, + collision=CollisionInfo(address=0x8024C28, c_name="metal_box_seg8_collision_08024C28"), + ), + "Mips": ActorPresetInfo( + decomp_path="actors/mips", + group="group15", + animation=AnimInfo( + address=0x6015724, behaviours=0x130044FC, names=["Idle", "Hopping", "Thrown", "Thrown (Unused)", "Held"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_MIPS"), geolayout=0xD000448), + ), + "Mist": ActorPresetInfo( + decomp_path="actors/mist", + group="common1", + models={ + "Mist": ModelInfo(model_id=ModelIDInfo(0x8E, "MODEL_MIST"), geolayout=0x16000000), + "White Puff": ModelInfo(model_id=ModelIDInfo(0xE0, "MODEL_WHITE_PUFF"), geolayout=0x16000020), + }, + ), + "Moneybag": ActorPresetInfo( + decomp_path="actors/moneybag", + group="group16", + animation=AnimInfo(address=0x6005E5C, behaviours=0x130039A0, names=["Idle", "Prepare", "Jump", "Land", "Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_MONEYBAG"), geolayout=0xD0000F0), + ), + "Monty Mole": ActorPresetInfo( + decomp_path="actors/monty_mole", + group="group6", + animation=AnimInfo( + address=0x5007248, + behaviours=0x13004A00, + names=[ + "Jump Into Hole", + "Rise", + "Get Rock", + "Begin Jump Into Hole", + "Jump Out Of Hole Down", + "Unused 5", # TODO: Figure out + "Unused 6", + "Unused 7", + "Throw Rock", + "Jump Out Of Hole Up", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_MONTY_MOLE"), geolayout=0xC000000), + ), + "Montey Mole Hole": ActorPresetInfo( + decomp_path="actors/monty_mole_hole", + group="group6", + models=ModelInfo( + model_id=ModelIDInfo(0x54, "MODEL_DL_MONTY_MOLE_HOLE"), + displaylist=DisplaylistInfo(0x5000840, "monty_mole_hole_seg5_dl_05000840"), + ), + ), + "Mr. I Eyeball": ActorPresetInfo( + decomp_path="actors/mr_i_eyeball", + group="group16", + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_MR_I"), geolayout=0xD000000), + ), + "Mr. I Iris": ActorPresetInfo( + decomp_path="actors/mr_i_iris", + group="group16", + models=ModelInfo(model_id=ModelIDInfo(0x66, "MODEL_MR_I_IRIS"), geolayout=0xD00001C), + ), + "Mushroom 1up": ActorPresetInfo( + decomp_path="actors/mushroom_1up", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xD4, "MODEL_1UP"), geolayout=0x16000E84), + ), + "Orange Numbers": ActorPresetInfo( + decomp_path="actors/number", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0xDB, "MODEL_NUMBER"), geolayout=0x16000E14), + ), + "Peach": ActorPresetInfo( + decomp_path="actors/peach", + group="group10", + animation=AnimInfo( + address=0x501C504, + behaviours={"Peach (Beginning)": 0x13005638, "Peach (End)": 0x13000EAC}, + names=[ + "Listen Everybody", + "Turning Away", + "Walking away", + "Walking away 2", + "Descend", + "Descend And Look Down", + "Look Up And Open Eyes", + "Mario", + "Power Of The Stars", + "Thanks To You", + "Kiss", + "Waving", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDE, "MODEL_PEACH"), geolayout=0xC000410), + ), + "Pebble": ActorPresetInfo( + decomp_path="actors/pebble", + group="common1", + models=ModelInfo( + model_id=ModelIDInfo(0xA1, "MODEL_PEBBLE"), + displaylist=DisplaylistInfo(0x301CB00, "pebble_seg3_dl_0301CB00"), + ), + ), + "Penguin": ActorPresetInfo( + decomp_path="actors/penguin", + group="group7", + animation=AnimInfo( + address=0x5008B74, + behaviours={ + "Penguin (Tuxies Mother)": 0x13002088, + "Penguin (Small)": 0x130020E8, + "Penguin (SML)": 0x13002E58, + "Racing Penguin": 0x13005380, + }, + size=5, + names=["Walk", "Dive Slide", "Stand Up", "Idle", "Walk"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_PENGUIN"), geolayout=0xC000104), + collision=CollisionInfo(address=0x5008B88, c_name="penguin_seg5_collision_05008B88"), + ), + "Piranha Plant": ActorPresetInfo( + decomp_path="actors/piranha_plant", + group="group14", + animation=AnimInfo( + address=0x601C31C, + behaviours={"Fire Piranha Plant": 0x13005120, "Piranha Plant": 0x13001FBC}, + names=[ + "Bite", + "Sleeping? (Unused)", + "Falling over", + "Bite (Unused)", + "Grow", + "Attacked", + "Stop Bitting", + "Sleeping (Unused)", + "Sleeping", + "Bite (Duplicate)", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_PIRANHA_PLANT"), geolayout=0xD000358), + ), + "Pokey": ActorPresetInfo( + decomp_path="actors/pokey", + group="group5", + models={ + "Pokey Head": ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_POKEY_HEAD"), geolayout=0xC000610), + "Pokey Body Part": ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_POKEY_BODY_PART"), geolayout=0xC000644), + }, + ), + "Wooden Post": ActorPresetInfo( + decomp_path="actors/poundable_pole", + group="group14", + models=ModelInfo(model_id=ModelIDInfo(0x6B, "MODEL_WOODEN_POST"), geolayout=0xD0000B8), + collision=CollisionInfo(address=0x6002490, c_name="poundable_pole_collision_06002490"), + ), + # Should the power meter be included? + "Power Meter": ActorPresetInfo( + decomp_path="actors/power_meter", + group="common1", + models={ + "Power Meter (Base)": ModelInfo(displaylist=DisplaylistInfo(0x3029480, "dl_power_meter_base")), + "Power Meter (Health)": ModelInfo( + displaylist=DisplaylistInfo(0x3029570, "dl_power_meter_health_segments_begin") + ), + }, + ), + "Purple Switch": ActorPresetInfo( + decomp_path="actors/purple_switch", + group="common0", + models=ModelInfo(model_id=ModelIDInfo(0xCF, "MODEL_PURPLE_SWITCH"), geolayout=0xF0004CC), + collision=CollisionInfo(address=0x800C7A8, c_name="purple_switch_seg8_collision_0800C7A8"), + ), + "Sand": ActorPresetInfo( + decomp_path="actors/sand", + group="common1", + models=ModelInfo( + model_id=ModelIDInfo(0x9F, "MODEL_SAND_DUST"), + displaylist=DisplaylistInfo(0x302BCD0, "sand_seg3_dl_0302BCD0"), + ), + ), + "Scuttlebug": ActorPresetInfo( + decomp_path="actors/scuttlebug", + group="group17", + animation=AnimInfo(address=0x6015064, behaviours=0x13002B5C, names=["Walking"]), + models=ModelInfo(model_id=ModelIDInfo(0x65, "MODEL_SCUTTLEBUG"), geolayout=0xD000394), + ), + "Seaweed": ActorPresetInfo( + decomp_path="actors/seaweed", + group="group13", + animation=AnimInfo(address=0x0600A4D4, behaviours=0x13003134, size=1, names=["Wave"]), + models=ModelInfo(model_id=ModelIDInfo(0xC1, "MODEL_SEAWEED"), geolayout=0xD000284), + ), + "Skeeter": ActorPresetInfo( + decomp_path="actors/skeeter", + group="group13", + animation=AnimInfo( + address=0x6007DE0, behaviours=0x13005468, size=4, names=["Water Lunge", "Water Idle", "Walk", "Idle"] + ), + models=ModelInfo(model_id=ModelIDInfo(0x69, "MODEL_SKEETER"), geolayout=0xD000000), + ), + "(Beta) Boo Key": ActorPresetInfo( + decomp_path="actors/small_key", + group="group9", + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_BETA_BOO_KEY"), geolayout=0xC000188), + ), + "(Unused) Smoke": ActorPresetInfo( # TODO: double check + decomp_path="actors/smoke", + group="group6", + models=ModelInfo(displaylist=DisplaylistInfo(0x5007AF8, "smoke_seg5_dl_05007AF8")), + ), + "Mr. Blizzard": ActorPresetInfo( + decomp_path="actors/snowman", + group="group7", + animation=AnimInfo( + address=0x500D118, behaviours={"Mr. Blizzard": 0x13004DBC}, names=["Spawn Snowball", "Throw Snowball"] + ), + models={ + "Mr. Blizzard": ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_MR_BLIZZARD"), geolayout=0xC000348), + "Mr. Blizzard (Hidden)": ModelInfo( + model_id=ModelIDInfo(0x55, "MODEL_MR_BLIZZARD_HIDDEN"), geolayout=0xC00021C + ), + }, + ), + "Snufit": ActorPresetInfo( + decomp_path="actors/snufit", + group="group17", + models=ModelInfo(model_id=ModelIDInfo(0xCE, "MODEL_SNUFIT"), geolayout=0xD0001A0), + ), + "Sparkle": ActorPresetInfo( + decomp_path="actors/sparkle", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x95, "MODEL_SPARKLES"), geolayout=0x170001BC), + ), + "Sparkle Animation": ActorPresetInfo( + decomp_path="actors/sparkle_animation", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x8F, "MODEL_SPARKLES_ANIMATION"), geolayout=0x17000284), + ), + "Spindrift": ActorPresetInfo( + decomp_path="actors/spindrift", + group="group7", + animation=AnimInfo(address=0x5002D68, behaviours=0x130012B4, names=["Flying"]), + models=ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_SPINDRIFT"), geolayout=0xC000000), + ), + "Spiny": ActorPresetInfo( + decomp_path="actors/spiny", + group="group11", + animation=AnimInfo(address=0x5016EAC, behaviours={"Spiny": 0x130049C8}, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_SPINY"), geolayout=0xC000328), + ), + "Spiny Egg": ActorPresetInfo( + decomp_path="actors/spiny_egg", + group="group11", + animation=AnimInfo(address=0x50157E4, names=["Default"]), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_SPINY_BALL"), geolayout=0xC000290), + ), + "Springboard": ActorPresetInfo( + decomp_path="actors/springboard", + group="group8", + models={ + "Springboard Top": ModelInfo(model_id=ModelIDInfo(0xB5, "MODEL_TRAMPOLINE"), geolayout=0xC000000), + "Springboard Middle": ModelInfo(model_id=ModelIDInfo(0xB6, "MODEL_TRAMPOLINE_CENTER"), geolayout=0xC000018), + "Springboard Bottom": ModelInfo(model_id=ModelIDInfo(0xB7, "MODEL_TRAMPOLINE_BASE"), geolayout=0xC000030), + }, + ), + "Star": ActorPresetInfo( + decomp_path="actors/star", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x7A, "MODEL_STAR"), geolayout=0x16000EA0), + ), + "Small Water Splash": ActorPresetInfo( + decomp_path="actors/stomp_smoke", + group="group0", + models={ + "Small Water Splash": ModelInfo( + model_id=ModelIDInfo(0xA5, "MODEL_SMALL_WATER_SPLASH"), geolayout=0x1700009C + ), + "(Unused) Small Water Splash": ModelInfo(geolayout=0x170000E0), + }, + ), + "Sushi Shark": ActorPresetInfo( + decomp_path="actors/sushi", + group="group4", + animation=AnimInfo(address=0x500AE54, behaviours=0x13002338, size=1, names=["Swimming", "Diving"]), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_SUSHI"), geolayout=0xC000068), + ), + "Swoop": ActorPresetInfo( + decomp_path="actors/swoop", + group="group17", + animation=AnimInfo(address=0x60070D0, behaviours=0x13004698, size=2, names=["Idle", "Move"]), + models=ModelInfo(model_id=ModelIDInfo(0x64, "MODEL_SWOOP"), geolayout=0xD0000DC), + ), + "Test Plataform": ActorPresetInfo( + decomp_path="actors/test_plataform", + group="common0", + collision=CollisionInfo(address=0x80262F8, c_name="unknown_seg8_collision_080262F8"), + ), + "Thwomp": ActorPresetInfo( + decomp_path="actors/thwomp", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_THWOMP"), geolayout=0xC000248), + collision={ + "Thwomp": CollisionInfo(address=0x500B7D0, c_name="thwomp_seg5_collision_0500B7D0"), + "Thwomp 2": CollisionInfo(address=0x500B92C, c_name="thwomp_seg5_collision_0500B92C"), + }, + ), + "Toad": ActorPresetInfo( + decomp_path="actors/toad", + group="group15", + animation=AnimInfo( + address=0x600FC48, + behaviours={"End Toad": 0x13000E88, "Toad Message": 0x13002EF8}, + size=8, + names=[ + "Wave Then Run (West)", + "Walking (West)", + "Node Then Turn (East)", + "Walking (East)", + "Standing (West)", + "Standing (East)", + "Waving Both Arms (West)", + "Waving One Arm (East)", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0xDD, "MODEL_TOAD"), geolayout=0xD0003E4), + ), + "Tweester/Tornado": ActorPresetInfo( + decomp_path="actors/tornado", + group="group5", + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_TWEESTER"), geolayout=0x5014630), + ), + "Transparent Star": ActorPresetInfo( + decomp_path="actors/transperant_star", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x79, "MODEL_TRANSPARENT_STAR"), geolayout=0x16000F6C), + ), + "Treasure Chest": ActorPresetInfo( + decomp_path="actors/treasure_chest", + group="group13", + models={ + "Treasure Chest Base": ModelInfo( + model_id=ModelIDInfo(0x65, "MODEL_TREASURE_CHEST_BASE"), geolayout=0xD000450 + ), + "Treasure Chest Lid": ModelInfo( + model_id=ModelIDInfo(0x66, "MODEL_TREASURE_CHEST_LID"), geolayout=0xD000468 + ), + }, + ), + "Tree": ActorPresetInfo( + decomp_path="actors/tree", + group="common1", + models={ + "Bubbly Tree": ModelInfo( + model_id=[ + ModelIDInfo(0x17, "MODEL_BOB_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_WDW_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_CASTLE_GROUNDS_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_WF_BUBBLY_TREE"), + ModelIDInfo(0x17, "MODEL_THI_BUBBLY_TREE"), + ], + geolayout=0x16000FE8, + ), + "Pine Tree": ModelInfo(model_id=ModelIDInfo(0x18, "MODEL_COURTYARD_SPIKY_TREE"), geolayout=0x16001000), + "(Unused) Pine Tree": ModelInfo(geolayout=0x16001030), + "Snow Tree": ModelInfo( + model_id=[ModelIDInfo(0x19, "MODEL_CCM_SNOW_TREE"), ModelIDInfo(0x19, "MODEL_SL_SNOW_TREE")], + geolayout=0x16001018, + ), + "Palm Tree": ModelInfo(model_id=ModelIDInfo(0x1B, "MODEL_SSL_PALM_TREE"), geolayout=0x16001048), + }, + ), + "Ukiki": ActorPresetInfo( + decomp_path="actors/ukiki", + group="group6", + animation=AnimInfo( + address=0x5015784, + behaviours={"Ukiki": 0x13001CB0}, + names=[ + "Run", + "Walk (Unused)", + "Apose (Unused)", + "Death (Unused)", + "Screech", + "Jump Clap", + "Hop (Unused)", + "Land", + "Jump", + "Itch", + "Handstand", + "Turn", + "Held", + ], + ), + models=ModelInfo(model_id=ModelIDInfo(0x56, "MODEL_UKIKI"), geolayout=0xC000110), + ), + "Unagi": ActorPresetInfo( + decomp_path="actors/unagi", + group="group4", + animation=AnimInfo( + address=0x5012824, + behaviours=0x13004F40, + size=7, + names=["Yawn", "Bite", "Swimming", "Static Straight", "Idle", "Open Mouth", "Idle 2"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_UNAGI"), geolayout=0xC00010C), + ), + "Smoke": ActorPresetInfo( + decomp_path="actors/walk_smoke", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0x96, "MODEL_SMOKE"), geolayout=0x17000038), + ), + "Warp Collision": ActorPresetInfo( + decomp_path="actors/warp_collision", + group="common1", + collision={ + "Door": CollisionInfo(address=0x301CE78, c_name="door_seg3_collision_0301CE78"), + "LLL Hexagonal Mesh": CollisionInfo(address=0x301CECC, c_name="lll_hexagonal_mesh_seg3_collision_0301CECC"), + }, + ), + "Warp Pipe": ActorPresetInfo( + decomp_path="actors/warp_pipe", + group="common1", + models=ModelInfo( + model_id=[ + ModelIDInfo(0x49, "MODEL_BITS_WARP_PIPE"), + ModelIDInfo(0x12, "MODEL_BITDW_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_THI_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_VCUTM_WARP_PIPE"), + ModelIDInfo(0x16, "MODEL_CASTLE_GROUNDS_WARP_PIPE"), + ], + geolayout=0x16000388, + ), + ), + "Water Bomb": ActorPresetInfo( + decomp_path="actors/water_bubble", + group="group3", + models={ + "Water Bomb": ModelInfo(model_id=ModelIDInfo(0x54, "MODEL_WATER_BOMB"), geolayout=0xC000308), + "Water Bomb's Shadow": ModelInfo( + model_id=ModelIDInfo(0x55, "MODEL_WATER_BOMB_SHADOW"), geolayout=0xC000328 + ), + }, + ), + "Water Mine": ActorPresetInfo( + decomp_path="actors/water_mine", + group="group13", + models=ModelInfo(model_id=ModelIDInfo(0xB3, "MODEL_WATER_MINE"), geolayout=0xD0002F4), + ), + "Water Ring": ActorPresetInfo( + decomp_path="actors/water_ring", + group="group13", + animation=AnimInfo( + address=0x6013F7C, + behaviours={"Water Ring (Jet Stream)": 0x13003750, "Water Ring (Manta Ray)": 0xC66C16}, + names=["Wobble"], + ), + models=ModelInfo(model_id=ModelIDInfo(0x68, "MODEL_WATER_RING"), geolayout=0xD000414), + ), + "Water Splash": ActorPresetInfo( + decomp_path="actors/water_splash", + group="group0", + models=ModelInfo(model_id=ModelIDInfo(0xA7, "MODEL_WATER_SPLASH"), geolayout=0x17000230), + ), + "Water Wave": ActorPresetInfo( + decomp_path="actors/water_wave", + group="group0", + models={ + "Idle Water Wave": ModelInfo(model_id=ModelIDInfo(0xA6, "MODEL_IDLE_WATER_WAVE"), geolayout=0x17000124), + "Water Wave Trail": ModelInfo(model_id=ModelIDInfo(0xA3, "MODEL_WAVE_TRAIL"), geolayout=0x17000168), + }, + ), + "Whirlpool": ActorPresetInfo( + decomp_path="actors/whirlpool", + group="group4", + models=ModelInfo( + model_id=ModelIDInfo(0x57, "MODEL_DL_WHIRLPOOL"), + displaylist=DisplaylistInfo(0x5013CB8, "whirlpool_seg5_dl_05013CB8"), + ), + ), + "White Particle": ActorPresetInfo( + decomp_path="actors/white_particle", + group="common1", + models={ + "White Particle": ModelInfo(model_id=ModelIDInfo(0xA0, "MODEL_WHITE_PARTICLE"), geolayout=0x16000F98), + "White Particle (DL)": ModelInfo( + model_id=ModelIDInfo(0x9E, "MODEL_WHITE_PARTICLE_DL"), + displaylist=DisplaylistInfo(0x302C8A0, "white_particle_dl"), + ), + }, + ), + "White Particle Small": ActorPresetInfo( + decomp_path="actors/white_particle_small", + group="group0", + models={ + "White Particle Small": ModelInfo( + model_id=ModelIDInfo(0xA4, "MODEL_WHITE_PARTICLE_SMALL"), + displaylist=DisplaylistInfo(0x4032A18, "white_particle_small_dl"), + ), + "(Unused) White Particle Small": ModelInfo( + displaylist=DisplaylistInfo(0x4032A30, "white_particle_small_unused_dl") + ), + }, + ), + "Whomp": ActorPresetInfo( + decomp_path="actors/whomp", + group="group14", + animation=AnimInfo(address=0x6020A04, behaviours=0x13002BCC, size=2, names=["Walk", "Jump"]), + models=ModelInfo(model_id=ModelIDInfo(0x67, "MODEL_WHOMP"), geolayout=0xD000480), + collision=CollisionInfo(address=0x6020A0C, c_name="whomp_seg6_collision_06020A0C"), + ), + "Wiggler Body": ActorPresetInfo( + decomp_path="actors/wiggler_body", + group="group11", + animation=AnimInfo(address=0x500C874, behaviours=0x130048E0, size=1, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x58, "MODEL_WIGGLER_BODY"), geolayout=0x500C778), + ), + "Wiggler Head": ActorPresetInfo( + decomp_path="actors/wiggler_head", + group="group11", + animation=AnimInfo(address=0x500EC8C, behaviours=0x13004898, size=1, names=["Walk"]), + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_WIGGLER_HEAD"), geolayout=0xC000030), + ), + "Wooden Signpost": ActorPresetInfo( + decomp_path="actors/wooden_signpost", + group="common1", + models=ModelInfo(model_id=ModelIDInfo(0x7C, "MODEL_WOODEN_SIGNPOST"), geolayout=0x16000FB4), + collision=CollisionInfo(address=0x302DD80, c_name="wooden_signpost_seg3_collision_0302DD80"), + ), + "Yellow Sphere (Bowser 1)": ActorPresetInfo( + decomp_path="actors/yellow_sphere", + group="group12", + models=ModelInfo(model_id=ModelIDInfo(0x3, "MODEL_LEVEL_GEOMETRY_03"), geolayout=0xD0000B0), + ), + "Yellow Sphere": ActorPresetInfo( + decomp_path="actors/yellow_sphere_small", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_YELLOW_SPHERE"), geolayout=0xC000000), + ), + "Yoshi": ActorPresetInfo( + decomp_path="actors/yoshi", + group="group10", + animation=AnimInfo(address=0x50241E8, behaviours=0x13004538, names=["Idle", "Walk", "Jump"]), + models=ModelInfo(model_id=ModelIDInfo(0x55, "MODEL_YOSHI"), geolayout=0xC000468), + ), + "(Unused) Yoshi Egg": ActorPresetInfo( + decomp_path="actors/yoshi_egg", + group="group1", + models=ModelInfo(model_id=ModelIDInfo(0x57, "MODEL_YOSHI_EGG"), geolayout=0xC0001E4), + ), + "Castle Flag": ActorPresetInfo( + decomp_path="levels/castle_grounds/areas/1/11", + level="CG", + animation=AnimInfo(address=0x700C95C, behaviours=0x13003C58, size=1, names=["Wave"]), + models=ModelInfo(model_id=ModelIDInfo(0x37, "MODEL_CASTLE_GROUNDS_FLAG"), geolayout=0xE000660), + ), +} + sm64_world_defaults = { "geometryMode": { "zBuffer": True, diff --git a/fast64_internal/sm64/sm64_f3d_parser.py b/fast64_internal/sm64/sm64_f3d_parser.py index 6eaabb21e..45152ccd5 100644 --- a/fast64_internal/sm64/sm64_f3d_parser.py +++ b/fast64_internal/sm64/sm64_f3d_parser.py @@ -7,9 +7,9 @@ from bpy.props import StringProperty, EnumProperty, BoolProperty from ..panels import SM64_Panel from ..f3d.f3d_parser import F3DtoBlenderObject -from .sm64_constants import level_enums, level_pointers +from .sm64_constants import enumLevelNames from .sm64_utility import import_rom_checks -from .sm64_level_parser import parseLevelAtPointer +from .sm64_level_parser import parse_level_binary from ..utility import ( PluginError, @@ -40,7 +40,7 @@ def execute(self, context): try: import_rom_checks(abspath(context.scene.fast64.sm64.import_rom)) romfileSrc = open(abspath(context.scene.fast64.sm64.import_rom), "rb") - levelParsed = parseLevelAtPointer(romfileSrc, level_pointers[context.scene.levelDLImport]) + levelParsed = parse_level_binary(romfileSrc, context.scene.levelDLImport) segmentData = levelParsed.segmentData start = ( decodeSegmentedAddr(int(context.scene.DLImportStart, 16).to_bytes(4, "big"), segmentData) @@ -102,7 +102,7 @@ def sm64_dl_parser_register(): register_class(cls) Scene.DLImportStart = StringProperty(name="Start Address", default="A3BE1C") - Scene.levelDLImport = EnumProperty(items=level_enums, name="Level", default="CG") + Scene.levelDLImport = EnumProperty(items=enumLevelNames, name="Level", default="castle_grounds") Scene.isSegmentedAddrDLImport = BoolProperty(name="Is Segmented Address", default=False) diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 09449c370..647b1ead7 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -1,27 +1,39 @@ +from pathlib import Path import shutil, copy, bpy, re, os from io import BytesIO from math import ceil, log, radians from mathutils import Matrix, Vector from bpy.utils import register_class, unregister_class from ..panels import SM64_Panel -from ..f3d.f3d_writer import exportF3DCommon +from ..f3d.f3d_writer import exportF3DCommon, saveModeSetting from ..f3d.f3d_texture_writer import TexInfo from ..f3d.f3d_material import ( TextureProperty, - tmemUsageUI, all_combiner_uses, + ui_image, ui_procAnim, update_world_default_rendermode, ) from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import export_rom_checks, starSelectWarning -from .sm64_level_parser import parseLevelAtPointer +from .sm64_utility import ( + END_IF_FOOTER, + ModifyFoundDescriptor, + export_rom_checks, + starSelectWarning, + update_actor_includes, + write_or_delete_if_found, + write_material_headers, +) +from .sm64_level_parser import parse_level_binary from .sm64_rom_tweaks import ExtendBank0x04 -from typing import Tuple, Union, Iterable +from typing import Tuple from ..f3d.f3d_bleed import BleedGraphics from ..f3d.f3d_gbi import ( + DPSetCombineMode, + DPSetTextureLUT, + FMesh, get_F3D_GBI, GbiMacro, GfxTag, @@ -31,7 +43,6 @@ GfxMatWriteMethod, ScrollMethod, DLFormat, - SPDisplayList, GfxList, GfxListTag, FTexRect, @@ -45,14 +56,12 @@ SPTexture, SPEndDisplayList, TextureExportSettings, - FSetTileSizeScrollField, - FImageKey, vertexScrollTemplate, get_tile_scroll_code, - GFX_SIZE, ) from ..utility import ( + CData, CScrollData, PluginError, raisePluginError, @@ -61,11 +70,9 @@ applyRotation, toAlnum, checkIfPathExists, - writeIfNotFound, overwriteData, getExportDir, writeMaterialFiles, - writeMaterialHeaders, get64bitAlignedAddr, writeInsertableFile, getPathAndLevel, @@ -77,18 +84,10 @@ decompFolderMessage, makeWriteInfoBox, writeBoxExportType, - enumExportHeaderType, create_or_get_world, ) -from .sm64_constants import ( - level_enums, - enumLevelNames, - level_pointers, - defaultExtendSegment4, - bank0Segment, - insertableBinaryTypes, -) +from .sm64_constants import defaultExtendSegment4, bank0Segment, insertableBinaryTypes enumHUDExportLocation = [ @@ -99,7 +98,7 @@ # filepath, function to insert before enumHUDPaths = { "HUD": ("src/game/hud.c", "void render_hud(void)"), - "Menu": ("src/game/ingame_menu.c", "s16 render_menus_and_dialogs()"), + "Menu": ("src/game/ingame_menu.c", "s16 render_menus_and_dialogs("), } @@ -107,6 +106,8 @@ class SM64Model(FModel): def __init__(self, name, DLFormat, matWriteMethod): FModel.__init__(self, name, DLFormat, matWriteMethod) self.no_light_direction = bpy.context.scene.fast64.sm64.matstack_fix + self.layer_adapted_fmats = {} + self.draw_overrides: dict[FMesh, dict[tuple, tuple[GfxList, list["DisplayListNode"]]]] = {} def getDrawLayerV3(self, obj): return int(obj.draw_layer_static) @@ -115,7 +116,7 @@ def getRenderMode(self, drawLayer): world = create_or_get_world(bpy.context.scene) cycle1 = getattr(world, "draw_layer_" + str(drawLayer) + "_cycle_1") cycle2 = getattr(world, "draw_layer_" + str(drawLayer) + "_cycle_2") - return [cycle1, cycle2] + return (cycle1, cycle2) class SM64GfxFormatter(GfxFormatter): @@ -167,13 +168,11 @@ def exportTexRectToC(dirPath, texProp, texDir, savePNG, name, exportToProject, p if name is None or name == "": raise PluginError("Name cannot be empty.") - exportData = fTexRect.to_c(savePNG, texDir, SM64GfxFormatter(ScrollMethod.Vertex)) - staticData = exportData.staticData - dynamicData = exportData.dynamicData + formater = SM64GfxFormatter(ScrollMethod.Vertex) - declaration = staticData.header + dynamicData = CData() + dynamicData.append(fTexRect.draw.to_c(fTexRect.f3d)) code = modifyDLForHUD(dynamicData.source) - data = staticData.source if exportToProject: seg2CPath = os.path.join(dirPath, "bin/segment2.c") @@ -186,22 +185,43 @@ def exportTexRectToC(dirPath, texProp, texDir, savePNG, name, exportToProject, p checkIfPathExists(seg2TexDir) checkIfPathExists(hudPath) - fTexRect.save_textures(seg2TexDir, not savePNG) + if savePNG: + fTexRect.save_textures(seg2TexDir) - textures = [] + include_dir = Path(texDir).as_posix() + "/" for _, fImage in fTexRect.textures.items(): - textures.append(fImage) - - # Append/Overwrite texture definition to segment2.c - overwriteData("const\s*u8\s*", textures[0].name, data, seg2CPath, None, False) + if savePNG: + data = fImage.to_c_tex_separate(include_dir, formater.texArrayBitSize) + else: + data = fImage.to_c(formater.texArrayBitSize) + + # Append/Overwrite texture definition to segment2.c + overwriteData( + rf"(Gfx\s+{fImage.aligner_name}\s*\[\s*\]\s*=\s*\{{\s*gsSPEndDisplayList\s*\(\s*\)\s*\}}\s*;\s*)?" + rf"u{str(formater.texArrayBitSize)}\s*", + fImage.name, + data.source, + seg2CPath, + None, + False, + post_regex=r"\s?\s?", # tex to c includes 2 newlines + ) # Append texture declaration to segment2.h - writeIfNotFound(seg2HPath, declaration, "#endif") + write_or_delete_if_found( + Path(seg2HPath), ModifyFoundDescriptor(data.header), path_must_exist=True, footer=END_IF_FOOTER + ) # Write/Overwrite function to hud.c - overwriteData("void\s*", fTexRect.name, code, hudPath, projectExportData[1], True) + overwriteData("void\s*", fTexRect.name, code, hudPath, projectExportData[1], True, post_regex=r"\s?") else: + exportData = fTexRect.to_c(savePNG, texDir, formater) + staticData = exportData.staticData + + declaration = staticData.header + data = staticData.source + singleFileData = "" singleFileData += "// Copy this function to src/game/hud.c or src/game/ingame_menu.c.\n" singleFileData += "// Call the function in render_hud() or render_menus_and_dialogs() respectively.\n" @@ -275,73 +295,55 @@ def modifyDLForHUD(data): # data = data[:matchResult.start(7)] + 'segmented_to_virtual(&' + \ # matchResult.group(7) + ")" +data[matchResult.end(7):] - return data + return data.removesuffix("\n") def exportTexRectCommon(texProp, name, convertTextureData): - tex = texProp.tex - if tex is None: - raise PluginError("No texture is selected.") - - texProp.S.low = 0 - texProp.S.high = texProp.tex.size[0] - 1 - texProp.S.mask = ceil(log(texProp.tex.size[0], 2) - 0.001) - texProp.S.shift = 0 + use_copy_mode = texProp.tlut_mode == "G_TT_RGBA16" or texProp.tex_format == "RGBA16" - texProp.T.low = 0 - texProp.T.high = texProp.tex.size[1] - 1 - texProp.T.mask = ceil(log(texProp.tex.size[1], 2) - 0.001) - texProp.T.shift = 0 + defaults = create_or_get_world(bpy.context.scene).rdp_defaults fTexRect = FTexRect(toAlnum(name), GfxMatWriteMethod.WriteDifferingAndRevert) - fMaterial = FMaterial(toAlnum(name) + "_mat", DLFormat.Dynamic) - - # dl_hud_img_begin - fTexRect.draw.commands.extend( - [ - DPPipeSync(), - DPSetCycleType("G_CYC_COPY"), - DPSetTexturePersp("G_TP_NONE"), - DPSetAlphaCompare("G_AC_THRESHOLD"), - DPSetBlendColor(0xFF, 0xFF, 0xFF, 0xFF), - DPSetRenderMode(["G_RM_AA_XLU_SURF", "G_RM_AA_XLU_SURF2"], None), - ] - ) + fMaterial = fTexRect.addMaterial(toAlnum(name) + "_mat") - drawEndCommands = GfxList("temp", GfxListTag.Draw, DLFormat.Dynamic) + # use_copy_mode is based on dl_hud_img_begin and dl_hud_img_end + if use_copy_mode: + saveModeSetting(fMaterial, "G_CYC_COPY", defaults.g_mdsft_cycletype, DPSetCycleType) + else: + saveModeSetting(fMaterial, "G_CYC_1CYCLE", defaults.g_mdsft_cycletype, DPSetCycleType) + fMaterial.mat_only_DL.commands.append( + DPSetCombineMode(*fTexRect.f3d.G_CC_DECALRGBA, *fTexRect.f3d.G_CC_DECALRGBA) + ) + fMaterial.revert.commands.append(DPSetCombineMode(*fTexRect.f3d.G_CC_SHADE, *fTexRect.f3d.G_CC_SHADE)) + saveModeSetting(fMaterial, "G_TP_NONE", defaults.g_mdsft_textpersp, DPSetTexturePersp) + saveModeSetting(fMaterial, "G_AC_THRESHOLD", defaults.g_mdsft_alpha_compare, DPSetAlphaCompare) + fMaterial.mat_only_DL.commands.append(DPSetBlendColor(0xFF, 0xFF, 0xFF, 0xFF)) + + fMaterial.mat_only_DL.commands.append(DPSetRenderMode(("G_RM_AA_XLU_SURF", "G_RM_AA_XLU_SURF2"), None)) + fMaterial.revert.commands.append(DPSetRenderMode(("G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"), None)) + saveModeSetting(fMaterial, texProp.tlut_mode, defaults.g_mdsft_textlut, DPSetTextureLUT) ti = TexInfo() - if not ti.fromProp(texProp, 0): - raise PluginError(f"In {name}: {texProp.errorMsg}.") - if not ti.useTex: - raise PluginError(f"In {name}: texture disabled.") - if ti.isTexCI: - raise PluginError(f"In {name}: CI textures not compatible with exportTexRectCommon (because copy mode).") - if ti.tmemSize > 512: - raise PluginError(f"In {name}: texture is too big (> 4 KiB).") - if ti.texFormat != "RGBA16": - raise PluginError(f"In {name}: texture format must be RGBA16 (because copy mode).") - ti.imDependencies = [tex] - ti.writeAll(fTexRect.draw, fMaterial, fTexRect, convertTextureData) + ti.fromProp(texProp, index=0, ignore_tex_set=True) + ti.materialless_setup() + ti.setup_single_tex(texProp.is_ci, False) + ti.writeAll(fMaterial, fTexRect, convertTextureData) + fTexRect.materials[texProp] = (fMaterial, ti.imageDims) + + if use_copy_mode: + dsdx = 4 << 10 + dtdy = 1 << 10 + else: + dsdx = dtdy = 4096 // 4 + fTexRect.draw.commands.extend(fMaterial.mat_only_DL.commands) + fTexRect.draw.commands.extend(fMaterial.texture_DL.commands) fTexRect.draw.commands.append( - SPScisTextureRectangle(0, 0, (texDimensions[0] - 1) << 2, (texDimensions[1] - 1) << 2, 0, 0, 0) - ) - - fTexRect.draw.commands.extend(drawEndCommands.commands) - - # dl_hud_img_end - fTexRect.draw.commands.extend( - [ - DPPipeSync(), - DPSetCycleType("G_CYC_1CYCLE"), - SPTexture(0xFFFF, 0xFFFF, 0, "G_TX_RENDERTILE", "G_OFF"), - DPSetTexturePersp("G_TP_PERSP"), - DPSetAlphaCompare("G_AC_NONE"), - DPSetRenderMode(["G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"], None), - SPEndDisplayList(), - ] + SPScisTextureRectangle(0, 0, (ti.imageDims[0] - 1) << 2, (ti.imageDims[1] - 1) << 2, 0, 0, 0, dsdx, dtdy) ) + fTexRect.draw.commands.append(DPPipeSync()) + fTexRect.draw.commands.extend(fMaterial.revert.commands) + fTexRect.draw.commands.append(SPEndDisplayList()) return fTexRect @@ -367,7 +369,7 @@ def sm64ExportF3DtoC( fModel = SM64Model( name, DLFormat, - GfxMatWriteMethod.WriteDifferingAndRevert if not inline else GfxMatWriteMethod.WriteAll, + bpy.context.scene.fast64.sm64.gfx_write_method, ) fMeshes = exportF3DCommon(obj, fModel, transformMatrix, includeChildren, name, DLFormat, not savePNG) @@ -425,24 +427,17 @@ def sm64ExportF3DtoC( cDefFile.write(staticData.header) cDefFile.close() + update_actor_includes( + headerType, groupName, Path(dirPath), name, levelName, [Path("model.inc.c")], [Path("header.h")] + ) fileStatus = None if not customExport: if headerType == "Actor": - # Write to group files - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + toAlnum(name) + '/model.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + toAlnum(name) + '/header.h"', "\n#endif") - if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders( - basePath, - '#include "actors/' + toAlnum(name) + '/material.inc.c"', - '#include "actors/' + toAlnum(name) + '/material.inc.h"', + write_material_headers( + Path(basePath), + Path("actors", toAlnum(name), "material.inc.c"), + Path("actors", toAlnum(name), "material.inc.h"), ) texscrollIncludeC = '#include "actors/' + name + '/texscroll.inc.c"' @@ -451,19 +446,11 @@ def sm64ExportF3DtoC( texscrollGroupInclude = '#include "actors/' + groupName + '.h"' elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + toAlnum(name) + '/model.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + toAlnum(name) + '/header.h"', "\n#endif" - ) - if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders( + write_material_headers( basePath, - '#include "levels/' + levelName + "/" + toAlnum(name) + '/material.inc.c"', - '#include "levels/' + levelName + "/" + toAlnum(name) + '/material.inc.h"', + Path("actors", levelName, toAlnum(name), "material.inc.c"), + Path("actors", levelName, toAlnum(name), "material.inc.h"), ) texscrollIncludeC = '#include "levels/' + levelName + "/" + name + '/texscroll.inc.c"' @@ -488,10 +475,16 @@ def sm64ExportF3DtoC( def exportF3DtoBinary(romfile, exportRange, transformMatrix, obj, segmentData, includeChildren): - fModel = SM64Model(obj.name, DLFormat, GfxMatWriteMethod.WriteDifferingAndRevert) + inline = bpy.context.scene.exportInlineF3D + fModel = SM64Model(obj.name, DLFormat, bpy.context.scene.fast64.sm64.gfx_write_method) fMeshes = exportF3DCommon(obj, fModel, transformMatrix, includeChildren, obj.name, DLFormat.Static, True) - fMesh = fMeshes[fModel.getDrawLayerV3(obj)] + + if inline: + bleed_gfx = BleedGraphics() + bleed_gfx.bleed_fModel(fModel, fMeshes) fModel.freePalettes() + assert len(fMeshes) == 1, "Less or more than one fmesh" + fMesh = list(fMeshes.values())[0] addrRange = fModel.set_addr(exportRange[0]) if addrRange[1] > exportRange[1]: @@ -508,9 +501,17 @@ def exportF3DtoBinary(romfile, exportRange, transformMatrix, obj, segmentData, i def exportF3DtoBinaryBank0(romfile, exportRange, transformMatrix, obj, RAMAddr, includeChildren): - fModel = SM64Model(obj.name, DLFormat, GfxMatWriteMethod.WriteDifferingAndRevert) + inline = bpy.context.scene.exportInlineF3D + fModel = SM64Model(obj.name, DLFormat, bpy.context.scene.fast64.sm64.gfx_write_method) fMeshes = exportF3DCommon(obj, fModel, transformMatrix, includeChildren, obj.name, DLFormat.Static, True) - fMesh = fMeshes[fModel.getDrawLayerV3(obj)] + + if inline: + bleed_gfx = BleedGraphics() + bleed_gfx.bleed_fModel(fModel, fMeshes) + fModel.freePalettes() + assert len(fMeshes) == 1, "Less or more than one fmesh" + fMesh = list(fMeshes.values())[0] + segmentData = copy.copy(bank0Segment) data, startRAM = getBinaryBank0F3DData(fModel, RAMAddr, exportRange) @@ -528,9 +529,16 @@ def exportF3DtoBinaryBank0(romfile, exportRange, transformMatrix, obj, RAMAddr, def exportF3DtoInsertableBinary(filepath, transformMatrix, obj, includeChildren): - fModel = SM64Model(obj.name, DLFormat, GfxMatWriteMethod.WriteDifferingAndRevert) + inline = bpy.context.scene.exportInlineF3D + fModel = SM64Model(obj.name, DLFormat, bpy.context.scene.fast64.sm64.gfx_write_method) fMeshes = exportF3DCommon(obj, fModel, transformMatrix, includeChildren, obj.name, DLFormat.Static, True) - fMesh = fMeshes[fModel.getDrawLayerV3(obj)] + + if inline: + bleed_gfx = BleedGraphics() + bleed_gfx.bleed_fModel(fModel, fMeshes) + fModel.freePalettes() + assert len(fMeshes) == 1, "Less or more than one fmesh" + fMesh = list(fMeshes.values())[0] data, startRAM = getBinaryBank0F3DData(fModel, 0, [0, 0xFFFFFF]) # must happen after getBinaryBank0F3DData @@ -569,6 +577,7 @@ class SM64_ExportDL(bpy.types.Operator): def execute(self, context): romfileOutput = None tempROM = None + props = context.scene.fast64.sm64.combined_export try: if context.mode != "OBJECT": raise PluginError("Operator can only be used in object mode.") @@ -590,27 +599,27 @@ def execute(self, context): applyRotation([obj], radians(90), "X") if context.scene.fast64.sm64.export_type == "C": exportPath, levelName = getPathAndLevel( - context.scene.DLCustomExport, - context.scene.DLExportPath, - context.scene.DLLevelName, - context.scene.DLLevelOption, + props.is_actor_custom_export, + props.actor_custom_path, + props.export_level_name, + props.level_name, ) - if not context.scene.DLCustomExport: + if not props.is_actor_custom_export: applyBasicTweaks(exportPath) fileStatus = sm64ExportF3DtoC( exportPath, obj, DLFormat.Static if context.scene.DLExportisStatic else DLFormat.Dynamic, finalTransform, - bpy.context.scene.DLTexDir, + props.custom_include_directory, bpy.context.scene.saveTextures, bpy.context.scene.DLSeparateTextureDef, bpy.context.scene.DLincludeChildren, bpy.context.scene.DLName, levelName, - context.scene.DLGroupName, - context.scene.DLCustomExport, - context.scene.DLExportHeaderType, + props.actor_group_name, + props.is_actor_custom_export, + props.export_header_type, ) starSelectWarning(self, fileStatus) @@ -632,7 +641,7 @@ def execute(self, context): romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") - levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelDLExport]) + levelParsed = parse_level_binary(romfileOutput, props.level_name) segmentData = levelParsed.segmentData if context.scene.fast64.sm64.extend_bank_4: ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) @@ -713,27 +722,28 @@ class SM64_ExportDLPanel(SM64_Panel): def draw(self, context): col = self.layout.column() propsDLE = col.operator(SM64_ExportDL.bl_idname) + props = context.scene.fast64.sm64.combined_export if context.scene.fast64.sm64.export_type == "C": col.prop(context.scene, "DLExportisStatic") - col.prop(context.scene, "DLCustomExport") - if context.scene.DLCustomExport: - col.prop(context.scene, "DLExportPath") - prop_split(col, context.scene, "DLName", "Name") + prop_split(col, props, "export_header_type", "Export Type") + prop_split(col, context.scene, "DLName", "Name") + if props.is_actor_custom_export: + prop_split(col, props, "custom_export_path", "Custom Path") if context.scene.saveTextures: - prop_split(col, context.scene, "DLTexDir", "Texture Include Path") + prop_split(col, props, "custom_include_directory", "Texture Include Path") col.prop(context.scene, "DLSeparateTextureDef") customExportWarning(col) else: - prop_split(col, context.scene, "DLExportHeaderType", "Export Type") - prop_split(col, context.scene, "DLName", "Name") - if context.scene.DLExportHeaderType == "Actor": - prop_split(col, context.scene, "DLGroupName", "Group Name") - elif context.scene.DLExportHeaderType == "Level": - prop_split(col, context.scene, "DLLevelOption", "Level") - if context.scene.DLLevelOption == "Custom": - prop_split(col, context.scene, "DLLevelName", "Level Name") + if props.export_header_type == "Actor": + prop_split(col, props, "group_name", "Group") + if props.group_name == "Custom": + prop_split(col, props, "custom_group_name", "Group Name") + elif props.export_header_type == "Level": + prop_split(col, props, "level_name", "Level") + if props.level_name == "Custom": + prop_split(col, props, "custom_level_name", "Level Name") if context.scene.saveTextures: col.prop(context.scene, "DLSeparateTextureDef") @@ -741,10 +751,10 @@ def draw(self, context): writeBox = makeWriteInfoBox(col) writeBoxExportType( writeBox, - context.scene.DLExportHeaderType, + props.export_header_type, context.scene.DLName, - context.scene.DLLevelName, - context.scene.DLLevelOption, + props.export_level_name, + props.level_name, ) elif context.scene.fast64.sm64.export_type == "Insertable Binary": @@ -756,7 +766,7 @@ def draw(self, context): if context.scene.DLUseBank0: prop_split(col, context.scene, "DLRAMAddr", "RAM Address") else: - col.prop(context.scene, "levelDLExport") + col.prop(props, "level_name") col.prop(context.scene, "overwriteGeoPtr") if context.scene.overwriteGeoPtr: prop_split(col, context.scene, "DLExportGeoPtr", "Geolayout Pointer") @@ -822,25 +832,7 @@ class ExportTexRectDrawPanel(SM64_Panel): # called every frame def draw(self, context): col = self.layout.column() - propsTexRectE = col.operator(ExportTexRectDraw.bl_idname) - - textureProp = context.scene.texrect - tex = textureProp.tex - col.label(text="This is for decomp only.") - col.template_ID(textureProp, "tex", new="image.new", open="image.open", unlink="image.texrect_unlink") - # col.prop(textureProp, 'tex') - - tmemUsageUI(col, textureProp) - if tex is not None and tex.size[0] > 0 and tex.size[1] > 0: - col.prop(textureProp, "tex_format", text="Format") - if textureProp.tex_format[:2] == "CI": - col.prop(textureProp, "ci_format", text="CI Format") - col.prop(textureProp.S, "clamp", text="Clamp S") - col.prop(textureProp.T, "clamp", text="Clamp T") - col.prop(textureProp.S, "mirror", text="Mirror S") - col.prop(textureProp.T, "mirror", text="Mirror T") - prop_split(col, context.scene, "TexRectName", "Name") col.prop(context.scene, "TexRectCustomExport") if context.scene.TexRectCustomExport: col.prop(context.scene, "TexRectExportPath") @@ -857,6 +849,9 @@ def draw(self, context): infoBox.label(text="After export, call your hud's draw function in ") infoBox.label(text=enumHUDPaths[context.scene.TexRectExportType][0] + ": ") infoBox.label(text=enumHUDPaths[context.scene.TexRectExportType][1] + ".") + prop_split(col, context.scene, "TexRectName", "Name") + ui_image(False, col, None, context.scene.texrect, context.scene.TexRectName, False, hide_lowhigh=True) + col.operator(ExportTexRectDraw.bl_idname) class SM64_DrawLayersPanel(bpy.types.Panel): @@ -999,27 +994,19 @@ def sm64_dl_writer_register(): bpy.types.Scene.DLExportStart = bpy.props.StringProperty(name="Start", default="11D8930") bpy.types.Scene.DLExportEnd = bpy.props.StringProperty(name="End", default="11FFF00") - bpy.types.Scene.levelDLExport = bpy.props.EnumProperty(items=level_enums, name="Level", default="WF") bpy.types.Scene.DLExportGeoPtr = bpy.props.StringProperty(name="Geolayout Pointer", default="132AA8") bpy.types.Scene.overwriteGeoPtr = bpy.props.BoolProperty(name="Overwrite geolayout pointer", default=False) bpy.types.Scene.internalObjectPath = bpy.props.StringProperty(name="Directory", default="objects/gameplay_keep") bpy.types.Scene.DLExportPath = bpy.props.StringProperty(name="Directory", subtype="FILE_PATH") + bpy.types.Scene.DLExportisStatic = bpy.props.BoolProperty(name="Static DL", default=True) bpy.types.Scene.DLDefinePath = bpy.props.StringProperty(name="Definitions Filepath", subtype="FILE_PATH") bpy.types.Scene.DLUseBank0 = bpy.props.BoolProperty(name="Use Bank 0") bpy.types.Scene.DLRAMAddr = bpy.props.StringProperty(name="RAM Address", default="80000000") - bpy.types.Scene.DLTexDir = bpy.props.StringProperty(name="Include Path", default="levels/bob") bpy.types.Scene.DLSeparateTextureDef = bpy.props.BoolProperty(name="Save texture.inc.c separately") bpy.types.Scene.DLincludeChildren = bpy.props.BoolProperty(name="Include Children") bpy.types.Scene.DLInsertableBinaryPath = bpy.props.StringProperty(name="Filepath", subtype="FILE_PATH") bpy.types.Scene.DLName = bpy.props.StringProperty(name="Name", default="mario") - bpy.types.Scene.DLCustomExport = bpy.props.BoolProperty(name="Custom Export Path") - bpy.types.Scene.DLExportHeaderType = bpy.props.EnumProperty( - items=enumExportHeaderType, name="Header Export", default="Actor" - ) - bpy.types.Scene.DLGroupName = bpy.props.StringProperty(name="Group Name", default="group0") - bpy.types.Scene.DLLevelName = bpy.props.StringProperty(name="Level", default="bob") - bpy.types.Scene.DLLevelOption = bpy.props.EnumProperty(items=enumLevelNames, name="Level", default="bob") bpy.types.Scene.texrect = bpy.props.PointerProperty(type=TextureProperty) bpy.types.Scene.texrectImageTexture = bpy.props.PointerProperty(type=bpy.types.ImageTexture) @@ -1034,26 +1021,18 @@ def sm64_dl_writer_unregister(): for cls in reversed(sm64_dl_writer_classes): unregister_class(cls) - del bpy.types.Scene.levelDLExport del bpy.types.Scene.DLExportStart del bpy.types.Scene.DLExportEnd del bpy.types.Scene.DLExportGeoPtr del bpy.types.Scene.overwriteGeoPtr - del bpy.types.Scene.DLExportPath del bpy.types.Scene.DLExportisStatic del bpy.types.Scene.DLDefinePath del bpy.types.Scene.DLUseBank0 del bpy.types.Scene.DLRAMAddr - del bpy.types.Scene.DLTexDir del bpy.types.Scene.DLSeparateTextureDef del bpy.types.Scene.DLincludeChildren del bpy.types.Scene.DLInsertableBinaryPath del bpy.types.Scene.DLName - del bpy.types.Scene.DLCustomExport - del bpy.types.Scene.DLExportHeaderType - del bpy.types.Scene.DLGroupName - del bpy.types.Scene.DLLevelName - del bpy.types.Scene.DLLevelOption del bpy.types.Scene.texrect del bpy.types.Scene.TexRectExportPath diff --git a/fast64_internal/sm64/sm64_geolayout_bone.py b/fast64_internal/sm64/sm64_geolayout_bone.py index c0ee8e11a..9bb807453 100644 --- a/fast64_internal/sm64/sm64_geolayout_bone.py +++ b/fast64_internal/sm64/sm64_geolayout_bone.py @@ -1,10 +1,11 @@ import bpy from bpy.ops import object -from bpy.types import Bone, Object, Panel, Operator, Armature, Mesh, Material, PropertyGroup +from bpy.types import Bone, Object, Context, Panel, Operator, Armature, Mesh, Material, PropertyGroup from bpy.utils import register_class, unregister_class -from ..utility import PluginError, prop_split, obj_scale_is_unified +from ..utility import PluginError, get_first_set_prop, prop_split, obj_scale_is_unified, upgrade_old_prop from ..f3d.f3d_material import sm64EnumDrawLayers -from .sm64_geolayout_utility import createBoneGroups, addBoneToGroup +from .sm64_geolayout_utility import updateBone +from .custom_cmd.properties import SM64_CustomCmdProperties from bpy.props import ( StringProperty, @@ -34,12 +35,10 @@ ("Ignore", "Ignore", "Ignore bones when exporting."), ("SwitchOption", "Switch Option", "Switch Option"), ("DisplayListWithOffset", "Animated Part (0x13)", "Animated Part (Animatable Bone)"), - ("CustomAnimated", "Custom Animated", "Custom Bone used for animation"), - ("CustomNonAnimated", "Custom (Non-animated)", "Custom geolayout bone, non animated"), + ("", "", ""), + ("Custom", "Custom", "Custom bone using command presets"), ] -animatableBoneTypes = {"DisplayListWithOffset", "CustomAnimated"} - enumGeoStaticType = [ ("Billboard", "Billboard (0x14)", "Billboard"), ("DisplayListWithOffset", "Animated Part (0x13)", "Animated Part (Animatable Bone)"), @@ -81,16 +80,27 @@ ] -def drawGeoInfo(panel: Panel, bone: Bone): +def drawGeoInfo(panel: Panel, context: Context): panel.layout.box().label(text="Geolayout Inspector") + bone = context.bone if bone is None: panel.layout.label(text="Edit geolayout properties in Pose mode.") return - + bone_props: "SM64_BoneProperties" = bone.fast64.sm64 + sm64_props: "SM64_Properties" = context.scene.fast64.sm64 col = panel.layout.column() prop_split(col, bone, "geo_cmd", "Geolayout Command") + if bpy.context.scene.exportInlineF3D: + revert_split = col.split(factor=0.4) + revert_split.label(text="Revert Material") + revert_row = revert_split.row() + revert_row.prop( + bone_props, + "revert_before_func" if bone.geo_cmd in {"Function", "HeldObject"} else "revert_previous_mat", + text="Previous", + ) if bone.geo_cmd in [ "TranslateRotate", "Translate", @@ -99,9 +109,10 @@ def drawGeoInfo(panel: Panel, bone: Bone): "DisplayList", "Scale", "DisplayListWithOffset", - "CustomAnimated", ]: drawLayerWarningBox(col, bone, "draw_layer") + if bpy.context.scene.exportInlineF3D: + revert_row.prop(bone_props, "revert_after_mat", text="After") if bone.geo_cmd == "Scale": prop_split(col, bone, "geo_scale", "Scale") @@ -137,14 +148,10 @@ def drawGeoInfo(panel: Panel, bone: Bone): infoBoxRenderArea.label(text="See the object properties window for the armature instead.") prop_split(col, bone, "culling_radius", "Culling Radius") - elif bone.geo_cmd in {"CustomAnimated", "CustomNonAnimated"}: - prop_split(col, bone.fast64.sm64, "custom_geo_cmd_macro", "Geo Command Macro") - if bone.geo_cmd == "CustomNonAnimated": - prop_split(col, bone.fast64.sm64, "custom_geo_cmd_args", "Geo Command Args") - else: # It's animated - infobox = col.box() - infobox.label(text="Command's args will be filled with layer, translate, and rotate", icon="INFO") - infobox.label(text="e.g. `GEO_CUSTOM(layer, tX, tY, tZ, rX, rY, rZ, displayList)`") + elif bone.geo_cmd == "Custom": + bone_props.custom.draw_props( + col, sm64_props.binary_export, context.bone, "NO_PRESET", sm64_props.blender_to_sm64_scale + ) # if bone.geo_cmd == 'SwitchOption': # prop_split(col, bone, 'switch_bone', 'Switch Bone') @@ -168,7 +175,7 @@ def poll(cls, context): return context.scene.gameEditorMode == "SM64" def draw(self, context): - drawGeoInfo(self, context.bone) + drawGeoInfo(self, context) class GeolayoutArmaturePanel(Panel): @@ -251,10 +258,10 @@ def draw(self, context): prop_split(col, geo_asm, "param", "Parameter") col.prop(obj, "ignore_render") col.prop(obj, "ignore_collision") - if bpy.context.scene.f3d_type == "F3DEX3": - box.prop(obj, "is_occlusion_planes") - if obj.is_occlusion_planes and (not obj.ignore_render or not obj.ignore_collision): - box.label(icon="INFO", text="Suggest Ignore Render & Ignore Collision.") + # if bpy.context.scene.f3d_type == "F3DEX3": + # box.prop(obj, "is_occlusion_planes") + # if obj.is_occlusion_planes and (not obj.ignore_render or not obj.ignore_collision): + # box.label(icon="INFO", text="Suggest Ignore Render & Ignore Collision.") if context.scene.exportInlineF3D: col.prop(obj, "bleed_independently") if obj_scale_is_unified(obj) and len(obj.modifiers) == 0: @@ -446,23 +453,26 @@ def getSwitchOptionBone(switchArmature): return optionBones[0] -def updateBone(bone, context): - armatureObj = context.object - - createBoneGroups(armatureObj) - if bone.geo_cmd not in animatableBoneTypes: - addBoneToGroup(armatureObj, bone.name, bone.geo_cmd) - object.mode_set(mode="POSE") - else: - addBoneToGroup(armatureObj, bone.name, None) - object.mode_set(mode="POSE") - - class SM64_BoneProperties(PropertyGroup): version: IntProperty(name="SM64_BoneProperties Version", default=0) + custom: PointerProperty(type=SM64_CustomCmdProperties) + revert_previous_mat: BoolProperty(name="Revert Previous Material", default=False) + revert_after_mat: BoolProperty( + name="Revert After Material", + default=False, + description="If disabled the last material of each layer will still be reverted at the end", + ) + revert_before_func: BoolProperty(name="Revert Before Function", default=True) + + def upgrade_bone(self, bone): + self.custom.upgrade_bone(bone) - custom_geo_cmd_macro: StringProperty(name="Geo Command Macro", default="GEO_BONE") - custom_geo_cmd_args: StringProperty(name="Geo Command Args", default="") + @staticmethod + def upgrade_changed_props(): + for obj in bpy.data.objects: + if obj.type == "ARMATURE": + for bone in obj.data.bones: + bone.fast64.sm64.upgrade_bone(bone) sm64_bone_classes = ( diff --git a/fast64_internal/sm64/sm64_geolayout_classes.py b/fast64_internal/sm64/sm64_geolayout_classes.py index 1118cf79e..d0c5908d8 100644 --- a/fast64_internal/sm64/sm64_geolayout_classes.py +++ b/fast64_internal/sm64/sm64_geolayout_classes.py @@ -2,7 +2,7 @@ import bpy from struct import pack -from copy import copy +from copy import copy, deepcopy from ..utility import ( PluginError, @@ -19,7 +19,7 @@ geoNodeRotateOrder, ) from ..f3d.f3d_bleed import BleedGraphics -from ..f3d.f3d_gbi import FModel +from ..f3d.f3d_gbi import FMaterial, FModel, GbiMacro, GfxList from .sm64_geolayout_constants import ( nodeGroupCmds, @@ -50,6 +50,8 @@ GEO_SETUP_OBJ_RENDER, GEO_SET_BG, ) +from .sm64_geolayout_utility import BaseDisplayListNode +from .custom_cmd.exporting import CustomCmd from .sm64_utility import convert_addr_to_func drawLayerNames = { @@ -89,7 +91,8 @@ class GeolayoutGraph: def __init__(self, name): self.startGeolayout = Geolayout(name, True) # dict of Object : Geolayout - self.secondaryGeolayouts = {} + self.secondary_geolayouts: list[Geolayout] = [] + self.secondary_geolayouts_dict: dict[object, Geolayout] = {} # dict of Geolayout : Geolayout List (which geolayouts are called) self.geolayoutCalls = {} self.sortedList = [] @@ -99,6 +102,11 @@ def checkListSorted(self): if not self.sortedListGenerated: raise PluginError("Must generate sorted geolayout list first " + "before calling this function.") + @property + def names(self): + for geolayout in [self.startGeolayout] + self.secondary_geolayouts: + yield geolayout.name + def get_ptr_addresses(self): self.checkListSorted() addresses = [] @@ -114,9 +122,17 @@ def size(self): return size - def addGeolayout(self, obj, name): + def addGeolayout(self, obj: object | None, start_name: str): + name, i = start_name, 0 + while True: + if name not in self.names: + break + i += 1 + name = f"{start_name}_{i}" geolayout = Geolayout(name, False) - self.secondaryGeolayouts[obj] = geolayout + self.secondary_geolayouts.append(geolayout) + if obj is not None: + self.secondary_geolayouts_dict[obj] = geolayout return geolayout def addJumpNode(self, parentNode, caller, callee, index=None): @@ -194,7 +210,7 @@ def convertToDynamic(self): def getDrawLayers(self): drawLayers = self.startGeolayout.getDrawLayers() - for obj, geolayout in self.secondaryGeolayouts.items(): + for geolayout in self.secondary_geolayouts: drawLayers |= geolayout.getDrawLayers() return drawLayers @@ -265,39 +281,6 @@ def getDrawLayers(self): return drawLayers -class BaseDisplayListNode: - """Base displaylist node with common helper functions dealing with displaylists""" - - dl_ext = "WITH_DL" # add dl_ext to geo command if command has a displaylist - bleed_independently = False # base behavior, can be changed with obj boolProp - - def get_dl_address(self): - if self.hasDL and (self.dlRef or self.DLmicrocode is not None): - return self.dlRef or self.DLmicrocode.startAddress - return None - - def get_dl_name(self): - if self.hasDL and (self.dlRef or self.DLmicrocode is not None): - return self.dlRef or self.DLmicrocode.name - return "NULL" - - def get_c_func_macro(self, base_cmd: str): - return f"{base_cmd}_{self.dl_ext}" if self.hasDL else base_cmd - - def c_func_macro(self, base_cmd: str, *args: str): - """ - Supply base command and all arguments for command. - if self.hasDL: - this will add self.dl_ext to the command, and - adds the name of the displaylist to the end of the command - Example return: 'GEO_YOUR_COMMAND_WITH_DL(arg, arg2),' - """ - all_args = list(args) - if self.hasDL: - all_args.append(self.get_dl_name()) - return f'{self.get_c_func_macro(base_cmd)}({", ".join(all_args)}),' - - class TransformNode: def __init__(self, node): self.node = node @@ -305,6 +288,22 @@ def __init__(self, node): self.parent = None self.skinned = False self.skinnedWithoutDL = False + # base behavior, can be changed with obj boolProp + self.revert_previous_mat = False + self.revert_after_mat = False + + def do_export_checks(self): + if self.node is not None: + if hasattr(self.node, "do_export_checks"): + self.node.do_export_checks(len(self.children)) + + @property + def groups(self): + if isinstance(self.node, tuple(nodeGroupClasses)): + return True + if hasattr(self.node, "group_children"): + return self.node.group_children + return False def convertToDynamic(self): if self.node.hasDL: @@ -341,7 +340,7 @@ def has_data(self): if self.node is not None: if getattr(self.node, "hasDL", False): return True - if type(self.node) in (JumpNode, SwitchNode, FunctionNode, ShadowNode, CustomNode, CustomAnimatedNode): + if type(self.node) in (JumpNode, SwitchNode, FunctionNode, ShadowNode, CustomCmd): return True for child in self.children: if child.has_data(): @@ -350,16 +349,17 @@ def has_data(self): def size(self): size = self.node.size() if self.node is not None else 0 - if len(self.children) > 0 and type(self.node) in nodeGroupClasses: + if len(self.children) > 0 and self.groups: size += 8 # node open/close - for child in self.children: - size += child.size() + for child in self.children: + size += child.size() return size # Function commands usually effect the following command, so it is similar # to a parent child relationship. def to_binary(self, segmentData): + self.do_export_checks() if self.node is not None: data = self.node.to_binary(segmentData) else: @@ -368,37 +368,39 @@ def to_binary(self, segmentData): if type(self.node) is FunctionNode: raise PluginError("An FunctionNode cannot have children.") - if data[0] in nodeGroupCmds: + if self.groups: data.extend(bytearray([GEO_NODE_OPEN, 0x00, 0x00, 0x00])) for child in self.children: data.extend(child.to_binary(segmentData)) - if data[0] in nodeGroupCmds: + if self.groups: data.extend(bytearray([GEO_NODE_CLOSE, 0x00, 0x00, 0x00])) elif type(self.node) is SwitchNode: raise PluginError("A switch bone must have at least one child bone.") return data def to_c(self, depth): + self.do_export_checks() if self.node is not None: - nodeC = self.node.to_c() + nodeC = self.node.to_c(depth) if nodeC is not None: # Should only be the case for DisplayListNode with no DL - data = depth * "\t" + self.node.to_c() + "\n" + data = ("\t" * depth) + f"{nodeC},\n" else: data = "" else: data = "" if len(self.children) > 0: - if type(self.node) in nodeGroupClasses: - data += depth * "\t" + "GEO_OPEN_NODE(),\n" + if self.groups: + data += ("\t" * depth) + "GEO_OPEN_NODE(),\n" for child in self.children: - data += child.to_c(depth + (1 if type(self.node) in nodeGroupClasses else 0)) - if type(self.node) in nodeGroupClasses: - data += depth * "\t" + "GEO_CLOSE_NODE(),\n" + data += child.to_c(depth + (1 if self.groups else 0)) + if self.groups: + data += ("\t" * depth) + "GEO_CLOSE_NODE(),\n" elif type(self.node) is SwitchNode: raise PluginError("A switch bone must have at least one child bone.") return data def toTextDump(self, nodeLevel, segmentData): + self.do_export_checks() data = "" if self.node is not None: command = self.node.to_binary(segmentData) @@ -411,11 +413,11 @@ def toTextDump(self, nodeLevel, segmentData): data += "\n" if len(self.children) > 0: - if len(command) == 0 or command[0] in nodeGroupCmds: + if self.groups: data += "\t" * nodeLevel + "04 00 00 00\n" for child in self.children: - data += child.toTextDump(nodeLevel + 1, segmentData) - if len(command) == 0 or command[0] in nodeGroupCmds: + data += child.toTextDump(nodeLevel + (1 if self.groups else 0), segmentData) + if self.groups: data += "\t" * nodeLevel + "05 00 00 00\n" elif type(self.node) is SwitchNode: raise PluginError("A switch bone must have at least one child bone.") @@ -439,6 +441,7 @@ def __init__(self, material, specificMat, drawLayer, overrideType, texDimensions self.drawLayer = drawLayer self.overrideType = overrideType self.texDimensions = texDimensions # None implies a draw layer override + self.hasDL = False class JumpNode: @@ -464,51 +467,110 @@ def to_binary(self, segmentData): command.extend(startAddress) return command - def to_c(self): + def to_c(self, _depth=0): geo_name = self.geoRef or self.geolayout.name - return "GEO_BRANCH(" + ("1, " if self.storeReturn else "0, ") + geo_name + ")," + return "GEO_BRANCH(" + ("1, " if self.storeReturn else "0, ") + geo_name + ")" + + +LastMaterials = dict[int, tuple[FMaterial | None, list[tuple[GfxList, dict[type, GbiMacro]]]]] class GeoLayoutBleed(BleedGraphics): def bleed_geo_layout_graph(self, fModel: FModel, geo_layout_graph: GeolayoutGraph, use_rooms: bool = False): - last_materials = dict() # last used material should be kept track of per layer + # last used material, last used cmd list and resets per layer + last_materials = {} + + def copy_last(last_materials: LastMaterials) -> LastMaterials: + return {dl: [lm, [(c, deepcopy(r)) for c, r in lcr]] for dl, (lm, lcr) in last_materials.items()} + + def reset_layer(last_materials: LastMaterials, draw_layer: int) -> LastMaterials: + _, cmds_resets = last_materials.get(draw_layer, (None, [])) + for i, (cmd_list, reset_cmd_dict) in enumerate(copy(cmds_resets)): + # only discard reset if the reset was actually applied + if self.add_reset_cmds( + cmd_list, reset_cmd_dict, fModel.matWriteMethod, fModel.getRenderMode(draw_layer) + ): + cmds_resets[i] = None + while None in cmds_resets: + cmds_resets.remove(None) + if not cmds_resets: + last_materials.pop(draw_layer, 0) + return last_materials - def walk(node, last_materials): + def reset_all_layers(last_materials: LastMaterials) -> LastMaterials: + for draw_layer in copy(list(last_materials.keys())): + last_materials = reset_layer(last_materials, draw_layer) + return last_materials + + def walk(node, last_materials: LastMaterials) -> LastMaterials: + last_materials = copy_last(last_materials) base_node = node.node if type(base_node) == JumpNode: if base_node.geolayout: for node in base_node.geolayout.nodes: - last_materials = ( - walk(node, last_materials if not use_rooms else dict()) if not use_rooms else dict() - ) - else: - last_materials = dict() + last_materials = walk(node, last_materials) + fMesh = getattr(base_node, "fMesh", None) - if fMesh: - cmd_list = fMesh.drawMatOverrides.get(base_node.override_hash, None) or fMesh.draw - last_mat = last_materials.get(base_node.drawLayer, None) + last_mat, last_cmds_resets = None, [] + + if node.revert_previous_mat: + if fMesh is not None: + # add reset commands to previous cmd lists, reset last mat and reset dict + last_materials = reset_layer(last_materials, base_node.drawLayer) + else: + last_materials = reset_all_layers(last_materials) + + if fMesh is not None: + last_mat, last_cmds_resets = last_materials.get(base_node.drawLayer, (None, [])) + + base_node: BaseDisplayListNode + cmd_list = base_node.DLmicrocode default_render_mode = fModel.getRenderMode(base_node.drawLayer) + + reset_cmd_dict = {typ: cmd for _, reset_cmds in last_cmds_resets for typ, cmd in reset_cmds.items()} last_mat = self.bleed_fmesh( - fMesh, - last_mat if not base_node.bleed_independently else None, + last_mat, + reset_cmd_dict, cmd_list, fModel.getAllMaterials().items(), + fModel.matWriteMethod, default_render_mode, ) - # if the mesh has culling, it can be culled, and create invalid combinations of f3d to represent the current full DL - if fMesh.cullVertexList: - last_materials[base_node.drawLayer] = None - else: - last_materials[base_node.drawLayer] = last_mat - # don't carry over last_mat if it is a switch node or geo asm node + last_materials[base_node.drawLayer] = [last_mat, [(cmd_list, reset_cmd_dict)]] + # if the mesh has culling, we must revert to avoid bleed issues + if fMesh.cullVertexList or node.revert_after_mat: + last_materials = reset_layer(last_materials, base_node.drawLayer) + elif node.revert_after_mat: # if no mesh but still forced revert, revert all + last_materials = reset_all_layers(last_materials) + + cur_last_materials = copy_last(last_materials) + set_layers = set() + is_switch = type(base_node) in {SwitchNode} for child in node.children: - if type(base_node) in [SwitchNode, FunctionNode]: - last_materials = dict() - last_materials = walk(child, last_materials) + if is_switch: # parent node is switch or function + new_materials = walk(child, cur_last_materials) # last material info from current switch option + # add switch option reverts, to either revert at the end or in the option itself + for draw_layer, (last_mat, cmds_resets) in new_materials.items(): + # resets were added or removed in the option, therefor the option can reset that layer + if cmds_resets != cur_last_materials.get(draw_layer, (None, []))[1]: + set_layers.add(draw_layer) + last_materials.setdefault(draw_layer, [last_mat, []])[1].extend(cmds_resets) + last_materials[draw_layer][0] = None # reset last material + else: + last_materials = walk(child, last_materials) + if is_switch: + # if a switch took up the responsability of its reset, remove any previous reset of that layer + for draw_layer in set_layers: + last_mat, cmds_resets = cur_last_materials.get(draw_layer, (None, [])) + for i in range(len(cmds_resets)): + last_materials[draw_layer][1][i] = None + while None in last_materials[draw_layer][1]: + last_materials[draw_layer][1].remove(None) return last_materials for node in geo_layout_graph.startGeolayout.nodes: last_materials = walk(node, last_materials) + reset_all_layers(last_materials) self.clear_gfx_lists(fModel) @@ -521,6 +583,12 @@ def __init__(self, geo_func, func_param): self.func_param = func_param self.hasDL = False + def do_export_checks(self, children_count: int): + if children_count > 0: + raise PluginError( + "Function bones cannot have children. They instead affect the next sibling bone in alphabetical order." + ) + def size(self): return 8 @@ -531,8 +599,8 @@ def to_binary(self, segmentData): addFuncAddress(command, self.geo_func) return command - def to_c(self): - return "GEO_ASM(" + str(self.func_param) + ", " + convert_addr_to_func(self.geo_func) + ")," + def to_c(self, _depth=0): + return "GEO_ASM(" + str(self.func_param) + ", " + convert_addr_to_func(self.geo_func) + ")" class HeldObjectNode: @@ -551,7 +619,7 @@ def to_binary(self, segmentData): addFuncAddress(command, self.geo_func) return command - def to_c(self): + def to_c(self, _depth=0): return ( "GEO_HELD_OBJECT(0, " + str(convertFloatToShort(self.translate[0])) @@ -561,7 +629,7 @@ def to_c(self): + str(convertFloatToShort(self.translate[2])) + ", " + convert_addr_to_func(self.geo_func) - + ")," + + ")" ) @@ -576,8 +644,8 @@ def to_binary(self, segmentData): command = bytearray([GEO_START, 0x00, 0x00, 0x00]) return command - def to_c(self): - return "GEO_NODE_START()," + def to_c(self, _depth=0): + return "GEO_NODE_START()" class EndNode: @@ -591,8 +659,8 @@ def to_binary(self, segmentData): command = bytearray([GEO_END, 0x00, 0x00, 0x00]) return command - def to_c(self): - return "GEO_END()," + def to_c(self, _depth=0): + return "GEO_END()" # Geolayout node hierarchy is first generated without material/draw layer @@ -616,8 +684,8 @@ def to_binary(self, segmentData): addFuncAddress(command, self.switchFunc) return command - def to_c(self): - return "GEO_SWITCH_CASE(" + str(self.defaultCase) + ", " + convert_addr_to_func(self.switchFunc) + ")," + def to_c(self, _depth=0): + return "GEO_SWITCH_CASE(" + str(self.defaultCase) + ", " + convert_addr_to_func(self.switchFunc) + ")" class TranslateRotateNode(BaseDisplayListNode): @@ -688,7 +756,7 @@ def to_binary(self, segmentData): command.extend(bytearray([0x00] * 4)) return command - def to_c(self): + def to_c(self, _depth=0): if self.fieldLayout == 0: return self.c_func_macro( "GEO_TRANSLATE_ROTATE", @@ -755,7 +823,7 @@ def to_binary(self, segmentData): command.extend(bytearray([0x00] * 4)) return command - def to_c(self): + def to_c(self, _depth=0): return self.c_func_macro( "GEO_TRANSLATE_NODE", getDrawLayerName(self.drawLayer), @@ -798,7 +866,7 @@ def to_binary(self, segmentData): command.extend(bytearray([0x00] * 4)) return command - def to_c(self): + def to_c(self, _depth=0): return self.c_func_macro( "GEO_ROTATION_NODE", getDrawLayerName(self.drawLayer), @@ -840,7 +908,7 @@ def to_binary(self, segmentData): command.extend(bytearray([0x00] * 4)) return command - def to_c(self): + def to_c(self, _depth=0): return self.c_func_macro( "GEO_BILLBOARD_WITH_PARAMS", getDrawLayerName(self.drawLayer), @@ -875,11 +943,11 @@ def to_binary(self, segmentData): command.extend(bytearray([0x00] * 4)) return command - def to_c(self): + def to_c(self, _depth=0): if not self.hasDL: return None args = [getDrawLayerName(self.drawLayer), self.get_dl_name()] - return f"GEO_DISPLAY_LIST({join_c_args(args)})," + return f"GEO_DISPLAY_LIST({join_c_args(args)})" class ShadowNode: @@ -899,9 +967,9 @@ def to_binary(self, segmentData): command.extend(self.shadowScale.to_bytes(2, "big")) return command - def to_c(self): + def to_c(self, _depth=0): return ( - "GEO_SHADOW(" + str(self.shadowType) + ", " + str(self.shadowSolidity) + ", " + str(self.shadowScale) + ")," + "GEO_SHADOW(" + str(self.shadowType) + ", " + str(self.shadowSolidity) + ", " + str(self.shadowScale) + ")" ) @@ -933,7 +1001,7 @@ def to_binary(self, segmentData): command.extend(bytearray([0x00] * 4)) return command - def to_c(self): + def to_c(self, _depth=0): return self.c_func_macro( "GEO_SCALE", getDrawLayerName(self.drawLayer), str(int(round(self.scaleValue * 0x10000))) ) @@ -952,12 +1020,12 @@ def to_binary(self, segmentData): command.extend(convertFloatToShort(self.cullingRadius).to_bytes(2, "big")) return command - def to_c(self): + def to_c(self, _depth=0): cullingRadius = convertFloatToShort(self.cullingRadius) # if abs(cullingRadius) > 2**15 - 1: # raise PluginError("A render area node has a culling radius that does not fit an s16.\n Radius is " +\ # str(cullingRadius) + ' when converted to SM64 units.') - return "GEO_CULLING_RADIUS(" + str(convertFloatToShort(self.cullingRadius)) + ")," + return "GEO_CULLING_RADIUS(" + str(convertFloatToShort(self.cullingRadius)) + ")" class RenderRangeNode: @@ -975,13 +1043,13 @@ def to_binary(self, segmentData): command.extend(convertFloatToShort(self.maxDist).to_bytes(2, "big")) return command - def to_c(self): + def to_c(self, _depth=0): minDist = convertFloatToShort(self.minDist) maxDist = convertFloatToShort(self.maxDist) # if (abs(minDist) > 2**15 - 1) or (abs(maxDist) > 2**15 - 1): # raise PluginError("A render range (LOD) node has a range that does not fit an s16.\n Range is " +\ # str(minDist) + ', ' + str(maxDist) + ' when converted to SM64 units.') - return "GEO_RENDER_RANGE(" + str(minDist) + ", " + str(maxDist) + ")," + return "GEO_RENDER_RANGE(" + str(minDist) + ", " + str(maxDist) + ")" class DisplayListWithOffsetNode(BaseDisplayListNode): @@ -1012,7 +1080,7 @@ def to_binary(self, segmentData): command.extend(bytearray([0x00] * 4)) return command - def to_c(self): + def to_c(self, _depth=0): args = [ getDrawLayerName(self.drawLayer), str(convertFloatToShort(self.translate[0])), @@ -1020,7 +1088,7 @@ def to_c(self): str(convertFloatToShort(self.translate[2])), self.get_dl_name(), # This node requires 'NULL' if there is no DL ] - return f"GEO_ANIMATED_PART({join_c_args(args)})," + return f"GEO_ANIMATED_PART({join_c_args(args)})" class ScreenAreaNode: @@ -1046,10 +1114,10 @@ def to_binary(self, segmentData): command.extend(dimensions[1].to_bytes(2, "big", signed=True)) return command - def to_c(self): + def to_c(self, _depth=0): if self.useDefaults: return ( - "GEO_NODE_SCREEN_AREA(10, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2)," + "GEO_NODE_SCREEN_AREA(10, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2, " + "SCREEN_WIDTH/2, SCREEN_HEIGHT/2)" ) else: return ( @@ -1063,7 +1131,7 @@ def to_c(self): + str(self.dimensions[0]) + ", " + str(self.dimensions[1]) - + ")," + + ")" ) @@ -1081,8 +1149,8 @@ def to_binary(self, segmentData): command.extend(bytearray(pack(">f", self.scale))) return command - def to_c(self): - return "GEO_NODE_ORTHO(" + format(self.scale, ".4f") + ")," + def to_c(self, _depth=0): + return "GEO_NODE_ORTHO(" + format(self.scale, ".4f") + ")" class FrustumNode: @@ -1106,9 +1174,9 @@ def to_binary(self, segmentData): command.extend(bytes.fromhex("8029AA3C")) return command - def to_c(self): + def to_c(self, _depth=0): if not self.useFunc: - return "GEO_CAMERA_FRUSTUM(" + format(self.fov, ".4f") + ", " + str(self.near) + ", " + str(self.far) + ")," + return "GEO_CAMERA_FRUSTUM(" + format(self.fov, ".4f") + ", " + str(self.near) + ", " + str(self.far) + ")" else: return ( "GEO_CAMERA_FRUSTUM_WITH_FUNC(" @@ -1117,7 +1185,7 @@ def to_c(self): + str(self.near) + ", " + str(self.far) - + ", geo_camera_fov)," + + ", geo_camera_fov)" ) @@ -1133,8 +1201,8 @@ def to_binary(self, segmentData): command = bytearray([GEO_SET_Z_BUF, 0x01 if self.enable else 0x00, 0x00, 0x00]) return command - def to_c(self): - return "GEO_ZBUFFER(" + ("1" if self.enable else "0") + ")," + def to_c(self, _depth=0): + return "GEO_ZBUFFER(" + ("1" if self.enable else "0") + ")" class CameraNode: @@ -1160,7 +1228,7 @@ def to_binary(self, segmentData): addFuncAddress(command, self.geo_func) return command - def to_c(self): + def to_c(self, _depth=0): return ( "GEO_CAMERA(" + str(self.camType) @@ -1178,7 +1246,7 @@ def to_c(self): + str(self.lookAt[2]) + ", " + convert_addr_to_func(self.geo_func) - + ")," + + ")" ) @@ -1194,8 +1262,8 @@ def to_binary(self, segmentData): command = bytearray([GEO_SETUP_OBJ_RENDER, 0x00, 0x00, 0x00]) return command - def to_c(self): - return "GEO_RENDER_OBJ()," + def to_c(self, _depth=0): + return "GEO_RENDER_OBJ()" class BackgroundNode: @@ -1217,61 +1285,11 @@ def to_binary(self, segmentData): addFuncAddress(command, self.geo_func) return command - def to_c(self): + def to_c(self, _depth=0): if self.isColor: - return "GEO_BACKGROUND_COLOR(0x" + format(self.backgroundValue, "04x").upper() + ")," + return "GEO_BACKGROUND_COLOR(0x" + format(self.backgroundValue, "04x").upper() + ")" else: - return "GEO_BACKGROUND(" + str(self.backgroundValue) + ", " + convert_addr_to_func(self.geo_func) + ")," - - -class CustomNode: - def __init__(self, command: str, args: str): - self.command = command - self.args = args or "" # command may not have args - self.hasDL = False - - def size(self): - return 8 - - def to_binary(self, segmentData): - raise PluginError("Custom Geo Nodes are not supported for binary exports.") - - def to_c(self): - return f"{self.command}({self.args})," - - -class CustomAnimatedNode(BaseDisplayListNode): - def __init__(self, command: str, drawLayer, translate, rotate, dlRef: str = None): - self.command = command - self.drawLayer = drawLayer - self.hasDL = True - self.translate = translate - self.rotate = rotate - self.fMesh = None - self.DLmicrocode = None - self.dlRef = dlRef - # exists to get the override DL from an fMesh - self.override_hash = None - - def size(self): - return 16 - - def get_ptr_offsets(self): - return [] - - def to_binary(self, segmentData): - raise PluginError("Custom Geo Nodes are not supported for binary exports.") - - def to_c(self): - args = [ - getDrawLayerName(self.drawLayer), - str(convertFloatToShort(self.translate[0])), - str(convertFloatToShort(self.translate[1])), - str(convertFloatToShort(self.translate[2])), - *(str(radians_to_s16(r)) for r in self.rotate.to_euler("XYZ")), - self.get_dl_name(), # This node requires 'NULL' if there is no DL - ] - return f"{self.command}({join_c_args(args)})," + return "GEO_BACKGROUND(" + str(self.backgroundValue) + ", " + convert_addr_to_func(self.geo_func) + ")" nodeGroupClasses = [ @@ -1291,8 +1309,6 @@ def to_c(self): ZBufferNode, CameraNode, RenderRangeNode, - CustomNode, - CustomAnimatedNode, ] DLNodes = [ @@ -1303,5 +1319,4 @@ def to_c(self): ScaleNode, DisplayListNode, DisplayListWithOffsetNode, - CustomAnimatedNode, ] diff --git a/fast64_internal/sm64/sm64_geolayout_parser.py b/fast64_internal/sm64/sm64_geolayout_parser.py index db724a4cd..095dfa89e 100644 --- a/fast64_internal/sm64/sm64_geolayout_parser.py +++ b/fast64_internal/sm64/sm64_geolayout_parser.py @@ -2,9 +2,10 @@ from bpy.utils import register_class, unregister_class from ..f3d.f3d_parser import createBlankMaterial, parseF3DBinary from ..panels import SM64_Panel -from .sm64_level_parser import parseLevelAtPointer -from .sm64_constants import level_pointers, level_enums -from .sm64_geolayout_bone import enumShadowType, animatableBoneTypes, enumBoneType +from .sm64_level_parser import parse_level_binary +from .sm64_constants import enumLevelNames +from .sm64_geolayout_bone import enumShadowType +from .sm64_geolayout_utility import is_bone_animatable from .sm64_geolayout_constants import getGeoLayoutCmdLength, nodeGroupCmds, GEO_BRANCH_STORE from .sm64_utility import import_rom_checks @@ -22,6 +23,8 @@ prop_split, sm64BoneUp, geoNodeRotateOrder, + selectSingleObject, + deselectAllObjects, ) from .sm64_geolayout_utility import ( @@ -131,8 +134,7 @@ def parseGeoLayout( if shadeSmooth: if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") - listObj.select_set(True) + selectSingleObject(listObj) bpy.ops.object.shade_smooth() # Dont remove doubles here, as importing geolayout all at once results @@ -150,7 +152,7 @@ def parseGeoLayout( # Apply mesh to armature. if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() obj.select_set(True) switchArmatureObj.select_set(True) bpy.context.view_layer.objects.active = switchArmatureObj @@ -168,13 +170,6 @@ def parseGeoLayout( if bpy.app.version < (4, 0, 0) and useArmature: armatureObj.data.layers[1] = True - """ - if useMetarig: - metaBones = [bone for bone in armatureObj.data.bones if \ - bone.layers[boneLayers['meta']] or bone.layers[boneLayers['visual']]] - for bone in metaBones: - addBoneToGroup(armatureObj, bone.name, 'Ignore') - """ return armatureMeshGroups, armatureObj @@ -535,11 +530,9 @@ def traverseArmatureForMetarig(armatureObj, boneName, parentName): if bpy.app.version >= (4, 0, 0): if "Ignore" in bone.collections: return - nonAnimatableBoneTypes = set([item[0] for item in enumBoneType]) - animatableBoneTypes - isAnimatableBone = not any([item in bone.collections for item in nonAnimatableBoneTypes]) - if isAnimatableBone: + if is_bone_animatable(bone): processBoneMeta(armatureObj, boneName, parentName) - nextParentName = boneName if isAnimatableBone else parentName + nextParentName = boneName if is_bone_animatable(bone) else parentName bone = armature.bones[boneName] # re-obtain reference after edit mode changes childrenNames = [child.name for child in bone.children] @@ -701,38 +694,36 @@ def createConnectBone(armatureObj, childName, parentName): def createBone(armatureObj, parentBoneName, boneName, currentTransform, boneGroup, loadDL): - if bpy.context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") - bpy.context.view_layer.objects.active = armatureObj - bpy.ops.object.mode_set(mode="EDIT") - bone = armatureObj.data.edit_bones.new(boneName) - bone.use_connect = False + if bpy.context.mode != "EDIT": + bpy.ops.object.mode_set(mode="EDIT") + edit_bone: bpy.types.EditBone = armatureObj.data.edit_bones.new(boneName) + boneName = edit_bone.name + edit_bone.use_connect = False if parentBoneName is not None: - bone.parent = armatureObj.data.edit_bones[parentBoneName] - bone.head = currentTransform @ mathutils.Vector((0, 0, 0)) - bone.tail = bone.head + ( + edit_bone.parent = armatureObj.data.edit_bones[parentBoneName] + edit_bone.head = currentTransform @ mathutils.Vector((0, 0, 0)) + edit_bone.tail = edit_bone.head + ( currentTransform.to_quaternion() @ mathutils.Vector((0, 1, 0)) * (0.2 if boneGroup != "DisplayList" else 0.1) ) # Connect bone to parent if it is possible without changing parent direction. if parentBoneName is not None: - nodeOffsetVector = mathutils.Vector(bone.head - bone.parent.head) + nodeOffsetVector = mathutils.Vector(edit_bone.head - edit_bone.parent.head) # set fallback to nonzero to avoid creating zero length bones - if nodeOffsetVector.angle(bone.parent.tail - bone.parent.head, 1) < 0.0001 and loadDL: - for child in bone.parent.children: - if child != bone: + if nodeOffsetVector.angle(edit_bone.parent.tail - edit_bone.parent.head, 1) < 0.0001 and loadDL: + for child in edit_bone.parent.children: + if child != edit_bone: child.use_connect = False - bone.parent.tail = bone.head - bone.use_connect = True - elif bone.head == bone.parent.head and bone.tail == bone.parent.tail: - bone.tail += currentTransform.to_quaternion() @ mathutils.Vector((0, 1, 0)) * 0.02 + edit_bone.parent.tail = edit_bone.head + edit_bone.use_connect = True + elif edit_bone.head == edit_bone.parent.head and edit_bone.tail == edit_bone.parent.tail: + edit_bone.tail += currentTransform.to_quaternion() @ mathutils.Vector((0, 1, 0)) * 0.02 - boneName = bone.name - addBoneToGroup(armatureObj, bone.name, boneGroup) - bone = armatureObj.data.bones[boneName] + bpy.ops.object.mode_set(mode="OBJECT") + bone: bpy.types.Bone = armatureObj.data.bones[boneName] bone.geo_cmd = boneGroup if boneGroup is not None else "DisplayListWithOffset" + addBoneToGroup(armatureObj, boneName) return boneName @@ -741,7 +732,7 @@ def createSwitchOption( armatureObj, switchBoneName, boneName, currentTransform, nextParentTransform, switchLevel, switchCount ): bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() # bpy.context.view_layer.objects.active = armatureObj # bpy.ops.object.mode_set(mode="EDIT") # bone = armatureObj.data.edit_bones.new(boneName) @@ -802,7 +793,7 @@ def createSwitchOption( bMesh = bmesh.new() bMesh.from_mesh(mesh) - addBoneToGroup(switchArmature, boneName, "SwitchOption") + addBoneToGroup(switchArmature, boneName) return boneName, (switchArmature, bMesh, obj), finalTransform, finalNextParentTransform @@ -1540,7 +1531,7 @@ def execute(self, context): armatureObj = None # Get segment data - levelParsed = parseLevelAtPointer(romfileSrc, level_pointers[levelGeoImport]) + levelParsed = parse_level_binary(romfileSrc, levelGeoImport) segmentData = levelParsed.segmentData geoStart = int(geoImportAddr, 16) if context.scene.geoIsSegPtr: @@ -1559,21 +1550,19 @@ def execute(self, context): ) romfileSrc.close() - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() if armatureObj is not None: for armatureMeshGroup in armatureMeshGroups: armatureMeshGroup[0].select_set(True) doRotation(math.radians(-90), "X") for armatureMeshGroup in armatureMeshGroups: - bpy.ops.object.select_all(action="DESELECT") - armatureMeshGroup[0].select_set(True) - bpy.context.view_layer.objects.active = armatureMeshGroup[0] + selectSingleObject(armatureMeshGroup[0]) bpy.ops.object.make_single_user(obdata=True) bpy.ops.object.transform_apply(location=False, rotation=True, scale=False, properties=False) else: doRotation(math.radians(-90), "X") - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() # objs[-1].select_set(True) self.report({"INFO"}, "Generic import succeeded.") @@ -1636,7 +1625,7 @@ def sm64_geo_parser_register(): bpy.types.Scene.geoImportAddr = bpy.props.StringProperty(name="Start Address", default="1F1D60") bpy.types.Scene.generateArmature = bpy.props.BoolProperty(name="Generate Armature?", default=True) - bpy.types.Scene.levelGeoImport = bpy.props.EnumProperty(items=level_enums, name="Level", default="HMC") + bpy.types.Scene.levelGeoImport = bpy.props.EnumProperty(items=enumLevelNames, name="Level", default="hmc") bpy.types.Scene.ignoreSwitch = bpy.props.BoolProperty(name="Ignore Switch Nodes", default=True) diff --git a/fast64_internal/sm64/sm64_geolayout_utility.py b/fast64_internal/sm64/sm64_geolayout_utility.py index f42220558..d94be95b1 100644 --- a/fast64_internal/sm64/sm64_geolayout_utility.py +++ b/fast64_internal/sm64/sm64_geolayout_utility.py @@ -1,7 +1,20 @@ import bpy +from bpy.types import Object, Armature, Bone, PoseBone + +from ..f3d.f3d_gbi import GfxList from ..utility import PluginError +def is_bone_animatable(bone: Bone): + bone_props: "SM64_BoneProperties" = bone.fast64.sm64 + geo_cmd: str = bone.geo_cmd + if geo_cmd == "DisplayListWithOffset": + return True + elif geo_cmd == "Custom" and bone_props.custom.is_animated: + return True + return False + + def getBoneGroupByName(armatureObj, name): for boneGroup in armatureObj.pose.bone_groups: if boneGroup.name == name: @@ -42,6 +55,8 @@ def __init__(self, deform, theme): "StartRenderArea": BoneNodeProperties(True, "THEME13"), # 0x20 "Ignore": BoneNodeProperties(False, "THEME08"), # Used for rigging "SwitchOption": BoneNodeProperties(False, "THEME11"), + "DisplayListWithOffset": BoneNodeProperties(True, "THEME00"), + "Custom": BoneNodeProperties(True, "THEME15"), } boneLayers = {"anim": 0, "other": 1, "meta": 2, "visual": 3} @@ -66,51 +81,94 @@ def createBoneGroups(armatureObj): boneGroup.color_set = properties.theme -def addBoneToGroup(armatureObj, boneName, groupName): - armature = armatureObj.data - if groupName is None: - if bpy.context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - posebone = armatureObj.pose.bones[boneName] - bone = armature.bones[boneName] - bone.use_deform = True +def addBoneToGroup(armature_obj: Object, name: str): + armature: Armature = armature_obj.data + pose_bone: PoseBone = armature_obj.pose.bones[name] + bone: Bone = armature.bones[name] + geo_cmd: str = bone.geo_cmd + if geo_cmd not in boneNodeProperties: + raise PluginError(f"Bone group {geo_cmd} doesn't exist.") + + lock_location, lock_rotation, lock_scale = False, False, False + + if is_bone_animatable(bone): if bpy.app.version >= (4, 0, 0): if not "anim" in armature.collections: armature.collections.new(name="anim") armature.collections["anim"].assign(bone) else: - posebone.bone_group = None + pose_bone.bone_group = None bone.layers = createBoneLayerMask([boneLayers["anim"]]) - posebone.lock_location = (False, False, False) - posebone.lock_rotation = (False, False, False) - posebone.lock_scale = (False, False, False) - return - - elif groupName not in boneNodeProperties: - raise PluginError("Bone group " + groupName + " doesn't exist.") - - if bpy.context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - - posebone = armatureObj.pose.bones[boneName] - bone = armatureObj.data.bones[boneName] if bpy.app.version >= (4, 0, 0): - armature.collections[groupName].assign(bone) + armature.collections[geo_cmd].assign(bone) else: - posebone.bone_group_index = getBoneGroupIndex(armatureObj, groupName) - - if groupName != "Ignore": - bone.use_deform = boneNodeProperties[groupName].deform - if groupName != "DisplayList": - if bpy.app.version >= (4, 0, 0): - if not "other" in armature.collections: - armature.collections.new(name="other") - armature.collections["other"].assign(bone) - else: - bone.layers = createBoneLayerMask([boneLayers["other"]]) - - if groupName != "SwitchOption": - posebone.lock_location = (True, True, True) - posebone.lock_rotation = (True, True, True) - posebone.lock_scale = (True, True, True) + pose_bone.bone_group_index = getBoneGroupIndex(armature_obj, geo_cmd) + + if geo_cmd == "Custom": + custom = bone.fast64.sm64.custom + bone.use_deform = custom.dl_option != "NONE" + if not custom.is_animated: + lock_location = lock_rotation = lock_scale = True + elif geo_cmd != "Ignore": + bone.use_deform = boneNodeProperties[geo_cmd].deform + if geo_cmd != "SwitchOption": + lock_location = True + lock_rotation = lock_scale = True + if geo_cmd not in {"Ignore", "DisplayList"}: + if bpy.app.version >= (4, 0, 0): + if not "other" in armature.collections: + armature.collections.new(name="other") + armature.collections["other"].assign(bone) + else: + bone.layers = createBoneLayerMask([boneLayers["other"]]) + + pose_bone.lock_location = (lock_location, lock_location, lock_location) + pose_bone.lock_rotation = (lock_rotation, lock_rotation, lock_rotation) + pose_bone.lock_scale = (lock_scale, lock_scale, lock_scale) + + +def updateBone(bone, context): + armatureObj = context.object + + createBoneGroups(armatureObj) + addBoneToGroup(armatureObj, bone.name) + + +class BaseDisplayListNode: + """Base displaylist node with common helper functions dealing with displaylists""" + + dl_ext = "WITH_DL" # add dl_ext to geo command if command has a displaylist + override_layer = False + dlRef: str | GfxList | None + + def get_dl_address(self): + assert not isinstance(self.dlRef, str), "dlRef string not supported in binary" + if isinstance(self.dlRef, GfxList): + return self.dlRef.startAddress + if self.hasDL and self.DLmicrocode is not None: + return self.DLmicrocode.startAddress + return None + + def get_dl_name(self): + if isinstance(self.dlRef, GfxList): + return self.dlRef.name + if self.hasDL and (self.dlRef or self.DLmicrocode is not None): + return self.dlRef or self.DLmicrocode.name + return "NULL" + + def get_c_func_macro(self, base_cmd: str): + return f"{base_cmd}_{self.dl_ext}" if self.hasDL else base_cmd + + def c_func_macro(self, base_cmd: str, *args: str): + """ + Supply base command and all arguments for command. + if self.hasDL: + this will add self.dl_ext to the command, and + adds the name of the displaylist to the end of the command + Example return: 'GEO_YOUR_COMMAND_WITH_DL(arg, arg2),' + """ + all_args = list(args) + if self.hasDL: + all_args.append(self.get_dl_name()) + return f'{self.get_c_func_macro(base_cmd)}({", ".join(all_args)})' diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index 7a3fb7abd..cd4d7f0a0 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -1,4 +1,6 @@ from __future__ import annotations +from pathlib import Path +import typing import bpy, mathutils, math, copy, os, shutil, re from bpy.utils import register_class, unregister_class @@ -7,17 +9,18 @@ from ..operators import ObjectDataExporter from ..panels import SM64_Panel from .sm64_objects import InlineGeolayoutObjConfig, inlineGeoLayoutObjects -from .sm64_geolayout_bone import getSwitchOptionBone, animatableBoneTypes +from .sm64_geolayout_bone import getSwitchOptionBone from .sm64_camera import saveCameraSettingsToGeolayout from .sm64_f3d_writer import SM64Model, SM64GfxFormatter from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_level_parser import parseLevelAtPointer +from .sm64_level_parser import parse_level_binary from .sm64_rom_tweaks import ExtendBank0x04 -from .sm64_utility import export_rom_checks, starSelectWarning +from .sm64_utility import export_rom_checks, starSelectWarning, update_actor_includes, write_material_headers from ..utility import ( PluginError, VertexWeightError, + z_up_to_y_up_matrix, setOrigin, raisePluginError, findStartBones, @@ -26,10 +29,8 @@ getExportDir, toAlnum, writeMaterialFiles, - writeIfNotFound, get64bitAlignedAddr, encodeSegmentedAddr, - writeMaterialHeaders, writeInsertableFile, bytesToHex, checkSM64EmptyUsesGeoLayout, @@ -51,12 +52,9 @@ tempName, getAddressFromRAMAddress, prop_split, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, - writeBoxExportType, - enumExportHeaderType, geoNodeRotateOrder, + deselectAllObjects, + selectSingleObject, ) from ..f3d.f3d_bleed import ( @@ -99,10 +97,10 @@ DLFormat, SPEndDisplayList, SPDisplayList, - FMaterial, ) from .sm64_geolayout_classes import ( + BaseDisplayListNode, DisplayListNode, TransformNode, StartNode, @@ -116,25 +114,32 @@ RotateNode, TranslateRotateNode, FunctionNode, - CustomNode, BillboardNode, ScaleNode, RenderRangeNode, ShadowNode, DisplayListWithOffsetNode, - CustomAnimatedNode, HeldObjectNode, Geolayout, ) -from .sm64_constants import ( - insertableBinaryTypes, - bank0Segment, - level_pointers, - defaultExtendSegment4, - level_enums, - enumLevelNames, -) +from .sm64_constants import insertableBinaryTypes, bank0Segment, defaultExtendSegment4 + +if typing.TYPE_CHECKING: + from .sm64_geolayout_bone import SM64_BoneProperties + + +def get_custom_cmd_with_transform(node: "CustomNode", parentTransformNode: TransformNode, translate, rotate, scale): + types = {a["arg_type"] for a in node.data["args"]} + has_translation, has_rotation, has_scale = "TRANSLATION" in types, "ROTATION" in types, "SCALE" in types + if (not has_translation and not isZeroTranslation(translate)) or (not has_rotation and not isZeroRotation(rotate)): + field = 0 if not (has_translation or has_rotation) else (1 if has_rotation else 2) + parentTransformNode = addParentNode( + parentTransformNode, TranslateRotateNode(node.drawLayer, field, False, translate, rotate) + ) + if not has_scale and not isZeroScaleChange(scale): + parentTransformNode = addParentNode(parentTransformNode, ScaleNode(node.drawLayer, scale[0], False)) + return node, parentTransformNode, has_translation, has_rotation, has_scale def appendSecondaryGeolayout(geoDirPath, geoName1, geoName2, additionalNode=""): @@ -293,12 +298,10 @@ def replaceDLReferenceInGeo(geoPath, pattern, replacement): def prepareGeolayoutExport(armatureObj, obj): # Make object and armature space the same. - setOrigin(armatureObj, obj) + setOrigin(obj, armatureObj.location) # Apply armature scale. - bpy.ops.object.select_all(action="DESELECT") - armatureObj.select_set(True) - bpy.context.view_layer.objects.active = armatureObj + selectSingleObject(armatureObj) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False) @@ -335,56 +338,122 @@ def getCameraObj(camera): raise PluginError("The level camera " + camera.name + " is no longer in the scene.") -def appendRevertToGeolayout(geolayoutGraph, fModel): - materialRevert = GfxList( - fModel.name + "_" + "material_revert_render_settings", GfxListTag.MaterialRevert, fModel.DLFormat +DrawLayerDict = dict[int, list[TransformNode]] + + +def append_revert_to_geolayout(graph: GeolayoutGraph, f_model: SM64Model): + material_revert = GfxList( + f_model.name + "_" + "material_revert_render_settings", GfxListTag.MaterialRevert, f_model.DLFormat ) - revertMatAndEndDraw(materialRevert, [DPSetEnvColor(0xFF, 0xFF, 0xFF, 0xFF), DPSetAlphaCompare("G_AC_NONE")]) + revertMatAndEndDraw(material_revert, [DPSetEnvColor(0xFF, 0xFF, 0xFF, 0xFF), DPSetAlphaCompare("G_AC_NONE")]) # walk the geo layout graph to find the last used DL for each layer # each switch child will be considered a last used DL, unless subsequent # DL is drawn outside switch root - def walk(node, last_gfx_list: list[dict]): + def walk(node, draw_layer_dict: DrawLayerDict) -> DrawLayerDict: base_node = node.node if type(base_node) == JumpNode: if base_node.geolayout: for node in base_node.geolayout.nodes: - last_gfx_list = walk(node, last_gfx_list) + draw_layer_dict = walk(node, draw_layer_dict.copy()) fMesh = getattr(base_node, "fMesh", None) if fMesh: - cmd_list = fMesh.drawMatOverrides.get(base_node.override_hash, None) or fMesh.draw - for draw_layer_dict in last_gfx_list: - draw_layer_dict[base_node.drawLayer] = cmd_list - switch_gfx_lists = [] + draw_layer_dict[base_node.drawLayer] = [node] + + start_draw_layer_dict = draw_layer_dict.copy() for child in node.children: if type(base_node) == SwitchNode: - switch_gfx_lists.extend(walk(child, [dict()])) + option_resets = walk(child, {}) + for ( + draw_layer, + nodes, + ) in option_resets.items(): # add draw layers that are not already in draw_layer_dict + if draw_layer not in start_draw_layer_dict: + if draw_layer not in draw_layer_dict: + draw_layer_dict[draw_layer] = [] + draw_layer_dict[draw_layer].extend(nodes) + for draw_layer, nodes in start_draw_layer_dict.items(): + if draw_layer in option_resets: # option overrides a previous draw layer + nodes.clear() + nodes.extend(option_resets[draw_layer]) else: - last_gfx_list = walk(child, last_gfx_list) - # update the non switch nodes with the last switch node of each layer drawn - # that node will be overridden by at least one of the switch nodes - # for that layer, later items in the list will cover unique switch nodes - if switch_gfx_lists: - for draw_layer_dict in last_gfx_list: - draw_layer_dict.update(switch_gfx_lists[-1]) - last_gfx_list.extend(switch_gfx_lists) - return last_gfx_list - - for node in geolayoutGraph.startGeolayout.nodes: - last_gfx_list = walk(node, [dict()]) + draw_layer_dict = walk(child, draw_layer_dict.copy()) + return draw_layer_dict + + draw_layer_dict: DrawLayerDict = {} + for node in graph.startGeolayout.nodes: + draw_layer_dict = walk(node, draw_layer_dict.copy()) + + def create_revert_node(draw_layer, node: DisplayListNode | None = None): + f_mesh = f_model.addMesh("final_revert", f_model.name, draw_layer, False, None, dedup=True) + f_mesh.draw = gfx_list = GfxList(f_mesh.name, GfxListTag.Draw, f_model.DLFormat) + gfx_list.commands.extend(material_revert.commands) + revert_node = DisplayListNode(draw_layer) + revert_node.DLmicrocode = gfx_list + revert_node.fMesh = f_mesh + if node is None: + graph.startGeolayout.nodes.append(TransformNode(revert_node)) + else: + addParentNode(node, revert_node) # Revert settings in each unique draw layer - reverted_gfx_lists = set() - for draw_layer_dict in last_gfx_list: - for gfx_list in draw_layer_dict.values(): - if gfx_list in reverted_gfx_lists: + for draw_layer, nodes in draw_layer_dict.items(): + if len(nodes) == 0: + create_revert_node(draw_layer) + for transform_node in nodes: + node = transform_node.node + f_mesh: FMesh = node.fMesh + cmd_list: GfxList = node.DLmicrocode + if f_mesh.cullVertexList: + create_revert_node(draw_layer, transform_node) + else: + if (hasattr(f_mesh, "override_layer") and f_mesh.override_layer) or node.override_hash: + draw_overrides = f_model.draw_overrides.setdefault(f_mesh, {}) + if node.override_hash is None: + node.override_hash = (5, node.drawLayer) + else: + node.override_hash = (5, *node.override_hash) + existing_cmd_list, existing_nodes = draw_overrides.get(node.override_hash, (None, [])) + if existing_cmd_list is not None: + node.DLmicrocode = existing_cmd_list + existing_nodes.append(node) + continue + else: + node.DLmicrocode = cmd_list = copy.copy(cmd_list) + if node.override_hash not in draw_overrides: + cmd_list.name += f"_with_layer_{node.drawLayer}_revert" + else: + cmd_list.name += "_with_revert" + cmd_list.commands = cmd_list.commands.copy() + draw_overrides[node.override_hash] = (cmd_list, [node]) + # remove SPEndDisplayList from gfx_list, material_revert has its own SPEndDisplayList cmd + while SPEndDisplayList() in cmd_list.commands: + cmd_list.commands.remove(SPEndDisplayList()) + cmd_list.commands.extend(material_revert.commands) + + +def add_overrides_to_fmodel(f_model: SM64Model): + for f_mesh, draw_overrides in f_model.draw_overrides.items(): + nodes = [node for _, nodes in draw_overrides.items() for node in nodes] + if all(node.override_hash is not None for _, (_, nodes) in draw_overrides.items() for node in nodes): + # all nodes use an override, make the first override the main draw + override_hash, cmd_list, nodes = next( + (override_hash, cmd_list, nodes) + for override_hash, (cmd_list, nodes) in draw_overrides.items() + if override_hash is not None and any(node.override_hash == override_hash for node in nodes) + ) + for node in nodes: + if node.override_hash == override_hash: + node.DLmicrocode = cmd_list + node.override_hash = None + f_mesh.draw = cmd_list + draw_overrides.pop(override_hash) + for override_hash, (cmd_list, nodes) in draw_overrides.items(): + # remove no longer used overrides + if all(node.override_hash is None or node.override_hash != override_hash for node in nodes): continue - # remove SPEndDisplayList from gfx_list, materialRevert has its own SPEndDisplayList cmd - while SPEndDisplayList() in gfx_list.commands: - gfx_list.commands.remove(SPEndDisplayList()) - - gfx_list.commands.extend(materialRevert.commands) - reverted_gfx_lists.add(gfx_list) + if cmd_list not in f_mesh.draw_overrides: + f_mesh.draw_overrides.append(cmd_list) # Convert to Geolayout @@ -393,7 +462,7 @@ def convertArmatureToGeolayout(armatureObj, obj, convertTransformMatrix, camera, fModel = SM64Model( name, DLFormat, - GfxMatWriteMethod.WriteDifferingAndRevert if not inline else GfxMatWriteMethod.WriteAll, + bpy.context.scene.fast64.sm64.gfx_write_method, ) if len(armatureObj.children) == 0: @@ -433,6 +502,7 @@ def convertArmatureToGeolayout(armatureObj, obj, convertTransformMatrix, camera, None, None, None, + None, meshGeolayout.nodes[i], [], name, @@ -441,8 +511,16 @@ def convertArmatureToGeolayout(armatureObj, obj, convertTransformMatrix, camera, infoDict, convertTextureData, ) - generateSwitchOptions(meshGeolayout.nodes[0], meshGeolayout, geolayoutGraph, name) - appendRevertToGeolayout(geolayoutGraph, fModel) + + children = meshGeolayout.nodes + meshGeolayout.nodes = [] + for node in children: + node = copy.copy(node) + node.node = copy.copy(node.node) + meshGeolayout.nodes.append(generate_overrides(fModel, node, [], meshGeolayout, geolayoutGraph)) + + append_revert_to_geolayout(geolayoutGraph, fModel) + add_overrides_to_fmodel(fModel) geolayoutGraph.generateSortedList() if inline: bleed_gfx = GeoLayoutBleed() @@ -452,16 +530,15 @@ def convertArmatureToGeolayout(armatureObj, obj, convertTransformMatrix, camera, return geolayoutGraph, fModel -# Camera is unused here def convertObjectToGeolayout( - obj, convertTransformMatrix, camera, name, fModel: FModel, areaObj, DLFormat, convertTextureData + obj, convertTransformMatrix, is_actor: bool, name, fModel: FModel, areaObj, DLFormat, convertTextureData ): inline = bpy.context.scene.exportInlineF3D if fModel is None: fModel = SM64Model( name, DLFormat, - GfxMatWriteMethod.WriteDifferingAndRevert if not inline else GfxMatWriteMethod.WriteAll, + bpy.context.scene.fast64.sm64.gfx_write_method, ) # convertTransformMatrix = convertTransformMatrix @ \ @@ -473,9 +550,11 @@ def convertObjectToGeolayout( # cameraObj = getCameraObj(camera) meshGeolayout = saveCameraSettingsToGeolayout(geolayoutGraph, areaObj, obj, name + "_geo") rootObj = areaObj - fModel.global_data.addAreaData( - areaObj.areaIndex, FAreaData(FFogData(areaObj.area_fog_position, areaObj.area_fog_color)) - ) + if areaObj.fast64.sm64.area.set_fog: + fog_data = FFogData(areaObj.area_fog_position, areaObj.area_fog_color) + else: + fog_data = None + fModel.global_data.addAreaData(areaObj.areaIndex, FAreaData(fog_data)) else: geolayoutGraph = GeolayoutGraph(name + "_geo") @@ -502,8 +581,8 @@ def convertObjectToGeolayout( True, convertTextureData, ) - if not meshGeolayout.has_data(): - raise PluginError("No gfx data to export, gfx export cancelled") + if is_actor and not meshGeolayout.has_data(): + raise PluginError("No gfx data to export, gfx export cancelled", PluginError.exc_warn) except Exception as e: raise Exception(str(e)) finally: @@ -511,7 +590,8 @@ def convertObjectToGeolayout( rootObj.select_set(True) bpy.context.view_layer.objects.active = rootObj - appendRevertToGeolayout(geolayoutGraph, fModel) + append_revert_to_geolayout(geolayoutGraph, fModel) + add_overrides_to_fmodel(fModel) geolayoutGraph.generateSortedList() if inline: bleed_gfx = GeoLayoutBleed() @@ -569,7 +649,6 @@ def exportGeolayoutObjectC( texDir, savePNG, texSeparate, - camera, groupName, headerType, dirName, @@ -579,7 +658,7 @@ def exportGeolayoutObjectC( DLFormat, ): geolayoutGraph, fModel = convertObjectToGeolayout( - obj, convertTransformMatrix, camera, dirName, None, None, DLFormat, not savePNG + obj, convertTransformMatrix, True, dirName, None, None, DLFormat, not savePNG ) return saveGeolayoutC( @@ -659,38 +738,12 @@ def saveGeolayoutC( geoData = geolayoutGraph.to_c() if headerType == "Actor": - matCInclude = '#include "actors/' + dirName + '/material.inc.c"' - matHInclude = '#include "actors/' + dirName + '/material.inc.h"' + matCInclude = Path("actors", dirName, "material.inc.c") + matHInclude = Path("actors", dirName, "material.inc.h") headerInclude = '#include "actors/' + dirName + '/geo_header.h"' - - if not customExport: - # Group name checking, before anything is exported to prevent invalid state on error. - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathGeoC = os.path.join(dirPath, groupName + "_geo.c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - if not os.path.exists(groupPathC): - raise PluginError( - groupPathC + ' not found.\n Most likely issue is that "' + groupName + '" is an invalid group name.' - ) - elif not os.path.exists(groupPathGeoC): - raise PluginError( - groupPathGeoC - + ' not found.\n Most likely issue is that "' - + groupName - + '" is an invalid group name.' - ) - elif not os.path.exists(groupPathH): - raise PluginError( - groupPathH + ' not found.\n Most likely issue is that "' + groupName + '" is an invalid group name.' - ) - else: - matCInclude = '#include "levels/' + levelName + "/" + dirName + '/material.inc.c"' - matHInclude = '#include "levels/' + levelName + "/" + dirName + '/material.inc.h"' + matCInclude = Path("levels", levelName, dirName, "material.inc.c") + matHInclude = Path("levels", levelName, dirName, "material.inc.h") headerInclude = '#include "levels/' + levelName + "/" + dirName + '/geo_header.h"' modifyTexScrollFiles(exportDir, geoDirPath, scrollData) @@ -736,6 +789,16 @@ def saveGeolayoutC( cDefFile.close() fileStatus = None + update_actor_includes( + headerType, + groupName, + Path(dirPath), + dirName, + levelName, + [Path("model.inc.c")], + [Path("geo_header.h")], + [Path("geo.inc.c")], + ) if not customExport: if headerType == "Actor": if dirName == "star" and bpy.context.scene.replaceStarRefs: @@ -787,31 +850,12 @@ def saveGeolayoutC( appendSecondaryGeolayout(geoDirPath, 'bully', 'bully_boss', 'GEO_SCALE(0x00, 0x2000), GEO_NODE_OPEN(),') """ - # Write to group files - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathGeoC = os.path.join(dirPath, groupName + "_geo.c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + dirName + '/model.inc.c"', "") - writeIfNotFound(groupPathGeoC, '\n#include "' + dirName + '/geo.inc.c"', "") - writeIfNotFound(groupPathH, '\n#include "' + dirName + '/geo_header.h"', "\n#endif") - texscrollIncludeC = '#include "actors/' + dirName + '/texscroll.inc.c"' texscrollIncludeH = '#include "actors/' + dirName + '/texscroll.inc.h"' texscrollGroup = groupName texscrollGroupInclude = '#include "actors/' + groupName + '.h"' elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathGeoC = os.path.join(dirPath, "geo.c") - groupPathH = os.path.join(dirPath, "header.h") - - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + dirName + '/model.inc.c"', "") - writeIfNotFound(groupPathGeoC, '\n#include "levels/' + levelName + "/" + dirName + '/geo.inc.c"', "") - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + dirName + '/geo_header.h"', "\n#endif" - ) - texscrollIncludeC = '#include "levels/' + levelName + "/" + dirName + '/texscroll.inc.c"' texscrollIncludeH = '#include "levels/' + levelName + "/" + dirName + '/texscroll.inc.h"' texscrollGroup = levelName @@ -828,7 +872,7 @@ def saveGeolayoutC( ) if DLFormat != DLFormat.Static: # Change this - writeMaterialHeaders(exportDir, matCInclude, matHInclude) + write_material_headers(Path(exportDir), matCInclude, matHInclude) return staticData.header, fileStatus @@ -842,9 +886,9 @@ def exportGeolayoutArmatureInsertableBinary(armatureObj, obj, convertTransformMa saveGeolayoutInsertableBinary(geolayoutGraph, fModel, filepath) -def exportGeolayoutObjectInsertableBinary(obj, convertTransformMatrix, filepath, camera): +def exportGeolayoutObjectInsertableBinary(obj, convertTransformMatrix, filepath): geolayoutGraph, fModel = convertObjectToGeolayout( - obj, convertTransformMatrix, camera, obj.name, None, None, DLFormat.Static, True + obj, convertTransformMatrix, True, obj.name, None, None, DLFormat.Static, True ) saveGeolayoutInsertableBinary(geolayoutGraph, fModel, filepath) @@ -892,10 +936,9 @@ def exportGeolayoutObjectBinaryBank0( modelID, textDumpFilePath, RAMAddr, - camera, ): geolayoutGraph, fModel = convertObjectToGeolayout( - obj, convertTransformMatrix, camera, obj.name, None, None, DLFormat.Static, True + obj, convertTransformMatrix, True, obj.name, None, None, DLFormat.Static, True ) return saveGeolayoutBinaryBank0( @@ -975,10 +1018,9 @@ def exportGeolayoutObjectBinary( levelCommandPos, modelID, textDumpFilePath, - camera, ): geolayoutGraph, fModel = convertObjectToGeolayout( - obj, convertTransformMatrix, camera, obj.name, None, None, DLFormat.Static, True + obj, convertTransformMatrix, True, obj.name, None, None, DLFormat.Static, True ) return saveGeolayoutBinary( @@ -1028,190 +1070,80 @@ def geoWriteTextDump(textDumpFilePath, geolayoutGraph, levelData): openfile.close() -# Switch Handling Process -# When convert armature to geolayout node hierarchy, mesh switch options -# are converted to switch node children, but material/draw layer options -# are converted to SwitchOverrideNodes. During this process, any material -# override geometry will be generated as well. - - -# Afterward, the node hierarchy is traversed again, and any SwitchOverride -# nodes are converted to actual geolayout node hierarchies. -def generateSwitchOptions(transformNode, geolayout, geolayoutGraph, prefix): - if isinstance(transformNode.node, JumpNode): - for node in transformNode.node.geolayout.nodes: - generateSwitchOptions(node, transformNode.node.geolayout, geolayoutGraph, prefix) - overrideNodes = [] - if isinstance(transformNode.node, SwitchNode): - switchName = transformNode.node.name - prefix += "_" + switchName - # prefix = switchName - - materialOverrideTexDimensions = None - - i = 0 - while i < len(transformNode.children): - prefixName = prefix + "_opt" + str(i) - childNode = transformNode.children[i] - if isinstance(childNode.node, SwitchOverrideNode): - drawLayer = childNode.node.drawLayer - material = childNode.node.material - specificMat = childNode.node.specificMat - overrideType = childNode.node.overrideType - texDimensions = childNode.node.texDimensions - if ( - texDimensions is not None - and materialOverrideTexDimensions is not None - and materialOverrideTexDimensions != tuple(texDimensions) - ): - raise PluginError( - 'In switch bone "' - + switchName - + '", some material ' - + "overrides \nhave textures with dimensions differing from the original material.\n" - + "UV coordinates are in pixel units, so there will be UV errors in those overrides.\n " - + "Make sure that all overrides have the same texture dimensions as the original material.\n" - + "Note that materials with no textures default to dimensions of 32x32." - ) - - if texDimensions is not None: - materialOverrideTexDimensions = tuple(texDimensions) - - # This should be a 0xB node - # copyNode = duplicateNode(transformNode.children[0], - # transformNode, transformNode.children.index(childNode)) - index = transformNode.children.index(childNode) - transformNode.children.remove(childNode) - - # Switch option bones should have unique names across all - # armatures. - optionGeolayout = geolayoutGraph.addGeolayout(childNode, prefixName) - geolayoutGraph.addJumpNode(transformNode, geolayout, optionGeolayout, index) - optionGeolayout.nodes.append(TransformNode(StartNode())) - copyNode = optionGeolayout.nodes[0] - - # i -= 1 - # Assumes first child is a start node, where option 0 is - # assumes overrideChild starts with a Start node - option0Nodes = [transformNode.children[0]] - if len(option0Nodes) == 1 and isinstance(option0Nodes[0].node, StartNode): - for startChild in option0Nodes[0].children: - generateOverrideHierarchy( - copyNode, - startChild, - material, - specificMat, - overrideType, - drawLayer, - option0Nodes[0].children.index(startChild), - optionGeolayout, - geolayoutGraph, - optionGeolayout.name, - ) - else: - for overrideChild in option0Nodes: - generateOverrideHierarchy( - copyNode, - overrideChild, - material, - specificMat, - overrideType, - drawLayer, - option0Nodes.index(overrideChild), - optionGeolayout, - geolayoutGraph, - optionGeolayout.name, - ) - if material is not None: - overrideNodes.append(copyNode) - i += 1 - for i in range(len(transformNode.children)): - childNode = transformNode.children[i] - if isinstance(transformNode.node, SwitchNode): - prefixName = prefix + "_opt" + str(i) - else: - prefixName = prefix - - if childNode not in overrideNodes: - generateSwitchOptions(childNode, geolayout, geolayoutGraph, prefixName) - - -def generateOverrideHierarchy( - parentCopyNode, - transformNode, - material, - specificMat, - overrideType, - drawLayer, - index, - geolayout, - geolayoutGraph, - switchOptionName, +def generate_overrides( + fModel: SM64Model, + transform_node: TransformNode, + switch_stack: list[SwitchOverrideNode], + geolayout: Geolayout, + graph: GeolayoutGraph, + name: str = "", ): - # print(transformNode.node) - if isinstance(transformNode.node, SwitchOverrideNode) and material is not None: - return - - copyNode = TransformNode(copy.copy(transformNode.node)) - copyNode.parent = parentCopyNode - parentCopyNode.children.insert(index, copyNode) - if isinstance(transformNode.node, JumpNode): - jumpName = switchOptionName + "_jump_" + transformNode.node.geolayout.name - jumpGeolayout = geolayoutGraph.addGeolayout(transformNode, jumpName) - oldGeolayout = copyNode.node.geolayout - copyNode.node.geolayout = jumpGeolayout - geolayoutGraph.addGeolayoutCall(geolayout, jumpGeolayout) - startNode = TransformNode(StartNode()) - jumpGeolayout.nodes.append(startNode) - if len(oldGeolayout.nodes) == 1 and isinstance(oldGeolayout.nodes[0].node, StartNode): - for node in oldGeolayout.nodes[0].children: - generateOverrideHierarchy( - startNode, - node, - material, - specificMat, - overrideType, - drawLayer, - oldGeolayout.nodes[0].children.index(node), - jumpGeolayout, - geolayoutGraph, - jumpName, - ) + node = transform_node.node + children = transform_node.children + transform_node.children = [] + if isinstance(node, JumpNode): + start_nodes, new_name = node.geolayout.nodes, name + if switch_stack: + new_name = f"{node.geolayout.name}{name}" + new_geolayout = graph.addGeolayout(transform_node, new_name) + node.geolayout = new_geolayout + graph.addGeolayoutCall(geolayout, new_geolayout) else: - for node in oldGeolayout.nodes: - generateOverrideHierarchy( - startNode, + node.geolayout.nodes = [] + for child in start_nodes: + child = copy.copy(child) + child.node = copy.copy(child.node) + node.geolayout.nodes.append(generate_overrides(fModel, child, switch_stack.copy(), geolayout, graph, name)) + elif node.hasDL or hasattr(node, "drawLayer"): + for i, override_node in enumerate(switch_stack): + if node.hasDL: + dl, override_hash = save_override_draw( + fModel, + node.DLmicrocode, + name, + node.override_hash, + override_node.material, + override_node.specificMat, + override_node.drawLayer, + override_node.overrideType, + node.fMesh, node, - material, - specificMat, - overrideType, - drawLayer, - oldGeolayout.nodes.index(node), - jumpGeolayout, - geolayoutGraph, - jumpName, + node.drawLayer, + True, ) - - elif not isinstance(copyNode.node, SwitchOverrideNode) and copyNode.node.hasDL: - if material is not None: - copyNode.node.DLmicrocode = copyNode.node.fMesh.drawMatOverrides[(material, specificMat, overrideType)] - copyNode.node.override_hash = (material, specificMat, overrideType) - if drawLayer is not None: - copyNode.node.drawLayer = drawLayer - - for child in transformNode.children: - generateOverrideHierarchy( - copyNode, - child, - material, - specificMat, - overrideType, - drawLayer, - transformNode.children.index(child), - geolayout, - geolayoutGraph, - switchOptionName, - ) + if dl is not None and override_hash is not None: + node.DLmicrocode = dl + node.override_hash = override_hash + if override_node.drawLayer is not None and node.drawLayer != override_node.drawLayer: + node.drawLayer = override_node.drawLayer + if node.fMesh is not None: + node.fMesh.override_layer = True + if node.hasDL: + draw_overrides = fModel.draw_overrides.setdefault(node.fMesh, {}) + _, nodes = draw_overrides.setdefault(node.override_hash, (node.DLmicrocode, [])) + nodes.append(node) + for i, child in enumerate(children): + child = copy.copy(child) + child_node = child.node = copy.copy(child.node) + if isinstance(child_node, SwitchOverrideNode): + child.parent = None + assert i != 0, "Switch override must not be the first child of its parent" + override_switch_stack = [*switch_stack, child_node] + option0 = copy.copy(children[0]) + new_name = toAlnum(f"{name}_opt_{i}") + new_geolayout = graph.addGeolayout(transform_node, geolayout.name + new_name) + graph.addGeolayoutCall(geolayout, new_geolayout) + new_geolayout.nodes.append( + generate_overrides(fModel, option0, override_switch_stack.copy(), new_geolayout, graph, new_name) + ) + option_child = TransformNode(JumpNode(True, new_geolayout)) + transform_node.children.append(option_child) + option_child.parent = transform_node + else: + child = generate_overrides(fModel, child, switch_stack.copy(), geolayout, graph, name) + transform_node.children.append(child) + child.parent = transform_node + return transform_node def addParentNode(parentTransformNode: TransformNode, geoNode): @@ -1229,7 +1161,7 @@ def duplicateNode(transformNode, parentNode, index): def partOfGeolayout(obj): - useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj.sm64_obj_type) + useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj) return obj.type == "MESH" or useGeoEmpty @@ -1300,8 +1232,6 @@ def processPreInlineGeo( node = JumpNode(True, None, obj.geoReference) elif inlineGeoConfig.name == "Geo Displaylist": node = DisplayListNode(int(obj.draw_layer_static), obj.dlReference) - elif inlineGeoConfig.name == "Custom Geo Command": - node = CustomNode(obj.customGeoCommand, obj.customGeoCommandArgs) addParentNode(parentTransformNode, node) # Allow this node to be translated/rotated @@ -1323,7 +1253,25 @@ def processInlineGeoNode( elif inlineGeoConfig.name == "Geo Rotation Node": node = RotateNode(obj.draw_layer_static, obj.useDLReference, rotate, obj.dlReference) elif inlineGeoConfig.name == "Geo Scale": - node = ScaleNode(obj.draw_layer_static, scale, obj.useDLReference, obj.dlReference) + node = ScaleNode(obj.draw_layer_static, scale[0], obj.useDLReference, obj.dlReference) + elif inlineGeoConfig.name == "Custom": + local_matrix = ( + mathutils.Matrix.Translation(translate) + @ rotate.to_matrix().to_4x4() + @ mathutils.Matrix.Diagonal(scale).to_4x4() + ) + node = obj.fast64.sm64.custom.get_final_cmd( + obj, + bpy.context.scene.fast64.sm64.blender_to_sm64_scale, + z_up_to_y_up_matrix @ mathutils.Matrix(obj.get("original_mtx_world")) @ z_up_to_y_up_matrix.inverted(), + local_matrix, + obj.draw_layer_static, + obj.useDLReference, + obj.dlReference, + ) + node, parentTransformNode, _, _, _ = get_custom_cmd_with_transform( + node, parentTransformNode, translate, rotate, scale + ) else: raise PluginError(f"Ooops! Didnt implement inline geo exporting for {inlineGeoConfig.name}") @@ -1344,11 +1292,11 @@ def processMesh( ): # final_transform = copy.deepcopy(transformMatrix) - useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj.sm64_obj_type) + useGeoEmpty = obj.type == "EMPTY" and checkSM64EmptyUsesGeoLayout(obj) useSwitchNode = obj.type == "EMPTY" and obj.sm64_obj_type == "Switch" - useInlineGeo = obj.type == "EMPTY" and checkIsSM64InlineGeoLayout(obj.sm64_obj_type) + useInlineGeo = obj.type == "EMPTY" and checkIsSM64InlineGeoLayout(obj) addRooms = isRoot and obj.type == "EMPTY" and obj.sm64_obj_type == "Area Root" and obj.enableRoomSwitch @@ -1358,7 +1306,7 @@ def processMesh( inlineGeoConfig: InlineGeolayoutObjConfig = inlineGeoLayoutObjects.get(obj.sm64_obj_type) processed_inline_geo = False - isPreInlineGeoLayout = checkIsSM64PreInlineGeoLayout(obj.sm64_obj_type) + isPreInlineGeoLayout = checkIsSM64PreInlineGeoLayout(obj) if useInlineGeo and isPreInlineGeoLayout: processed_inline_geo = True processPreInlineGeo(inlineGeoConfig, obj, parentTransformNode) @@ -1437,7 +1385,7 @@ def processMesh( else: if useInlineGeo and not processed_inline_geo: node, parentTransformNode = processInlineGeoNode( - inlineGeoConfig, obj, parentTransformNode, translate, rotate, scale[0] + inlineGeoConfig, obj, parentTransformNode, translate, rotate, scale ) processed_inline_geo = True @@ -1536,19 +1484,29 @@ def processMesh( if len(src_meshes): fMeshes = {} - node.dlRef = src_meshes[0]["name"] + # find dl + draw, name = None, src_meshes[0]["dl_name"] + for fmesh in fModel.meshes.values(): + for fmesh_draw in [fmesh.draw] + fmesh.draw_overrides: + if fmesh_draw.name == name: + draw = fmesh_draw + break + node.dlRef = draw node.drawLayer = src_meshes[0]["layer"] processed_inline_geo = True for src_mesh in src_meshes[1:]: additionalNode = ( - DisplayListNode(src_mesh["layer"], src_mesh["name"]) + DisplayListNode(src_mesh["layer"], src_mesh["dl_name"]) if not isinstance(node, BillboardNode) - else BillboardNode(src_mesh["layer"], True, [0, 0, 0], src_mesh["name"]) + else BillboardNode(src_mesh["layer"], True, [0, 0, 0], src_mesh["dl_name"]) ) additionalTransformNode = TransformNode(additionalNode) transformNode.children.append(additionalTransformNode) additionalTransformNode.parent = transformNode + additionalTransformNode.revert_previous_mat = ( + additionalTransformNode.revert_after_mat + ) = obj.bleed_independently else: triConverterInfo = TriangleConverterInfo( @@ -1559,9 +1517,9 @@ def processMesh( ) if fMeshes: temp_obj["src_meshes"] = [ - ({"name": fMesh.draw.name, "layer": drawLayer}) for drawLayer, fMesh in fMeshes.items() + ({"dl_name": fMesh.draw.name, "layer": drawLayer}) for drawLayer, fMesh in fMeshes.items() ] - node.dlRef = temp_obj["src_meshes"][0]["name"] + node.dlRef = temp_obj["src_meshes"][0]["dl_name"] else: # TODO: Display warning to the user that there is an object that doesn't have polygons print("Object", obj.original_name, "does not have any polygons.") @@ -1577,11 +1535,11 @@ def processMesh( node.hasDL = False else: firstNodeProcessed = False + node: BaseDisplayListNode for drawLayer, fMesh in fMeshes.items(): if not firstNodeProcessed: node.DLmicrocode = fMesh.draw node.fMesh = fMesh - node.bleed_independently = obj.bleed_independently node.drawLayer = drawLayer # previous drawLayer assigments useless? firstNodeProcessed = True else: @@ -1592,13 +1550,16 @@ def processMesh( ) additionalNode.DLmicrocode = fMesh.draw additionalNode.fMesh = fMesh - additionalNode.bleed_independently = obj.bleed_independently additionalTransformNode = TransformNode(additionalNode) + additionalTransformNode.revert_previous_mat = ( + additionalTransformNode.revert_after_mat + ) = obj.bleed_independently transformNode.children.append(additionalTransformNode) additionalTransformNode.parent = transformNode parentTransformNode.children.append(transformNode) transformNode.parent = parentTransformNode + transformNode.revert_previous_mat = transformNode.revert_after_mat = obj.bleed_independently alphabeticalChildren = sorted(obj.children, key=lambda childObj: childObj.original_name.lower()) for childObj in alphabeticalChildren: @@ -1631,6 +1592,7 @@ def processBone( transformMatrix, lastTranslateName, lastRotateName, + last_scale_name, lastDeformName, parentTransformNode, materialOverrides, @@ -1641,6 +1603,8 @@ def processBone( convertTextureData, ): bone = armatureObj.data.bones[boneName] + bone_props: "SM64_BoneProperties" = bone.fast64.sm64 + poseBone = armatureObj.pose.bones[boneName] final_transform = copy.deepcopy(transformMatrix) materialOverrides = copy.copy(materialOverrides) @@ -1664,40 +1628,38 @@ def processBone( rotateParent = None rotate = bone.matrix_local.decompose()[1] + # Get scale + if last_scale_name is not None: + scaleParent = armatureObj.data.bones[last_scale_name] + scale = (scaleParent.matrix_local.inverted() @ bone.matrix_local).decompose()[2] + else: + scaleParent = None + scale = bone.matrix_local.decompose()[2] + translation = mathutils.Matrix.Translation(translate) rotation = rotate.to_matrix().to_4x4() zeroTranslation = isZeroTranslation(translate) zeroRotation = isZeroRotation(rotate) + zero_scale = isZeroScaleChange(scale) # hasDL = bone.use_deform hasDL = True - if bone.geo_cmd in animatableBoneTypes: - if bone.geo_cmd == "CustomAnimated": - if not bone.fast64.sm64.custom_geo_cmd_macro: - raise PluginError(f'Bone "{boneName}" on armature "{armatureObj.name}" needs a geo command macro.') - node = CustomAnimatedNode(bone.fast64.sm64.custom_geo_cmd_macro, int(bone.draw_layer), translate, rotate) - lastTranslateName = boneName - lastRotateName = boneName - else: # DisplayListWithOffset - if not zeroRotation: - node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, mathutils.Vector((0, 0, 0))) + if bone.geo_cmd == "DisplayListWithOffset": + if not zeroRotation: + node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, mathutils.Vector((0, 0, 0))) - parentTransformNode = addParentNode( - parentTransformNode, TranslateRotateNode(1, 0, False, translate, rotate) - ) + parentTransformNode = addParentNode( + parentTransformNode, TranslateRotateNode(1, 0, False, translate, rotate) + ) - lastTranslateName = boneName - lastRotateName = boneName - else: - node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, translate) - lastTranslateName = boneName + lastTranslateName = boneName + lastRotateName = boneName + else: + node = DisplayListWithOffsetNode(int(bone.draw_layer), hasDL, translate) + lastTranslateName = boneName final_transform = transformMatrix @ translation - elif bone.geo_cmd == "CustomNonAnimated": - if bone.fast64.sm64.custom_geo_cmd_macro == "": - raise PluginError(f'Bone "{boneName}" on armature "{armatureObj.name}" needs a geo command macro.') - node = CustomNode(bone.fast64.sm64.custom_geo_cmd_macro, bone.fast64.sm64.custom_geo_cmd_args) elif bone.geo_cmd == "Function": if bone.geo_func == "": raise PluginError("Function bone " + boneName + " function value is empty.") @@ -1770,6 +1732,21 @@ def processBone( final_transform = transformMatrix @ mathutils.Matrix.Scale(node.scaleValue, 4) elif bone.geo_cmd == "StartRenderArea": node = StartRenderAreaNode(bone.culling_radius) + elif bone.geo_cmd == "Custom": + local_matrix = mathutils.Matrix.LocRotScale(translate, rotate, scale) + world_matrix = z_up_to_y_up_matrix @ bone.matrix_local @ z_up_to_y_up_matrix.inverted() + node = bone_props.custom.get_final_cmd( + bone, bpy.context.scene.fast64.sm64.blender_to_sm64_scale, world_matrix, local_matrix, None, hasDL + ) + node, parentTransformNode, has_translation, has_rotation, has_scale = get_custom_cmd_with_transform( + node, parentTransformNode, translate, rotate, scale + ) + if has_translation: + lastTranslateName = boneName + elif has_rotation: + lastRotateName = boneName + elif has_scale: + last_scale_name = boneName else: raise PluginError("Invalid geometry command: " + bone.geo_cmd) @@ -1862,15 +1839,19 @@ def processBone( parentTransformNode.children.append(transformNode) transformNode.parent = parentTransformNode + new_node: TransformNode + for new_node in additionalNodes + [transformNode]: + new_node.revert_previous_mat = ( + bone_props.revert_before_func + if bone.geo_cmd in {"Function", "HeldObject"} + else bone_props.revert_previous_mat + ) + if isinstance(new_node.node, BaseDisplayListNode): + new_node.revert_after_mat = bone_props.revert_after_mat + if not isinstance(transformNode.node, SwitchNode): # print(boneGroup.name if boneGroup is not None else "Offset") if len(bone.children) > 0: - # print("\tHas Children") - if bone.geo_cmd == "Function": - raise PluginError( - "Function bones cannot have children. They instead affect the next sibling bone in alphabetical order." - ) - # Handle child nodes # nonDeformTransformData should be modified to be sent to children, # otherwise it should not be modified for parent. @@ -1885,6 +1866,7 @@ def processBone( final_transform, lastTranslateName, lastRotateName, + last_scale_name, lastDeformName, transformNode, materialOverrides, @@ -1897,7 +1879,6 @@ def processBone( # transformNode.children.append(childNode) # childNode.parent = transformNode - # see generateSwitchOptions() for explanation. else: # print(boneGroup.name if boneGroup is not None else "Offset") if len(bone.children) > 0: @@ -1921,6 +1902,7 @@ def processBone( final_transform, lastTranslateName, lastRotateName, + last_scale_name, lastDeformName, nextStartNode, materialOverrides, @@ -1956,8 +1938,8 @@ def processBone( + str(switchIndex) + ", the object provided is not an armature." ) - elif optionArmature in geolayoutGraph.secondaryGeolayouts: - optionGeolayout = geolayoutGraph.secondaryGeolayouts[optionArmature] + elif optionArmature in geolayoutGraph.secondary_geolayouts_dict: + optionGeolayout = geolayoutGraph.secondary_geolayouts_dict[optionArmature] geolayoutGraph.addJumpNode(transformNode, geolayout, optionGeolayout) continue @@ -2012,6 +1994,7 @@ def processBone( optionBone.name, optionBone.name, optionBone.name, + optionBone.name, startNode, materialOverrides, namePrefix + "_" + optionBone.name, @@ -2033,7 +2016,7 @@ def processBone( specificMat = tuple([matPtr.material for matPtr in switchOption.specificIgnoreArray]) else: material = None - specificMat = None + specificMat = tuple() drawLayer = int(switchOption.drawLayer) texDimensions = getTexDimensions(material) if material is not None else None @@ -2076,8 +2059,11 @@ def processSwitchBoneMatOverrides(materialOverrides, switchBone): + " has a material ignore field that is None." ) specificMat = tuple([matPtr.material for matPtr in switchOption.specificIgnoreArray]) - - materialOverrides.append((switchOption.materialOverride, specificMat, switchOption.materialOverrideType)) + materialOverrides.append( + (switchOption.materialOverride, specificMat, None, switchOption.materialOverrideType) + ) + elif switchOption.switchType == "Draw Layer": + materialOverrides.append((None, (), int(switchOption.drawLayer), "All")) def getGroupIndex(vert, armatureObj, obj): @@ -2167,10 +2153,15 @@ def addSkinnedMeshNode(armatureObj, boneName, skinnedMesh, transformNode, parent # Get skinned node bone = armatureObj.data.bones[boneName] + bone_props: "SM64_BoneProperties" = bone.fast64.sm64 skinnedNode = DisplayListNode(drawLayer) skinnedNode.fMesh = skinnedMesh skinnedNode.DLmicrocode = skinnedMesh.draw skinnedTransformNode = TransformNode(skinnedNode) + skinnedTransformNode.revert_previous_mat, skinnedTransformNode.revert_after_mat = ( + bone_props.revert_previous_mat, + bone_props.revert_after_mat, + ) # Ascend heirarchy until reaching first node before a deform parent. # We duplicate the hierarchy along the way to possibly use later. @@ -2439,7 +2430,7 @@ def saveModelGivenVertexGroup( fMesh = fModel.addMesh(vertexGroup, namePrefix, drawLayer, False, None) fMeshes[drawLayer] = fMesh - for material_index, bFaces in materialFaces.items(): + for material_index, bFaces in sorted(materialFaces.items()): material = obj.material_slots[material_index].material checkForF3dMaterialInFaces(obj, material) fMaterial, texDimensions = saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData) @@ -2480,82 +2471,134 @@ def saveModelGivenVertexGroup( ] ) - # Must be done after all geometry saved - for material, specificMat, overrideType in materialOverrides: - for drawLayer, fMesh in fMeshes.items(): - saveOverrideDraw(obj, fModel, material, specificMat, overrideType, fMesh, drawLayer, convertTextureData) - for drawLayer, fMesh in fSkinnedMeshes.items(): - saveOverrideDraw(obj, fModel, material, specificMat, overrideType, fMesh, drawLayer, convertTextureData) - return fMeshes, fSkinnedMeshes, usedDrawLayers -def saveOverrideDraw( - obj: bpy.types.Object, - fModel: FModel, - material: bpy.types.Material, - specificMat: tuple[bpy.types.Material], - overrideType: str, +def save_override_draw( + f_model: SM64Model, + draw: GfxList, + prefix: str, + existing_hash, + override_mat: bpy.types.Material | None, + specific_mats: tuple[bpy.types.Material] | None, + override_layer: int | None, + override_type: str, fMesh: FMesh, - drawLayer: int, - convertTextureData: bool, + obj: object, + draw_layer: int, + convert_texture_data: bool, ): - fOverrideMat, texDimensions = saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData) - overrideIndex = str(len(fMesh.drawMatOverrides)) - if (material, specificMat, overrideType) in fMesh.drawMatOverrides: - overrideIndex = fMesh.drawMatOverrides[(material, specificMat, overrideType)].name[-1] - meshMatOverride = GfxList( - fMesh.name + "_mat_override_" + toAlnum(material.name) + "_" + overrideIndex, GfxListTag.Draw, fModel.DLFormat - ) - meshMatOverride.commands = [copy.copy(cmd) for cmd in fMesh.draw.commands] - fMesh.drawMatOverrides[(material, specificMat, overrideType)] = meshMatOverride + draw_overrides = f_model.draw_overrides.setdefault(fMesh, {}) + specific_mats = specific_mats or tuple() + f_override_mat = override_tex_dimensions = None + new_layer = draw_layer if override_layer is None else override_layer + material_hash = override_mat, new_layer, convert_texture_data + g_tex_gen = False + + if override_mat is not None: + f_override_mat, override_tex_dimensions = saveOrGetF3DMaterial( + override_mat, f_model, None, new_layer, convert_texture_data + ) + g_tex_gen = override_mat.f3d_mat.rdp_settings.g_tex_gen + + name = f"{fMesh.name}{prefix}" + new_name = name + override_index = -1 + while new_name in [x.name for x, _ in draw_overrides.values()]: + override_index += 1 + new_name = f"{name}_{override_index}" + name = new_name + + new_dl_override = GfxList(name, GfxListTag.Draw, f_model.DLFormat) + new_dl_override.commands = [copy.copy(cmd) for cmd in draw.commands] + save_mesh_override = False prev_material = None last_replaced = None command_index = 0 - while command_index < len(meshMatOverride.commands): - command = meshMatOverride.commands[command_index] + new_hash = [] if existing_hash is None else [*existing_hash] + while command_index < len(new_dl_override.commands): + command = new_dl_override.commands[command_index] if not isinstance(command, SPDisplayList): command_index += 1 continue # get the material referenced, and then check if it should be overriden # a material override will either have a list of mats it overrides, or a mask of mats it doesn't based on type - bpy_material, fmaterial = find_material_from_jump_cmd(fModel.getAllMaterials().items(), command) - shouldModify = (overrideType == "Specific" and bpy_material in specificMat) or ( - overrideType == "All" and bpy_material not in specificMat + bpy_material, fmaterial = find_material_from_jump_cmd(f_model.getAllMaterials().items(), command) + should_modify = override_mat is not None and ( + (override_type == "Specific" and bpy_material in specific_mats) + or (override_type == "All" and bpy_material not in specific_mats) ) + if should_modify and bpy_material is not None and override_tex_dimensions is not None and not g_tex_gen: + _, tex_dimensions = saveOrGetF3DMaterial(bpy_material, f_model, None, new_layer, convert_texture_data) + if tex_dimensions != override_tex_dimensions: + raise PluginError( + f'Material "{bpy_material.name}" has a texture with dimensions of {tex_dimensions}\n' + f'but is being overriden by material "{override_mat.name}" with dimensions of {override_tex_dimensions}.\n' + + "UV coordinates are in pixel units, so there will be UV errors in those overrides.\n " + + "Make sure that all overrides have the same texture dimensions as the original material.\n" + + "Note that materials with no textures default to dimensions of 32x32." + ) + + new_mat: FMaterial = f_override_mat if should_modify else None + cur_bpy_material = override_mat if should_modify else bpy_material + if cur_bpy_material is not None: + material_hash = (cur_bpy_material, new_layer, convert_texture_data) + # generate a new material for the specific layer if rendermode is set + if material_hash not in f_model.layer_adapted_fmats: + f_model.layer_adapted_fmats[material_hash] = None + rdp = cur_bpy_material.f3d_mat.rdp_settings + preset = (rdp.rendermode_preset_cycle_1, rdp.rendermode_preset_cycle_2) + cur_preset = f_model.getRenderMode(new_layer) + if rdp.set_rendermode and (rdp.rendermode_advanced_enabled or preset != cur_preset): + new_mat: FMaterial = saveOrGetF3DMaterial( + cur_bpy_material, f_model, None, new_layer, convert_texture_data + )[0] + if override_mat is None: + new_mat.material = copy.copy(new_mat.material) # so we can change the tag + new_mat.material.tag |= GfxListTag.NoExport + f_model.layer_adapted_fmats[material_hash] = new_mat + new_mat = f_model.layer_adapted_fmats.get(material_hash) or new_mat + # replace the material load if necessary # if we replaced the previous load with the same override, then remove the cmd to optimize DL if command.displayList.tag & GfxListTag.Material: curMaterial = fmaterial - if shouldModify: + # if layer ever changes the main material use new_mat here + if should_modify: + save_mesh_override = True + new_hash.append((0, f_override_mat)) last_replaced = fmaterial - curMaterial = fOverrideMat - command.displayList = fOverrideMat.material + curMaterial = f_override_mat + command.displayList = f_override_mat.material # remove cmd if it is a repeat load - if prev_material == curMaterial: - meshMatOverride.commands.pop(command_index) + if prev_material is not None and prev_material == curMaterial: + save_mesh_override = True + new_hash.append((1, curMaterial)) + new_dl_override.commands.pop(command_index) command_index -= 1 # if we added a revert for our material redundant load, remove that as well prevIndex = command_index - 1 - prev_command = meshMatOverride.commands[prevIndex] + prev_command = new_dl_override.commands[prevIndex] if ( prevIndex > 0 and isinstance(prev_command, SPDisplayList) and prev_command.displayList == curMaterial.revert ): - meshMatOverride.commands.pop(prevIndex) + new_dl_override.commands.pop(prevIndex) command_index -= 1 # update the last loaded material prev_material = curMaterial # replace the revert if the override has a revert, otherwise remove the command - if command.displayList.tag & GfxListTag.MaterialRevert and shouldModify: - if fOverrideMat.revert is not None: - command.displayList = fOverrideMat.revert + if command.displayList.tag & GfxListTag.MaterialRevert and new_mat is not None: + new_hash.append((2, new_mat)) + save_mesh_override = True + if new_mat.revert is not None: + command.displayList = new_mat.revert else: - meshMatOverride.commands.pop(command_index) + new_dl_override.commands.pop(command_index) command_index -= 1 if not command.displayList.tag & GfxListTag.Geometry: @@ -2563,13 +2606,15 @@ def saveOverrideDraw( continue # If the previous command was a revert we added, remove it. All reverts must be followed by a load prev_index = command_index - 1 - prev_command = meshMatOverride.commands[prev_index] + prev_command = new_dl_override.commands[prev_index] if ( prev_index > 0 and isinstance(prev_command, SPDisplayList) - and prev_command.displayList == fOverrideMat.revert + and (new_mat is not None and prev_command.displayList == new_mat.revert) ): - meshMatOverride.commands.pop(prev_index) + new_hash.append((3, new_mat)) + save_mesh_override = True + new_dl_override.commands.pop(prev_index) command_index -= 1 # If the override material has a revert and the original material didn't, insert a revert after this command. # This is needed to ensure that override materials that need a revert get them. @@ -2577,20 +2622,30 @@ def saveOverrideDraw( if ( last_replaced and last_replaced.revert is None - and fOverrideMat.revert is not None - and prev_material == fOverrideMat + and new_mat is not None + and new_mat.revert is not None + and prev_material == new_mat ): - next_command = meshMatOverride.commands[command_index + 1] + next_command = new_dl_override.commands[command_index + 1] if ( isinstance(next_command, SPDisplayList) and next_command.displayList.tag & GfxListTag.Material and next_command.displayList != prev_material.material ) or (isinstance(next_command, SPEndDisplayList)): - meshMatOverride.commands.insert(command_index + 1, SPDisplayList(fOverrideMat.revert)) + new_hash.append((4, new_mat)) + save_mesh_override = True + new_dl_override.commands.insert(command_index + 1, SPDisplayList(new_mat.revert)) command_index += 1 # iterate to the next cmd command_index += 1 + new_hash = tuple(new_hash) + if save_mesh_override: + new_dl_override, nodes = draw_overrides.setdefault(new_hash, (new_dl_override, [])) + nodes.append(obj) + return new_dl_override, new_hash + return None, None + def findVertIndexInBuffer(loop, buffer, loopDict): i = 0 @@ -2622,7 +2677,7 @@ def splitSkinnedFacesIntoTwoGroups(skinnedFaces, fModel, obj, uv_data, drawLayer # For selecting on error notInGroupBlenderVerts = [] loopDict = {} - for material_index, skinnedFaceArray in skinnedFaces.items(): + for material_index, skinnedFaceArray in sorted(skinnedFaces.items()): # These MUST be arrays (not dicts) as order is important inGroupVerts = [] inGroupVertArray.append([material_index, inGroupVerts]) @@ -2708,7 +2763,7 @@ def saveSkinnedMeshByMaterial( # It seems like material setup must be done BEFORE triangles are drawn. # Because of this we cannot share verts between materials (?) curIndex = 0 - for material_index, vertData in notInGroupVertArray: + for material_index, vertData in sorted(notInGroupVertArray, key=lambda x: x[0]): material = obj.material_slots[material_index].material checkForF3dMaterialInFaces(obj, material) f3dMat = material.f3d_mat if material.mat_ver > 3 else material @@ -2751,7 +2806,7 @@ def saveSkinnedMeshByMaterial( # Load current group vertices, then draw commands by material existingVertData, matRegionDict = convertVertDictToArray(notInGroupVertArray) - for material_index, skinnedFaceArray in skinnedFaces.items(): + for material_index, skinnedFaceArray in sorted(skinnedFaces.items()): material = obj.material_slots[material_index].material faces = [skinnedFace.bFace for skinnedFace in skinnedFaceArray] fMaterial, texDimensions = saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData) @@ -2858,10 +2913,9 @@ def execute(self, context): obj, final_transform, export_path, - bpy.context.scene.geoTexDir, + props.custom_include_directory, save_textures, save_textures and bpy.context.scene.geoSeparateTextureDef, - None, props.actor_group_name, props.export_header_type, props.obj_name_gfx, @@ -2876,7 +2930,6 @@ def execute(self, context): obj, final_transform, bpy.path.abspath(bpy.context.scene.geoInsertableBinaryPath), - None, ) self.report({"INFO"}, "Success! Data at " + context.scene.geoInsertableBinaryPath) else: @@ -2887,7 +2940,7 @@ def execute(self, context): romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") - levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelGeoExport]) + levelParsed = parse_level_binary(romfileOutput, props.level_name) segmentData = levelParsed.segmentData if context.scene.fast64.sm64.extend_bank_4: @@ -2911,7 +2964,6 @@ def execute(self, context): *modelLoadInfo, textDumpFilePath, getAddressFromRAMAddress(int(context.scene.geoRAMAddr, 16)), - None, ) else: addrRange, segPointer = exportGeolayoutObjectBinary( @@ -2922,13 +2974,10 @@ def execute(self, context): segmentData, *modelLoadInfo, textDumpFilePath, - None, ) romfileOutput.close() - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) - context.view_layer.objects.active = obj + selectSingleObject(obj) if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) @@ -3036,7 +3085,7 @@ def execute(self, context): applyRotation([armatureObj] + linkedArmatures, math.radians(90), "X") # You must ALSO apply object rotation after armature rotation. - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() for linkedArmature, linkedMesh in linkedArmatureDict.items(): linkedMesh.select_set(True) obj.select_set(True) @@ -3058,7 +3107,7 @@ def execute(self, context): obj, final_transform, export_path, - bpy.context.scene.geoTexDir, + props.custom_include_directory, save_textures, save_textures and bpy.context.scene.geoSeparateTextureDef, None, @@ -3089,7 +3138,7 @@ def execute(self, context): romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") - levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelGeoExport]) + levelParsed = parse_level_binary(romfileOutput, props.level_name) segmentData = levelParsed.segmentData if context.scene.fast64.sm64.extend_bank_4: @@ -3130,9 +3179,7 @@ def execute(self, context): ) romfileOutput.close() - bpy.ops.object.select_all(action="DESELECT") - armatureObj.select_set(True) - context.view_layer.objects.active = armatureObj + selectSingleObject(armatureObj) if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) @@ -3195,6 +3242,7 @@ def draw(self, context): col = self.layout.column() propsGeoE = col.operator(SM64_ExportGeolayoutArmature.bl_idname) propsGeoE = col.operator(SM64_ExportGeolayoutObject.bl_idname) + props = context.scene.fast64.sm64.combined_export if context.scene.fast64.sm64.export_type == "Insertable Binary": col.prop(context.scene, "geoInsertableBinaryPath") else: @@ -3205,7 +3253,7 @@ def draw(self, context): if context.scene.geoUseBank0: prop_split(col, context.scene, "geoRAMAddr", "RAM Address") else: - col.prop(context.scene, "levelGeoExport") + prop_split(col, props, "level_name", "Level") col.prop(context.scene, "overwriteModelLoad") if context.scene.overwriteModelLoad: @@ -3238,7 +3286,6 @@ def sm64_geo_writer_register(): for cls in sm64_geo_writer_classes: register_class(cls) - bpy.types.Scene.levelGeoExport = bpy.props.EnumProperty(items=level_enums, name="Level", default="HMC") bpy.types.Scene.geoExportStart = bpy.props.StringProperty(name="Start", default="11D8930") bpy.types.Scene.geoExportEnd = bpy.props.StringProperty(name="End", default="11FFF00") @@ -3252,7 +3299,6 @@ def sm64_geo_writer_register(): bpy.types.Scene.textDumpGeoPath = bpy.props.StringProperty(name="Text Dump Path", subtype="FILE_PATH") bpy.types.Scene.geoUseBank0 = bpy.props.BoolProperty(name="Use Bank 0") bpy.types.Scene.geoRAMAddr = bpy.props.StringProperty(name="RAM Address", default="80000000") - bpy.types.Scene.geoTexDir = bpy.props.StringProperty(name="Include Path", default="actors/mario/") bpy.types.Scene.geoSeparateTextureDef = bpy.props.BoolProperty(name="Save texture.inc.c separately") bpy.types.Scene.geoInsertableBinaryPath = bpy.props.StringProperty(name="Filepath", subtype="FILE_PATH") bpy.types.Scene.geoIsSegPtr = bpy.props.BoolProperty(name="Is Segmented Address") @@ -3272,7 +3318,6 @@ def sm64_geo_writer_unregister(): for cls in reversed(sm64_geo_writer_classes): unregister_class(cls) - del bpy.types.Scene.levelGeoExport del bpy.types.Scene.geoExportStart del bpy.types.Scene.geoExportEnd del bpy.types.Scene.overwriteModelLoad @@ -3282,7 +3327,6 @@ def sm64_geo_writer_unregister(): del bpy.types.Scene.textDumpGeoPath del bpy.types.Scene.geoUseBank0 del bpy.types.Scene.geoRAMAddr - del bpy.types.Scene.geoTexDir del bpy.types.Scene.geoSeparateTextureDef del bpy.types.Scene.geoInsertableBinaryPath del bpy.types.Scene.geoIsSegPtr diff --git a/fast64_internal/sm64/sm64_level_parser.py b/fast64_internal/sm64/sm64_level_parser.py index 15a801a60..86dba2b1d 100644 --- a/fast64_internal/sm64/sm64_level_parser.py +++ b/fast64_internal/sm64/sm64_level_parser.py @@ -41,6 +41,13 @@ L_PLACE_MACRO_OBJECT, L_JET_STREAM, ) +from .sm64_constants import level_pointers + + +def parse_level_binary(romfile, name: str): + if name == "Custom": + raise PluginError("Custom levels not supported for binary exports.") + return parseLevelAtPointer(romfile, level_pointers[name]) def parseLevelAtPointer(romfile, pointerAddress): diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index 1587b43fe..6df955257 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -1,7 +1,8 @@ +from pathlib import Path import bpy, os, math, re, shutil, mathutils from collections import defaultdict from typing import NamedTuple -from dataclasses import dataclass +from dataclasses import dataclass, field from bpy.utils import register_class, unregister_class from ..panels import SM64_Panel from ..operators import ObjectDataExporter @@ -11,29 +12,28 @@ from .sm64_f3d_writer import SM64Model, SM64GfxFormatter from .sm64_geolayout_writer import setRooms, convertObjectToGeolayout from .sm64_f3d_writer import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import cameraWarning, starSelectWarning +from .sm64_utility import ( + END_IF_FOOTER, + cameraWarning, + starSelectWarning, + to_include_descriptor, + write_or_delete_if_found, + write_material_headers, +) from ..utility import ( + yUpToZUp, PluginError, - writeIfNotFound, getDataFromFile, saveDataToFile, unhideAllAndGetHiddenState, restoreHiddenState, overwriteData, selectSingleObject, - deleteIfFound, applyBasicTweaks, applyRotation, - prop_split, - toAlnum, - writeMaterialHeaders, raisePluginError, - customExportWarning, - decompFolderMessage, - makeWriteInfoBox, writeMaterialFiles, - getPathAndLevel, ) from ..f3d.f3d_gbi import ( @@ -71,9 +71,7 @@ def createGeoFile(levelName, filepath): + '#include "game/screen_transition.h"\n' + '#include "game/paintings.h"\n\n' + '#include "make_const_nonconst.h"\n\n' - + '#include "levels/' - + levelName - + '/header.h"\n\n' + + '#include "header.h"\n\n' ) geoFile = open(filepath, "w", newline="\n") @@ -308,6 +306,7 @@ def __init__(self, name): self.marioStart = None self.persistentBlocks = PersistentBlocks.new() self.sub_scripts: LevelScript = [] + self.custom_cmds: list["SM64_CustomCmdProperties"] = [] # this is basically a smaller script jumped to from the main one def add_subscript(self, name: str): @@ -360,6 +359,7 @@ def to_c(self, areaString): macrosToString(self.segmentLoads), f"\tALLOC_LEVEL_POOL(),", f"\t{self.mario}", + *[f"\t{cmd.to_c(1)}," for cmd in self.custom_cmds], macrosToString(self.levelFunctions), macrosToString(self.modelLoads), f"{self.get_persistent_block(PersistentBlocks.levelCommands, nTabs=1)}\n", @@ -443,19 +443,25 @@ def parseZoomMasks(filepath): return ZoomOutMasks(zoomMacros, cameraData) -def replaceSegmentLoad(levelscript, segmentName, command, changedSegment): +def replace_segment_load(level: LevelScript, name: str, commands: list[str], new_segment: int): + args = ["0x0{:X}".format(new_segment), name + "SegmentRomStart", name + "SegmentRomEnd"] changedLoad = None - for segmentLoad in levelscript.segmentLoads: + for i, segmentLoad in enumerate(level.segmentLoads): segmentString = segmentLoad[1][0].lower() - segment = int(segmentString, 16 if "x" in segmentString else 10) - if segmentLoad[0] == command and segment == changedSegment: + segment = int(segmentString, 0) + if segmentLoad[0] in commands and segment == new_segment: + level.segmentLoads[i] = Macro(commands[0], args, segmentLoad[2]) changedLoad = segmentLoad if changedLoad is None: - changedLoad = Macro(command, [hex(changedSegment), "", ""], "") - levelscript.segmentLoads.append(changedLoad) + changedLoad = Macro(commands[0], args, "") + level.segmentLoads.append(changedLoad) - changedLoad[1][1] = segmentName + "SegmentRomStart" - changedLoad[1][2] = segmentName + "SegmentRomEnd" + +def remove_segment_load(levelscript: LevelScript, segment_num: int): + for segment_load in levelscript.segmentLoads: + segment_string = segment_load[1][0].lower() + if int(segment_string, 0) == segment_num: + levelscript.segmentLoads.remove(segment_load) def replaceScriptLoads(levelscript, obj): @@ -465,19 +471,13 @@ def replaceScriptLoads(levelscript, obj): if "script_func_global_" not in target: newFuncs.append(jumpLink) continue - scriptNum = int(re.findall(r"\d+", target)[-1]) - # this is common0 - if scriptNum == 1: - newFuncs.append(jumpLink) - continue - if scriptNum < 13: - newNum = obj.fast64.sm64.segment_loads.group5 - else: - newNum = obj.fast64.sm64.segment_loads.group6 - if newNum == "Do Not Write": - newFuncs.append(jumpLink) - continue - newFuncs.append(Macro("JUMP_LINK", [newNum], jumpLink.comment)) + + group_seg_loads = obj.fast64.sm64.segment_loads + scriptFuncs = (group_seg_loads.group8, group_seg_loads.group5, group_seg_loads.group6) + for func in scriptFuncs: + if func is not None: + newFuncs.append(Macro("JUMP_LINK", [func], "")) + levelscript.levelFunctions = newFuncs @@ -759,7 +759,7 @@ def include_proto(file_name): geolayoutGraph, fModel = convertObjectToGeolayout( obj, transformMatrix, - area_root.areaCamera, + False, f"{level_name}_{areaName}", fModel, area_root, @@ -789,11 +789,15 @@ def include_proto(file_name): level_data.header_data += roomsC.header # Get area - area = exportAreaCommon( - area_root, transformMatrix, geolayoutGraph.startGeolayout, collision, f"{level_name}_{areaName}" - ) + try: + area = exportAreaCommon( + area_root, transformMatrix, geolayoutGraph.startGeolayout, collision, f"{level_name}_{areaName}" + ) + except Exception as exc: + raise PluginError(f"Error while creating area {area_root.areaIndex}: {str(exc)}") from exc if area.mario_start is not None: prev_level_script.marioStart = area.mario_start + prev_level_script.custom_cmds += area.custom_cmds persistentBlockString = prev_level_script.get_persistent_block( PersistentBlocks.areaCommands, nTabs=2, areaIndex=str(area.index) ) @@ -818,38 +822,49 @@ def include_proto(file_name): def export_level_script_c(obj, prev_level_script, level_name, level_data, level_dir, uses_env_fx): compressionFmt = bpy.context.scene.fast64.sm64.compression_format + + def replace_compressed_segment_load(name: str, segment: int, add_compression_fmt=True): + compression_fmts = [compressionFmt.upper()] + list({"MIO0", "YAY0", "RAW"} - {compressionFmt.upper()}) + valid_cmds = [f"LOAD_{fmt}" for fmt in compression_fmts] + if add_compression_fmt: + name += f"_{compressionFmt}" + replace_segment_load(prev_level_script, f"_{name}", valid_cmds, segment) + # replace level loads - replaceSegmentLoad(prev_level_script, f"_{level_name}_segment_7", f"LOAD_{compressionFmt.upper()}", 0x07) + replace_compressed_segment_load(f"{level_name}_segment_7", 0x07, False) if uses_env_fx: - replaceSegmentLoad(prev_level_script, f"_effect_{compressionFmt}", f"LOAD_{compressionFmt.upper()}", 0x0B) + replace_compressed_segment_load("effect", 0x0B) if not obj.useBackgroundColor: if obj.background == "CUSTOM": segment = obj.fast64.sm64.level.backgroundSegment else: segment = f"{backgroundSegments[obj.background]}_skybox" - replaceSegmentLoad(prev_level_script, f"_{segment}_{compressionFmt}", f"LOAD_{compressionFmt.upper()}", 0x0A) + replace_compressed_segment_load(segment, 0x0A) # replace actor loads group_seg_loads = obj.fast64.sm64.segment_loads - if group_seg_loads.seg5_enum != "Do Not Write": - replaceSegmentLoad( - prev_level_script, - f"_{group_seg_loads.seg5}_{compressionFmt}", - f"LOAD_{compressionFmt.upper()}", - 0x05, - ) - replaceSegmentLoad(prev_level_script, f"_{group_seg_loads.seg5}_geo", "LOAD_RAW", 0x0C) - if group_seg_loads.seg6_enum != "Do Not Write": - replaceSegmentLoad( - prev_level_script, - f"_{group_seg_loads.seg6}_{compressionFmt}", - f"LOAD_{compressionFmt.upper()}", - 0x06, - ) - replaceSegmentLoad(prev_level_script, f"_{group_seg_loads.seg6}_geo", "LOAD_RAW", 0x0D) + if group_seg_loads.write_actor_loads: + if group_seg_loads.seg5_enum != "None": + replace_compressed_segment_load(group_seg_loads.seg5, 0x05) + replace_segment_load(prev_level_script, f"_{group_seg_loads.seg5}_geo", ["LOAD_RAW"], 0x0C) + else: + remove_segment_load(prev_level_script, 0x05) + remove_segment_load(prev_level_script, 0x0C) + if group_seg_loads.seg6_enum != "None": + replace_compressed_segment_load(group_seg_loads.seg6, 0x06) + replace_segment_load(prev_level_script, f"_{group_seg_loads.seg6}_geo", ["LOAD_RAW"], 0x0D) + else: + remove_segment_load(prev_level_script, 0x06) + remove_segment_load(prev_level_script, 0x0D) + if group_seg_loads.seg8_enum != "None": + replace_compressed_segment_load(group_seg_loads.seg8, 0x08) + replace_segment_load(prev_level_script, f"_{group_seg_loads.seg8}_geo", ["LOAD_RAW"], 0x0F) + else: + remove_segment_load(prev_level_script, 0x08) + remove_segment_load(prev_level_script, 0x0F) + replaceScriptLoads(prev_level_script, obj) # write data - replaceScriptLoads(prev_level_script, obj) saveDataToFile(os.path.join(level_dir, "script.c"), prev_level_script.to_c(level_data.area_data)) return level_data @@ -873,16 +888,32 @@ def exportLevelC(obj, transformMatrix, level_name, exportDir, savePNG, customExp level_data = LevelData(camera_data=f"struct CameraTrigger {levelCameraVolumeName}[] = {{\n") - inline = bpy.context.scene.exportInlineF3D fModel = SM64Model( level_name + "_dl", DLFormat, - GfxMatWriteMethod.WriteDifferingAndRevert if not inline else GfxMatWriteMethod.WriteAll, + bpy.context.scene.fast64.sm64.gfx_write_method, ) childAreas = [child for child in obj.children if child.type == "EMPTY" and child.sm64_obj_type == "Area Root"] if len(childAreas) == 0: raise PluginError("The level root has no child empties with the 'Area Root' object type.") + for child in obj.children: + if child.type == "EMPTY" and child.sm64_obj_type == "Custom": + custom_props = child.fast64.sm64.custom + if custom_props.preset != "NONE" and custom_props.section == "AREA": + raise PluginError( + f"Object {obj.name} is parented to the level root but should be parented to an area root." + ) + prev_level_script.custom_cmds.append( + custom_props.get_final_cmd( + obj, + bpy.context.scene.fast64.sm64.blender_to_sm64_scale, + child.matrix_world @ yUpToZUp, + child.matrix_local, + name=obj.name, + ) + ) + uses_env_fx = False echoLevels = ["0x00", "0x00", "0x00"] zoomFlags = [False, False, False, False] @@ -905,7 +936,7 @@ def exportLevelC(obj, transformMatrix, level_name, exportDir, savePNG, customExp echoLevels[area_root.areaIndex - 1] = area_root.echoLevel # write area specific files - level_data, fModel, uses_env_fx = export_area_c( + level_data, fModel, area_uses_env_fx = export_area_c( obj, level_data, area_root, @@ -917,6 +948,7 @@ def exportLevelC(obj, transformMatrix, level_name, exportDir, savePNG, customExp DLFormat, savePNG, ) + uses_env_fx |= area_uses_env_fx level_data.camera_data += "\tNULL_TRIGGER\n};" @@ -944,6 +976,14 @@ def include_proto(file_name, new_line_first=False): include += "\n" return include + def write_include(path: Path, include: Path, before_endif=False): + return write_or_delete_if_found( + path, + [to_include_descriptor(include, Path("levels", level_name, include))], + path_must_exist=True, + footer=END_IF_FOOTER if before_endif else None, + ) + gfxFormatter = SM64GfxFormatter(ScrollMethod.Vertex) exportData = fModel.to_c(TextureExportSettings(savePNG, savePNG, f"levels/{level_name}", level_dir), gfxFormatter) staticData = exportData.staticData @@ -1008,10 +1048,10 @@ def include_proto(file_name, new_line_first=False): if not customExport: if DLFormat != DLFormat.Static: # Write material headers - writeMaterialHeaders( - exportDir, - include_proto("material.inc.c"), - include_proto("material.inc.h"), + write_material_headers( + Path(exportDir), + Path("levels", level_name, "material.inc.c"), + Path("levels", level_name, "material.inc.h"), ) # Export camera triggers @@ -1082,19 +1122,20 @@ def include_proto(file_name, new_line_first=False): createHeaderFile(level_name, headerPath) # Write level data - writeIfNotFound(geoPath, include_proto("geo.inc.c", new_line_first=True), "") - writeIfNotFound(levelDataPath, include_proto("leveldata.inc.c", new_line_first=True), "") - writeIfNotFound(headerPath, include_proto("header.inc.h", new_line_first=True), "#endif") + write_include(Path(geoPath), Path("geo.inc.c")) + write_include(Path(levelDataPath), Path("leveldata.inc.c")) + write_include(Path(headerPath), Path("header.inc.h"), before_endif=True) + old_include = to_include_descriptor(Path("levels", level_name, "texture_include.inc.c")) if fModel.texturesSavedLastExport == 0: textureIncludePath = os.path.join(level_dir, "texture_include.inc.c") if os.path.exists(textureIncludePath): os.remove(textureIncludePath) # This one is for backwards compatibility purposes - deleteIfFound(os.path.join(level_dir, "texture.inc.c"), include_proto("texture_include.inc.c")) + write_or_delete_if_found(Path(level_dir, "texture.inc.c"), to_remove=[old_include]) # This one is for backwards compatibility purposes - deleteIfFound(levelDataPath, include_proto("texture_include.inc.c")) + write_or_delete_if_found(Path(levelDataPath), to_remove=[old_include]) texscrollIncludeC = include_proto("texscroll.inc.c") texscrollIncludeH = include_proto("texscroll.inc.h") diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index 951e8bcc5..082ecff62 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -1,8 +1,11 @@ import math, bpy, mathutils -import os from bpy.utils import register_class, unregister_class +from bpy.types import UILayout from re import findall, sub from pathlib import Path + +from bpy.utils import register_class, unregister_class + from ..panels import SM64_Panel from ..operators import ObjectDataExporter @@ -10,6 +13,8 @@ PluginError, CData, Vector, + yUpToZUp, + y_up_to_z_up, directory_ui_warnings, filepath_ui_warnings, toAlnum, @@ -22,6 +27,7 @@ multilineLabel, raisePluginError, enumExportHeaderType, + selectSingleObject, ) from ..f3d.f3d_gbi import ( @@ -44,6 +50,7 @@ obj_group_enums, groupsSeg5, groupsSeg6, + groups_seg8, groups_obj_export, ) from .sm64_utility import convert_addr_to_func @@ -60,11 +67,19 @@ RotateNode, TranslateRotateNode, FunctionNode, - CustomNode, BillboardNode, ScaleNode, ) +from .animation import ( + export_animation, + export_animation_table, + get_anim_obj, + is_obj_animatable, + SM64_ArmatureAnimProperties, +) + +from .custom_cmd.properties import SM64_CustomCmdProperties enumTerrain = [ ("Custom", "Custom", "Custom"), @@ -226,26 +241,28 @@ def __init__( "Geo Billboard": InlineGeolayoutObjConfig("Geo Billboard", BillboardNode, can_have_dl=True, uses_location=True), "Geo Scale": InlineGeolayoutObjConfig("Geo Scale", ScaleNode, can_have_dl=True, uses_scale=True), "Geo Displaylist": InlineGeolayoutObjConfig("Geo Displaylist", DisplayListNode, must_have_dl=True), - "Custom Geo Command": InlineGeolayoutObjConfig("Custom Geo Command", CustomNode), + "Custom": InlineGeolayoutObjConfig("Custom", "CustomCmd"), } # When adding new types related to geolayout, # Make sure to add exceptions to enumSM64EmptyWithGeolayout enumObjectType = [ - ("None", "None", "None"), - ("Level Root", "Level Root", "Level Root"), - ("Area Root", "Area Root", "Area Root"), - ("Object", "Object", "Object"), - ("Macro", "Macro", "Macro"), - ("Special", "Special", "Special"), - ("Mario Start", "Mario Start", "Mario Start"), - ("Whirlpool", "Whirlpool", "Whirlpool"), - ("Water Box", "Water Box", "Water Box"), - ("Camera Volume", "Camera Volume", "Camera Volume"), - ("Switch", "Switch Node", "Switch Node"), - ("Puppycam Volume", "Puppycam Volume", "Puppycam Volume"), - ("", "Inline Geolayout Commands", ""), # This displays as a column header for the next set of options - *[(key, key, key) for key in inlineGeoLayoutObjects.keys()], + ("None", "None", "None", 0), + ("Level Root", "Level Root", "Level Root", 1), + ("Area Root", "Area Root", "Area Root", 2), + ("Object", "Object", "Object", 3), + ("Macro", "Macro", "Macro", 4), + ("Special", "Special", "Special", 5), + ("Mario Start", "Mario Start", "Mario Start", 6), + ("Whirlpool", "Whirlpool", "Whirlpool", 7), + ("Water Box", "Water Box", "Water Box", 8), + ("Camera Volume", "Camera Volume", "Camera Volume", 9), + ("Switch", "Switch Node", "Switch Node", 10), + ("Puppycam Volume", "Puppycam Volume", "Puppycam Volume", 11), + ("", "Inline Geolayout Commands", "", 12), # This displays as a column header for the next set of options + *[(key, key, key, i) for i, key in enumerate(list(inlineGeoLayoutObjects.keys())[:-1], start=13)], # exclude custom + ("", "", "", 12), + ("Custom", "Custom", "Custom level script command", 21), ] enumPuppycamMode = [ @@ -292,7 +309,7 @@ def __init__(self, model, position, rotation, behaviour, bparam, acts, name): self.rotation = rotation self.name = name # to sort by when exporting - def to_c(self): + def to_c(self, _depth=0): if self.acts == 0x1F: return ( "OBJECT(" @@ -349,7 +366,7 @@ def __init__(self, index, condition, strength, position): self.position = position self.name = "whirlpool" # for sorting - def to_c(self): + def to_c(self, _depth=0): return ( "WHIRPOOL(" + str(self.index) @@ -374,7 +391,7 @@ def __init__(self, preset, position, rotation, bparam): self.position = position self.rotation = rotation - def to_c(self): + def to_c(self, _depth=0): if self.bparam is None: return ( "MACRO_OBJECT(" @@ -426,7 +443,7 @@ def to_binary(self): data.extend(int(self.bparam).to_bytes(2, "big")) return data - def to_c(self): + def to_c(self, _depth=0): if self.rotation is None: return ( "SPECIAL_OBJECT(" @@ -437,7 +454,7 @@ def to_c(self): + str(int(round(self.position[1]))) + ", " + str(int(round(self.position[2]))) - + "),\n" + + ")" ) elif self.bparam is None: return ( @@ -451,7 +468,7 @@ def to_c(self): + str(int(round(self.position[2]))) + ", " + str(int(round(math.degrees(self.rotation[1])))) - + "),\n" + + ")" ) else: return ( @@ -467,7 +484,7 @@ def to_c(self): + str(int(round(math.degrees(self.rotation[1])))) + ", " + str(self.bparam) - + "),\n" + + ")" ) @@ -478,7 +495,7 @@ def __init__(self, area, position, rotation): self.rotation = rotation self.name = "Mario" # for sorting - def to_c(self): + def to_c(self, _depth=0): return ( "MARIO_POS(" + str(self.area) @@ -515,6 +532,7 @@ def __init__( self.mario_start = None self.splines = [] self.startDialog = startDialog + self.custom_cmds = [] def macros_name(self): return self.name + "_macro_objs" @@ -526,7 +544,7 @@ def to_c_script(self, includeRooms, persistentBlockString: str = ""): data += "\t\t" + warpNode + ",\n" # export objects in name order for obj in sorted(self.objects, key=(lambda obj: obj.name)): - data += "\t\t" + obj.to_c() + ",\n" + data += "\t\t" + obj.to_c(2) + ",\n" data += "\t\tTERRAIN(" + self.collision.name + "),\n" if includeRooms: data += "\t\tROOMS(" + self.collision.rooms_name() + "),\n" @@ -547,7 +565,7 @@ def to_c_macros(self): data.header = "extern const MacroObject " + self.macros_name() + "[];\n" data.source += "const MacroObject " + self.macros_name() + "[] = {\n" for macro in self.macros: - data.source += "\t" + macro.to_c() + ",\n" + data.source += "\t" + macro.to_c(1) + ",\n" data.source += "\tMACRO_OBJECT_END(),\n};\n\n" return data @@ -555,13 +573,13 @@ def to_c_macros(self): def to_c_camera_volumes(self): data = "" for camVolume in self.cameraVolumes: - data += "\t" + camVolume.to_c() + "\n" + data += "\t" + camVolume.to_c(1) + ",\n" return data def to_c_puppycam_volumes(self): data = "" for puppycamVolume in self.puppycamVolumes: - data += "\t" + puppycamVolume.to_c() + "\n" + data += "\t" + puppycamVolume.to_c(1) + ",\n" return data def hasCutsceneSpline(self): @@ -598,7 +616,7 @@ def to_binary(self): data.extend(int(round(self.height)).to_bytes(2, "big", signed=True)) return data - def to_c(self): + def to_c(self, _depth=0): data = ( "COL_WATER_BOX(" + ("0x00" if self.waterBoxType == "Water" else "0x32") @@ -612,7 +630,7 @@ def to_c(self): + str(int(round(self.high[1]))) + ", " + str(int(round(self.height))) - + "),\n" + + ")" ) return data @@ -630,7 +648,7 @@ def __init__(self, area, functionName, position, rotation, scale, emptyScale): def to_binary(self): raise PluginError("Binary exporting not implemented for camera volumens.") - def to_c(self): + def to_c(self, _depth=0): data = ( "{" + str(self.area) @@ -650,7 +668,7 @@ def to_c(self): + str(int(round(self.scale[2]))) + ", " + str(convertRadiansToS16(self.rotation[1])) - + "}," + + "}" ) return data @@ -683,7 +701,7 @@ def __init__(self, area, level, permaswap, functionName, position, scale, emptyS def to_binary(self): raise PluginError("Binary exporting not implemented for puppycam volumes.") - def to_c(self): + def to_c(self, _depth=0): data = ( "{" + str(self.level) @@ -719,14 +737,13 @@ def to_c(self): + str(int(round(self.camFocus[1]))) + ", " + str(int(round(self.camFocus[2]))) - + "}," + + "}" ) return data def exportAreaCommon(areaObj, transformMatrix, geolayout, collision, name): - bpy.ops.object.select_all(action="DESELECT") - areaObj.select_set(True) + selectSingleObject(areaObj) if not areaObj.noMusic: if areaObj.musicSeqEnum != "Custom": @@ -795,12 +812,40 @@ def process_sm64_objects(obj, area, rootMatrix, transformMatrix, specialsOnly): ) # Hacky solution to handle Z-up to Y-up conversion - rotation = (originalRotation @ mathutils.Quaternion((1, 0, 0), math.radians(90.0))).to_euler("ZXY") + rotation = (originalRotation @ y_up_to_z_up).to_euler("ZXY") if obj.type == "EMPTY": if obj.sm64_obj_type == "Area Root" and obj.areaIndex != area.index: return - if specialsOnly: + obj_props: SM64_ObjectProperties = obj.fast64.sm64 + if obj.sm64_obj_type == "Custom" and ( + (obj_props.custom.cmd_type == "Collision" and specialsOnly) + or ((obj_props.custom.cmd_type == "Level") and not specialsOnly) + ): + # HACK: alternatively we could just ignore transformMatrix since it only has the blender to sm64 scale + sm64_scale = bpy.context.scene.fast64.sm64.blender_to_sm64_scale + reverse = mathutils.Matrix.Diagonal((sm64_scale,) * 3).to_4x4().inverted() + local_matrix = reverse @ final_transform @ yUpToZUp + cmd = obj_props.custom.get_final_cmd( + obj, + bpy.context.scene.fast64.sm64.blender_to_sm64_scale, + reverse @ (transformMatrix @ obj.matrix_world) @ yUpToZUp, + local_matrix, + name=obj.name, + ) + if specialsOnly: + area.specials.append(cmd) + else: + if obj_props.custom.preset != "NONE": + if obj_props.custom.section == "FORCE_LEVEL": + area.custom_cmds.append(cmd) + return + elif obj_props.custom.section == "LEVEL": + raise PluginError( + f"Object {obj.name} is parented to an area but should be parented to the level root instead." + ) + area.objects.append(cmd) + elif specialsOnly: if obj.sm64_obj_type == "Special": preset = obj.sm64_special_enum if obj.sm64_special_enum != "Custom" else obj.sm64_obj_preset area.specials.append( @@ -1113,11 +1158,6 @@ def draw_inline_obj(self, box: bpy.types.UILayout, obj: bpy.types.Object): prop_split(box, obj.fast64.sm64.geo_asm, "param", "Parameter") return - elif obj.sm64_obj_type == "Custom Geo Command": - prop_split(box, obj, "customGeoCommand", "Geo Macro") - prop_split(box, obj, "customGeoCommandArgs", "Parameters") - return - if obj_details.can_have_dl: prop_split(box, obj, "draw_layer_static", "Draw Layer") @@ -1142,7 +1182,7 @@ def draw_inline_obj(self, box: bpy.types.UILayout, obj: bpy.types.Object): info_box.label(text="Scale", icon="DOT") if len(obj.children): - if checkIsSM64PreInlineGeoLayout(obj.sm64_obj_type): + if checkIsSM64PreInlineGeoLayout(obj): box.box().label(text="Children of this object will just be the following geo commands.") else: box.box().label(text="Children of this object will be wrapped in GEO_OPEN_NODE and GEO_CLOSE_NODE.") @@ -1171,11 +1211,15 @@ def draw_behavior_params(self, obj: bpy.types.Object, parent_box: bpy.types.UILa parent_box.separator() def draw(self, context): + sm64_props = context.scene.fast64.sm64 + prop_split(self.layout, context.scene, "gameEditorMode", "Game") box = self.layout.box().column() column = self.layout.box().column() # added just for puppycam trigger importing box.box().label(text="SM64 Object Inspector") obj = context.object + obj_props: SM64_ObjectProperties = obj.fast64.sm64 + prop_split(box, obj, "sm64_obj_type", "Object Type") if obj.sm64_obj_type == "Object": prop_split(box, obj, "sm64_model_enum", "Model") @@ -1245,18 +1289,18 @@ def draw(self, context): prop_split(box, levelObj, "backgroundSegment", "Custom Background Segment") segmentExportBox = box.box() segmentExportBox.label( - text=f"Exported Segment: _{levelObj.backgroundSegment}_{context.scene.fast64.sm64.compression_format}SegmentRomStart" + text=f"Exported Segment: _{levelObj.backgroundSegment}_{sm64_props.compression_format}SegmentRomStart" ) box.prop(obj, "useBackgroundColor") # box.box().label(text = 'Background IDs defined in include/geo_commands.h.') box.prop(obj, "actSelectorIgnore") box.prop(obj, "setAsStartLevel") - grid = box.grid_flow(columns=2) - obj.fast64.sm64.segment_loads.draw(grid) + obj.fast64.sm64.segment_loads.draw_props(box) prop_split(box, obj, "acousticReach", "Acoustic Reach") obj.starGetCutscenes.draw(box) elif obj.sm64_obj_type == "Area Root": + area_props = obj_props.area # Code that used to be in area inspector prop_split(box, obj, "areaIndex", "Area Index") box.prop(obj, "noMusic", text="Disable Music") @@ -1279,13 +1323,21 @@ def draw(self, context): camBox.label(text="Warning: Camera modes can be overriden by area specific camera code.") camBox.label(text="Check the switch statment in camera_course_processing() in src/game/camera.c.") - fogBox = box.box() - fogInfoBox = fogBox.box() - fogInfoBox.label(text="Warning: Fog only applies to materials that:") - fogInfoBox.label(text="- use fog") - fogInfoBox.label(text="- have global fog enabled.") - prop_split(fogBox, obj, "area_fog_color", "Area Fog Color") - prop_split(fogBox, obj, "area_fog_position", "Area Fog Position") + fog_box = box.box().column() + fog_box.prop(area_props, "set_fog") + fog_props = fog_box.column() + fog_props.enabled = area_props.set_fog + multilineLabel( + fog_props, + "All materials in the area with fog and\n" + '"Use Area\'s Fog" enabled will use these fog\n' + "settings.\n" + "Each material will have its own fog\n" + "applied as vanilla SM64 has no fog system.", + icon="INFO", + ) + prop_split(fog_props, obj, "area_fog_color", "Color") + prop_split(fog_props, obj, "area_fog_position", "Position") if obj.areaIndex == 1 or obj.areaIndex == 2 or obj.areaIndex == 3: prop_split(box, obj, "echoLevel", "Echo Level") @@ -1293,7 +1345,7 @@ def draw(self, context): if obj.areaIndex == 1 or obj.areaIndex == 2 or obj.areaIndex == 3 or obj.areaIndex == 4: box.prop(obj, "zoomOutOnPause") - box.prop(obj.fast64.sm64.area, "disable_background") + box.prop(area_props, "disable_background") areaLayout = box.box() areaLayout.enabled = not obj.fast64.sm64.area.disable_background @@ -1371,12 +1423,19 @@ def draw(self, context): prop_split(box, obj, "switchParam", "Parameter") box.box().label(text="Children will ordered alphabetically.") + elif obj.sm64_obj_type == "Custom": + custom_props: SM64_CustomCmdProperties = obj_props.custom + custom_props.draw_props(box, sm64_props.binary_export, obj, blender_scale=sm64_props.blender_to_sm64_scale) + elif obj.sm64_obj_type in inlineGeoLayoutObjects: self.draw_inline_obj(box, obj) elif obj.sm64_obj_type == "None": box.box().label(text="This can be used as an empty transform node in a geolayout hierarchy.") + else: + multilineLabel(box, "Unknown object type: " + obj.sm64_obj_type) + def draw_acts(self, obj, layout): layout.label(text="Acts") acts = layout.row() @@ -1443,6 +1502,8 @@ class BehaviorScriptProperty(bpy.types.PropertyGroup): _inheritable_macros = { "LOAD_COLLISION_DATA", "SET_MODEL", + "LOAD_ANIMATIONS", + "ANIMATE" # add support later maybe # "SET_HITBOX_WITH_OFFSET", # "SET_HITBOX", @@ -1496,6 +1557,18 @@ def get_inherit_args(self, context, props): if not props.export_col: raise PluginError("Can't inherit collision without exporting collision data") return props.collision_name + if self.macro == "LOAD_ANIMATIONS": + if not props.export_anim: + raise PluginError("Can't inherit animation table without exporting animation data") + if not props.anims_name: + raise PluginError("No animation name to inherit in behavior script") + return f"oAnimations, {props.anims_name}" + if self.macro == "ANIMATE": + if not props.export_anim: + raise PluginError("Can't inherit animation table without exporting animation data") + if not props.anim_object: + raise PluginError("No animation properties to inherit in behavior script") + return f"oAnimations, {props.anim_object.fast64.sm64.animation.beginning_animation}" return self.macro_args def get_args(self, context, props): @@ -1548,7 +1621,7 @@ def write_file_lines(self, path, file_lines): # exports the model ID load into the appropriate script.c location def export_script_load(self, context, props): - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path if props.export_header_type == "Level": # for some reason full_level_path doesn't work here if props.non_decomp_level: @@ -1594,7 +1667,7 @@ def export_model_id(self, context, props, offset): if props.non_decomp_level: return # check if model_ids.h exists - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path model_ids = decomp_path / "include" / "model_ids.h" if not model_ids.exists(): PluginError("Could not find model_ids.h") @@ -1711,7 +1784,7 @@ def export_level_specific_load(self, script_path, props): def export_behavior_header(self, context, props): # check if behavior_header.h exists - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path behavior_header = decomp_path / "include" / "behavior_data.h" if not behavior_header.exists(): PluginError("Could not find behavior_data.h") @@ -1745,7 +1818,7 @@ def export_behavior_script(self, context, props): raise PluginError("Behavior must have more than 0 cmds to export") # export the behavior script itself - decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path behavior_data = decomp_path / "data" / "behavior_data.c" if not behavior_data.exists(): PluginError("Could not find behavior_data.c") @@ -1812,7 +1885,7 @@ def verify_context(self, context, props): raise PluginError("Operator can only be used in object mode.") if context.scene.fast64.sm64.export_type != "C": raise PluginError("Combined Object Export only supports C exporting") - if not props.col_object and not props.gfx_object and not props.bhv_object: + if not props.col_object and not props.gfx_object and not props.anim_object and not props.bhv_object: raise PluginError("No export object selected") if ( context.active_object @@ -1823,7 +1896,7 @@ def verify_context(self, context, props): def get_export_objects(self, context, props): if not props.export_all_selected: - return {props.col_object, props.gfx_object, props.bhv_object}.difference({None}) + return {props.col_object, props.gfx_object, props.anim_object, props.bhv_object}.difference({None}) def obj_root(object, context): while object.parent and object.parent in context.selected_objects: @@ -1852,7 +1925,7 @@ def execute_col(self, props, obj): bpy.ops.object.sm64_export_collision(export_obj=obj.name) except Exception as exc: # pass on multiple export, throw on singular - if not props.export_all_selected: + if not props.export_all_selected or not PluginError.check_exc_warn(exc): raise Exception(exc) from exc # writes model.inc.c, geo.inc.c file, geo_header.h @@ -1872,10 +1945,26 @@ def execute_gfx(self, props, context, obj, index): if props.export_script_loads and props.model_id != 0: self.export_model_id(context, props, index) self.export_script_load(context, props) - except Exception as e: + except Exception as exc: + # pass on multiple export, throw on singular + if not props.export_all_selected or not PluginError.check_exc_warn(exc): + raise Exception(exc) + + # writes table.inc.c file, anim_header.h + # writes include into aggregate file in export location (leveldata.c/.c) + # writes name to header in aggregate file location (actor/level) + # var name is: static const struct Animation *const _anims[] (or custom name) + def execute_anim(self, props, context, obj): + try: + if props.export_anim and obj is props.anim_object: + if props.export_single_action: + export_animation(context, obj) + else: + export_animation_table(context, obj) + except Exception as exc: # pass on multiple export, throw on singular if not props.export_all_selected: - raise Exception(e) + raise Exception(exc) from exc def execute(self, context): props = context.scene.fast64.sm64.combined_export @@ -1887,6 +1976,7 @@ def execute(self, context): props.context_obj = obj self.execute_col(props, obj) self.execute_gfx(props, context, obj, index) + self.execute_anim(props, context, obj) # do not export behaviors with multiple selection if props.export_bhv and props.obj_name_bhv and not props.export_all_selected: self.export_behavior_script(context, props) @@ -1898,6 +1988,7 @@ def execute(self, context): props.context_obj = None # you've done it!~ self.report({"INFO"}, "Success!") + return {"FINISHED"} @@ -1941,6 +2032,7 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): group_name: bpy.props.EnumProperty(name="Group Name", default="group0", items=groups_obj_export) # custom export path, no headers written custom_export_path: bpy.props.StringProperty(name="Custom Path", subtype="FILE_PATH") + custom_include_directory: bpy.props.StringProperty(name="Include directory", subtype="FILE_PATH") # common export opts custom_group_name: bpy.props.StringProperty(name="custom") # for custom group @@ -1959,6 +2051,16 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): name="Export Rooms", description="Collision export will generate rooms.inc.c file" ) + # anim export options + quick_anim_read: bpy.props.BoolProperty( + name="Quick Data Read", description="Read fcurves directly, should work with the majority of rigs", default=True + ) + export_single_action: bpy.props.BoolProperty( + name="Selected Action", + description="Animation export will only export the armature's current action like in older versions of fast64", + ) + insertable_directory: bpy.props.StringProperty(name="Directory Path", subtype="FILE_PATH") + # export options export_bhv: bpy.props.BoolProperty( name="Export Behavior", default=False, description="Export behavior with given object name" @@ -1969,6 +2071,7 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): export_gfx: bpy.props.BoolProperty( name="Export Graphics", description="Export geo layouts for linked or selected mesh that have collision data" ) + export_anim: bpy.props.BoolProperty(name="Export Animations", description="Export animation table of an armature") export_script_loads: bpy.props.BoolProperty( name="Export Script Loads", description="Exports the Model ID and adds a level script load in the appropriate place", @@ -1991,6 +2094,7 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): collision_object: bpy.props.PointerProperty(type=bpy.types.Object) graphics_object: bpy.props.PointerProperty(type=bpy.types.Object) + animation_object: bpy.props.PointerProperty(type=bpy.types.Object, poll=lambda self, obj: is_obj_animatable(obj)) # is this abuse of properties? @property @@ -2011,6 +2115,18 @@ def gfx_object(self): else: return self.graphics_object or self.context_obj or bpy.context.active_object + @property + def anim_object(self): + if not self.export_anim: + return None + obj = get_anim_obj(bpy.context) + context_obj = self.context_obj if self.context_obj and is_obj_animatable(self.context_obj) else None + if self.export_all_selected: + return context_obj or obj + else: + assert not self.animation_object or is_obj_animatable(self.animation_object) + return self.animation_object or context_obj or obj + @property def bhv_object(self): if not self.export_bhv or self.export_all_selected: @@ -2054,6 +2170,15 @@ def obj_name_bhv(self): else: return self.filter_name(self.object_name or self.bhv_object.name) + @property + def obj_name_anim(self): + if self.export_all_selected and self.anim_object: + return self.filter_name(self.anim_object.name) + if not self.object_name and not self.anim_object: + return "" + else: + return self.filter_name(self.object_name or self.anim_object.name) + @property def bhv_name(self): return "bhv" + "".join([word.title() for word in toAlnum(self.obj_name_bhv).split("_")]) @@ -2070,6 +2195,12 @@ def collision_name(self): def model_id_define(self): return f"MODEL_{toAlnum(self.obj_name_gfx)}".upper() + @property + def anims_name(self): + if not self.anim_object: + return "" + return self.anim_object.fast64.sm64.animation.get_table_name(self.obj_name_anim) + @property def export_level_name(self): if self.level_name == "Custom" or self.non_decomp_level: @@ -2104,29 +2235,42 @@ def actor_custom_path(self): return self.custom_export_path @property - def level_directory(self): + def level_directory(self) -> Path: if self.non_decomp_level: - return self.custom_level_name + return Path(self.custom_level_name) level_name = self.custom_level_name if self.level_name == "Custom" else self.level_name - return os.path.join("/levels/", level_name) + return Path("levels") / level_name @property def base_level_path(self): if self.non_decomp_level: - return bpy.path.abspath(self.custom_level_path) - return bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + return Path(bpy.path.abspath(self.custom_level_path)) + return bpy.context.scene.fast64.sm64.abs_decomp_path @property def full_level_path(self): - return os.path.join(self.base_level_path, self.level_directory) + return self.base_level_path / self.level_directory # remove user prefixes/naming that I will be adding, such as _col, _geo etc. - def filter_name(self, name): - if self.use_name_filtering: + def filter_name(self, name, force_filtering=False): + if self.use_name_filtering or force_filtering: return sub("(_col)?(_geo)?(_bhv)?(lision)?", "", name) else: return name + def draw_anim_props(self, layout: UILayout, export_type="C", is_dma=False): + col = layout.column() + col.prop(self, "quick_anim_read") + if self.quick_anim_read: + col.label(text="May Break!", icon="INFO") + if not is_dma and export_type == "C": + col.prop(self, "export_single_action") + if export_type == "Binary": + if not is_dma: + prop_split(col, self, "level_name", "Level") + elif export_type == "Insertable Binary": + prop_split(col, self, "insertable_directory", "Directory") + def draw_export_options(self, layout): split = layout.row(align=True) @@ -2149,6 +2293,14 @@ def draw_export_options(self, layout): box.prop(self, "graphics_object", icon_only=True) if self.export_script_loads: box.prop(self, "model_id", text="Model ID") + + box = split.box().column() + box.prop(self, "export_anim", toggle=1) + if self.export_anim: + self.draw_anim_props(box) + if not self.export_all_selected: + box.prop(self, "animation_object", icon_only=True) + col = layout.column() col.prop(self, "export_all_selected") col.prop(self, "use_name_filtering") @@ -2156,8 +2308,30 @@ def draw_export_options(self, layout): col.prop(self, "export_bhv") self.draw_obj_name(layout) + @property + def actor_names(self) -> list: + return list(dict.fromkeys(filter(None, [self.obj_name_col, self.obj_name_gfx, self.obj_name_anim])).keys()) + + @property + def export_locations(self) -> str | None: + names = self.actor_names + if len(names) > 1: + return f"({'/'.join(names)})" + elif len(names) == 1: + return names[0] + return None + + @property + def export_locations(self) -> str | None: + names = self.actor_names + if len(names) > 1: + return f"{{{','.join(names)}}}" + elif len(names) == 1: + return names[0] + return None + def draw_level_path(self, layout): - if not directory_ui_warnings(layout, bpy.path.abspath(self.base_level_path)): + if not directory_ui_warnings(layout, self.base_level_path): return if self.non_decomp_level: layout.label(text=f"Level export path: {self.full_level_path}") @@ -2166,12 +2340,24 @@ def draw_level_path(self, layout): return True def draw_actor_path(self, layout): - actor_path = Path(bpy.context.scene.fast64.sm64.decomp_path) / "actors" - if not filepath_ui_warnings(layout, (actor_path / self.actor_group_name).with_suffix(".c")): - return - export_locations = ",".join({self.obj_name_col, self.obj_name_gfx}) - # can this be more clear? - layout.label(text=f"Actor export path: actors/{export_locations}") + if self.export_locations is None: + return False + decomp_path = bpy.context.scene.fast64.sm64.abs_decomp_path + if self.export_header_type == "Actor": + actor_path = decomp_path / "actors" + if not filepath_ui_warnings(layout, (actor_path / self.actor_group_name).with_suffix(".c")): + return False + layout.label(text=f"Actor export path: actors/{self.export_locations}/") + elif self.export_header_type == "Level": + if not directory_ui_warnings(layout, self.full_level_path): + return False + level_path = self.full_level_path if self.non_decomp_level else self.level_directory + layout.label(text=f"Actor export path: {level_path / self.export_locations}/") + elif self.export_header_type == "Custom": + custom_path = Path(bpy.path.abspath(self.custom_export_path)) + if not directory_ui_warnings(layout, custom_path): + return False + layout.label(text=f"Actor export path: {custom_path / self.export_locations}/") return True def draw_col_names(self, layout): @@ -2184,6 +2370,12 @@ def draw_gfx_names(self, layout): if self.export_script_loads: layout.label(text=f"Model ID: {self.model_id_define}") + def draw_anim_names(self, layout): + anim_props = self.anim_object.fast64.sm64.animation + if anim_props.is_dma: + layout.label(text=f"Animation path: {anim_props.dma_folder}(.c)") + layout.label(text=f"Animation table name: {self.anims_name}") + def draw_obj_name(self, layout): split_1 = layout.split(factor=0.45) split_2 = split_1.split(factor=0.45) @@ -2218,8 +2410,8 @@ def draw_props(self, layout): self.draw_level_path(box.box()) col.separator() # object exports - box = col.box() - if not self.export_col and not self.export_bhv and not self.export_gfx: + box = col.box().column() + if not self.export_col and not self.export_bhv and not self.export_gfx and not self.export_anim: col = box.column() col.operator("object.sm64_export_combined_object", text="Export Object") col.enabled = False @@ -2231,7 +2423,7 @@ def draw_props(self, layout): self.draw_export_options(box) # bhv export only, so enable bhv draw only - if not self.export_col and not self.export_gfx: + if not self.export_col and not self.export_gfx and not self.export_anim: return self.draw_bhv_options(col) # pathing for gfx/col exports @@ -2239,6 +2431,8 @@ def draw_props(self, layout): if self.export_header_type == "Custom": prop_split(box, self, "custom_export_path", "Custom Path") + if bpy.context.scene.saveTextures: + prop_split(box, self, "custom_include_directory", "Texture Include Directory") elif self.export_header_type == "Actor": prop_split(box, self, "group_name", "Group") @@ -2266,17 +2460,24 @@ def draw_props(self, layout): "Duplicates objects will be exported! Use with Caution.", icon="ERROR", ) + return info_box = box.box() info_box.scale_y = 0.5 - if self.export_header_type == "Level": - if not self.draw_level_path(info_box): - return + if not self.draw_actor_path(info_box): + return - elif self.export_header_type == "Actor": - if not self.draw_actor_path(info_box): - return + if self.export_header_type == "Custom" and bpy.context.scene.saveTextures: + if self.custom_include_directory: + info_box.label(text=f'Include directory "{self.custom_include_directory}"') + else: + actor_names = self.actor_names + joined = ",".join(self.actor_names) + if len(actor_names) > 1: + joined = "{" f"{joined}" "}" + directory = f"{Path(bpy.path.abspath(self.custom_export_path)).name}/{joined}" + info_box.label(text=f'Empty include directory, defaults to "{directory}"') if self.obj_name_gfx and self.export_gfx: self.draw_gfx_names(info_box) @@ -2284,6 +2485,9 @@ def draw_props(self, layout): if self.obj_name_col and self.export_col: self.draw_col_names(info_box) + if self.anim_object is not None and self.export_anim: + self.draw_anim_names(info_box) + if self.obj_name_bhv: info_box.label(text=f"Behavior name: {self.bhv_name}") @@ -2325,6 +2529,8 @@ class WarpNodeProperty(bpy.types.PropertyGroup): expand: bpy.props.BoolProperty() def uses_area_nodes(self): + if self.instantWarpObject1 is None or self.instantWarpObject2 is None: + raise PluginError(f"Warp Start and Warp End in Warp Node {self.warpID} must have objects selected.") return ( self.instantWarpObject1.sm64_obj_type == "Area Root" and self.instantWarpObject2.sm64_obj_type == "Area Root" @@ -2346,7 +2552,7 @@ def calc_offsets_from_objects(self, reverse=False): ret.z = int(round(-difference.y * bpy.context.scene.blenderF3DScale)) return ret - def to_c(self): + def to_c(self, _depth=0): if self.warpType == "Instant": offset = Vector() @@ -2648,6 +2854,11 @@ class SM64_AreaProperties(bpy.types.PropertyGroup): default=False, description="Disable rendering background. Ideal for interiors or areas that should never see a background.", ) + set_fog: bpy.props.BoolProperty( + name="Set Fog Settings", + default=True, + description='All materials in the area with fog and "Use Area\'s Fog" enabled will use these fog settings. Each material will have its own fog applied as vanilla SM64 has no fog system', + ) class SM64_LevelProperties(bpy.types.PropertyGroup): @@ -2721,28 +2932,35 @@ def get_behavior_params(self): class SM64_SegmentProperties(bpy.types.PropertyGroup): + write_actor_loads: bpy.props.BoolProperty(name="Write Actor Loads") seg5_load_custom: bpy.props.StringProperty(name="Segment 5 Seg") seg5_group_custom: bpy.props.StringProperty(name="Segment 5 Group") seg6_load_custom: bpy.props.StringProperty(name="Segment 6 Seg") seg6_group_custom: bpy.props.StringProperty(name="Segment 6 Group") - seg5_enum: bpy.props.EnumProperty(name="Segment 5 Group", default="Do Not Write", items=groupsSeg5) - seg6_enum: bpy.props.EnumProperty(name="Segment 6 Group", default="Do Not Write", items=groupsSeg6) + seg8_load_custom: bpy.props.StringProperty(name="Segment 8 Seg") + seg8_group_custom: bpy.props.StringProperty(name="Segment 8 Group") + seg5_enum: bpy.props.EnumProperty(name="Segment 5 Group", default="None", items=groupsSeg5) + seg6_enum: bpy.props.EnumProperty(name="Segment 6 Group", default="None", items=groupsSeg6) + seg8_enum: bpy.props.EnumProperty(name="Segment 8 Group", default="None", items=groups_seg8) - def draw(self, layout): - col = layout.column() - prop_split(col, self, "seg5_enum", "Segment 5 Select") - if self.seg5_enum == "Custom": - prop_split(col, self, "seg5_load_custom", "Segment 5 Seg") - prop_split(col, self, "seg5_group_custom", "Segment 5 Group") + def draw_props(self, layout): col = layout.column() - prop_split(col, self, "seg6_enum", "Segment 6 Select") - if self.seg6_enum == "Custom": - prop_split(col, self, "seg6_load_custom", "Segment 6 Seg") - prop_split(col, self, "seg6_group_custom", "Segment 6 Group") + col.prop(self, "write_actor_loads") + if not self.write_actor_loads: + return + + for seg in (5, 6, 8): + prop_split(col, self, f"seg{seg}_enum", f"Segment {seg} Select") + if getattr(self, f"seg{seg}_enum") == "Custom": + prop_split(col, self, f"seg{seg}_load_custom", "Segment") + prop_split(col, self, f"seg{seg}_group_custom", "Group") + col.separator() def jump_link_from_enum(self, grp): - if grp == "Do Not Write": - return grp + if grp == "None": + return None + elif grp == "common0": + return "script_func_global_1" num = int(grp.removeprefix("group")) + 1 return f"script_func_global_{num}" @@ -2760,6 +2978,13 @@ def seg6(self): else: return self.seg6_enum + @property + def seg8(self): + if self.seg8_enum == "Custom": + return self.seg8_load_custom + else: + return self.seg8_enum + @property def group5(self): if self.seg5_enum == "Custom": @@ -2774,16 +2999,26 @@ def group6(self): else: return self.jump_link_from_enum(self.seg6_enum) + @property + def group8(self): + if self.seg8_enum == "Custom": + return self.seg8_group_custom + else: + return self.jump_link_from_enum(self.seg8_enum) + class SM64_ObjectProperties(bpy.types.PropertyGroup): version: bpy.props.IntProperty(name="SM64_ObjectProperties Version", default=0) - cur_version = 3 # version after property migration + cur_version = 4 # version after property migration geo_asm: bpy.props.PointerProperty(type=SM64_GeoASMProperties) level: bpy.props.PointerProperty(type=SM64_LevelProperties) area: bpy.props.PointerProperty(type=SM64_AreaProperties) game_object: bpy.props.PointerProperty(type=SM64_GameObjectProperties) segment_loads: bpy.props.PointerProperty(type=SM64_SegmentProperties) + custom: bpy.props.PointerProperty(type=SM64_CustomCmdProperties) + + animation: bpy.props.PointerProperty(type=SM64_ArmatureAnimProperties) @staticmethod def upgrade_changed_props(): @@ -2792,6 +3027,7 @@ def upgrade_changed_props(): SM64_GeoASMProperties.upgrade_object(obj) if obj.fast64.sm64.version < 3: SM64_GameObjectProperties.upgrade_object(obj) + obj.fast64.sm64.custom.upgrade_object(obj) obj.fast64.sm64.version = SM64_ObjectProperties.cur_version @@ -2936,6 +3172,7 @@ def sm64_obj_register(): size=2, min=0, max=0x7FFFFFFF, + step=100, default=(985, 1000), update=sm64_on_update_area_render_settings, ) @@ -3005,9 +3242,6 @@ def sm64_obj_register(): bpy.types.Object.geoReference = bpy.props.StringProperty(name="Geolayout variable name or hex address for binary") - bpy.types.Object.customGeoCommand = bpy.props.StringProperty(name="Geolayout macro command", default="") - bpy.types.Object.customGeoCommandArgs = bpy.props.StringProperty(name="Geolayout macro arguments", default="") - bpy.types.Object.enableRoomSwitch = bpy.props.BoolProperty(name="Enable Room System") diff --git a/fast64_internal/sm64/sm64_texscroll.py b/fast64_internal/sm64/sm64_texscroll.py index f68fe995f..c39de7fd0 100644 --- a/fast64_internal/sm64/sm64_texscroll.py +++ b/fast64_internal/sm64/sm64_texscroll.py @@ -1,7 +1,8 @@ +from pathlib import Path import os, re, bpy -from ..utility import PluginError, writeIfNotFound, getDataFromFile, saveDataToFile, CScrollData, CData +from ..utility import PluginError, getDataFromFile, saveDataToFile, CScrollData, CData from .c_templates.tile_scroll import tile_scroll_c, tile_scroll_h -from .sm64_utility import getMemoryCFilePath +from .sm64_utility import END_IF_FOOTER, ModifyFoundDescriptor, getMemoryCFilePath, write_or_delete_if_found # This is for writing framework for scroll code. # Actual scroll code found in f3d_gbi.py (FVertexScrollData) @@ -78,7 +79,16 @@ def writeSegmentROMTable(baseDir): memFile.close() # Add extern definition of segment table - writeIfNotFound(os.path.join(baseDir, "src/game/memory.h"), "\nextern uintptr_t sSegmentROMTable[32];", "#endif") + write_or_delete_if_found( + Path(baseDir, "src", "game", "memory.h"), + [ + ModifyFoundDescriptor( + "extern uintptr_t sSegmentROMTable[32];", r"extern\h*uintptr_t\h*sSegmentROMTable\[.*?\]\h*?;" + ) + ], + path_must_exist=True, + footer=END_IF_FOOTER, + ) def writeScrollTextureCall(path, include, callString): diff --git a/fast64_internal/sm64/sm64_utility.py b/fast64_internal/sm64/sm64_utility.py index c7c55de1f..f408bc17d 100644 --- a/fast64_internal/sm64/sm64_utility.py +++ b/fast64_internal/sm64/sm64_utility.py @@ -1,8 +1,24 @@ +import dataclasses +from typing import NamedTuple, Optional +from pathlib import Path +from io import StringIO +import random +import string import os +import re + import bpy from bpy.types import UILayout -from ..utility import PluginError, filepath_checks, run_and_draw_errors, multilineLabel, prop_split +from ..utility import ( + filepath_checks, + run_and_draw_errors, + multilineLabel, + prop_split, + as_posix, + PluginError, + COMMENT_PATTERN, +) from .sm64_function_map import func_map @@ -122,3 +138,319 @@ def convert_addr_to_func(addr: str): return refresh_func_map[addr.lower()] else: return addr + + +def temp_file_path(path: Path): + """Generates a temporary file path that does not exist from the given path.""" + result, size = path.with_suffix(".tmp"), 0 + for size in range(5, 15): + if not result.exists(): + return result + random_suffix = "".join(random.choice(string.ascii_letters) for _ in range(size)) + result = path.with_suffix(f".{random_suffix}.tmp") + size += 1 + raise PluginError("Cannot create unique temporary file. 10 tries exceeded.") + + +class ModifyFoundDescriptor: + string: str + regex: str + + def __init__(self, string: str, regex: str = ""): + self.string = string + if regex: + self.regex = regex.replace(r"\h", r"[^\v\S]") # /h is invalid... for some reason + else: + self.regex = re.escape(string) + r"\n?" + + +@dataclasses.dataclass +class DescriptorMatch: + string: str + start: int + end: int + + def __iter__(self): + return iter((self.string, self.start, self.end)) + + +class CommentMatch(NamedTuple): + commentless_pos: int + size: int + + +def adjust_start_end(starting_start: int, starting_end: int, comment_map: list[CommentMatch]): + """ + Adjust start and end positions in a commentless string to account for comments positions + in comment_map. + """ + start, end = starting_start, starting_end + for commentless_pos, comment_size in comment_map: + if starting_start >= commentless_pos: + start += comment_size + if starting_end >= commentless_pos or starting_start > commentless_pos: + end += comment_size + return start, end + + +def find_descriptor_in_text( + value: ModifyFoundDescriptor, + commentless: str, + comment_map: list[CommentMatch], + start=0, + end=-1, + adjust=True, +): + """ + Find all matches of a descriptor in a commentless string with respect to comment positions + in comment_map. + """ + matches: list[DescriptorMatch] = [] + for match in re.finditer(value.regex, commentless[start:end]): + match_start, match_end = match.start() + start, match.end() + start + if adjust: + match_start, match_end = adjust_start_end(match_start, match_end, comment_map) + matches.append(DescriptorMatch(match.group(0), match_start, match_end)) + return matches + + +def get_comment_map(text: str): + """Get a string without comments and a list of the removed comment positions.""" + comment_map: list[CommentMatch] = [] + commentless, last_pos, commentless_pos = StringIO(), 0, 0 + for match in re.finditer(COMMENT_PATTERN, text): + commentless_pos += commentless.write(text[last_pos : match.start()]) # add text before comment + match_string = match.group(0) + if match_string.startswith("/"): # actual comment + comment_map.append(CommentMatch(commentless_pos, len(match_string) - 1)) + commentless_pos += commentless.write(" ") + else: # stuff like strings + commentless_pos += commentless.write(match_string) + last_pos = match.end() + + commentless.write(text[last_pos:]) # add any remaining text after the last match + return commentless.getvalue(), comment_map + + +def find_descriptors( + text: str, + descriptors: list[ModifyFoundDescriptor], + error_if_no_header=False, + header: Optional[ModifyFoundDescriptor] = None, + error_if_no_footer=False, + footer: Optional[ModifyFoundDescriptor] = None, + ignore_comments=True, +): + """Returns: The found matches mapped to the descriptors, the footer pos + (the end of the text if none)""" + if ignore_comments: + commentless, comment_map = get_comment_map(text) + else: + commentless, comment_map = text, [] + + header_matches = ( + find_descriptor_in_text(header, commentless, comment_map, adjust=False) if header is not None else [] + ) + footer_matches = ( + find_descriptor_in_text(footer, commentless, comment_map, adjust=False) if footer is not None else [] + ) + + header_pos = 0 + if len(header_matches) > 0: + _, header_pos, _ = header_matches[0] + elif header is not None and error_if_no_header: + raise PluginError(f"Header {header.string} does not exist.") + + # find first footer after the header + if footer_matches: + if header_matches: + footer_pos = next((pos for _, pos, _ in footer_matches if pos >= header_pos), footer_matches[-1].start) + else: + _, footer_pos, _ = footer_matches[-1] + else: + if footer is not None and error_if_no_footer: + raise PluginError(f"Footer {footer.string} does not exist.") + footer_pos = len(commentless) + + found_matches: dict[ModifyFoundDescriptor, list[DescriptorMatch]] = {} + for descriptor in descriptors: + matches = find_descriptor_in_text(descriptor, commentless, comment_map, header_pos, footer_pos) + if matches: + found_matches.setdefault(descriptor, []).extend(matches) + return found_matches, adjust_start_end(footer_pos, footer_pos, comment_map)[0] + + +def write_or_delete_if_found( + path: Path, + to_add: Optional[list[ModifyFoundDescriptor]] = None, + to_remove: Optional[list[ModifyFoundDescriptor]] = None, + path_must_exist=False, + create_new=False, + error_if_no_header=False, + header: Optional[ModifyFoundDescriptor] = None, + error_if_no_footer=False, + footer: Optional[ModifyFoundDescriptor] = None, + ignore_comments=True, +): + """ + This function reads the content of a file at the given path and modifies it by either + adding or removing descriptors (using regex). + path_must_exist will raise an error if the file does not exist, while create_new will + always replace the file. + error_if_no_header/error_if_no_footer will raise errors if the header/footer is not found. + ignore_comments will ignore comments in the file, possibly breaking the search for matches. + header defines the start of a writable area in the file + footer defines the end of a writable area in the file after the header, the footer expects a header beforehand + + Returns True if the file was modified. + """ + + changed = False + to_add, to_remove = to_add or [], to_remove or [] + + assert not (path_must_exist and create_new), "path_must_exist and create_new" + if path_must_exist: + filepath_checks(path) + if not create_new and not to_add and not to_remove: + return False + + if os.path.exists(path) and not create_new: + text = path.read_text() + if text and text[-1] not in {"\n", "\r"}: # add end new line if not there + text += "\n" + found_matches, footer_pos = find_descriptors( + text, to_add + to_remove, error_if_no_header, header, error_if_no_footer, footer, ignore_comments + ) + else: + text, found_matches, footer_pos = "", {}, 0 + + for descriptor in to_remove: + matches = found_matches.get(descriptor) + if matches is None: + continue + print(f"Removing {descriptor.string} in {str(path)}") + for match in matches: + changed = True + text = text[: match.start] + text[match.end :] # Remove match + diff = match.end - match.start + for other_match in (other_match for matches in found_matches.values() for other_match in matches): + if other_match.start > match.start: + other_match.start -= diff + other_match.end -= diff + if footer_pos > match.start: + footer_pos -= diff + + additions = "" + if text and text[footer_pos - 1] not in {"\n", "\r"}: # add new line if not there + additions += "\n" + for descriptor in to_add: + if descriptor in found_matches: + continue + print(f"Adding {descriptor.string} in {str(path)}") + additions += f"{descriptor.string}\n" + changed = True + text = text[:footer_pos] + additions + text[footer_pos:] + + if changed or create_new: + path.write_text(text) + return True + return False + + +def to_include_descriptor(include: Path, *alternatives: Path): + """ + Returns a ModifyFoundDescriptor for an include, string being the include for the path + while the regex matches for the path or any of the alternatives. + """ + base_regex = r'\n?#\h*?include\h*?"{0}"' + regex = base_regex.format(as_posix(include)) + for alternative in alternatives: + regex += f"|{base_regex.format(as_posix(alternative))}" + return ModifyFoundDescriptor(f'#include "{as_posix(include)}"', regex) + + +END_IF_FOOTER = ModifyFoundDescriptor("#endif", r"#\h*?endif") + + +def write_includes( + path: Path, includes: Optional[list[Path]] = None, path_must_exist=False, create_new=False, before_endif=False +): + """ + Write includes to the path. path_must_exist will raise an error if the file does not exist, + while create_new will always replace the file. before_endif will add the includes before the + endif if it exists. + """ + to_add = [] + for include in includes or []: + to_add.append(to_include_descriptor(include)) + return write_or_delete_if_found( + path, + to_add, + path_must_exist=path_must_exist, + create_new=create_new, + footer=END_IF_FOOTER if before_endif else None, + ) + + +def update_actor_includes( + header_type: str, + group_name: str, + header_dir: Path, + dir_name: str, + level_name: str, # for backwards compatibility + data_includes: Optional[list[str | Path]] = None, + header_includes: Optional[list[str | Path]] = None, + geo_includes: Optional[list[str | Path]] = None, +): + """ + Update actor data, header, and geo includes for "Actor" and "Level" header types. + group_name is used for actors, level_name for levels (tho for backwards compatibility). + header_dir is the base path where the function expects to find the group/level specific headers. + dir_name is the actor's folder name. + """ + if header_type == "Actor": + if not group_name: + raise PluginError("Empty group name") + data_path = header_dir / f"{group_name}.c" + header_path = header_dir / f"{group_name}.h" + geo_path = header_dir / f"{group_name}_geo.c" + elif header_type == "Level": + data_path = header_dir / "leveldata.c" + header_path = header_dir / "header.h" + geo_path = header_dir / "geo.c" + elif header_type == "Custom": + return # Custom doesn't update includes + else: + raise PluginError(f'Unknown header type "{header_type}"') + + def write_includes_with_alternate(path: Path, includes: Optional[list[Path]], before_endif=False): + if includes is None: + return False + if header_type == "Level": + path_and_alternates = [ + [ + Path(dir_name, include), + Path("levels", level_name, dir_name, include), # backwards compatability + ] + for include in includes + ] + else: + path_and_alternates = [[Path(dir_name, include)] for include in includes] + return write_or_delete_if_found( + path, + [to_include_descriptor(*paths) for paths in path_and_alternates], + path_must_exist=True, + footer=END_IF_FOOTER if before_endif else None, + ) + + if write_includes_with_alternate(data_path, data_includes): + print(f"Updated data includes at {data_path}.") + if write_includes_with_alternate(header_path, header_includes, before_endif=True): + print(f"Updated header includes at {header_path}.") + if write_includes_with_alternate(geo_path, geo_includes): + print(f"Updated geo data at {geo_path}.") + + +def write_material_headers(decomp: Path, c_include: Path, h_include: Path): + write_includes(decomp / "src/game/materials.c", [c_include]) + write_includes(decomp / "src/game/materials.h", [h_include], before_endif=True) diff --git a/fast64_internal/sm64/tools/operators.py b/fast64_internal/sm64/tools/operators.py index d8d85fed4..01e82f549 100644 --- a/fast64_internal/sm64/tools/operators.py +++ b/fast64_internal/sm64/tools/operators.py @@ -6,13 +6,13 @@ from bpy.path import abspath from ...operators import OperatorBase, AddWaterBox -from ...utility import PluginError, decodeSegmentedAddr, encodeSegmentedAddr +from ...utility import PluginError, decodeSegmentedAddr, encodeSegmentedAddr, selectSingleObject from ...f3d.f3d_material import getDefaultMaterialPreset, createF3DMat, add_f3d_mat_to_obj from ...utility import parentObject, intToHex, bytesToHex -from ..sm64_constants import level_pointers, levelIDNames, level_enums +from ..sm64_constants import levelIDNames, enumLevelNames from ..sm64_utility import import_rom_checks, int_from_str -from ..sm64_level_parser import parseLevelAtPointer +from ..sm64_level_parser import parse_level_binary from ..sm64_geolayout_utility import createBoneGroups from ..sm64_geolayout_parser import generateMetarig @@ -31,7 +31,7 @@ class SM64_AddrConv(OperatorBase): rom: StringProperty(name="ROM", subtype="FILE_PATH") # Using an enum here looks cleaner when using this as an operator option: EnumProperty(name="Conversion type", items=enum_address_conversion_options) - level: EnumProperty(items=level_enums, name="Level", default="IC") + level: EnumProperty(items=enumLevelNames, name="Level", default="castle_inside") addr: StringProperty(name="Address") clipboard: BoolProperty(name="Copy to clipboard", default=True) result: StringProperty(name="Result") @@ -41,7 +41,7 @@ def execute_operator(self, context: Context): import_rom_path = abspath(self.rom) import_rom_checks(import_rom_path) with open(import_rom_path, "rb") as romfile: - level_parsed = parseLevelAtPointer(romfile, level_pointers[self.level]) + level_parsed = parse_level_binary(romfile, self.level) segment_data = level_parsed.segmentData if self.option == "TO_VIR": result = intToHex(decodeSegmentedAddr(addr.to_bytes(4, "big"), segment_data)) @@ -143,6 +143,10 @@ class SM64_CreateSimpleLevel(OperatorBase): add_death_plane: BoolProperty(name="Add Death Plane") set_as_start_level: BoolProperty(name="Set As Start Level") respawn_in_level: BoolProperty(name="Respawn In The Same Level") + bounds: EnumProperty( + name="Bounds", + items=[("1", "1x", "1x"), ("2", "2x", "2x"), ("4", "4x", "4x"), ("NONE", "None", "No bounding box")], + ) def execute_operator(self, context: Context): scene = context.scene @@ -245,9 +249,15 @@ def execute_operator(self, context: Context): warp_game_object.bparam2 = "0x0A" warp_game_object.bparams = "0x000A0000" - bpy.ops.object.select_all(action="DESELECT") - level_object.select_set(True) - bpy.context.view_layer.objects.active = level_object + if self.bounds != "NONE": + bounds_loc = location_offset[0], location_offset[1], location_offset[2] + (0.05 * scale) + bounds_object = create_sm64_empty(f"Bounds ({self.bounds}x)", "Object", location=bounds_loc) + bounds_object.sm64_obj_type = "None" + radius = 8192.0 * float(self.bounds) / scale + bounds_object.scale = (radius, radius, 1.40381 * radius / int(self.bounds)) + parentObject(level_object, bounds_object) + + selectSingleObject(level_object) class SM64_AddWaterBox(AddWaterBox): diff --git a/fast64_internal/sm64/tools/panels.py b/fast64_internal/sm64/tools/panels.py index 5a41accbe..c22aae05c 100644 --- a/fast64_internal/sm64/tools/panels.py +++ b/fast64_internal/sm64/tools/panels.py @@ -1,11 +1,11 @@ from bpy.utils import register_class, unregister_class +from typing import TYPE_CHECKING + from ...panels import SM64_Panel from .operators import SM64_CreateSimpleLevel, SM64_AddWaterBox, SM64_AddBoneGroups, SM64_CreateMetarig -from typing import TYPE_CHECKING - if TYPE_CHECKING: from ..settings.properties import SM64_Properties diff --git a/fast64_internal/sm64/tools/properties.py b/fast64_internal/sm64/tools/properties.py index 342df25d1..b64690c59 100644 --- a/fast64_internal/sm64/tools/properties.py +++ b/fast64_internal/sm64/tools/properties.py @@ -7,7 +7,7 @@ from ...utility import prop_split, upgrade_old_prop from ..sm64_utility import string_int_prop, import_rom_ui_warnings -from ..sm64_constants import level_enums +from ..sm64_constants import enumLevelNames from .operators import SM64_AddrConv @@ -18,7 +18,7 @@ class SM64_AddrConvProperties(PropertyGroup): rom: StringProperty(name="Import ROM", subtype="FILE_PATH") address: StringProperty(name="Address") - level: EnumProperty(items=level_enums, name="Level", default="IC") + level: EnumProperty(items=enumLevelNames, name="Level", default="castle_inside") clipboard: BoolProperty(name="Copy to Clipboard", default=True) def upgrade_changed_props(self, scene: Scene): diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index b66eead44..a2f7184aa 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -1,21 +1,57 @@ -import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator +from pathlib import Path +import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator, inspect from math import pi, ceil, degrees, radians, copysign from mathutils import * -from .utility_anim import * + from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union -from bpy.types import UILayout, Scene, World +from bpy.types import UILayout, Scene, World, Object +from bpy.props import FloatVectorProperty CollectionProperty = Any # collection prop as defined by using bpy.props.CollectionProperty class PluginError(Exception): - pass + # arguments for exception processing + exc_halt = "exc_halt" + exc_warn = "exc_warn" + + """ + because exceptions generally go through multiple funcs + and layers, the easiest way to check if we have an exception + of a certain type is to check for our input string + """ + + @classmethod + def check_exc_warn(self, exc): + for arg in exc.args: + if type(arg) is str and self.exc_warn in arg: + return True + return False class VertexWeightError(PluginError): pass +class Matrix4x4Property(bpy.types.PropertyGroup): # blender's matrix subtype is broken :)))) + row0: FloatVectorProperty(size=4, default=(1, 0, 0, 0)) + row1: FloatVectorProperty(size=4, default=(0, 1, 0, 0)) + row2: FloatVectorProperty(size=4, default=(0, 0, 1, 0)) + row3: FloatVectorProperty(size=4, default=(0, 0, 0, 1)) + + def to_matrix(self): + return mathutils.Matrix((tuple(self.row0), tuple(self.row1), tuple(self.row2), tuple(self.row3))) + + def from_matrix(self, matrix: mathutils.Matrix): + for i in range(4): + setattr(self, f"row{i}", tuple(matrix[i])) + + def draw_props(self, layout: UILayout): + layout.label(text="Row: → | Column: ↓", icon="INFO") + for i in range(4): + layout.row().prop(self, f"row{i}", text="") + + # default indentation to use when writing to decomp files indent = " " * 4 @@ -24,7 +60,11 @@ class VertexWeightError(PluginError): transform_mtx_blender_to_n64 = lambda: Matrix(((1, 0, 0, 0), (0, 0, 1, 0), (0, -1, 0, 0), (0, 0, 0, 1))) -yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)).to_matrix().to_4x4() +y_up_to_z_up = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) +yUpToZUp = y_up_to_z_up.to_matrix().to_4x4() + +z_up_to_y_up = mathutils.Quaternion((1, 0, 0), math.radians(-90.0)) +z_up_to_y_up_matrix = z_up_to_y_up.to_matrix().to_4x4() axis_enums = [ ("X", "X", "X"), @@ -164,18 +204,26 @@ def checkObjectReference(obj, title): ) -def selectSingleObject(obj: bpy.types.Object): - bpy.ops.object.select_all(action="DESELECT") +def setActiveObject(obj: bpy.types.Object): obj.select_set(True) bpy.context.view_layer.objects.active = obj +def deselectAllObjects(): + for obj in bpy.data.objects: + obj.select_set(False) + + +def selectSingleObject(obj: bpy.types.Object): + deselectAllObjects() + setActiveObject(obj) + + def parentObject(parent, child): - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() child.select_set(True) - parent.select_set(True) - bpy.context.view_layer.objects.active = parent + setActiveObject(parent) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) @@ -225,10 +273,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": @@ -237,18 +286,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): @@ -438,10 +487,10 @@ def extendedRAMLabel(layout): def getPathAndLevel(is_custom_export, custom_export_path, custom_level_name, level_enum): if is_custom_export: - export_path = bpy.path.abspath(custom_export_path) + export_path = bpy.path.abspath(str(custom_export_path)) level_name = custom_level_name else: - export_path = bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + export_path = str(bpy.context.scene.fast64.sm64.abs_decomp_path) if level_enum == "Custom": level_name = custom_level_name else: @@ -500,6 +549,7 @@ def saveDataToFile(filepath, data): def applyBasicTweaks(baseDir): + directory_path_checks(baseDir, "Empty directory path.") if bpy.context.scene.fast64.sm64.force_extended_ram: enableExtendedRAM(baseDir) @@ -528,11 +578,6 @@ def enableExtendedRAM(baseDir): segmentFile.close() -def writeMaterialHeaders(exportDir, matCInclude, matHInclude): - writeIfNotFound(os.path.join(exportDir, "src/game/materials.c"), "\n" + matCInclude, "") - writeIfNotFound(os.path.join(exportDir, "src/game/materials.h"), "\n" + matHInclude, "#endif") - - def writeMaterialFiles( exportDir, assetDir, headerInclude, matHInclude, headerDynamic, dynamic_data, geoString, customExport ): @@ -657,11 +702,9 @@ def highlightWeightErrors(obj, elements, elementType): return # Doesn't work currently if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) - bpy.context.view_layer.objects.active = obj + selectSingleObject(obj) bpy.ops.object.mode_set(mode="EDIT") - bpy.ops.mesh.select_all(action="DESELECT") + deselectAllObjects() bpy.ops.mesh.select_mode(type=elementType) bpy.ops.object.mode_set(mode="OBJECT") print(elements) @@ -687,14 +730,27 @@ def checkIdentityRotation(obj, rotation, allowYaw): ) -def setOrigin(target, obj): - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - bpy.ops.object.transform_apply() - bpy.context.scene.cursor.location = target.location - bpy.ops.object.origin_set(type="ORIGIN_CURSOR") - bpy.ops.object.select_all(action="DESELECT") +def setOrigin(obj: bpy.types.Object, target_loc: mathutils.Vector): + """ + Sets the object's origin to a new world-space location without moving the + object's mesh in the world. + + HACK: Historically this applies all transforms to the mesh data, this is kept to prevent breaking things + """ + assert obj.type == "MESH", "Object is not a mesh" + mesh: bpy.types.Mesh = obj.data + + original_mat = obj.matrix_world.copy() + mesh.transform(original_mat) + + target_mat = original_mat.copy() + target_mat.translation = target_loc + mesh.transform(target_mat.inverted()) + obj.matrix_world = target_mat + + delta = original_mat.translation - target_mat.translation + for child in obj.children_recursive: + child.location += delta def checkIfPathExists(filePath): @@ -709,11 +765,17 @@ def makeWriteInfoBox(layout): def writeBoxExportType(writeBox, headerType, name, levelName, levelOption): + if not name: + writeBox.label(text="Empty actor name", icon="ERROR") + return if headerType == "Actor": writeBox.label(text="actors/" + toAlnum(name)) elif headerType == "Level": if levelOption != "Custom": levelName = levelOption + if not name: + writeBox.label(text="Empty level name", icon="ERROR") + return writeBox.label(text="levels/" + toAlnum(levelName) + "/" + toAlnum(name)) @@ -726,11 +788,13 @@ def getExportDir(customExport, dirPath, headerType, levelName, texDir, dirName): elif headerType == "Level": dirPath = os.path.join(dirPath, "levels/" + levelName) texDir = "levels/" + levelName + elif not texDir: + texDir = (Path(dirPath).name / Path(dirName)).as_posix() return dirPath, texDir -def overwriteData(headerRegex, name, value, filePath, writeNewBeforeString, isFunction): +def overwriteData(headerRegex, name, value, filePath, writeNewBeforeString, isFunction, post_regex=""): if os.path.exists(filePath): dataFile = open(filePath, "r") data = dataFile.read() @@ -739,7 +803,8 @@ def overwriteData(headerRegex, name, value, filePath, writeNewBeforeString, isFu matchResult = re.search( headerRegex + re.escape(name) - + ("\s*\((((?!\)).)*)\)\s*\{(((?!\}).)*)\}" if isFunction else "\[\]\s*=\s*\{(((?!;).)*);"), + + ("\s*\((((?!\)).)*)\)\s*\{(((?!\}).)*)\}" if isFunction else "\[\]\s*=\s*\{(((?!;).)*);") + + post_regex, data, re.DOTALL, ) @@ -760,40 +825,6 @@ def overwriteData(headerRegex, name, value, filePath, writeNewBeforeString, isFu raise PluginError(filePath + " does not exist.") -def writeIfNotFound(filePath, stringValue, footer): - if os.path.exists(filePath): - fileData = open(filePath, "r") - fileData.seek(0) - stringData = fileData.read() - fileData.close() - if stringValue not in stringData: - if len(footer) > 0: - footerIndex = stringData.rfind(footer) - if footerIndex == -1: - raise PluginError("Footer " + footer + " does not exist.") - stringData = stringData[:footerIndex] + stringValue + "\n" + stringData[footerIndex:] - else: - stringData += stringValue - fileData = open(filePath, "w", newline="\n") - fileData.write(stringData) - fileData.close() - else: - raise PluginError(filePath + " does not exist.") - - -def deleteIfFound(filePath, stringValue): - if os.path.exists(filePath): - fileData = open(filePath, "r") - fileData.seek(0) - stringData = fileData.read() - fileData.close() - if stringValue in stringData: - stringData = stringData.replace(stringValue, "") - fileData = open(filePath, "w", newline="\n") - fileData.write(stringData) - fileData.close() - - def yield_children(obj: bpy.types.Object): yield obj if obj.children: @@ -808,6 +839,8 @@ def store_original_mtx(): # scales will be applied to the transform for each object loc, rot, _scale = obj.matrix_local.decompose() obj["original_mtx"] = Matrix.LocRotScale(loc, rot, None) + loc, rot, scale = obj.matrix_world.decompose() + obj["original_mtx_world"] = Matrix.LocRotScale(loc, rot, scale) def rotate_bounds(bounds, mtx: mathutils.Matrix): @@ -829,6 +862,13 @@ def scale_mtx_from_vector(scale: mathutils.Vector): return mathutils.Matrix.Diagonal(scale[0:3]).to_4x4() +def attemptModifierApply(modifier): + try: + bpy.ops.object.modifier_apply(modifier=modifier.name) + except Exception as e: + print("Skipping modifier " + str(modifier.name)) + + def copy_object_and_apply(obj: bpy.types.Object, apply_scale=False, apply_modifiers=False): if apply_scale or apply_modifiers: # it's a unique mesh, use object name @@ -917,25 +957,21 @@ def get_obj_temp_mesh(obj): def apply_objects_modifiers_and_transformations(allObjs: Iterable[bpy.types.Object]): # first apply modifiers so that any objects that affect each other are taken into consideration for selectedObj in allObjs: - bpy.ops.object.select_all(action="DESELECT") - selectedObj.select_set(True) - bpy.context.view_layer.objects.active = selectedObj + selectSingleObject(selectedObj) for modifier in selectedObj.modifiers: attemptModifierApply(modifier) # apply transformations now that world space changes are applied for selectedObj in allObjs: - bpy.ops.object.select_all(action="DESELECT") - selectedObj.select_set(True) - bpy.context.view_layer.objects.active = selectedObj + selectSingleObject(selectedObj) bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) def duplicateHierarchy(obj, ignoreAttr, includeEmpties, areaIndex): # Duplicate objects to apply scale / modifiers / linked data - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() selectMeshChildrenOnly(obj, None, includeEmpties, areaIndex) obj.select_set(True) bpy.context.view_layer.objects.active = obj @@ -951,9 +987,7 @@ def duplicateHierarchy(obj, ignoreAttr, includeEmpties, areaIndex): for selectedObj in allObjs: if ignoreAttr is not None and getattr(selectedObj, ignoreAttr): for child in selectedObj.children: - bpy.ops.object.select_all(action="DESELECT") - child.select_set(True) - bpy.context.view_layer.objects.active = child + selectSingleObject(child) bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") selectedObj.parent.select_set(True) bpy.context.view_layer.objects.active = selectedObj.parent @@ -967,35 +1001,35 @@ def duplicateHierarchy(obj, ignoreAttr, includeEmpties, areaIndex): raise Exception(str(e)) -enumSM64PreInlineGeoLayoutObjects = {"Geo ASM", "Geo Branch", "Geo Displaylist", "Custom Geo Command"} +enumSM64PreInlineGeoLayoutObjects = {"Geo ASM", "Geo Branch", "Geo Displaylist"} -def checkIsSM64PreInlineGeoLayout(sm64_obj_type): - return sm64_obj_type in enumSM64PreInlineGeoLayoutObjects +def checkIsSM64PreInlineGeoLayout(obj): + return obj.sm64_obj_type in enumSM64PreInlineGeoLayoutObjects enumSM64InlineGeoLayoutObjects = { - "Geo ASM", - "Geo Branch", "Geo Translate/Rotate", "Geo Translate Node", "Geo Rotation Node", "Geo Billboard", "Geo Scale", - "Geo Displaylist", - "Custom Geo Command", } -def checkIsSM64InlineGeoLayout(sm64_obj_type): - return sm64_obj_type in enumSM64InlineGeoLayoutObjects +def checkIsSM64InlineGeoLayout(obj): + return ( + obj.sm64_obj_type in enumSM64InlineGeoLayoutObjects + or checkIsSM64PreInlineGeoLayout(obj) + or (obj.sm64_obj_type == "Custom" and obj.fast64.sm64.custom.cmd_type == "Geo") + ) enumSM64EmptyWithGeolayout = {"None", "Level Root", "Area Root", "Switch"} -def checkSM64EmptyUsesGeoLayout(sm64_obj_type): - return sm64_obj_type in enumSM64EmptyWithGeolayout or checkIsSM64InlineGeoLayout(sm64_obj_type) +def checkSM64EmptyUsesGeoLayout(obj): + return obj.sm64_obj_type in enumSM64EmptyWithGeolayout or checkIsSM64InlineGeoLayout(obj) def selectMeshChildrenOnly(obj, ignoreAttr, includeEmpties, areaIndex): @@ -1004,7 +1038,7 @@ def selectMeshChildrenOnly(obj, ignoreAttr, includeEmpties, areaIndex): return ignoreObj = ignoreAttr is not None and getattr(obj, ignoreAttr) isMesh = obj.type == "MESH" - isEmpty = obj.type == "EMPTY" and includeEmpties and checkSM64EmptyUsesGeoLayout(obj.sm64_obj_type) + isEmpty = obj.type == "EMPTY" and includeEmpties and checkSM64EmptyUsesGeoLayout(obj) if (isMesh or isEmpty) and not ignoreObj: obj.select_set(True) obj.original_name = obj.name @@ -1038,6 +1072,8 @@ def cleanupTempMeshes(): del obj["instanced_mesh_name"] if obj.get("original_mtx"): del obj["original_mtx"] + if obj.get("original_mtx_world"): + del obj["original_mtx_world"] for data in remove_data: data_type = type(data) @@ -1051,7 +1087,7 @@ def combineObjects(obj, includeChildren, ignoreAttr, areaIndex): obj.original_name = obj.name # Duplicate objects to apply scale / modifiers / linked data - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() if includeChildren: selectMeshChildrenOnly(obj, ignoreAttr, False, areaIndex) else: @@ -1067,7 +1103,7 @@ def combineObjects(obj, includeChildren, ignoreAttr, areaIndex): apply_objects_modifiers_and_transformations(allObjs) - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() # Joining causes orphan data, so we remove it manually. meshList = [] @@ -1080,11 +1116,9 @@ def combineObjects(obj, includeChildren, ignoreAttr, areaIndex): joinedObj.select_set(True) meshList.remove(joinedObj.data) bpy.ops.object.join() - setOrigin(obj, joinedObj) + setOrigin(joinedObj, obj.location) - bpy.ops.object.select_all(action="DESELECT") - bpy.context.view_layer.objects.active = joinedObj - joinedObj.select_set(True) + selectSingleObject(joinedObj) # Need to clear parent transform in order to correctly apply transform. bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") @@ -1144,6 +1178,17 @@ def writeInsertableFile(filepath, dataType, address_ptrs, startPtr, data): openfile.close() +def quantize_color(color: mathutils.Color, bit_counts: tuple[int]): + """Quantize a color to the specified bit counts.""" + assert len(color) == len(bit_counts), "Number of color channels does not match number of bit counts" + result = 0 + pos = 0 + for c, bit_count in zip(reversed(color), reversed(bit_counts)): + result |= round(c * (2**bit_count - 1)) << pos + pos += bit_count + return result + + def colorTo16bitRGBA(color): r = int(round(color[0] * 31)) g = int(round(color[1] * 31)) @@ -1161,20 +1206,32 @@ def getDirectionGivenAppVersion(): return 1 -def applyRotation(objList, angle, axis): - bpy.context.scene.tool_settings.use_transform_data_origin = False - bpy.context.scene.tool_settings.use_transform_pivot_point_align = False - bpy.context.scene.tool_settings.use_transform_skip_children = False +def applyRotation(objs: list[Object], angle: float, axis: str): + rot_mat = Matrix.Rotation(angle, 4, axis).inverted() - bpy.ops.object.select_all(action="DESELECT") - for obj in objList: - obj.select_set(True) - bpy.context.view_layer.objects.active = objList[0] + for obj in objs: + # rotate object + obj.matrix_world = rot_mat @ obj.matrix_world + bpy.context.view_layer.update() - direction = getDirectionGivenAppVersion() + original_basis = obj.matrix_basis.copy() + local_loc = original_basis.translation.copy() - bpy.ops.transform.rotate(value=direction * angle, orient_axis=axis, orient_type="GLOBAL") - bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) + bake_matrix = original_basis.copy() + # don´t apply translation to the mesh + bake_matrix.translation = (0, 0, 0) + obj.matrix_basis = Matrix.Translation(local_loc) + + # apply transformations + if obj.data is not None: + if hasattr(obj.data, "transform"): + obj.data.transform(bake_matrix) + if hasattr(obj.data, "update"): + obj.data.update() + + for child in obj.children: # apply the same matrix we applied to the mesh to the children's transforms + child.matrix_local = bake_matrix @ child.matrix_local + bpy.context.view_layer.update() def doRotation(angle, axis): @@ -1309,7 +1366,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)): @@ -1364,8 +1421,14 @@ def exportColor(lightColor): return [scaleToU8(value) for value in gammaCorrect(lightColor)] -def get_clean_color(srgb: list, include_alpha=False, round_color=True) -> list: - return [round(channel, 4) if round_color else channel for channel in list(srgb[: 4 if include_alpha else 3])] +def get_clean_color(color: list, include_alpha=False, round_color=True, srgb_to_linear=False) -> list: + color = list(color) + if srgb_to_linear: + color = gammaCorrect(color[:3]) + color[3:] + color = color[: 4 if include_alpha else 3] + if include_alpha and len(color) < 4: + color = color + [1.0] + return tuple(round(channel, 4) if round_color else channel for channel in color) def printBlenderMessage(msgSet, message, blenderOp): @@ -1380,15 +1443,15 @@ def bytesToInt(value): def bytesToHex(value, byteSize=4): - return format(bytesToInt(value), "#0" + str(byteSize * 2 + 2) + "x") + return format(bytesToInt(value), f"#0{(byteSize * 2 + 2)}x") def bytesToHexClean(value, byteSize=4): - return format(bytesToInt(value), "0" + str(byteSize * 2) + "x") + return format(bytesToInt(value), f"#0{(byteSize * 2)}x") -def intToHex(value, byteSize=4): - return format(value, "#0" + str(byteSize * 2 + 2) + "x") +def intToHex(value, byte_size=4, signed=True): + return format(value if signed else cast_integer(value, byte_size * 8, False), f"#0{(byte_size * 2 + 2)}x") def intToBytes(value, byteSize): @@ -1565,18 +1628,26 @@ def normToSigned8Vector(normal): def unpackNormalS8(packedNormal: int) -> Tuple[int, int, int]: assert isinstance(packedNormal, int) and packedNormal >= 0 and packedNormal <= 0xFFFF - xo, yo = packedNormal >> 8, packedNormal & 0xFF - # This is following the instructions in F3DEX3 - x, y = xo & 0x7F, yo & 0x7F - z = x + y - zNeg = bool(z & 0x80) - x2, y2 = x ^ 0x7F, y ^ 0x7F # this is actually producing 7F - x, 7F - y - z = z ^ 0x7F # 7F - x - y; using xor saves an instruction and a register on the RSP - if zNeg: - x, y = x2, y2 - x, y = -x if xo & 0x80 else x, -y if yo & 0x80 else y - z = z - 0x100 if z & 0x80 else z - assert abs(x) + abs(y) + abs(z) == 127 + if bpy.context.scene.packed_normals_algorithm == "565": + x = packedNormal & 0xF800 + y = (packedNormal & 0x07E0) << 5 + z = (packedNormal & 0x001F) << 11 + x, y, z = tuple(map(lambda n: (n - 0x10000 if n & 0x8000 else n), (x, y, z))) + elif bpy.context.scene.packed_normals_algorithm == "Octahedral": + xo, yo = packedNormal >> 8, packedNormal & 0xFF + # This is following the instructions in F3DEX3 + x, y = xo & 0x7F, yo & 0x7F + z = x + y + zNeg = bool(z & 0x80) + x2, y2 = x ^ 0x7F, y ^ 0x7F # this is actually producing 7F - x, 7F - y + z = z ^ 0x7F # 7F - x - y; using xor saves an instruction and a register on the RSP + if zNeg: + x, y = x2, y2 + x, y = -x if xo & 0x80 else x, -y if yo & 0x80 else y + z = z - 0x100 if z & 0x80 else z + assert abs(x) + abs(y) + abs(z) == 127 + else: + raise PluginError("Invalid packed normals algorithm") return x, y, z @@ -1586,24 +1657,41 @@ def unpackNormal(packedNormal: int) -> Vector: def packNormal(normal: Vector) -> int: - # Convert standard normal to constant-L1 normal - assert len(normal) == 3 - l1norm = abs(normal[0]) + abs(normal[1]) + abs(normal[2]) - xo, yo, zo = tuple([int(round(a * 127.0 / l1norm)) for a in normal]) - if abs(xo) + abs(yo) > 127: - yo = int(math.copysign(127 - abs(xo), yo)) - zo = int(math.copysign(127 - abs(xo) - abs(yo), zo)) - assert abs(xo) + abs(yo) + abs(zo) == 127 - # Pack normals - xsign, ysign = xo & 0x80, yo & 0x80 - x, y = abs(xo), abs(yo) - if zo < 0: - x, y = 0x7F - x, 0x7F - y - x, y = x | xsign, y | ysign - packedNormal = x << 8 | y - # The only error is in the float to int rounding above. The packing and unpacking - # will precisely restore the original int values. - assert (xo, yo, zo) == unpackNormalS8(packedNormal) + if bpy.context.scene.packed_normals_algorithm == "565": + + def convertComponent(v: float, range: int): + v = int(round(v * float(range))) + v = min(max(v, -range), range - 1) + v = v if v >= 0 else v + 2 * range + return v + + x = convertComponent(normal[0], 16) << 11 + y = convertComponent(normal[1], 32) << 5 + z = convertComponent(normal[2], 16) + assert (x & y) == 0 and (y & z) == 0 and (x & z) == 0 + packedNormal = x | y | z + assert packedNormal >= 0 and packedNormal <= 0xFFFF + elif bpy.context.scene.packed_normals_algorithm == "Octahedral": + # Convert standard normal to constant-L1 normal + assert len(normal) == 3 + l1norm = abs(normal[0]) + abs(normal[1]) + abs(normal[2]) + xo, yo, zo = tuple([int(round(a * 127.0 / l1norm)) for a in normal]) + if abs(xo) + abs(yo) > 127: + yo = int(math.copysign(127 - abs(xo), yo)) + zo = int(math.copysign(127 - abs(xo) - abs(yo), zo)) + assert abs(xo) + abs(yo) + abs(zo) == 127 + # Pack normals + xsign, ysign = xo & 0x80, yo & 0x80 + x, y = abs(xo), abs(yo) + if zo < 0: + x, y = 0x7F - x, 0x7F - y + x, y = x | xsign, y | ysign + packedNormal = x << 8 | y + # The only error is in the float to int rounding above. The packing and unpacking + # will precisely restore the original int values. + assert (xo, yo, zo) == unpackNormalS8(packedNormal) + else: + raise PluginError("Invalid packed normals algorithm") return packedNormal @@ -1623,6 +1711,10 @@ def bitMask(data, offset, amount): return (~(-1 << amount) << offset & data) >> offset +def is_bit_active(x: int, index: int): + return ((x >> index) & 1) == 1 + + def read16bitRGBA(data): r = bitMask(data, 11, 5) / ((2**5) - 1) g = bitMask(data, 6, 5) / ((2**5) - 1) @@ -1677,7 +1769,9 @@ def lightDataToObj(lightData): for obj in bpy.context.scene.objects: if obj.data == lightData: return obj - raise PluginError("A material is referencing a light that is no longer in the scene (i.e. has been deleted).") + raise PluginError( + f'Referencing a light ("{lightData.name}") that is no longer in the scene (i.e. has been deleted).' + ) def ootGetSceneOrRoomHeader(parent, idx, isRoom): @@ -1714,14 +1808,17 @@ def ootGetBaseOrCustomLight(prop, idx, toExport: bool, errIfMissing: bool): col = getattr(prop, "diffuse" + str(idx)) dir = (mathutils.Vector((1.0, -1.0, 1.0)) * (1.0 if idx == 0 else -1.0)).normalized() if getattr(prop, "useCustomDiffuse" + str(idx)): - light = getattr(prop, "diffuse" + str(idx) + "Custom") - if light is None: - if errIfMissing: - raise PluginError("Error: Diffuse " + str(idx) + " light object not set in a scene lighting property.") - else: - col = tuple(c for c in light.color) + (1.0,) - lightObj = lightDataToObj(light) - dir = getObjDirectionVec(lightObj, toExport) + try: + light = getattr(prop, "diffuse" + str(idx) + "Custom") + if light is None: + if errIfMissing: + raise PluginError("Light object not set in a scene lighting property.") + else: + col = tuple(c for c in light.color) + (1.0,) + lightObj = lightDataToObj(light) + dir = getObjDirectionVec(lightObj, toExport) + except Exception as exc: + raise PluginError(f"In custom diffuse {idx}: {exc}") from exc col = mathutils.Vector(tuple(c for c in col)) if toExport: col, dir = exportColor(col), normToSigned8Vector(dir) @@ -1734,9 +1831,11 @@ def getTextureSuffixFromFormat(texFmt): return texFmt.lower() -def removeComments(text: str): - # https://stackoverflow.com/a/241506 +# https://stackoverflow.com/a/241506 +COMMENT_PATTERN = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE) + +def removeComments(text: str): def replacer(match: re.Match[str]): s = match.group(0) if s.startswith("/"): @@ -1744,9 +1843,7 @@ def replacer(match: re.Match[str]): else: return s - pattern = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE) - - return re.sub(pattern, replacer, text) + return re.sub(COMMENT_PATTERN, replacer, text) binOps = { @@ -1761,6 +1858,10 @@ def replacer(match: re.Match[str]): ast.BitOr: operator.or_, ast.BitAnd: operator.and_, ast.BitXor: operator.xor, + ast.Pow: operator.pow, + ast.FloorDiv: operator.floordiv, + ast.USub: operator.neg, + ast.UAdd: lambda a: a, } @@ -1798,12 +1899,40 @@ def json_to_prop_group(prop_group, data: dict, blacklist: list[str] = None, whit if prop in blacklist or (whitelist and prop not in whitelist): continue default = getattr(prop_group, prop) - if hasattr(default, "from_dict"): - default.from_dict(data.get(prop, None)) + if isinstance(default, list) or type(default).__name__ == "bpy_prop_collection_idprop": + if prop in data: + default.clear() + for element in data.get(prop, default): + default.add() + if hasattr(default[-1], "from_dict"): + default[-1].from_dict(element) + else: + json_to_prop_group(default[-1], element, blacklist, whitelist) + elif hasattr(default, "from_dict"): + default.from_dict(data.get(prop, {})) else: setattr(prop_group, prop, data.get(prop, default)) +def fix_invalid_props(prop_group): + """Fixes simple invalid values like deprecated enums and values that are out of range.""" + for prop_attr in iter_prop(prop_group): + if prop_attr in {"rna_type", "name"}: + continue + prop_value = getattr(prop_group, prop_attr) + prop_def: bpy.types.Property = prop_group.bl_rna.properties[prop_attr] + if prop_def.type == "COLLECTION": + for element in prop_value: + fix_invalid_props(element) + elif prop_def.type == "POINTER" and isinstance(prop_value, bpy.types.PropertyGroup): + fix_invalid_props(prop_value) + elif prop_def.type == "ENUM": + if prop_value not in [enum.identifier for enum in prop_def.enum_items]: + prop_group[prop_attr] = prop_def.default + elif prop_value is not None: # Sets this again, ensures ints, floats and colors are within their range + prop_group[prop_attr] = prop_value + + T = TypeVar("T") SetOrVal = T | list[T] @@ -1914,3 +2043,144 @@ def create_or_get_world(scene: Scene) -> World: WORLD_WARNING_COUNT = 0 print(f'No world in this file, creating world named "Fast64".') return bpy.data.worlds.new("Fast64") + + +def set_if_different(owner: object, prop: str, value): + if getattr(owner, prop) != value: + setattr(owner, prop, value) + + +def set_prop_if_in_data(owner: object, prop_name: str, data: dict, data_name: str): + if data_name in data: + set_if_different(owner, prop_name, data[data_name]) + + +def wrap_func_with_error_message(error_message: Callable): + """Decorator for big, reused functions that need generic info in errors, such as material exports.""" + + def decorator(func): + def wrapper(*args, **kwargs): + # Get the argument names and values (positional and keyword) + sig = inspect.signature(func) + bound_args = sig.bind(*args, **kwargs) + bound_args.apply_defaults() + try: + return func(*args, **kwargs) + except Exception as exc: + raise PluginError(f"{error_message(bound_args.arguments)} {exc}") from exc + + return wrapper + + return decorator + + +def as_posix(path: Path) -> str: + return path.as_posix().replace("\\", "/") # Windows path sometimes still has backslashes? + + +def oot_get_assets_path(base_path: str, check_exists: bool = True, use_decomp_path: bool = True): + # get the extracted path + extracted = bpy.context.scene.fast64.oot.get_extracted_path() + decomp_path = bpy.context.scene.ootDecompPath if use_decomp_path else "." + + # get the file's path + file_path = Path(f"{decomp_path}/{base_path}").resolve() + + # check if the path exists + if not file_path.exists(): + file_path = Path(f"{bpy.context.scene.ootDecompPath}/{extracted}/{base_path}").resolve() + + # if it doesn't check if the extracted path exists (we want to skip that for PNG files) + if check_exists and not file_path.exists(): + raise PluginError(f"ERROR: that file don't exist ({repr(base_path)})") + + return file_path + + +def get_include_data(include: str, strip: bool = False): + """ + Returns the file data pointed by an include's path (useful to parse *.inc.c files) + + Parameters: + - `include`: the line where the include directive is located + - `strip`: set to True to return the data without any newlines or whitespaces + """ + + # remove the unwanted parts + include = include.replace("\n", "").removeprefix("#include ").replace('"', "") + + if bpy.context.scene.gameEditorMode in {"OOT", "MM"}: + file_path = oot_get_assets_path(include) + else: + raise PluginError(f"ERROR: game not supported ({bpy.context.scene.gameEditorMode})") + + data = removeComments(file_path.read_text()) + + if strip: + return data.replace("\n", "").replace(" ", "") + + # return the data as a string + return data + + +def get_new_object( + name: str, + data: Optional[Any], + do_select: bool, + location=[0.0, 0.0, 0.0], + rotation_euler=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0], + parent: Optional[bpy.types.Object] = None, +) -> bpy.types.Object: + new_obj = bpy.data.objects.new(name=name, object_data=data) + bpy.context.view_layer.active_layer_collection.collection.objects.link(new_obj) + + if do_select: + new_obj.select_set(True) + bpy.context.view_layer.objects.active = new_obj + + new_obj.parent = parent + new_obj.location = location + new_obj.rotation_euler = rotation_euler + new_obj.scale = scale + return new_obj + + +def get_new_empty_object( + name: str, + do_select: bool = False, + location=[0.0, 0.0, 0.0], + rotation_euler=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0], + parent: Optional[bpy.types.Object] = None, +): + """Creates and returns a new empty object""" + return get_new_object(name, None, do_select, location, rotation_euler, scale, parent) + + +class ExportUtils: + def __init__(self): + # get areas that are currently in local view mode + self.areas = [] + for area in bpy.context.screen.areas: + if area.type == "VIEW_3D" and area.spaces.active.local_view is not None: + self.areas.append(area) + + def __enter__(self): + # disable local views if enabled + for area in self.areas: + with bpy.context.temp_override(area=area): + bpy.ops.view3d.localview() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + # restore local views + for area in self.areas: + with bpy.context.temp_override(area=area): + bpy.ops.view3d.localview() + + if exc_value: + print("\nExecution type:", exc_type) + print("\nExecution value:", exc_value) + print("\nTraceback:", traceback) diff --git a/fast64_internal/utility_anim.py b/fast64_internal/utility_anim.py index 982706a16..26837e9b5 100644 --- a/fast64_internal/utility_anim.py +++ b/fast64_internal/utility_anim.py @@ -1,5 +1,10 @@ import bpy, math, mathutils +from bpy.types import Object, Action, AnimData from bpy.utils import register_class, unregister_class +from bpy.props import StringProperty + +from .operators import OperatorBase +from .utility import attemptModifierApply, raisePluginError, PluginError from typing import TYPE_CHECKING @@ -23,8 +28,6 @@ class ArmatureApplyWithMeshOperator(bpy.types.Operator): # Called on demand (i.e. button press, menu item) # Can also be called from operator search menu (Spacebar) def execute(self, context): - from .utility import PluginError, raisePluginError - try: if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") @@ -46,6 +49,51 @@ def execute(self, context): return {"FINISHED"} # must return a set +class CreateAnimData(OperatorBase): + bl_idname = "scene.fast64_create_anim_data" + bl_label = "Create Animation Data" + bl_description = "Create animation data" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ANIM" + + def execute_operator(self, context): + obj = context.object + if obj is None: + raise PluginError("No selected object") + if obj.animation_data is None: + obj.animation_data_create() + + +class AddBasicAction(OperatorBase): + bl_idname = "scene.fast64_add_basic_action" + bl_label = "Add Basic Action" + bl_description = "Create animation data and add basic action" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "ACTION" + + def execute_operator(self, context): + if context.object is None: + raise PluginError("No selected object") + create_basic_action(context.object) + + +class StashAction(OperatorBase): + bl_idname = "scene.fast64_stash_action" + bl_label = "Stash Action" + bl_description = "Stash an action in an object's nla tracks if not already stashed" + context_mode = "OBJECT" + icon = "NLA" + + action: StringProperty() + + def execute_operator(self, context): + if context.object is None: + raise PluginError("No selected object") + stashActionInArmature(context.object, get_action(self.action)) + + # This code only handles root bone with no parent, which is the only bone that translates. def getTranslationRelativeToRest(bone: bpy.types.Bone, inputVector: mathutils.Vector) -> mathutils.Vector: zUpToYUp = mathutils.Quaternion((1, 0, 0), math.radians(-90.0)).to_matrix().to_4x4() @@ -63,14 +111,9 @@ def getRotationRelativeToRest(bone: bpy.types.Bone, inputEuler: mathutils.Euler) return (restRotation.inverted() @ inputEuler.to_matrix().to_4x4()).to_euler("XYZ", inputEuler) -def attemptModifierApply(modifier): - try: - bpy.ops.object.modifier_apply(modifier=modifier.name) - except Exception as e: - print("Skipping modifier " + str(modifier.name)) - - def armatureApplyWithMesh(armatureObj: bpy.types.Object, context: bpy.types.Context): + from .utility import selectSingleObject + for child in armatureObj.children: if child.type != "MESH": continue @@ -81,14 +124,12 @@ def armatureApplyWithMesh(armatureObj: bpy.types.Object, context: bpy.types.Cont if armatureModifier is None: continue - bpy.ops.object.select_all(action="DESELECT") - context.view_layer.objects.active = child + selectSingleObject(child) bpy.ops.object.modifier_copy(modifier=armatureModifier.name) print(len(child.modifiers)) attemptModifierApply(armatureModifier) - bpy.ops.object.select_all(action="DESELECT") - context.view_layer.objects.active = armatureObj + selectSingleObject(armatureObj) bpy.ops.object.mode_set(mode="POSE") bpy.ops.pose.armature_apply() if context.mode != "OBJECT": @@ -179,28 +220,61 @@ def getIntersectionInterval(): return range_get_by_choice[anim_range_choice]() -def stashActionInArmature(armatureObj: bpy.types.Object, action: bpy.types.Action): +def is_action_stashed(obj: Object, action: Action): + animation_data: AnimData | None = obj.animation_data + if animation_data is None: + return False + for track in animation_data.nla_tracks: + for strip in track.strips: + if strip.action is None: + continue + if strip.action.name == action.name: + return True + return False + + +def stashActionInArmature(obj: Object, action: Action): """ Stashes an animation (action) into an armature´s nla tracks. This prevents animations from being deleted by blender or purged by the user on accident. """ - for track in armatureObj.animation_data.nla_tracks: - for strip in track.strips: - if strip.action is None: - continue + if is_action_stashed(obj, action): + return - if strip.action.name == action.name: - return + print(f'Stashing "{action.name}" in the object "{obj.name}".') + if obj.animation_data is None: + obj.animation_data_create() + track = obj.animation_data.nla_tracks.new() + track.name = action.name + track.strips.new(action.name, int(action.frame_range[0]), action) - print(f'Stashing "{action.name}" in the object "{armatureObj.name}".') - track = armatureObj.animation_data.nla_tracks.new() - track.strips.new(action.name, int(action.frame_range[0]), action) +def create_basic_action(obj: Object, name=""): + if obj.animation_data is None: + obj.animation_data_create() + name = name or "Action" + action = bpy.data.actions.new(name) + stashActionInArmature(obj, action) + obj.animation_data.action = action + return action + + +def get_action(name: str): + if name == "": + raise ValueError("Empty action name.") + if not name in bpy.data.actions: + raise IndexError(f"Action ({name}) is not in this file´s action data.") + return bpy.data.actions[name] -classes = (ArmatureApplyWithMeshOperator,) +classes = ( + ArmatureApplyWithMeshOperator, + CreateAnimData, + AddBasicAction, + StashAction, +) def utility_anim_register(): diff --git a/fast64_internal/oot/README.md b/fast64_internal/z64/README.md similarity index 77% rename from fast64_internal/oot/README.md rename to fast64_internal/z64/README.md index abc3f3386..ee684aa2c 100644 --- a/fast64_internal/oot/README.md +++ b/fast64_internal/z64/README.md @@ -12,6 +12,7 @@ 9. [Custom Link Process](#custom-link-process) 10. [Custom Skeleton Mesh Process](#custom-skeleton-mesh-process) 11. [Cutscenes](#cutscenes) +12. [Animated Materials](#animated-materials) ### Getting Started 1. In the 3D view properties sidebar (default hotkey to show this is `n` in the viewport), go to the ``Fast64`` tab, then ``Fast64 Global Settings`` and set ``Game`` to ``OOT``. @@ -194,3 +195,76 @@ If the camera preview in Blender isn't following where you have the bones or if 2. If you moved / rotated / etc. one of the camera shots / armatures in object mode, this transformation will be ignored. You can fix this by selecting the shot / armature in object mode and clicking Object > Apply > All Transforms. That will convert the transform to actual changed positions for each bone. If the game crashes check the transitions if you use the transition command (check both the ones from the entrance table and your cutscene script), also it will crash if you try to use the map select without having a 5th entrance (or more depending on the number of cutscenes you have) in the group for your scene. + +### Animated Materials + +This is a feature you can use for Majora's Mask and OoT backports like HackerOoT (requires enabling `Enable MM Features` for non-HackerOoT OoT decomp projects). It allows you to do some animation on any material you want, on Majora's Mask it's used to animate some actor's textures, and it's used in scenes too, this is what makes the walls in Majora's Lair animated, for instance. + +**Important**: this requires the scene to use a specific draw config called `Material Animated` (or `Material Animated (manual step)` for special cases). + +**Getting Started** + +For non-scene export you'll need to either use the `Add Animated Material` button under the `Tools` tab, or manually adding an empty object and setting the object mode to `Animated Materials`. If doing the latter make sure the object is parented to the scene object, it can be parented to a room too, or anything else as long as the scene object is in the hierarchy, but it will be exported to the scene file. + +For scenes it's integrated as a tab in the scene header properties panel. + +**Creating the animated materials list** + +For non-scene export, click on `Add Item` to add a new animated material list. + +You can pick the segment number with the `Segment Number` field (make sure to use the same number on the material you want this to be used on), for convenience the exporter will add a macro to make it more readable. + +**Important**: when using texture references: +1. make sure to use a dedicated segment otherwise it will crash (for example, texture reference on segment 8 and having the checkbox for segment 8 enabled will crash, but if they are different it won't) +2. always use the segment address as the texture symbol (for example, `0x08000000` for segment 8) + +`Draw Handler Type` lets you choose what kind of animated material you want, it can be one of: +- `0` (`ANIM_MAT_TYPE_TEX_SCROLL`): Texture Scroll +- `1` (`ANIM_MAT_TYPE_TWO_TEX_SCROLL`): Two-textures Scroll +- `2` (`ANIM_MAT_TYPE_COLOR`): Color +- `3` (`ANIM_MAT_TYPE_COLOR_LERP`): Color LERP +- `4` (`ANIM_MAT_TYPE_COLOR_NON_LINEAR_INTERP`): Color Non-linear Interpolation +- `5` (`ANIM_MAT_TYPE_TEX_CYCLE`): Texture Cycle (like a GIF) +- `6` (`ANIM_MAT_TYPE_NONE`): nothing, only there for backward compatibility with MM +- `7` (`ANIM_MAT_TYPE_COLOR_CYCLE`): like `ANIM_MAT_TYPE_COLOR` except this takes a keyframe array to set duration values for each colors +- `8` (`ANIM_MAT_TYPE_TEX_TIMED_CYCLE`): displays a texture for a set amount of time then it goes to the next one until the end then it starts back at the beginning (requires texture reference) +- `9` (`ANIM_MAT_TYPE_TEXTURE`): displays a texture depending on the trigger event state, this can only take two textures (requires texture reference) +- `10` (`ANIM_MAT_TYPE_MULTITEXTURE`): displays two textures and allows you to transition from one to another and also make both transparent (optional: you can use texture reference), note: this may require the "Shaded Multitexture Lerp Transparent" preset (also you might need to uncheck "Environment Color", enabling "Cull Back" may help in some situations (example: reproducing the Kokiri Forest grass env alpha change)) +- `11` (`ANIM_MAT_TYPE_EVENT`): will hide the texture until the trigger event is completed +- `12` (`ANIM_MAT_TYPE_SURFACE_SWAP`): can change one or several surface types along with collision flags, if linking mesh objects it will change any surface type related to the triangles of the mesh +- `13` (`ANIM_MAT_TYPE_OSCILLATING_TWO_TEX`): exactly like `ANIM_MAT_TYPE_TWO_TEX_SCROLL` except the scroll is oscillating instead of going forever in the same direction + +Note: the enum names from above are currently only on HackerOoT. + +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 (types 0 to 5): +- `Step X`: step value on the X axis +- `Step Y`: step value on the Y axis +- `Texture Width`: the width of the texture +- `Texture Height`: the height of the texture + +All 3 color types will use the same elements (types 0 to 5): +- `Frame No.`: when to execute this entry (relative to the keyframe length), not available for `Draw Color` +- `Primitive LOD Frac`: unknown purpose, feel free to complete! +- `Primitive Color`: the primitive color to apply +- `Environment Color`: the environment color to apply, optional for `Draw Color` + +The texture cycle type (5) 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. + +**Trigger Events** + +This is only relevant if using HackerOoT. + +You can optionally connect an "event" to an animated material, this means you can set draw conditions. You can choose between 3 different types of events: +- Flag Events, those will be any flag (like switch flags, clear flags, inf flags, ...) +- Game Events, those will be anything related to the game, currently it's mostly focusing on the save data (inventory, equipment, quest items and health/rupee/magic/gold skulltula amounts) +- Time Events, those will be related to the current time of day, you can choose between having a specific time, or a time range, or only day/night + +Some of these events may require a "condition type", this is useful if you want the animated material to draw when you have specific amounts of something, it supports: +- equal (`a == b`) +- different (`a != b`) +- strictly inferior (`a < b`) +- strictly superior (`a > b`) +- inferior or equal (`a <= b`) +- superior or equal (`a >= b`) diff --git a/fast64_internal/oot/__init__.py b/fast64_internal/z64/__init__.py similarity index 60% rename from fast64_internal/oot/__init__.py rename to fast64_internal/z64/__init__.py index d853e36a4..b45fff3e2 100644 --- a/fast64_internal/oot/__init__.py +++ b/fast64_internal/z64/__init__.py @@ -1,13 +1,19 @@ import bpy + +from pathlib import Path from bpy.utils import register_class, unregister_class +from ..game_data import game_data +from ..utility import PluginError + from .scene.operators import scene_ops_register, scene_ops_unregister from .scene.properties import OOTBootupSceneOptions, scene_props_register, scene_props_unregister from .scene.panels import scene_panels_register, scene_panels_unregister from .props_panel_main import oot_obj_panel_register, oot_obj_panel_unregister, oot_obj_register, oot_obj_unregister from .skeleton.properties import OOTSkeletonImportSettings, OOTSkeletonExportSettings -from .oot_utility import oot_utility_register, oot_utility_unregister, setAllActorsVisibility +from .collection_utility import collections_register, collections_unregister +from .utility import setAllActorsVisibility from .file_settings import file_register, file_unregister from .collision.properties import OOTCollisionExportSettings @@ -51,6 +57,19 @@ from .spline.properties import spline_props_register, spline_props_unregister from .spline.panels import spline_panels_register, spline_panels_unregister +from .animated_mats.operators import animated_mats_ops_register, animated_mats_ops_unregister +from .animated_mats.panels import animated_mats_panels_register, animated_mats_panels_unregister +from .animated_mats.properties import ( + Z64_AnimatedMaterialExportSettings, + Z64_AnimatedMaterialImportSettings, + animated_mats_props_register, + animated_mats_props_unregister, +) + +from .hackeroot.operators import hackeroot_ops_register, hackeroot_ops_unregister +from .hackeroot.properties import HackerOoTSettings, hackeroot_props_register, hackeroot_props_unregister +from .hackeroot.panels import hackeroot_panels_register, hackeroot_panels_unregister + from .tools import ( oot_operator_panel_register, oot_operator_panel_unregister, @@ -59,29 +78,37 @@ ) -featureSetEnum = ( - ("Decomp", "Decomp", "Decomp"), - ("HackerOOT", "HackerOOT", "Hacker OOT"), - ("HM64", "HM64", "Harbour Masters"), +feature_set_enum = ( + ("default", "Default", "Default"), + ("hackeroot", "HackerOoT", "HackerOoT"), + ("hm64", "HM64", "Harbour Masters"), ) -def featureSetUpdate(self, context): - return - - oot_versions_items = [ - ("Custom", "Custom", "Custom"), - ("gc-jp", "gc-jp", "gc-jp"), - ("gc-jp-mq", "gc-jp-mq", "gc-jp-mq"), - ("gc-jp-ce", "gc-jp-ce", "gc-jp-ce"), - ("gc-us", "gc-us", "gc-us"), - ("gc-us-mq", "gc-us-mq", "gc-us-mq"), - ("gc-eu", "gc-eu", "gc-eu"), - ("gc-eu-mq", "gc-eu-mq", "gc-eu-mq"), - ("gc-eu-mq-dbg", "gc-eu-mq-dbg", "gc-eu-mq-dbg"), - ("hackeroot-mq", "HackerOoT", "hackeroot-mq"), # TODO: force this value if HackerOoT features are enabled? - ("legacy", "Legacy", "Older Decomp Version"), + ("Custom", "Custom", "Custom", 0), + ("ntsc-1.0", "ntsc-1.0", "ntsc-1.0", 11), + ("ntsc-1.1", "ntsc-1.1", "ntsc-1.1", 12), + ("pal-1.0", "pal-1.0", "pal-1.0", 13), + ("ntsc-1.2", "ntsc-1.2", "ntsc-1.2", 14), + ("pal-1.1", "pal-1.1", "pal-1.1", 15), + ("gc-jp", "gc-jp", "gc-jp", 1), + ("gc-jp-mq", "gc-jp-mq", "gc-jp-mq", 2), + ("gc-us", "gc-us", "gc-us", 4), + ("gc-us-mq", "gc-us-mq", "gc-us-mq", 5), + ("gc-eu-mq-dbg", "gc-eu-mq-dbg", "gc-eu-mq-dbg", 8), + ("gc-eu", "gc-eu", "gc-eu", 6), + ("gc-eu-mq", "gc-eu-mq", "gc-eu-mq", 7), + ("gc-jp-ce", "gc-jp-ce", "gc-jp-ce", 3), + ("ique-cn", "ique-cn", "ique-cn", 16), + ("hackeroot-mq", "HackerOoT (Legacy)", "hackeroot-mq", 9), + ("legacy", "Legacy", "Older Decomp Version", 10), +] + +mm_versions_items = [ + ("Custom", "Custom", "Custom", 0), + ("n64-us", "n64-us", "n64-us", 1), + ("legacy", "Legacy", "Older Decomp Version", 2), ] @@ -89,9 +116,7 @@ class OOT_Properties(bpy.types.PropertyGroup): """Global OOT Scene Properties found under scene.fast64.oot""" version: bpy.props.IntProperty(name="OOT_Properties Version", default=0) - featureSet: bpy.props.EnumProperty( - name="Feature Set", default="Decomp", items=featureSetEnum, update=featureSetUpdate - ) + feature_set: bpy.props.EnumProperty(name="Feature Set", default=0, items=feature_set_enum) headerTabAffectsVisibility: bpy.props.BoolProperty( default=False, name="Header Sets Actor Visibility", update=setAllActorsVisibility ) @@ -104,13 +129,38 @@ class OOT_Properties(bpy.types.PropertyGroup): animImportSettings: bpy.props.PointerProperty(type=OOTAnimImportSettingsProperty) collisionExportSettings: bpy.props.PointerProperty(type=OOTCollisionExportSettings) oot_version: bpy.props.EnumProperty(name="OoT Version", items=oot_versions_items, default="gc-eu-mq-dbg") + mm_version: bpy.props.EnumProperty(name="MM Version", items=mm_versions_items, default="n64-us") oot_version_custom: bpy.props.StringProperty(name="Custom Version") + mm_features: bpy.props.BoolProperty(name="Enable MM Features", default=False) + hackeroot_settings: bpy.props.PointerProperty(type=HackerOoTSettings) + anim_mats_export_settings: bpy.props.PointerProperty(type=Z64_AnimatedMaterialExportSettings) + anim_mats_import_settings: bpy.props.PointerProperty(type=Z64_AnimatedMaterialImportSettings) + export_cutscene_obj: bpy.props.PointerProperty( + type=bpy.types.Object, poll=lambda self, obj: obj.type == "EMPTY" and obj.ootEmptyType == "Cutscene" + ) def get_extracted_path(self): - if self.oot_version == "legacy": + version = self.oot_version if game_data.z64.is_oot() else self.mm_version + + if version == "legacy": return "." else: - return f"extracted/{self.oot_version if self.oot_version != 'Custom' else self.oot_version_custom}" + return f"extracted/{version if version != 'Custom' else self.oot_version_custom}" + + def is_include_present(self, include_file: str): + decomp_path = Path(bpy.context.scene.ootDecompPath).resolve() + + if not decomp_path.exists(): + raise PluginError(f"ERROR: invalid decomp path ('{decomp_path}').") + + include_file_path = decomp_path / "include" / include_file + return include_file_path.exists() + + def is_globalh_present(self): + return self.oot_version == "legacy" or self.is_include_present("global.h") + + def is_z64sceneh_present(self): + return self.is_include_present("z64scene.h") useDecompFeatures: bpy.props.BoolProperty( name="Use decomp for export", description="Use names and macros from decomp when exporting", default=True @@ -118,19 +168,31 @@ def get_extracted_path(self): exportMotionOnly: bpy.props.BoolProperty( name="Export CS Motion Data Only", - description=( - "Export everything or only the camera and actor motion data.\n" - + "This will insert the data into the cutscene." - ), + description="Export everything (unchecked) or only the camera and actor motion data (checked).", default=False, ) + use_new_actor_panel: bpy.props.BoolProperty( + name="Use newer actor panel", + description="Use the new actor panel which provides detailed informations to set actor parameters.", + default=True, + ) + + @staticmethod + def upgrade_changed_props(): + if "hackerFeaturesEnabled" in bpy.context.scene.fast64.oot: + bpy.context.scene.fast64.oot.feature_set = ( + "hackeroot" if bpy.context.scene.fast64.oot["hackerFeaturesEnabled"] else "default" + ) + del bpy.context.scene.fast64.oot["hackerFeaturesEnabled"] + oot_classes = (OOT_Properties,) def oot_panel_register(): oot_operator_panel_register() + hackeroot_panels_register() cutscene_panels_register() scene_panels_register() f3d_panels_register() @@ -139,10 +201,12 @@ def oot_panel_register(): spline_panels_register() anim_panels_register() skeleton_panels_register() + animated_mats_panels_register() def oot_panel_unregister(): oot_operator_panel_unregister() + hackeroot_panels_unregister() cutscene_panels_unregister() collision_panels_unregister() oot_obj_panel_unregister() @@ -151,21 +215,25 @@ def oot_panel_unregister(): f3d_panels_unregister() anim_panels_unregister() skeleton_panels_unregister() + animated_mats_panels_unregister() def oot_register(registerPanels): + hackeroot_props_register() + hackeroot_ops_register() oot_operator_register() - oot_utility_register() + collections_register() collision_ops_register() # register first, so panel goes above mat panel collision_props_register() cutscene_props_register() + animated_mats_ops_register() + animated_mats_props_register() scene_ops_register() scene_props_register() room_ops_register() room_props_register() actor_ops_register() actor_props_register() - oot_obj_register() spline_props_register() f3d_props_register() anim_ops_register() @@ -182,6 +250,8 @@ def oot_register(registerPanels): csMotion_preview_register() cutscene_preview_register() + oot_obj_register() + for cls in oot_classes: register_class(cls) @@ -190,30 +260,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() - oot_utility_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() @@ -221,5 +274,27 @@ def oot_unregister(unregisterPanels): csMotion_props_unregister() csMotion_ops_unregister() - if unregisterPanels: - oot_panel_unregister() + anim_props_unregister() + file_unregister() + f3d_ops_unregister() + cutscene_ops_unregister() + skeleton_props_unregister() + skeleton_ops_unregister() + anim_ops_unregister() + f3d_props_unregister() + spline_props_unregister() + actor_props_unregister() + actor_ops_unregister() + room_props_unregister() + room_ops_unregister() + scene_props_unregister() + scene_ops_unregister() + animated_mats_props_unregister() + animated_mats_ops_unregister() + cutscene_props_unregister() + collision_props_unregister() + collision_ops_unregister() + collections_unregister() + oot_operator_unregister() + hackeroot_ops_unregister() + hackeroot_props_unregister() diff --git a/fast64_internal/z64/actor/operators.py b/fast64_internal/z64/actor/operators.py new file mode 100644 index 000000000..93d5e8e27 --- /dev/null +++ b/fast64_internal/z64/actor/operators.py @@ -0,0 +1,94 @@ +import bpy +from bpy.types import Operator +from bpy.props import EnumProperty, StringProperty +from bpy.utils import register_class, unregister_class +from ...utility import PluginError +from ...game_data import game_data + + +class OOT_SearchChestContentEnumOperator(Operator): + bl_idname = "object.oot_search_chest_content_enum_operator" + bl_label = "Select Chest Content" + bl_property = "chest_content" + bl_options = {"REGISTER", "UNDO"} + + chest_content: EnumProperty(items=game_data.z64.actors.ootEnumChestContent, default="item_heart") + obj_name: StringProperty() + prop_name: StringProperty() + + def execute(self, context): + setattr(bpy.data.objects[self.obj_name].ootActorProperty, self.prop_name, self.chest_content) + context.region.tag_redraw() + self.report({"INFO"}, f"Selected: {self.chest_content}") + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + +class OOT_SearchNaviMsgIDEnumOperator(Operator): + bl_idname = "object.oot_search_navi_msg_id_enum_operator" + bl_label = "Select Message ID" + bl_property = "navi_msg_id" + bl_options = {"REGISTER", "UNDO"} + + navi_msg_id: EnumProperty(items=game_data.z64.actors.ootEnumNaviMessageData, default="msg_00") + obj_name: StringProperty() + prop_name: StringProperty() + + def execute(self, context): + setattr(bpy.data.objects[self.obj_name].ootActorProperty, self.prop_name, self.navi_msg_id) + context.region.tag_redraw() + self.report({"INFO"}, f"Selected: {self.navi_msg_id}") + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + +class OOT_SearchActorIDEnumOperator(Operator): + bl_idname = "object.oot_search_actor_id_enum_operator" + bl_label = "Select Actor ID" + bl_property = "actor_id" + bl_options = {"REGISTER", "UNDO"} + + actor_id: EnumProperty(items=lambda self, context: game_data.z64.actors.getItems(self.actor_user)) + actor_user: StringProperty(default="Actor") + obj_name: StringProperty() + + def execute(self, context): + obj = bpy.data.objects[self.obj_name] + + if self.actor_user == "Transition Actor": + obj.ootTransitionActorProperty.actor.actor_id = self.actor_id + elif self.actor_user == "Actor": + obj.ootActorProperty.actor_id = self.actor_id + else: + raise PluginError("Invalid actor user for search: " + str(self.actor_user)) + + context.region.tag_redraw() + self.report({"INFO"}, f"Selected: {self.actor_id}") + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + +classes = ( + OOT_SearchActorIDEnumOperator, + OOT_SearchChestContentEnumOperator, + OOT_SearchNaviMsgIDEnumOperator, +) + + +def actor_ops_register(): + for cls in classes: + register_class(cls) + + +def actor_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/actor/properties.py b/fast64_internal/z64/actor/properties.py new file mode 100644 index 000000000..967d3763b --- /dev/null +++ b/fast64_internal/z64/actor/properties.py @@ -0,0 +1,616 @@ +import bpy + +from bpy.types import Object, PropertyGroup, UILayout +from bpy.utils import register_class, unregister_class +from bpy.props import EnumProperty, StringProperty, IntProperty, BoolProperty, CollectionProperty, PointerProperty +from ...utility import PluginError, prop_split, label_split +from ...game_data import game_data +from ..constants import ootEnumCamTransition +from ..upgrade import upgradeActors +from ..scene.properties import OOTAlternateSceneHeaderProperty +from ..room.properties import OOTAlternateRoomHeaderProperty +from ..collection_utility import drawAddButton, drawCollectionOps +from .operators import ( + OOT_SearchActorIDEnumOperator, + OOT_SearchChestContentEnumOperator, + OOT_SearchNaviMsgIDEnumOperator, +) + +from ..utility import ( + getRoomObj, + getEnumName, + drawEnumWithCustom, + getEvalParams, + getEvalParamsInt, + getShiftFromMask, + getFormattedParams, +) + +ootEnumSceneSetupPreset = [ + ("Custom", "Custom", "Custom"), + ("All Scene Setups", "All Scene Setups", "All Scene Setups"), + ("All Non-Cutscene Scene Setups", "All Non-Cutscene Scene Setups", "All Non-Cutscene Scene Setups"), +] + + +def get_prop_name(actor_key: str, param_type: str, param_subtype: str, param_index: int): + flag_to_prop_suffix = {"Chest": "chestFlag", "Collectible": "collectibleFlag", "Switch": "switchFlag"} + param_to_prop_suffix = { + "Type": "type", + "Property": "props", + "Bool": "bool", + "Enum": "enum", + "ChestContent": "chestContent", + "Collectible": "collectibleDrop", + "Message": "naviMsg", + } + suffix = param_to_prop_suffix[param_type] if param_type != "Flag" else flag_to_prop_suffix[param_subtype] + return f"{actor_key}.{suffix}{param_index}" # e.g.: ``en_test.props1`` + + +def initOOTActorProperties(): + """This function is used to edit the OOTActorProperty class""" + + prop_annotations = getattr(OOTActorProperty, "__annotations__", None) + + if prop_annotations is None: + OOTActorProperty.__annotations__ = prop_annotations = {} + + param_type_to_enum_items = { + "ChestContent": game_data.z64.actors.ootEnumChestContent, + "Collectible": game_data.z64.actors.ootEnumCollectibleItems, + "Message": game_data.z64.actors.ootEnumNaviMessageData, + } + + for actor in game_data.z64.actors.actorList: + for param in actor.params: + prop_name = get_prop_name(actor.key, param.type, param.subType, param.index) + enum_items = None + + if len(param.items) > 0: + enum_items = [(f"0x{val:04X}", name, f"0x{val:04X}") for val, name in param.items] + enum_items.insert(0, ("Custom", "Custom Value", "Custom")) + elif param.type in {"ChestContent", "Collectible", "Message"}: + enum_items = param_type_to_enum_items[param.type] + + if param.type in {"Property", "Flag"}: + prop_annotations[prop_name] = StringProperty(name="", default="0x0") + elif param.type == "Bool": + prop_annotations[prop_name] = BoolProperty(name="", default=False) + elif param.type in {"Type", "Enum", "ChestContent", "Collectible", "Message"} and enum_items is not None: + prop_annotations[prop_name] = EnumProperty(name="", items=enum_items, default=enum_items[1][0]) + + if param.type in {"Type", "Enum", "ChestContent", "Collectible", "Message"}: + prop_annotations[f"{prop_name}_custom"] = StringProperty(name="", default="0x0") + + +class OOTActorHeaderItemProperty(PropertyGroup): + headerIndex: IntProperty(name="Scene Setup", min=4, default=4) + expandTab: BoolProperty(name="Expand Tab") + + def draw_props( + self, + layout: UILayout, + propUser: str, + index: int, + altProp: OOTAlternateSceneHeaderProperty | OOTAlternateRoomHeaderProperty, + objName: str, + ): + box = layout.column() + row = box.row() + row.prop(self, "headerIndex", text="") + drawCollectionOps(row.row(align=True), index, propUser, None, objName, compact=True) + if altProp is not None and self.headerIndex >= len(altProp.cutsceneHeaders) + 4: + box.label(text="Above header does not exist.", icon="QUESTION") + + +class OOTActorHeaderProperty(PropertyGroup): + sceneSetupPreset: EnumProperty(name="Scene Setup Preset", items=ootEnumSceneSetupPreset, default="All Scene Setups") + childDayHeader: BoolProperty(name="Child Day Header", default=True) + childNightHeader: BoolProperty(name="Child Night Header", default=True) + adultDayHeader: BoolProperty(name="Adult Day Header", default=True) + adultNightHeader: BoolProperty(name="Adult Night Header", default=True) + cutsceneHeaders: CollectionProperty(type=OOTActorHeaderItemProperty) + + def checkHeader(self, index: int) -> bool: + if index == 0: + return self.childDayHeader + elif index == 1: + return self.childNightHeader + elif index == 2: + return self.adultDayHeader + elif index == 3: + return self.adultNightHeader + else: + return index in [value.headerIndex for value in self.cutsceneHeaders] + + def draw_props( + self, + layout: UILayout, + propUser: str, + altProp: OOTAlternateSceneHeaderProperty | OOTAlternateRoomHeaderProperty, + objName: str, + ): + headerSetup = layout.column() + # headerSetup.box().label(text = "Alternate Headers") + prop_split(headerSetup, self, "sceneSetupPreset", "Scene Setup Preset") + if self.sceneSetupPreset == "Custom": + headerSetupBox = headerSetup.column() + headerSetupBox.prop(self, "childDayHeader", text="Child Day") + prevHeaderName = "childDayHeader" + childNightRow = headerSetupBox.row() + if altProp is None or altProp.childNightHeader.usePreviousHeader: + # Draw previous header checkbox (so get previous state), but labeled + # as current one and grayed out + childNightRow.prop(self, prevHeaderName, text="Child Night") + childNightRow.enabled = False + else: + childNightRow.prop(self, "childNightHeader", text="Child Night") + prevHeaderName = "childNightHeader" + adultDayRow = headerSetupBox.row() + if altProp is None or altProp.adultDayHeader.usePreviousHeader: + adultDayRow.prop(self, prevHeaderName, text="Adult Day") + adultDayRow.enabled = False + else: + adultDayRow.prop(self, "adultDayHeader", text="Adult Day") + prevHeaderName = "adultDayHeader" + adultNightRow = headerSetupBox.row() + if altProp is None or altProp.adultNightHeader.usePreviousHeader: + adultNightRow.prop(self, prevHeaderName, text="Adult Night") + adultNightRow.enabled = False + else: + adultNightRow.prop(self, "adultNightHeader", text="Adult Night") + + headerSetupBox.row().label(text="Cutscene headers to include this actor in:") + for i in range(len(self.cutsceneHeaders)): + headerItemProps: OOTActorHeaderItemProperty = self.cutsceneHeaders[i] + headerItemProps.draw_props(headerSetup, propUser, i, altProp, objName) + drawAddButton(headerSetup, len(self.cutsceneHeaders), propUser, None, objName) + + +class OOTActorProperty(PropertyGroup): + actor_id: EnumProperty(name="Actor", items=game_data.z64.actors.ootEnumActorID, default="ACTOR_PLAYER") + actor_id_custom: StringProperty(name="Actor ID", default="ACTOR_PLAYER") + + # only used for actors with the id "Custom" + # because of the get/set functions we need a way to input any value + params_custom: StringProperty(name="Actor Parameter", default="0x0000") + rot_override: BoolProperty(name="Override Rotation", default=False) + rot_x_custom: StringProperty(name="Rot X", default="0x0000") + rot_y_custom: StringProperty(name="Rot Y", default="0x0000") + rot_z_custom: StringProperty(name="Rot Z", default="0x0000") + + # non-custom actors + params: StringProperty( + name="Actor Parameter", + default="0x0000", + get=lambda self: self.get_param_value("Params"), + set=lambda self, value: self.set_param_value(value, "Params"), + ) + + rot_x: StringProperty( + name="Rot X", + default="0", + get=lambda self: self.get_param_value("XRot"), + set=lambda self, value: self.set_param_value(value, "XRot"), + ) + rot_y: StringProperty( + name="Rot Y", + default="0", + get=lambda self: self.get_param_value("YRot"), + set=lambda self, value: self.set_param_value(value, "YRot"), + ) + rot_z: StringProperty( + name="Rot Z", + default="0", + get=lambda self: self.get_param_value("ZRot"), + set=lambda self, value: self.set_param_value(value, "ZRot"), + ) + + headerSettings: PointerProperty(type=OOTActorHeaderProperty) + eval_params: BoolProperty(name="Eval Params", default=False) + + @staticmethod + def upgrade_object(obj: Object): + print(f"Processing '{obj.name}'...") + upgradeActors(obj) + + def is_rotation_used(self, target: str): + actor = game_data.z64.actors.actorsByID[self.actor_id] + selected_type = None + + for param in actor.params: + if param.type == "Type": + prop_name = get_prop_name(actor.key, param.type, param.subType, param.index) + base_val = getattr(self, prop_name) + + if base_val == "Custom": + base_val = getattr(self, f"{prop_name}_custom") + + selected_type = getEvalParamsInt(base_val) + + # the first parameter type is always the "Actor Type" + # because of that we need to make sure the current "Actor Type" value + # is included in type list of the property as not all properties are used sometimes + if selected_type is not None and selected_type in param.tiedTypes or len(param.tiedTypes) == 0: + if param.target != "Params" and target == param.target: + return True + + return False + + def is_value_in_range(self, value: int, min: int, max: int): + if min is not None and max is not None: + return value >= min and value <= max + return True + + def set_param_value(self, base_value: str | bool, target: str): + actor = game_data.z64.actors.actorsByID[self.actor_id] + base_value = getEvalParamsInt(base_value) + found_type = None + + for param in actor.params: + if target == param.target: + shift = getShiftFromMask(param.mask) + if param.type != "Type": + value = (base_value & param.mask) >> shift + else: + value = base_value & param.mask + + if "Rot" in target: + attr = getattr(self, get_prop_name(actor.key, "Type", None, 1), None) + found_type = getEvalParamsInt(attr) if attr is not None else None + else: + found_type = value + + is_in_range = self.is_value_in_range(value, param.valueRange[0], param.valueRange[1]) + found_type_in_tied_types = found_type is not None and found_type in param.tiedTypes + + if is_in_range and (found_type_in_tied_types or len(param.tiedTypes) == 0): + prop_name = get_prop_name(actor.key, param.type, param.subType, param.index) + + if param.type == "ChestContent": + prop_value = game_data.z64.actors.chestItemByValue[value].key + elif param.type == "Collectible": + prop_value = game_data.z64.actors.collectibleItemsByValue[value].key + elif param.type == "Message": + prop_value = game_data.z64.actors.messageItemsByValue[value].key + elif param.type == "Bool": + prop_value = bool(value) + else: + prop_value = f"0x{value:04X}" + + try: + setattr(self, prop_name, prop_value) + except: + if param.type in {"Type", "Enum", "ChestContent", "Collectible", "Message"}: + setattr(self, prop_name, "Custom") + setattr(self, f"{prop_name}_custom", prop_value) + print( + f"WARNING: invalid value '{prop_value}' ('{base_value}') for '{prop_name}'. " + + "Maybe `ActorList.xml` is missing informations?" + ) + + def get_param_value(self, target: str): + actor = game_data.z64.actors.actorsByID[self.actor_id] + param_list = [] + type_value = None + have_custom_value = False + + for param in actor.params: + if target == param.target: + param_val = None + prop_name = get_prop_name(actor.key, param.type, param.subType, param.index) + cur_prop_value = getattr(self, prop_name) + + if param.type not in {"Type", "Enum", "ChestContent", "Collectible", "Message"}: + if param.type == "Bool": + value_to_eval = "1" if cur_prop_value else "0" + else: + value_to_eval = cur_prop_value + + param_val = getEvalParamsInt(value_to_eval) + + # treat any invalid value as a custom value + if param_val is None: + param_list.append(value_to_eval) + have_custom_value = True + continue + else: + if cur_prop_value == "Custom": + cur_prop_value = getattr(self, f"{prop_name}_custom") + param_list.append(cur_prop_value) + have_custom_value = True + continue + + if param.type == "Type": + type_value = getEvalParamsInt(cur_prop_value) + else: + param_val = 0 + + if param.type == "ChestContent": + param_val = game_data.z64.actors.chestItemByKey[cur_prop_value].value + elif param.type == "Collectible": + param_val = game_data.z64.actors.collectibleItemsByKey[cur_prop_value].value + elif param.type == "Message": + param_val = game_data.z64.actors.messageItemsByKey[cur_prop_value].value + elif param.type == "Enum": + param_val = getEvalParamsInt(cur_prop_value) + + if "Rot" in target: + attr = getattr(self, get_prop_name(actor.key, "Type", None, 1), None) + type_value = getEvalParamsInt(attr) if attr is not None else None + + if type_value is not None and type_value in param.tiedTypes or len(param.tiedTypes) == 0: + val = ((param_val if param_val is not None else -1) & param.mask) >> getShiftFromMask(param.mask) + is_in_range = self.is_value_in_range(val, param.valueRange[0], param.valueRange[1]) + + if is_in_range and param.type != "Type" and param_val is not None: + value = getFormattedParams(param.mask, param_val, param.type == "Bool") + + if value is not None: + param_list.append(value) + + if len(param_list) > 0: + param_str = " | ".join(val for val in param_list) + else: + param_str = "0x0" + + if "Rot" in target: + type_value = None + + eval_type_value = type_value if type_value is not None else 0 + + # don't evaluate the params if there's a custom value + if not have_custom_value: + eval_param_value = getEvalParamsInt(param_str) + else: + eval_param_value = 0 + + if eval_type_value and (eval_param_value != 0 or have_custom_value) and type_value is not None: + param_str = f"(0x{type_value:04X} | ({param_str}))" + elif eval_type_value and not (eval_param_value != 0 or have_custom_value) and type_value is not None: + param_str = f"0x{type_value:04X}" + elif not eval_type_value and (eval_param_value != 0 or have_custom_value): + param_str = f"({param_str})" + else: + param_str = "0x0" + + if self.eval_params: + # return `param_str` if the eval failed + # should only happen if the user inputs invalid numbers (hex or dec) + # returns the non-evaluated value if the function returned None + try: + value = getEvalParams(param_str) + return value if value is not None else param_str + except: + pass + + return param_str + + def draw_params(self, layout: UILayout, obj: Object): + actor = game_data.z64.actors.actorsByID[self.actor_id] + selected_type = None + + for param in actor.params: + prop_name = get_prop_name(actor.key, param.type, param.subType, param.index) + + if param.type == "Type": + base_val = getattr(self, prop_name) + + if base_val == "Custom": + base_val = getattr(self, f"{prop_name}_custom") + + selected_type = getEvalParamsInt(base_val) + + # the first parameter type is always the "Actor Type" + # because of that we need to make sure the current "Actor Type" value + # is included in type list of the property as not all properties are used sometimes + is_type_in_tied_types = selected_type is not None and selected_type in param.tiedTypes + if is_type_in_tied_types or param.type == "Type" or len(param.tiedTypes) == 0: + if param.type in {"ChestContent", "Message"}: + key: str = getattr(self, prop_name) + + if param.type == "ChestContent": + search_op = layout.operator(OOT_SearchChestContentEnumOperator.bl_idname) + label_name = "Chest Content" + item_map = game_data.z64.actors.chestItemByKey + else: + search_op = layout.operator(OOT_SearchNaviMsgIDEnumOperator.bl_idname) + label_name = "Navi Message ID" + item_map = game_data.z64.actors.messageItemsByKey + + search_op.obj_name = obj.name + search_op.prop_name = prop_name + + if key != "Custom": + label_split(layout, label_name, item_map[key].name) + else: + prop_split(layout, self, f"{prop_name}_custom", f"{label_name} Custom") + else: + prop_split(layout, self, prop_name, param.name) + + if param.type in {"Type", "Enum", "Collectible"}: + if getattr(self, prop_name) == "Custom": + prop_split(layout, self, f"{prop_name}_custom", f"{param.name} Custom") + + def draw_props(self, layout: UILayout, altRoomProp: OOTAlternateRoomHeaderProperty, obj: Object): + actorIDBox = layout.column() + searchOp = actorIDBox.operator(OOT_SearchActorIDEnumOperator.bl_idname, icon="VIEWZOOM") + searchOp.actor_user = "Actor" + searchOp.obj_name = obj.name + + split = actorIDBox.split(factor=0.5) + + if self.actor_id == "None": + actorIDBox.box().label(text="This Actor was deleted from the XML file.") + return + + split.label(text="Actor ID") + split.label(text=getEnumName(game_data.z64.actors.ootEnumActorID, self.actor_id)) + + if bpy.context.scene.fast64.oot.use_new_actor_panel and self.actor_id != "Custom": + self.draw_params(actorIDBox, obj) + + if self.actor_id == "Custom": + prop_split(actorIDBox, self, "actor_id_custom", "") + + paramBox = actorIDBox.box() + paramBox.label(text="Actor Parameter") + + if bpy.context.scene.fast64.oot.use_new_actor_panel and self.actor_id != "Custom": + paramBox.prop(self, "eval_params") + paramBox.prop(self, "params", text="") + else: + paramBox.prop(self, "params_custom", text="") + + rotations_used = [] + + if bpy.context.scene.fast64.oot.use_new_actor_panel and self.actor_id != "Custom": + if self.is_rotation_used("XRot"): + rotations_used.append("X") + if self.is_rotation_used("YRot"): + rotations_used.append("Y") + if self.is_rotation_used("ZRot"): + rotations_used.append("Z") + elif self.rot_override: + rotations_used = ["X", "Y", "Z"] + + if not bpy.context.scene.fast64.oot.use_new_actor_panel or self.actor_id == "Custom": + paramBox.prop(self, "rot_override", text="Override Rotation (ignore Blender rot)") + + for rot in rotations_used: + custom = ( + "_custom" if not bpy.context.scene.fast64.oot.use_new_actor_panel or self.actor_id == "Custom" else "" + ) + prop_split(paramBox, self, f"rot_{rot.lower()}{custom}", f"Rot {rot}") + + headerProp: OOTActorHeaderProperty = self.headerSettings + headerProp.draw_props(actorIDBox, "Actor", altRoomProp, obj.name) + + +class OOTTransitionActorProperty(PropertyGroup): + fromRoom: PointerProperty(type=Object, poll=lambda self, object: self.isRoomEmptyObject(object)) + toRoom: PointerProperty(type=Object, poll=lambda self, object: self.isRoomEmptyObject(object)) + cameraTransitionFront: EnumProperty(items=ootEnumCamTransition, default="0x00") + cameraTransitionFrontCustom: StringProperty(default="0x00") + cameraTransitionBack: EnumProperty(items=ootEnumCamTransition, default="0x00") + cameraTransitionBackCustom: StringProperty(default="0x00") + isRoomTransition: BoolProperty(name="Is Room Transition", default=True) + + actor: PointerProperty(type=OOTActorProperty) + + def isRoomEmptyObject(self, obj: Object): + return obj.type == "EMPTY" and obj.ootEmptyType == "Room" + + def draw_props( + self, layout: UILayout, altSceneProp: OOTAlternateSceneHeaderProperty, roomObj: Object, objName: str + ): + actorIDBox = layout.column() + searchOp = actorIDBox.operator(OOT_SearchActorIDEnumOperator.bl_idname, icon="VIEWZOOM") + searchOp.actor_user = "Transition Actor" + searchOp.obj_name = objName + + split = actorIDBox.split(factor=0.5) + split.label(text="Actor ID") + split.label(text=getEnumName(game_data.z64.actors.ootEnumActorID, self.actor.actor_id)) + + if bpy.context.scene.fast64.oot.use_new_actor_panel and self.actor.actor_id != "Custom": + self.actor.draw_params(actorIDBox, roomObj) + + if self.actor.actor_id == "Custom": + prop_split(actorIDBox, self.actor, "actor_id_custom", "") + + paramBox = actorIDBox.box() + paramBox.label(text="Actor Parameter") + if bpy.context.scene.fast64.oot.use_new_actor_panel and self.actor.actor_id != "Custom": + paramBox.prop(self.actor, "eval_params") + paramBox.prop(self.actor, "params", text="") + else: + paramBox.prop(self.actor, "params_custom", text="") + + if roomObj is None: + actorIDBox.label(text="This must be part of a Room empty's hierarchy.", icon="OUTLINER") + else: + actorIDBox.prop(self, "isRoomTransition") + if self.isRoomTransition: + prop_split(actorIDBox, self, "fromRoom", "Room To Transition From") + prop_split(actorIDBox, self, "toRoom", "Room To Transition To") + if self.fromRoom == self.toRoom: + actorIDBox.label(text="Warning: You selected the same room!", icon="ERROR") + actorIDBox.label(text='Y+ side of door faces toward the "from" room.', icon="ORIENTATION_NORMAL") + drawEnumWithCustom(actorIDBox, self, "cameraTransitionFront", "Camera Transition Front", "") + drawEnumWithCustom(actorIDBox, self, "cameraTransitionBack", "Camera Transition Back", "") + + headerProps: OOTActorHeaderProperty = self.actor.headerSettings + headerProps.draw_props(actorIDBox, "Transition Actor", altSceneProp, objName) + + +class OOTEntranceProperty(PropertyGroup): + # This is also used in entrance list. + spawnIndex: IntProperty(min=0) + customActor: BoolProperty(name="Use Custom Actor") + actor: PointerProperty(type=OOTActorProperty) + + tiedRoom: PointerProperty( + type=Object, + poll=lambda self, object: self.isRoomEmptyObject(object), + description="Used to set the room index", + ) + + def isRoomEmptyObject(self, obj: Object): + return obj.type == "EMPTY" and obj.ootEmptyType == "Room" + + def draw_props(self, layout: UILayout, obj: Object, altSceneProp: OOTAlternateSceneHeaderProperty, objName: str): + box = layout.column() + roomObj = getRoomObj(obj) + if roomObj is None: + box.label(text="This must be part of a Room empty's hierarchy.", icon="OUTLINER") + + entranceProp = obj.ootEntranceProperty + prop_split(box, entranceProp, "tiedRoom", "Room") + prop_split(box, entranceProp, "spawnIndex", "Spawn Index") + + box.prop(entranceProp, "customActor") + if entranceProp.customActor: + prop_split(box, entranceProp.actor, "actor_id_custom", "Actor ID Custom") + + if bpy.context.scene.fast64.oot.use_new_actor_panel and not self.customActor: + self.actor.draw_params(box, obj) + + paramBox = box.box() + paramBox.label(text="Actor Parameter") + if bpy.context.scene.fast64.oot.use_new_actor_panel and not self.customActor: + paramBox.prop(self.actor, "eval_params") + paramBox.prop(self.actor, "params", text="") + else: + paramBox.prop(self.actor, "params_custom", text="") + + headerProps: OOTActorHeaderProperty = entranceProp.actor.headerSettings + headerProps.draw_props(box, "Entrance", altSceneProp, objName) + + +classes = ( + OOTActorHeaderItemProperty, + OOTActorHeaderProperty, + OOTActorProperty, + OOTTransitionActorProperty, + OOTEntranceProperty, +) + + +def actor_props_register(): + for cls in classes: + register_class(cls) + + Object.ootActorProperty = PointerProperty(type=OOTActorProperty) + Object.ootTransitionActorProperty = PointerProperty(type=OOTTransitionActorProperty) + Object.ootEntranceProperty = PointerProperty(type=OOTEntranceProperty) + + +def actor_props_unregister(): + del Object.ootActorProperty + del Object.ootTransitionActorProperty + del Object.ootEntranceProperty + + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/animated_mats/operators.py b/fast64_internal/z64/animated_mats/operators.py new file mode 100644 index 000000000..aec62be3e --- /dev/null +++ b/fast64_internal/z64/animated_mats/operators.py @@ -0,0 +1,55 @@ +from bpy.utils import register_class, unregister_class +from bpy.types import Operator + +from ...utility import ExportUtils, 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 + + with ExportUtils() as export_utils: + try: + SceneAnimatedMaterial.export() + self.report({"INFO"}, "Success!") + return {"FINISHED"} + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + +class Z64_ImportAnimatedMaterials(Operator): + bl_idname = "object.z64_import_animated_materials" + bl_label = "Import Animated Materials" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + from ..exporter.scene.animated_mats import SceneAnimatedMaterial + + try: + SceneAnimatedMaterial.from_data() + self.report({"INFO"}, "Success!") + return {"FINISHED"} + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + +classes = ( + Z64_ExportAnimatedMaterials, + Z64_ImportAnimatedMaterials, +) + + +def animated_mats_ops_register(): + for cls in classes: + register_class(cls) + + +def animated_mats_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/animated_mats/panels.py b/fast64_internal/z64/animated_mats/panels.py new file mode 100644 index 000000000..3dcb5a711 --- /dev/null +++ b/fast64_internal/z64/animated_mats/panels.py @@ -0,0 +1,29 @@ +from bpy.utils import register_class, unregister_class + +from ...panels import OOT_Panel +from ..utility import is_oot_features, is_hackeroot + + +class Z64_AnimatedMaterialsPanel(OOT_Panel): + bl_idname = "Z64_PT_animated_materials" + bl_label = "Animated Materials Exporter" + + def draw(self, context): + if not is_oot_features() or is_hackeroot(): + context.scene.fast64.oot.anim_mats_export_settings.draw_props(self.layout.box()) + context.scene.fast64.oot.anim_mats_import_settings.draw_props(self.layout.box()) + else: + self.layout.label(text="MM features are disabled.", icon="QUESTION") + + +panel_classes = (Z64_AnimatedMaterialsPanel,) + + +def animated_mats_panels_register(): + for cls in panel_classes: + register_class(cls) + + +def animated_mats_panels_unregister(): + for cls in reversed(panel_classes): + unregister_class(cls) diff --git a/fast64_internal/z64/animated_mats/properties.py b/fast64_internal/z64/animated_mats/properties.py new file mode 100644 index 000000000..8dd12186a --- /dev/null +++ b/fast64_internal/z64/animated_mats/properties.py @@ -0,0 +1,823 @@ +import bpy + +from bpy.utils import register_class, unregister_class +from bpy.types import PropertyGroup, UILayout, Object, Material +from bpy.props import ( + IntProperty, + PointerProperty, + BoolProperty, + EnumProperty, + StringProperty, + CollectionProperty, + FloatVectorProperty, +) + +from typing import Optional + +from ...game_data import game_data +from ...utility import prop_split +from ..collection_utility import drawCollectionOps, draw_utility_ops +from ..collision.properties import OOTMaterialCollisionProperty +from ..hackeroot.properties import HackerOoT_EventProperty +from ..utility import get_list_tab_text, getEnumIndex, is_oot_features, is_hackeroot +from .operators import Z64_ExportAnimatedMaterials, Z64_ImportAnimatedMaterials + + +# no custom since we only need to know where to export the data +enum_mode = [ + ("Scene", "Scene", "Scene"), + ("Actor", "Actor", "Actor"), +] + + +class Z64_AnimatedMatColorKeyFrame(PropertyGroup): + duration: IntProperty(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 validate_frame_num(self): + if self.internal_frame_num >= self.internal_length: + # TODO: figure out if having the same value is fine + self.internal_frame_num = self.internal_length - 1 + + def on_frame_num_set(self, value): + self.internal_frame_num = value + self.validate_frame_num() + + def on_frame_num_get(self): + self.validate_frame_num() + return self.internal_frame_num + + prim_lod_frac: IntProperty(name="Primitive LOD Frac", min=0, max=255, default=128) + prim_color: FloatVectorProperty( + name="Primitive Color", + subtype="COLOR", + size=4, + min=0, + max=1, + default=(1, 1, 1, 1), + ) + + env_color: FloatVectorProperty( + name="Environment Color", + subtype="COLOR", + size=4, + min=0, + max=1, + default=(1, 1, 1, 1), + ) + + def draw_props( + self, + layout: UILayout, + owner: Object, + header_index: int, + parent_index: int, + index: int, + color_type: str, + use_env_color: bool, + ): + drawCollectionOps( + layout, + index, + "Animated Mat. Color", + header_index, + owner.name, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + + is_draw_color = color_type in {"anim_mat_type_color", "anim_mat_type_color_cycle"} + + # "draw color" type don't need this + if not is_draw_color or color_type == "anim_mat_type_color_cycle": + if is_draw_color: + prop_split(layout, self, "duration", "Duration") + else: + prop_split(layout, self, "frame_num", "Frame No.") + + prop_split(layout, self, "prim_lod_frac", "Primitive LOD Frac") + prop_split(layout, self, "prim_color", "Primitive Color") + + if not is_draw_color or use_env_color: + prop_split(layout, self, "env_color", "Environment Color") + + +class Z64_AnimatedMatColorParams(PropertyGroup): + keyframe_length: IntProperty( + name="Keyframe Length", + min=0, + set=lambda self, value: self.on_length_set(value), + get=lambda self: self.on_length_get(), + ) + internal_keyframe_length: IntProperty(min=0) + + keyframes: CollectionProperty(type=Z64_AnimatedMatColorKeyFrame) + use_env_color: BoolProperty() + + # ui only props + show_entries: BoolProperty(default=False) + + internal_color_type: StringProperty() + + def update_keyframes(self): + for keyframe in self.keyframes: + keyframe.internal_length = self.internal_keyframe_length + + def on_length_set(self, value): + self.internal_keyframe_length = value + self.update_keyframes() + + def on_length_get(self): + self.update_keyframes() + return self.internal_keyframe_length + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int): + is_draw_color = self.internal_color_type in {"anim_mat_type_color", "anim_mat_type_color_cycle"} + + if not is_draw_color: + prop_split(layout, self, "keyframe_length", "Keyframe Length") + + if is_draw_color: + layout.prop(self, "use_env_color", text="Use Environment Color") + + prop_text = get_list_tab_text("Keyframes", len(self.keyframes)) + layout.prop(self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + + if self.show_entries: + for i, keyframe in enumerate(self.keyframes): + keyframe.draw_props( + layout, + owner, + header_index, + parent_index, + i, + self.internal_color_type, + not is_draw_color or self.use_env_color, + ) + + draw_utility_ops( + layout.row(), + len(self.keyframes), + "Animated Mat. Color", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + + +class Z64_AnimatedMatTexScrollItem(PropertyGroup): + step_x: IntProperty(default=0) + step_y: IntProperty(default=0) + width: IntProperty(min=0) + height: IntProperty(min=0) + + def set_from_data(self, raw_data: list[str]): + self.step_x = int(raw_data[0], base=0) + self.step_y = int(raw_data[1], base=0) + self.width = int(raw_data[2], base=0) + self.height = int(raw_data[3], base=0) + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "step_x", "Step X") + prop_split(layout, self, "step_y", "Step Y") + prop_split(layout, self, "width", "Texture Width") + prop_split(layout, self, "height", "Texture Height") + + +class Z64_AnimatedMatTexScrollParams(PropertyGroup): + texture_1: PointerProperty(type=Z64_AnimatedMatTexScrollItem) + texture_2: PointerProperty(type=Z64_AnimatedMatTexScrollItem) + + # ui only props + show_entries: BoolProperty(default=False) + + internal_scroll_type: StringProperty(default="anim_mat_type_two_tex_scroll") + + def draw_props(self, layout: UILayout): + tab_text = "Two-Texture Scroll" if "two_tex" in self.internal_scroll_type else "Texture Scroll" + layout.prop(self, "show_entries", text=tab_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + + if self.show_entries: + if self.internal_scroll_type == "anim_mat_type_two_tex_scroll": + tex1_box = layout.box().column() + tex1_box.label(text="Texture 1") + self.texture_1.draw_props(tex1_box) + + tex2_box = layout.box().column() + tex2_box.label(text="Texture 2") + self.texture_2.draw_props(tex2_box) + else: + self.texture_1.draw_props(layout) + + +class Z64_AnimatedMatTexCycleTexture(PropertyGroup): + symbol: StringProperty(name="Texture Symbol") + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int, index: int): + drawCollectionOps( + layout, + index, + "Animated Mat. Cycle (Texture)", + header_index, + owner.name, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + prop_split(layout, self, "symbol", "Texture Symbol") + + +class Z64_AnimatedMatTexCycleKeyFrame(PropertyGroup): + texture_index: IntProperty(min=0) + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int, index: int): + drawCollectionOps( + layout, + index, + "Animated Mat. Cycle (Index)", + header_index, + owner.name, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + prop_split(layout, self, "texture_index", "Texture Symbol") + + +class Z64_AnimatedMatTexCycleParams(PropertyGroup): + keyframes: CollectionProperty(type=Z64_AnimatedMatTexCycleKeyFrame) + textures: CollectionProperty(type=Z64_AnimatedMatTexCycleTexture) + + # ui only props + show_entries: BoolProperty(default=False) + show_textures: BoolProperty(default=False) + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int): + texture_box = layout.box() + prop_text = get_list_tab_text("Textures", len(self.textures)) + texture_box.prop( + self, "show_textures", text=prop_text, icon="TRIA_DOWN" if self.show_textures else "TRIA_RIGHT" + ) + if self.show_textures: + for i, texture in enumerate(self.textures): + texture.draw_props(texture_box, owner, header_index, parent_index, i) + draw_utility_ops( + texture_box.row(), + len(self.textures), + "Animated Mat. Cycle (Texture)", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + + index_box = layout.box() + prop_text = get_list_tab_text("Keyframes", len(self.keyframes)) + index_box.prop(self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + if self.show_entries: + for i, keyframe in enumerate(self.keyframes): + keyframe.draw_props(index_box, owner, header_index, parent_index, i) + draw_utility_ops( + index_box.row(), + len(self.keyframes), + "Animated Mat. Cycle (Index)", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + + +class Z64_AnimatedMatTexTimedCycleKeyFrame(PropertyGroup): + symbol: StringProperty() + duration: IntProperty(min=0) + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int, index: int): + drawCollectionOps( + layout, + index, + "Animated Mat. Timed Cycle", + header_index, + owner.name, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + prop_split(layout, self, "symbol", "Texture Symbol") + prop_split(layout, self, "duration", "Duration") + + +class Z64_AnimatedMatTexTimedCycleParams(PropertyGroup): + keyframes: CollectionProperty(type=Z64_AnimatedMatTexTimedCycleKeyFrame) + + # ui only props + show_entries: BoolProperty(default=False) + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int): + 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.column(), owner, header_index, parent_index, i) + draw_utility_ops( + index_box.row(), + len(self.keyframes), + "Animated Mat. Timed Cycle", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + + +class Z64_AnimatedMatTextureParams(PropertyGroup): + texture_1: StringProperty(description="Default Texture") + texture_2: StringProperty(description="Texture to draw when the event script is completed") + + # ui only props + show_entries: BoolProperty(default=False) + + def draw_props(self, layout: UILayout): + layout.prop(self, "show_entries", text="Texture", icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + + if self.show_entries: + texture_box = layout.box().column() + prop_split(texture_box, self, "texture_1", "Texture 1") + prop_split(texture_box, self, "texture_2", "Texture 2") + + +class Z64_AnimatedMatMultiTextureParams(PropertyGroup): + min_prim_alpha: IntProperty(min=0, max=255) + max_prim_alpha: IntProperty(min=0, max=255) + min_env_alpha: IntProperty(min=0, max=255) + max_env_alpha: IntProperty(min=0, max=255) + speed: IntProperty(min=0, description="Transition or blending speed, can be 0 to disable blending.") + + use_texture_refs: BoolProperty( + default=False, + description="Optionally, you can use texture references, you'll need to provide symbols and segment numbers.", + ) + texture_1: StringProperty(description="Symbol for Texture Reference No. 1") + texture_2: StringProperty(description="Symbol for Texture Reference No. 2") + segment_1: IntProperty(min=8, max=13, default=8, description="Segment corresponding to the Texture Reference No. 1") + segment_2: IntProperty(min=8, max=13, default=8, description="Segment corresponding to the Texture Reference No. 2") + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "min_prim_alpha", "Min. Primitive Alpha") + prop_split(layout, self, "max_prim_alpha", "Max. Primitive Alpha") + prop_split(layout, self, "min_env_alpha", "Min. Environment Alpha") + prop_split(layout, self, "max_env_alpha", "Max. Environment Alpha") + prop_split(layout, self, "speed", "Transition Speed") + + tex_box = layout.box().column() + tex_box.prop(self, "use_texture_refs", text="Use Texture References") + if self.use_texture_refs: + prop_split(tex_box, self, "texture_1", "Texture Symbol 1") + prop_split(tex_box, self, "segment_1", "Segment Number 1") + + prop_split(tex_box, self, "texture_2", "Texture Symbol 2") + prop_split(tex_box, self, "segment_2", "Segment Number 2") + + +class Z64_AnimatedMatTriIndexItem(PropertyGroup): + mesh_obj: PointerProperty(type=Object, poll=lambda self, obj: self.on_poll(obj)) + + def on_poll(self, obj: Object): + active_obj = bpy.context.view_layer.objects.active + assert active_obj is not None + return ( + active_obj.type == "EMPTY" + and active_obj.ootEmptyType == "Scene" + and obj.type == "MESH" + and obj in active_obj.children_recursive + ) + + def draw_props(self, layout: UILayout, owner: Object, index: int, header_index: int, parent_index: int): + layout.prop(self, "mesh_obj", text="") + + drawCollectionOps( + layout, + index, + "Animated Mat. Surface", + header_index, + owner.name, + compact=True, + collection_index=parent_index, + ask_for_copy=True, + ask_for_amount=True, + ) + + +class Z64_AnimatedMatSurfaceSwapParams(PropertyGroup): + col_settings: PointerProperty(type=OOTMaterialCollisionProperty) + + use_tris: BoolProperty(default=False) + material: PointerProperty( + type=Material, poll=lambda self, obj: self.on_poll(obj), description="Can be left empty if using tri indices" + ) + meshes: CollectionProperty(type=Z64_AnimatedMatTriIndexItem) + + use_multitexture: BoolProperty(default=False) + multitexture_params: PointerProperty( + type=Z64_AnimatedMatMultiTextureParams, + description="Can be left empty if you just want to swap the surface type", + ) + + # ui only props + show_entries: BoolProperty(default=False) + + def on_poll(self, obj: Material): + # TODO + return True + + def draw_props(self, layout: UILayout, owner: Object, header_index: int, parent_index: int): + layout.label(text="The segment number will be used for multitexture (if applicable).", icon="QUESTION") + self.col_settings.draw_props(layout.box().column()) + + tri_box = layout.box().column() + tri_box.prop(self, "use_tris", text="Use Triangle Indices") + + if self.use_tris: + tri_box.prop(self, "show_entries", text="Meshes", icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT") + + if self.show_entries: + for i, entry in enumerate(self.meshes): + entry.draw_props(tri_box.row(align=True), owner, i, header_index, parent_index) + + draw_utility_ops( + tri_box.row(), + len(self.meshes), + "Animated Mat. Surface", + header_index, + owner.name, + parent_index, + ask_for_amount=True, + ) + else: + prop_split(tri_box, self, "material", "Replace Surface Type from") + + multi_box = layout.box().column() + multi_box.prop(self, "use_multitexture", text="Use Multi-Texture") + if self.use_multitexture: + self.multitexture_params.draw_props(multi_box) + + +class Z64_AnimatedMatColorSwitchItem(PropertyGroup): + prim_lod_frac: IntProperty(name="Primitive LOD Frac", min=0, max=255, default=128) + prim_color: FloatVectorProperty( + name="Primitive Color", + subtype="COLOR", + size=4, + min=0, + max=1, + default=(1, 1, 1, 1), + ) + + use_env_color: BoolProperty() + 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, number: int): + layout.label(text=f"Color {number}") + layout.prop(self, "use_env_color", text="Use Environment Color") + + prop_split(layout, self, "prim_lod_frac", "Primitive LOD Frac") + prop_split(layout, self, "prim_color", "Primitive Color") + + if self.use_env_color: + prop_split(layout, self, "env_color", "Environment Color") + + +class Z64_AnimatedMatColorSwitchParams(PropertyGroup): + color_1: PointerProperty(type=Z64_AnimatedMatColorSwitchItem) + color_2: PointerProperty(type=Z64_AnimatedMatColorSwitchItem) + + def draw_props(self, layout: UILayout): + self.color_1.draw_props(layout.box().column(), "1") + self.color_2.draw_props(layout.box().column(), "2") + + +class Z64_AnimatedMaterialItem(PropertyGroup): + """see the `AnimatedMaterial` struct from `z64scene.h`""" + + segment_num: IntProperty(name="Segment Number", min=8, max=13, default=8) + + user_type: EnumProperty( + name="Draw Handler Type", + items=lambda self, context: game_data.z64.get_enum("anim_mats_type"), + default=2, + description="Index to `sMatAnimDrawHandlers`", + get=lambda self: self.on_type_get(), + set=lambda self, value: self.on_type_set(value), + ) + type: StringProperty(default=game_data.z64.enums.enum_anim_mats_type[2][0]) + type_custom: StringProperty(name="Custom Draw Handler Index", default="2") + + color_params: PointerProperty(type=Z64_AnimatedMatColorParams) + tex_scroll_params: PointerProperty(type=Z64_AnimatedMatTexScrollParams) + tex_cycle_params: PointerProperty(type=Z64_AnimatedMatTexCycleParams) + tex_timed_cycle_params: PointerProperty(type=Z64_AnimatedMatTexTimedCycleParams) + texture_params: PointerProperty(type=Z64_AnimatedMatTextureParams) + multitexture_params: PointerProperty(type=Z64_AnimatedMatMultiTextureParams) + surface_params: PointerProperty(type=Z64_AnimatedMatSurfaceSwapParams) + color_switch_params: PointerProperty(type=Z64_AnimatedMatColorSwitchParams) + + events: PointerProperty(type=HackerOoT_EventProperty) + + # ui only props + show_item: BoolProperty(default=False) + + def on_type_set(self, value: int): + self.type = game_data.z64.enums.enum_anim_mats_type[value][0] + + if "tex_scroll" in self.type: + self.tex_scroll_params.internal_scroll_type = self.type + elif "color" in self.type and self.type != "anim_mat_type_color_switch": + self.color_params.internal_color_type = self.type + + def on_type_get(self): + enum = game_data.z64.get_enum("anim_mats_type") + value = getEnumIndex(enum, self.type) + + if value is None: + print("WARNING: value is None, assuming old version!") + enum_value = f"anim_mat_type_{self.type}" + value = getEnumIndex(enum, enum_value) + + if enum_value != self.type: + self.user_type = enum_value + + assert value is not None, "ERROR: wrong get value on an animated material item." + return value + + 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" + ) + + vanilla_types = [ + "anim_mat_type_tex_scroll", + "anim_mat_type_two_tex_scroll", + "anim_mat_type_color", + "anim_mat_type_color_lerp", + "anim_mat_type_color_nonlinear_interp", + "anim_mat_type_tex_cycle", + "anim_mat_type_none", + ] + + if self.show_item: + drawCollectionOps(layout, index, "Animated Mat.", header_index, owner.name, ask_for_amount=True) + + prop_split(layout, self, "segment_num", "Segment Number") + + layout_type = layout.column() + prop_split(layout_type, self, "user_type", "Draw Handler Type") + + if self.type not in vanilla_types and not is_hackeroot(): + layout_type.label(text="This requires HackerOoT features.", icon="ERROR") + return + + self.events.draw_props(layout.column(), owner, "EventManager (Embed)", header_index, index) + + 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 "tex_scroll" in self.type or self.type == "anim_mat_type_oscillating_two_tex": + self.tex_scroll_params.draw_props(layout_type) + elif self.type == "anim_mat_type_color_switch": + self.color_switch_params.draw_props(layout_type) + elif "color" in self.type and self.type != "anim_mat_type_color_switch": + self.color_params.draw_props(layout_type, owner, header_index, index) + elif self.type == "anim_mat_type_tex_cycle": + self.tex_cycle_params.draw_props(layout_type, owner, header_index, index) + elif self.type == "anim_mat_type_tex_timed_cycle": + self.tex_timed_cycle_params.draw_props(layout_type, owner, header_index, index) + elif self.type == "anim_mat_type_texture": + self.texture_params.draw_props(layout_type) + elif self.type == "anim_mat_type_multitexture": + self.multitexture_params.draw_props(layout_type) + elif self.type == "anim_mat_type_event": + layout_type.label(text="This don't use parameters.") + layout_type.label(text="It will draw/hide based on the event.") + elif self.type == "anim_mat_type_surface_swap": + self.surface_params.draw_props(layout_type, owner, header_index, index) + elif self.type == "anim_mat_type_none": + layout_type.label(text="This won't be exported.", icon="ERROR") + + +class Z64_AnimatedMaterial(PropertyGroup): + """Defines an Animated Material array""" + + entries: CollectionProperty(type=Z64_AnimatedMaterialItem) + cam_type: EnumProperty( + items=lambda self, context: game_data.z64.get_enum("anim_mats_cam_type"), + default=1, + description="Optional camera/screen effect to apply", + ) + cam_type_custom: StringProperty() + cam_on_event: BoolProperty( + default=False, description="Trigger the camera/screen behavior when the events are completed" + ) + + # ui only props + show_list: BoolProperty(default=True) + show_entries: BoolProperty(default=True) + + def draw_props( + self, + layout: UILayout, + owner: Object, + index: Optional[int], + sub_index: Optional[int] = None, + is_scene: bool = True, + ): + layout = layout.column() + + if is_oot_features() and not is_hackeroot(): + layout.label(text="This requires MM features.", icon="ERROR") + return + + prop_split(layout, self, "cam_type", "Camera Type") + if self.cam_type == "Custom": + prop_split(layout, self, "cam_type_custom", "Camera Type Custom") + + if self.cam_type != "anim_mat_camera_type_none": + layout.prop(self, "cam_on_event", text="Camera On Event") + + if index is not None: + layout.prop( + self, "show_list", text=f"List No.{index + 1}", icon="TRIA_DOWN" if self.show_list else "TRIA_RIGHT" + ) + + if self.show_list: + if index is not None: + drawCollectionOps( + layout, + index, + "Animated Mat. List", + sub_index, + owner.name, + ask_for_copy=False, + ask_for_amount=False, + ) + + prop_text = get_list_tab_text("Animated Materials", len(self.entries)) + layout_entries = layout.column() + layout_entries.prop( + self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT" + ) + + if self.show_entries: + for i, item in enumerate(self.entries): + item.draw_props(layout_entries.box().column(), owner, sub_index, i) + + draw_utility_ops( + layout_entries.row(), + len(self.entries), + "Animated Mat.", + sub_index, + owner.name, + do_copy=is_scene, + ask_for_amount=True, + ) + + +class Z64_AnimatedMaterialProperty(PropertyGroup): + """List of Animated Material arrays""" + + # this is probably useless since usually you wouldn't use different animated materials + # on different headers but it's better to give users the choice + items: CollectionProperty(type=Z64_AnimatedMaterial) + + # ui only props + show_entries: BoolProperty(default=True) + + def draw_props(self, layout: UILayout, owner: Object): + layout = layout.column() + + if is_oot_features() and not is_hackeroot(): + layout.label(text="This requires MM features.", icon="ERROR") + return + + prop_text = get_list_tab_text("Animated Materials List", len(self.items)) + layout_entries = layout.column() + layout_entries.prop( + self, "show_entries", text=prop_text, icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT" + ) + + if self.show_entries: + for i, item in enumerate(self.items): + item.draw_props(layout_entries.box().column(), owner, i, i, is_scene=False) + + draw_utility_ops( + layout_entries.row(), len(self.items), "Animated Mat. List", None, owner.name, do_copy=False + ) + + +class Z64_AnimatedMaterialExportSettings(PropertyGroup): + object_name: StringProperty(default="gameplay_keep") + + include_name: StringProperty(default="animated_materials.h") + is_custom_inc: BoolProperty(default=False) + + export_path: StringProperty(name="File", subtype="DIR_PATH") + export_obj: PointerProperty(type=Object, poll=lambda self, obj: self.filter(obj)) + is_custom_path: BoolProperty(default=False) + + def filter(self, obj): + return obj.type == "EMPTY" and obj.ootEmptyType == "Animated Materials" + + def get_include_name(self): + if is_hackeroot(): + return "animated_materials.h" + + if self.is_custom_inc: + return self.include_name if self.include_name.endswith(".h") else f"{self.include_name}.h" + + if bpy.context.scene.fast64.oot.is_z64sceneh_present(): + return "z64scene.h" + + return "scene.h" + + def draw_props(self, layout: UILayout): + layout = layout.column() + layout.label(text="Animated Materials Exporter") + prop_split(layout, self, "export_obj", "Export Object") + + if not is_hackeroot(): + inc_box = layout.box() + inc_box.prop(self, "is_custom_inc", text="Custom Include") + if self.is_custom_inc: + prop_split(inc_box, self, "include_name", "Include") + + path_box = layout.box() + path_box.prop(self, "is_custom_path", text="Custom Path") + if self.is_custom_path: + path_box.label(text="The object name will be the file name", icon="QUESTION") + prop_split(path_box, self, "export_path", "Export To") + else: + prop_split(path_box, self, "object_name", "Object Name") + + layout.operator(Z64_ExportAnimatedMaterials.bl_idname) + + +class Z64_AnimatedMaterialImportSettings(PropertyGroup): + import_path: StringProperty(name="File", subtype="FILE_PATH") + + def draw_props(self, layout: UILayout): + layout = layout.column() + layout.label(text="Animated Materials Importer") + prop_split(layout, self, "import_path", "Import From") + layout.operator(Z64_ImportAnimatedMaterials.bl_idname) + + +classes = ( + Z64_AnimatedMatColorKeyFrame, + Z64_AnimatedMatColorParams, + Z64_AnimatedMatTexScrollItem, + Z64_AnimatedMatTexScrollParams, + Z64_AnimatedMatTexCycleTexture, + Z64_AnimatedMatTexCycleKeyFrame, + Z64_AnimatedMatTexCycleParams, + Z64_AnimatedMatTexTimedCycleKeyFrame, + Z64_AnimatedMatTexTimedCycleParams, + Z64_AnimatedMatTextureParams, + Z64_AnimatedMatMultiTextureParams, + Z64_AnimatedMatTriIndexItem, + Z64_AnimatedMatSurfaceSwapParams, + Z64_AnimatedMatColorSwitchItem, + Z64_AnimatedMatColorSwitchParams, + Z64_AnimatedMaterialItem, + Z64_AnimatedMaterial, + Z64_AnimatedMaterialProperty, + Z64_AnimatedMaterialExportSettings, + Z64_AnimatedMaterialImportSettings, +) + + +def animated_mats_props_register(): + for cls in classes: + register_class(cls) + + +def animated_mats_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/oot/animation/importer/__init__.py b/fast64_internal/z64/animation/importer/__init__.py similarity index 100% rename from fast64_internal/oot/animation/importer/__init__.py rename to fast64_internal/z64/animation/importer/__init__.py diff --git a/fast64_internal/oot/animation/importer/functions.py b/fast64_internal/z64/animation/importer/functions.py similarity index 81% rename from fast64_internal/oot/animation/importer/functions.py rename to fast64_internal/z64/animation/importer/functions.py index da35f233e..da5635915 100644 --- a/fast64_internal/oot/animation/importer/functions.py +++ b/fast64_internal/z64/animation/importer/functions.py @@ -2,9 +2,9 @@ import bpy import re import math -from ....utility import PluginError, hexOrDecInt +from ....utility import PluginError, hexOrDecInt, get_include_data, removeComments from ....f3d.f3d_parser import getImportData -from ...oot_model_classes import ootGetIncludedAssetData +from ...model_classes import ootGetIncludedAssetData from ....utility_anim import ( getTranslationRelativeToRest, @@ -12,7 +12,7 @@ stashActionInArmature, ) -from ...oot_utility import ( +from ...utility import ( getStartBone, getNextBone, ) @@ -27,10 +27,14 @@ def binangToRadians(value): def getFrameData(filepath: str, animData: str, frameDataName: str): - matchResult = re.search(re.escape(frameDataName) + "\s*\[\s*[0-9]*\s*\]\s*=\s*\{([^\}]*)\}", animData, re.DOTALL) + matchResult = re.search(re.escape(frameDataName) + "\s*\[.*?\]\s*=\s*\{([^\}]*)\}", animData, re.DOTALL) if matchResult is None: raise PluginError("Cannot find animation frame data named " + frameDataName + " in " + filepath) data = matchResult.group(1) + + if "#include" in data: + data = get_include_data(data, strip=True) + # split array into values as strings frameData_str = [value_stripped for value in data.split(",") if (value_stripped := value.strip()) != ""] # convert string values to int @@ -46,6 +50,10 @@ def getJointIndices(filepath, animData, jointIndicesName): if matchResult is None: raise PluginError("Cannot find animation joint indices data named " + jointIndicesName + " in " + filepath) data = matchResult.group(1) + + if "#include" in data: + data = get_include_data(data.removesuffix("}"), strip=True) + jointIndicesData = [ [hexOrDecInt(match.group(i)) for i in range(1, 4)] for match in re.finditer("\{([^,\}]*),([^,\}]*),([^,\}]*)\s*,?\s*\}", data, re.DOTALL) @@ -60,10 +68,24 @@ def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCu basePath = bpy.path.abspath(bpy.context.scene.ootDecompPath) animData = ootGetIncludedAssetData(basePath, [filepath], animData) + animData + matchResult = re.search(re.escape(animName) + r"\s*=\s*\{(.*?)\}\s*;", animData, re.DOTALL | re.MULTILINE) + + if matchResult is None: + raise PluginError("Cannot find definition named " + animName + " in " + filepath) + + if "#include" in matchResult.group(1): + anim_data = removeComments(get_include_data(matchResult.group(1))).replace("\n", "").replace(" ", "") + regex = r"\{(.*?),?\},(.*?),(.*?),(.*?)," + else: + anim_data = animData + regex = ( + re.escape(animName) + + r"\s*=\s*\{\s*\{\s*([^,\s]*)\s*\}*\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*\}\s*;" + ) + matchResult = re.search( - re.escape(animName) - + "\s*=\s*\{\s*\{\s*([^,\s]*)\s*\}*\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*\}\s*;", - animData, + regex, + anim_data, ) if matchResult is None: raise PluginError("Cannot find animation named " + animName + " in " + filepath) @@ -162,6 +184,7 @@ def ootImportLinkAnimationC( numLimbs: int, isCustomImport: bool, ): + header_data = getImportData([animFilepath.replace(".c", ".h")]) animHeaderData = getImportData([animHeaderFilepath]) animData = getImportData([animFilepath]) if not isCustomImport: @@ -170,12 +193,34 @@ def ootImportLinkAnimationC( animData = ootGetIncludedAssetData(basePath, [animFilepath], animData) + animData matchResult = re.search( - re.escape(animHeaderName) + "\s*=\s*\{\s*\{\s*([^,\s]*)\s*\}\s*,\s*([^,\s]*)\s*\}\s*;", - animHeaderData, + re.escape(animHeaderName) + r"\s*=\s*\{(.*?)\}\s*;", animHeaderData, re.DOTALL | re.MULTILINE + ) + + if matchResult is None: + raise PluginError("Cannot find definition named " + animHeaderName + " in " + animHeaderFilepath) + + if "#include" in matchResult.group(1): + anim_data = removeComments(get_include_data(matchResult.group(1))).replace("\n", "").replace(" ", "") + regex = r"\{\s*([^,\s]*)\s*,?\}\s*,\s*([^,\s]*)" + else: + anim_data = animHeaderData + regex = re.escape(animHeaderName) + r"\s*=\s*\{\s*\{\s*([^,\s]*)\s*\}\s*,\s*([^,\s]*)\s*\}\s*;" + + matchResult = re.search( + regex, + anim_data, ) if matchResult is None: raise PluginError("Cannot find animation named " + animHeaderName + " in " + animHeaderFilepath) - frameCount = hexOrDecInt(matchResult.group(1).strip()) + + frame_count_raw = matchResult.group(1).strip() + + if "FRAMECOUNT_" in frame_count_raw: + frame_count = re.search(rf"define\s*{frame_count_raw}\s*([0-9\-]*)", header_data, re.DOTALL) + assert frame_count is not None + frameCount = hexOrDecInt(frame_count.group(1)) + else: + frameCount = hexOrDecInt(frame_count_raw) frameDataName = matchResult.group(2).strip() frameData = getFrameData(animFilepath, animData, frameDataName) diff --git a/fast64_internal/oot/animation/operators.py b/fast64_internal/z64/animation/operators.py similarity index 83% rename from fast64_internal/oot/animation/operators.py rename to fast64_internal/z64/animation/operators.py index 3756b8168..1d4ecd534 100644 --- a/fast64_internal/oot/animation/operators.py +++ b/fast64_internal/z64/animation/operators.py @@ -3,12 +3,12 @@ from bpy.props import StringProperty, BoolProperty from bpy.utils import register_class, unregister_class from bpy.ops import object -from ...utility import PluginError, toAlnum, writeCData, raisePluginError +from ...utility import PluginError, ExportUtils, toAlnum, writeCData, raisePluginError from .properties import OOTAnimExportSettingsProperty, OOTAnimImportSettingsProperty -from .exporter import ootExportLinkAnimation, ootExportNonLinkAnimation +from ..exporter.animation import ootExportLinkAnimation, ootExportNonLinkAnimation from .importer import ootImportLinkAnimationC, ootImportNonLinkAnimationC -from ..oot_utility import ( +from ..utility import ( ootGetPath, addIncludeFiles, checkEmptyName, @@ -18,10 +18,17 @@ def exportAnimationC(armatureObj: bpy.types.Object, settings: OOTAnimExportSettingsProperty): + if settings.isCustom: + checkEmptyName(settings.customPath) + else: + checkEmptyName(settings.folderName) + + if settings.isCustomFilename: + checkEmptyName(settings.filename) + path = bpy.path.abspath(settings.customPath) exportPath = ootGetObjectPath(settings.isCustom, path, settings.folderName, False) - checkEmptyName(settings.folderName) checkEmptyName(armatureObj.name) name = toAlnum(armatureObj.name) filename = settings.filename if settings.isCustomFilename else name @@ -63,7 +70,7 @@ def exportAnimationC(armatureObj: bpy.types.Object, settings: OOTAnimExportSetti addIncludeFiles("gameplay_keep", headerPath, ootAnim.headerName) else: - ootAnim = ootExportNonLinkAnimation(armatureObj, convertTransformMatrix, name) + ootAnim = ootExportNonLinkAnimation(armatureObj, convertTransformMatrix, name, filename) ootAnimC = ootAnim.toC() path = ootGetPath(exportPath, settings.isCustom, "assets/objects/", settings.folderName, True, False) @@ -116,26 +123,27 @@ class OOT_ExportAnim(Operator): # Called on demand (i.e. button press, menu item) # Can also be called from operator search menu (Spacebar) def execute(self, context): - try: - if len(context.selected_objects) == 0 or not isinstance(context.selected_objects[0].data, Armature): - raise PluginError("Armature not selected.") - if len(context.selected_objects) > 1: - raise PluginError("Multiple objects selected, make sure to select only one.") - armatureObj = context.selected_objects[0] - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - try: - settings = context.scene.fast64.oot.animExportSettings - exportAnimationC(armatureObj, settings) - self.report({"INFO"}, "Success!") - - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} # must return a set + with ExportUtils() as export_utils: + try: + if len(context.selected_objects) == 0 or not isinstance(context.selected_objects[0].data, Armature): + raise PluginError("Armature not selected.") + if len(context.selected_objects) > 1: + raise PluginError("Multiple objects selected, make sure to select only one.") + armatureObj = context.selected_objects[0] + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + try: + settings = context.scene.fast64.oot.animExportSettings + exportAnimationC(armatureObj, settings) + self.report({"INFO"}, "Success!") + + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} # must return a set return {"FINISHED"} # must return a set diff --git a/fast64_internal/oot/animation/panels.py b/fast64_internal/z64/animation/panels.py similarity index 93% rename from fast64_internal/oot/animation/panels.py rename to fast64_internal/z64/animation/panels.py index ce3e8f00e..e40a8991e 100644 --- a/fast64_internal/oot/animation/panels.py +++ b/fast64_internal/z64/animation/panels.py @@ -17,7 +17,7 @@ class OOT_LinkAnimPanel(Panel): @classmethod def poll(cls, context): return ( - context.scene.gameEditorMode == "OOT" + context.scene.gameEditorMode in {"OOT", "MM"} and hasattr(context, "object") and context.object is not None and isinstance(context.object.data, Armature) @@ -33,8 +33,8 @@ def draw(self, context): class OOT_ExportAnimPanel(OOT_Panel): - bl_idname = "OOT_PT_export_anim" - bl_label = "OOT Animation Exporter" + bl_idname = "Z64_PT_export_anim" + bl_label = "Animation Exporter" # called every frame def draw(self, context): diff --git a/fast64_internal/oot/animation/properties.py b/fast64_internal/z64/animation/properties.py similarity index 100% rename from fast64_internal/oot/animation/properties.py rename to fast64_internal/z64/animation/properties.py diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py new file mode 100644 index 000000000..6c58de305 --- /dev/null +++ b/fast64_internal/z64/collection_utility.py @@ -0,0 +1,469 @@ +import bpy + +from bpy.types import Operator, UILayout +from bpy.utils import register_class, unregister_class +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): + bl_idname = "object.oot_collection_add" + bl_label = "Add Item" + bl_options = {"REGISTER", "UNDO"} + + option: IntProperty() + collectionType: StringProperty(default="Actor") + subIndex: IntProperty(default=0) + collection_index: IntProperty(default=0) + objName: StringProperty() + + ask_for_copy: BoolProperty(default=False) + do_copy_previous: BoolProperty(default=True) + + ask_for_amount: BoolProperty(default=False) + amount: IntProperty(min=1, default=1) + + def execute(self, context): + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) + + for i in range(self.amount): + new_entry = collection.add() + collection.move(len(collection) - 1, self.option + i) + + if self.ask_for_copy and self.do_copy_previous: + copyPropertyGroup(collection[self.option - 1 + i], new_entry) + + if not self.ask_for_amount: + # should always default to 1 but just in case force a break + break + + owner = bpy.data.objects[self.objName] + if self.collectionType == "Actor CS" and owner.ootEmptyType == "Actor Cutscene": + context.scene.fast64.oot.global_actor_cs_count = len(collection) + + 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" + bl_label = "Remove Item" + bl_options = {"REGISTER", "UNDO"} + + option: IntProperty() + collectionType: StringProperty(default="Actor") + subIndex: IntProperty(default=0) + collection_index: IntProperty(default=0) + objName: StringProperty() + + ask_for_amount: BoolProperty(default=False) + amount: IntProperty( + min=0, + default=0, + set=lambda self, value: OOTCollectionRemove.on_amount_set(self, value), + get=lambda self: OOTCollectionRemove.on_amount_get(self), + ) + internal_amount: IntProperty(min=0, default=0) + + # static methods because it doesn't work otherwise + @staticmethod + def on_amount_set(owner, value): + owner.internal_amount = value + + @staticmethod + def on_amount_get(owner): + collection = getCollection(owner.objName, owner.collectionType, owner.subIndex, owner.collection_index) + maximum = len(collection) - owner.option + + if owner.internal_amount >= maximum: + owner.internal_amount = maximum + + return owner.internal_amount + + def execute(self, context): + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) + + if self.amount > 0: + for _ in range(self.amount): + if not self.ask_for_amount or self.option >= len(collection): + break + + collection.remove(self.option) + else: + collection.remove(self.option) + + owner = bpy.data.objects[self.objName] + if self.collectionType == "Actor CS" and owner.ootEmptyType == "Actor Cutscene": + context.scene.fast64.oot.global_actor_cs_count = len(collection) + + context.region.tag_redraw() + return {"FINISHED"} + + def invoke(self, context, _): + collection = getCollection(self.objName, self.collectionType, self.subIndex, self.collection_index) + if self.ask_for_amount and self.option + 1 < len(collection): + return context.window_manager.invoke_props_dialog(self, width=300) + return self.execute(context) + + def draw(self, _): + layout = self.layout + + if self.ask_for_amount: + layout.prop(self, "amount", text="Number of following items to remove") + + if self.amount == 0: + text = f"Will remove Item No. {self.option + 1}." + elif self.amount == 1: + text = f"Will remove Item No. {self.option + 1} and the next one." + else: + text = f"Will remove Item No. {self.option + 1} and the next {self.amount}." + + layout.label(text=text) + + +class OOTCollectionMove(Operator): + bl_idname = "object.oot_collection_move" + bl_label = "Move Item" + bl_options = {"REGISTER", "UNDO"} + + 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, 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) + + +# 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: int, collection_index: int = 0): + obj = bpy.data.objects[objName] + if collectionType == "Actor": + collection = obj.ootActorProperty.headerSettings.cutsceneHeaders + elif collectionType == "Transition Actor": + collection = obj.ootTransitionActorProperty.actor.headerSettings.cutsceneHeaders + elif collectionType == "Entrance": + collection = obj.ootEntranceProperty.actor.headerSettings.cutsceneHeaders + elif collectionType == "Room": + collection = obj.ootAlternateRoomHeaders.cutsceneHeaders + elif collectionType == "Scene": + collection = obj.ootAlternateSceneHeaders.cutsceneHeaders + elif collectionType == "Light": + collection = getCollectionFromIndex(obj, "lightList", subIndex, False) + elif collectionType == "ToD Light": + collection = getCollectionFromIndex(obj, "tod_lights", subIndex, False) + elif collectionType == "Exit": + collection = getCollectionFromIndex(obj, "exitList", subIndex, False) + elif collectionType == "Object": + collection = getCollectionFromIndex(obj, "objectList", subIndex, True) + elif collectionType == "Animated Mat. List": + collection = obj.fast64.oot.animated_materials.items + elif collectionType.startswith("Animated Mat."): + if obj.ootEmptyType == "Scene": + header = ootGetSceneOrRoomHeader(obj, subIndex, False) + props = header.animated_material + else: + props = obj.fast64.oot.animated_materials.items[subIndex] + + if collectionType == "Animated Mat.": + collection = props.entries + elif collectionType == "Animated Mat. Color": + collection = props.entries[collection_index].color_params.keyframes + elif collectionType == "Animated Mat. Cycle (Index)": + collection = props.entries[collection_index].tex_cycle_params.keyframes + elif collectionType == "Animated Mat. Cycle (Texture)": + collection = props.entries[collection_index].tex_cycle_params.textures + elif collectionType == "Animated Mat. Timed Cycle": + collection = props.entries[collection_index].tex_timed_cycle_params.keyframes + elif collectionType == "Animated Mat. Surface": + collection = props.entries[collection_index].surface_params.meshes + elif collectionType == "Curve": + collection = obj.ootSplineProperty.headerSettings.cutsceneHeaders + elif collectionType.startswith("CSHdr."): + # CSHdr.HeaderNumber[.ListType] + # Specifying ListType means uses subIndex + toks = collectionType.split(".") + assert len(toks) in [2, 3] + hdrnum = int(toks[1]) + collection = getCollectionFromIndex(obj, "csLists", hdrnum, False) + if len(toks) == 3: + collection = getattr(collection[subIndex], toks[2]) + elif collectionType.startswith("Cutscene."): + # Cutscene.ListType + toks = collectionType.split(".") + assert len(toks) == 2 + collection = obj.ootCutsceneProperty.csLists + collection = getattr(collection[subIndex], toks[1]) + elif collectionType == "Cutscene": + collection = obj.ootCutsceneProperty.csLists + elif collectionType == "extraCutscenes": + collection = obj.ootSceneHeader.extraCutscenes + elif collectionType == "BgImage": + collection = obj.ootRoomHeader.bgImageList + elif collectionType == "EventManager (Embed)": + header = ootGetSceneOrRoomHeader(obj, subIndex, False) + props = header.animated_material + collection = props.entries[collection_index].events.entries + else: + raise PluginError("Invalid collection type: " + collectionType) + + return collection + + +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) + addOp.option = index + addOp.collectionType = collectionType + addOp.subIndex = subIndex + addOp.objName = objName + addOp.collection_index = collection_index + addOp.ask_for_copy = ask_for_copy + addOp.ask_for_amount = ask_for_amount + + +def draw_clear_button( + layout: UILayout, collection_type: str, sub_index: Optional[int], obj_name: str, collection_index: int = 0 +): + if sub_index is None: + sub_index = 0 + copy_op: OOTCollectionClear = layout.operator(OOTCollectionClear.bl_idname) + copy_op.collection_type = collection_type + copy_op.sub_index = sub_index + copy_op.obj_name = obj_name + copy_op.collection_index = collection_index + + +def draw_copy_button( + layout: UILayout, collection_type: str, sub_index: Optional[int], obj_name: str, collection_index: int = 0 +): + if sub_index is None: + sub_index = 0 + copy_op: OOTCollectionCopy = layout.operator(OOTCollectionCopy.bl_idname) + copy_op.collection_type = collection_type + copy_op.sub_index = sub_index + copy_op.obj_name = obj_name + copy_op.collection_index = collection_index + + +def draw_utility_ops( + layout: bpy.types.UILayout, + index: int, + collection_type: str, + header_index: Optional[int], + obj_name: str, + collection_index: int = 0, + do_copy: bool = True, + ask_for_copy: bool = False, + ask_for_amount: bool = False, +): + drawAddButton( + layout, index, collection_type, header_index, obj_name, collection_index, ask_for_copy, ask_for_amount + ) + draw_clear_button(layout, collection_type, header_index, obj_name, collection_index) + + if do_copy: + draw_copy_button(layout, collection_type, header_index, obj_name, collection_index) + + +def drawCollectionOps( + layout, + index, + collectionType, + subIndex, + objName, + allowAdd=True, + compact=False, + collection_index: int = 0, + ask_for_copy: bool = False, + ask_for_amount: bool = False, +): + if subIndex is None: + subIndex = 0 + + if not compact: + buttons = layout.row(align=True) + else: + buttons = layout + + if allowAdd: + addOp = buttons.operator(OOTCollectionAdd.bl_idname, text="Add" if not compact else "", icon="ADD") + addOp.option = index + 1 + addOp.collectionType = collectionType + addOp.subIndex = subIndex + addOp.objName = objName + addOp.collection_index = collection_index + addOp.ask_for_copy = ask_for_copy + addOp.ask_for_amount = ask_for_amount + + removeOp = buttons.operator(OOTCollectionRemove.bl_idname, text="Delete" if not compact else "", icon="REMOVE") + removeOp.option = index + removeOp.collectionType = collectionType + removeOp.subIndex = subIndex + removeOp.objName = objName + removeOp.collection_index = collection_index + removeOp.ask_for_amount = ask_for_amount + + moveUp = buttons.operator(OOTCollectionMove.bl_idname, text="Up" if not compact else "", icon="TRIA_UP") + moveUp.option = index + moveUp.offset = -1 + 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 + moveDown.offset = 1 + moveDown.collectionType = collectionType + moveDown.subIndex = subIndex + moveDown.objName = objName + moveDown.collection_index = collection_index + + +collections_classes = ( + OOTCollectionAdd, + OOTCollectionRemove, + OOTCollectionMove, + OOTCollectionClear, + OOTCollectionCopy, +) + + +def collections_register(): + for cls in collections_classes: + register_class(cls) + + +def collections_unregister(): + for cls in reversed(collections_classes): + unregister_class(cls) diff --git a/fast64_internal/z64/collision/constants.py b/fast64_internal/z64/collision/constants.py new file mode 100644 index 000000000..2c63f362e --- /dev/null +++ b/fast64_internal/z64/collision/constants.py @@ -0,0 +1,109 @@ +ootEnumConveyer = [ + ("None", "None", "None"), + ("Land", "Land", "Land"), + ("Water", "Water", "Water"), +] + +ootEnumWallSetting = [ + ("Custom", "Custom", "Custom"), + ("0x00", "None", "None"), + ("0x01", "No Ledge Grab", "No Ledge Grab"), + ("0x02", "Ladder", "Ladder"), + ("0x03", "Ladder Top", "Ladder Top"), + ("0x04", "Vines", "Vines"), + ("0x05", "Crawl Space", "Crawl Space"), + ("0x06", "Crawl Space 2", "Crawl Space 2"), + ("0x07", "Push Block", "Push Block"), +] + +ootEnumCollisionTerrain = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Walkable", "Walkable"), + ("0x01", "Steep", "Steep"), + ("0x02", "Walkable (Preserves Exit Flags)", "Walkable (Preserves Exit Flags)"), + ("0x03", "Walkable (?)", "Walkable (?)"), +] + +ootEnumCollisionSound = [ + ("Custom", "Custom", "Custom"), + ("0x00", "Dirt", "Dirt (aka Earth)"), + ("0x01", "Sand", "Sand"), + ("0x02", "Stone", "Stone"), + ("0x03", "Jabu", "Jabu-Jabu flesh (aka Wet Stone)"), + ("0x04", "Shallow Water", "Shallow Water"), + ("0x05", "Deep Water", "Deep Water"), + ("0x06", "Tall Grass", "Tall Grass"), + ("0x07", "Lava", "Lava (aka Goo)"), + ("0x08", "Grass", "Grass (aka Earth 2)"), + ("0x09", "Bridge", "Bridge (aka Wooden Plank)"), + ("0x0A", "Wood", "Wood (aka Packed Earth)"), + ("0x0B", "Soft Dirt", "Soft Dirt (aka Earth 3)"), + ("0x0C", "Ice", "Ice (aka Ceramic)"), + ("0x0D", "Carpet", "Carpet (aka Loose Earth)"), +] + +enum_conveyor_speed = [ + ("Custom", "Custom", "Custom"), + ("0x00", "None", "None"), + ("0x01", "Slow", "Slow"), + ("0x02", "Medium", "Medium"), + ("0x03", "Fast", "Fast"), +] + +ootEnumCameraCrawlspaceSType = [ + ("Custom", "Custom", "Custom"), + ("CAM_SET_CRAWLSPACE", "Crawlspace", "Crawlspace"), +] + +decomp_compat_map_CameraSType = { + "CAM_SET_HORSE0": "CAM_SET_HORSE", + "CAM_SET_BOSS_GOMA": "CAM_SET_BOSS_GOHMA", + "CAM_SET_BOSS_DODO": "CAM_SET_BOSS_DODONGO", + "CAM_SET_BOSS_BARI": "CAM_SET_BOSS_BARINADE", + "CAM_SET_BOSS_FGANON": "CAM_SET_BOSS_PHANTOM_GANON", + "CAM_SET_BOSS_BAL": "CAM_SET_BOSS_VOLVAGIA", + "CAM_SET_BOSS_SHADES": "CAM_SET_BOSS_BONGO", + "CAM_SET_BOSS_MOFA": "CAM_SET_BOSS_MORPHA", + "CAM_SET_TWIN0": "CAM_SET_BOSS_TWINROVA_PLATFORM", + "CAM_SET_TWIN1": "CAM_SET_BOSS_TWINROVA_FLOOR", + "CAM_SET_BOSS_GANON1": "CAM_SET_BOSS_GANONDORF", + "CAM_SET_BOSS_GANON2": "CAM_SET_BOSS_GANON", + "CAM_SET_TOWER0": "CAM_SET_TOWER_CLIMB", + "CAM_SET_TOWER1": "CAM_SET_TOWER_UNUSED", + "CAM_SET_FIXED0": "CAM_SET_MARKET_BALCONY", + "CAM_SET_FIXED1": "CAM_SET_CHU_BOWLING", + "CAM_SET_CIRCLE0": "CAM_SET_PIVOT_CRAWLSPACE", + "CAM_SET_CIRCLE2": "CAM_SET_PIVOT_SHOP_BROWSING", + "CAM_SET_CIRCLE3": "CAM_SET_PIVOT_IN_FRONT", + "CAM_SET_PREREND0": "CAM_SET_PREREND_FIXED", + "CAM_SET_PREREND1": "CAM_SET_PREREND_PIVOT", + "CAM_SET_PREREND3": "CAM_SET_PREREND_SIDE_SCROLL", + "CAM_SET_RAIL3": "CAM_SET_CRAWLSPACE", + "CAM_SET_CIRCLE4": "CAM_SET_PIVOT_CORNER", + "CAM_SET_CIRCLE5": "CAM_SET_PIVOT_WATER_SURFACE", + "CAM_SET_DEMO0": "CAM_SET_CS_0", + "CAM_SET_DEMO1": "CAM_SET_CS_TWISTED_HALLWAY", + "CAM_SET_MORI1": "CAM_SET_FOREST_BIRDS_EYE", + "CAM_SET_ITEM0": "CAM_SET_SLOW_CHEST_CS", + "CAM_SET_ITEM1": "CAM_SET_ITEM_UNUSED", + "CAM_SET_DEMO3": "CAM_SET_CS_3", + "CAM_SET_DEMO4": "CAM_SET_CS_ATTENTION", + "CAM_SET_UFOBEAN": "CAM_SET_BEAN_GENERIC", + "CAM_SET_LIFTBEAN": "CAM_SET_BEAN_LOST_WOODS", + "CAM_SET_SCENE0": "CAM_SET_SCENE_UNUSED", + "CAM_SET_SCENE1": "CAM_SET_SCENE_TRANSITION", + "CAM_SET_HIDAN1": "CAM_SET_ELEVATOR_PLATFORM", + "CAM_SET_HIDAN2": "CAM_SET_FIRE_STAIRCASE", + "CAM_SET_MORI2": "CAM_SET_FOREST_UNUSED", + "CAM_SET_MORI3": "CAM_SET_FOREST_DEFEAT_POE", + "CAM_SET_TAKO": "CAM_SET_BIG_OCTO", + "CAM_SET_SPOT05A": "CAM_SET_MEADOW_BIRDS_EYE", + "CAM_SET_SPOT05B": "CAM_SET_MEADOW_UNUSED", + "CAM_SET_HIDAN3": "CAM_SET_FIRE_BIRDS_EYE", + "CAM_SET_ITEM2": "CAM_SET_TURN_AROUND", + "CAM_SET_CIRCLE6": "CAM_SET_PIVOT_VERTICAL", + "CAM_SET_DEMOC": "CAM_SET_CS_C", + "CAM_SET_UO_FIBER": "CAM_SET_JABU_TENTACLE", + "CAM_SET_TEPPEN": "CAM_SET_DIRECTED_YAW", + "CAM_SET_CIRCLE7": "CAM_SET_PIVOT_FROM_SIDE", +} diff --git a/fast64_internal/z64/collision/operators.py b/fast64_internal/z64/collision/operators.py new file mode 100644 index 000000000..48321d093 --- /dev/null +++ b/fast64_internal/z64/collision/operators.py @@ -0,0 +1,53 @@ +from bpy.types import Operator +from bpy.utils import register_class, unregister_class +from bpy.ops import object +from mathutils import Matrix + +from ...utility import PluginError, ExportUtils, raisePluginError +from ..utility import getOOTScale +from ..exporter.collision import CollisionHeader +from .properties import OOTCollisionExportSettings + + +class OOT_ExportCollision(Operator): + # set bl_ properties + bl_idname = "object.oot_export_collision" + bl_label = "Export Collision" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + with ExportUtils() as export_utils: + obj = None + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + if len(context.selected_objects) == 0: + raise PluginError("No object selected.") + obj = context.active_object + if obj.type != "MESH": + raise PluginError("No mesh object selected.") + + try: + transform = Matrix.Scale(getOOTScale(obj.ootActorScale), 4) + settings: OOTCollisionExportSettings = context.scene.fast64.oot.collisionExportSettings + CollisionHeader.export(obj, transform, settings) + + self.report({"INFO"}, "Success!") + return {"FINISHED"} + except Exception as e: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + raisePluginError(self, e) + return {"CANCELLED"} # must return a set + + +oot_col_classes = (OOT_ExportCollision,) + + +def collision_ops_register(): + for cls in oot_col_classes: + register_class(cls) + + +def collision_ops_unregister(): + for cls in reversed(oot_col_classes): + unregister_class(cls) diff --git a/fast64_internal/oot/collision/panels.py b/fast64_internal/z64/collision/panels.py similarity index 87% rename from fast64_internal/oot/collision/panels.py rename to fast64_internal/z64/collision/panels.py index ee98b0745..b0cf54fe2 100644 --- a/fast64_internal/oot/collision/panels.py +++ b/fast64_internal/z64/collision/panels.py @@ -15,7 +15,7 @@ class OOT_CameraPosPanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and isinstance(context.object.data, Camera) + return context.scene.gameEditorMode in {"OOT", "MM"} and isinstance(context.object.data, Camera) def draw(self, context): box = self.layout.box().column() @@ -36,7 +36,7 @@ class OOT_CollisionPanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and context.material is not None + return context.scene.gameEditorMode in {"OOT", "MM"} and context.material is not None def draw(self, context): box = self.layout.box().column() @@ -46,8 +46,8 @@ def draw(self, context): class OOT_ExportCollisionPanel(OOT_Panel): - bl_idname = "OOT_PT_export_collision" - bl_label = "OOT Collision Exporter" + bl_idname = "Z64_PT_export_collision" + bl_label = "Collision Exporter" # called every frame def draw(self, context): diff --git a/fast64_internal/oot/collision/properties.py b/fast64_internal/z64/collision/properties.py similarity index 90% rename from fast64_internal/oot/collision/properties.py rename to fast64_internal/z64/collision/properties.py index 2b2373773..0abc4f207 100644 --- a/fast64_internal/oot/collision/properties.py +++ b/fast64_internal/z64/collision/properties.py @@ -1,19 +1,18 @@ import math + from bpy.props import StringProperty, PointerProperty, IntProperty, EnumProperty, BoolProperty, FloatProperty -from bpy.types import PropertyGroup, Camera, Object, Material, UILayout +from bpy.types import PropertyGroup, Object, Material, UILayout from bpy.utils import register_class, unregister_class + +from ...game_data import game_data from ...utility import prop_split -from ..oot_utility import drawEnumWithCustom -from ..oot_constants import ootEnumSceneID +from ..utility import drawEnumWithCustom from .constants import ( - ootEnumFloorSetting, ootEnumWallSetting, - ootEnumFloorProperty, ootEnumConveyer, - ootEnumConveyorSpeed, + enum_conveyor_speed, ootEnumCollisionTerrain, ootEnumCollisionSound, - ootEnumCameraSType, ) @@ -45,7 +44,7 @@ def draw_props(self, layout: UILayout): class OOTCameraPositionProperty(PropertyGroup): index: IntProperty(min=0) bgImageOverrideIndex: IntProperty(default=-1, min=-1) - camSType: EnumProperty(items=ootEnumCameraSType, default="CAM_SET_NONE") + camSType: EnumProperty(items=lambda self, context: game_data.z64.get_enum("camera_setting_type"), default=1) camSTypeCustom: StringProperty(default="CAM_SET_NONE") hasPositionData: BoolProperty(default=True, name="Has Position Data") @@ -68,17 +67,17 @@ class OOTMaterialCollisionProperty(PropertyGroup): eponaBlock: BoolProperty() decreaseHeight: BoolProperty() floorSettingCustom: StringProperty(default="0x00") - floorSetting: EnumProperty(items=ootEnumFloorSetting, default="0x00") + floorSetting: EnumProperty(items=lambda self, context: game_data.z64.get_enum("floor_property"), default=1) wallSettingCustom: StringProperty(default="0x00") wallSetting: EnumProperty(items=ootEnumWallSetting, default="0x00") floorPropertyCustom: StringProperty(default="0x00") - floorProperty: EnumProperty(items=ootEnumFloorProperty, default="0x00") + floorProperty: EnumProperty(items=lambda self, context: game_data.z64.get_enum("floor_type"), default=1) exitID: IntProperty(default=0, min=0) cameraID: IntProperty(default=0, min=0) isWallDamage: BoolProperty() conveyorOption: EnumProperty(items=ootEnumConveyer) conveyorRotation: FloatProperty(min=0, max=2 * math.pi, subtype="ANGLE") - conveyorSpeed: EnumProperty(items=ootEnumConveyorSpeed, default="0x00") + conveyorSpeed: EnumProperty(items=enum_conveyor_speed, default="0x00") conveyorSpeedCustom: StringProperty(default="0x00") conveyorKeepMomentum: BoolProperty() hookshotable: BoolProperty() @@ -93,7 +92,7 @@ def draw_props(self, layout: UILayout): layout.prop( self, "expandTab", - text="OOT Collision Properties", + text="Collision Settings", icon="TRIA_DOWN" if self.expandTab else "TRIA_RIGHT", ) if self.expandTab: @@ -109,9 +108,9 @@ def draw_props(self, layout: UILayout): layout.prop(self, "isWallDamage", text="Is Wall Damage") layout.prop(self, "hookshotable", text="Hookshotable") - drawEnumWithCustom(layout, self, "floorSetting", "Floor Setting", "") + drawEnumWithCustom(layout, self, "floorSetting", "Floor Property", "") drawEnumWithCustom(layout, self, "wallSetting", "Wall Setting", "") - drawEnumWithCustom(layout, self, "floorProperty", "Floor Property", "") + drawEnumWithCustom(layout, self, "floorProperty", "Floor Type", "") layout.prop(self, "ignoreCameraCollision", text="Ignore Camera Collision") layout.prop(self, "ignoreActorCollision", text="Ignore Actor Collision") diff --git a/fast64_internal/oot/oot_constants.py b/fast64_internal/z64/constants.py similarity index 76% rename from fast64_internal/oot/oot_constants.py rename to fast64_internal/z64/constants.py index a0b33813c..d12ef01ac 100644 --- a/fast64_internal/oot/oot_constants.py +++ b/fast64_internal/z64/constants.py @@ -1,7 +1,3 @@ -from .data import OoT_Data - -ootData = OoT_Data() - ootEnumRoomShapeType = [ # ("Custom", "Custom", "Custom"), ("ROOM_SHAPE_TYPE_NORMAL", "Normal", "Normal"), @@ -19,24 +15,6 @@ ("Child Day", "Child Day", "Child Day"), ] + ootEnumHeaderMenu -ootEnumLinkIdle = [ - ("Custom", "Custom", "Custom"), - ("0x00", "Default", "Default"), - ("0x01", "Sneezing", "Sneezing"), - ("0x02", "Wiping Forehead", "Wiping Forehead"), - ("0x04", "Yawning", "Yawning"), - ("0x07", "Gasping For Breath", "Gasping For Breath"), - ("0x09", "Brandish Sword", "Brandish Sword"), - ("0x0A", "Adjust Tunic", "Adjust Tunic"), - ("0xFF", "Hops On Epona", "Hops On Epona"), -] - -ootEnumCloudiness = [ - ("Custom", "Custom", "Custom"), - ("0x00", "Sunny", "Sunny"), - ("0x01", "Cloudy", "Cloudy"), -] - ootEnumCameraMode = [ ("Custom", "Custom", "Custom"), ("0x00", "Default", "Default"), @@ -74,37 +52,6 @@ ("0x16", "Grottos & Fairy Fountains", "Grottos & Fairy Fountains"), ] -ootEnumSkybox = [ - ("Custom", "Custom", "Custom"), - ("0x00", "None", "None"), - ("0x01", "Standard Sky", "Standard Sky"), - ("0x02", "Hylian Bazaar", "Hylian Bazaar"), - ("0x03", "Brown Cloudy Sky", "Brown Cloudy Sky"), - ("0x04", "Market Ruins", "Market Ruins"), - ("0x05", "Black Cloudy Night", "Black Cloudy Night"), - ("0x07", "Link's House", "Link's House"), - ("0x09", "Market (Main Square, Day)", "Market (Main Square, Day)"), - ("0x0A", "Market (Main Square, Night)", "Market (Main Square, Night)"), - ("0x0B", "Happy Mask Shop", "Happy Mask Shop"), - ("0x0C", "Know-It-All Brothers' House", "Know-It-All Brothers' House"), - ("0x0E", "Kokiri Twins' House", "Kokiri Twins' House"), - ("0x0F", "Stable", "Stable"), - ("0x10", "Stew Lady's House", "Stew Lady's House"), - ("0x11", "Kokiri Shop", "Kokiri Shop"), - ("0x13", "Goron Shop", "Goron Shop"), - ("0x14", "Zora Shop", "Zora Shop"), - ("0x16", "Kakariko Potions Shop", "Kakariko Potions Shop"), - ("0x17", "Hylian Potions Shop", "Hylian Potions Shop"), - ("0x18", "Bomb Shop", "Bomb Shop"), - ("0x1A", "Dog Lady's House", "Dog Lady's House"), - ("0x1B", "Impa's House", "Impa's House"), - ("0x1C", "Gerudo Tent", "Gerudo Tent"), - ("0x1D", "Environment Color", "Environment Color"), - ("0x20", "Mido's House", "Mido's House"), - ("0x21", "Saria's House", "Saria's House"), - ("0x22", "Dog Guy's House", "Dog Guy's House"), -] - ootEnumSkyboxLighting = [ # see ``LightMode`` enum in ``z64environment.h`` ("Custom", "Custom", "Custom"), @@ -238,41 +185,6 @@ ("NA_BGM_NATURE_SFX_RAIN", "Nature Ambiance: Rain", "Nature Ambiance: Rain"), ] -ootEnumNightSeq = [ - ("Custom", "Custom", "Custom"), - ("0x00", "Standard night [day and night cycle]", "0x00"), - ("0x01", "Standard night [Kakariko]", "0x01"), - ("0x02", "Distant storm [Graveyard]", "0x02"), - ("0x03", "Howling wind and cawing [Ganon's Castle]", "0x03"), - ("0x04", "Wind + night birds [Kokiri]", "0x04"), - ("0x05", "Wind + crickets", "0x05"), - ("0x06", "Wind", "0x06"), - ("0x07", "Howling wind", "0x07"), - ("0x08", "Wind + crickets", "0x08"), - ("0x09", "Wind + crickets", "0x09"), - ("0x0A", "Tubed howling wind [Wasteland]", "0x0A"), - ("0x0B", "Tubed howling wind [Colossus]", "0x0B"), - ("0x0C", "Wind", "0x0C"), - ("0x0D", "Wind + crickets", "0x0D"), - ("0x0E", "Wind + crickets", "0x0E"), - ("0x0F", "Wind + birds", "0x0F"), - ("0x10", "Wind + crickets", "0x10"), - ("0x11", "?", "0x11"), - ("0x12", "Wind + crickets", "0x12"), - ("0x13", "Day music always playing", "0x13"), - ("0x14", "Silence", "0x14"), - ("0x16", "Silence", "0x16"), - ("0x17", "High tubed wind + rain", "0x17"), - ("0x18", "Silence", "0x18"), - ("0x19", "Silence", "0x19"), - ("0x1A", "High tubed wind + rain", "0x1A"), - ("0x1B", "Silence", "0x1B"), - ("0x1C", "Rain", "0x1C"), - ("0x1D", "High tubed wind + rain", "0x1D"), - ("0x1E", "Silence", "0x1E"), - ("0x1F", "High tubed wind + rain ", "0x1F"), -] - ootEnumGlobalObject = [ ("Custom", "Custom", "Custom"), ("OBJECT_INVALID", "None", "None"), @@ -531,78 +443,6 @@ # ("0xFF", "0xFF", "0xFF"), ] -# see curRoom.behaviorType1 -ootEnumRoomBehaviour = [ - ("Custom", "Custom", "Custom"), - ("0x00", "Default", "Default"), - ("0x01", "Dungeon Behavior (Z-Target, Sun's Song)", "Dungeon Behavior (Z-Target, Sun's Song)"), - ("0x02", "Disable Backflips/Sidehops", "Disable Backflips/Sidehops"), - ("0x03", "Disable Color Dither", "Disable Color Dither"), - ("0x04", "(?) Horse Camera Related", "(?) Horse Camera Related"), - ("0x05", "Disable Darker Screen Effect (NL/Spins)", "Disable Darker Screen Effect (NL/Spins)"), -] - -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/oot/cutscene/classes.py b/fast64_internal/z64/cutscene/classes.py similarity index 90% rename from fast64_internal/oot/cutscene/classes.py rename to fast64_internal/z64/cutscene/classes.py index 72739a8cd..9f2329a85 100644 --- a/fast64_internal/oot/cutscene/classes.py +++ b/fast64_internal/z64/cutscene/classes.py @@ -3,7 +3,9 @@ from dataclasses import dataclass, field from bpy.types import Object from typing import Optional -from ..oot_constants import ootData + +from ...utility import get_new_object +from ...game_data import game_data from .motion.utility import getBlenderPosition, getBlenderRotation, getRotation, getInteger @@ -25,13 +27,13 @@ class CutsceneCmdBase: endFrame: Optional[int] = None def getEnumValue(self, enumKey: str, index: int, isSeqLegacy: bool = False): - enum = ootData.enumData.enumByKey[enumKey] - item = enum.itemById.get(self.params[index]) + enum = game_data.z64.enums.enumByKey[enumKey] + item = enum.item_by_id.get(self.params[index]) if item is None: setting = getInteger(self.params[index]) if isSeqLegacy: setting -= 1 - item = enum.itemByIndex.get(setting) + item = enum.item_by_index.get(setting) return item.key if item is not None else self.params[index] @@ -50,6 +52,8 @@ def __post_init__(self): if self.params is not None: self.continueFlag = self.params[0] self.camRoll = getInteger(self.params[1]) + if self.camRoll >= 0x80: + self.camRoll -= 0x100 self.frame = getInteger(self.params[2]) self.viewAngle = cs_import_float(self.params[3]) self.pos = [getInteger(self.params[4]), getInteger(self.params[5]), getInteger(self.params[6])] @@ -96,12 +100,12 @@ def __post_init__(self): self.entryTotal = getInteger(self.params[0]) else: self.commandType = self.params[0] - if self.commandType.startswith("0x"): + if "CS_CMD_" in self.commandType: + self.commandType = game_data.z64.enums.enumByKey["cs_cmd"].item_by_id[self.commandType].key + else: # make it a 4 digit hex self.commandType = self.commandType.removeprefix("0x") self.commandType = "0x" + "0" * (4 - len(self.commandType)) + self.commandType - else: - self.commandType = ootData.enumData.enumByKey["csCmd"].itemById[self.commandType].key self.entryTotal = getInteger(self.params[1].strip()) @@ -202,7 +206,7 @@ def __post_init__(self): if self.params is not None: self.startFrame = getInteger(self.params[1]) self.endFrame = getInteger(self.params[2]) - self.type = self.getEnumValue("csMiscType", 0) + self.type = self.getEnumValue("cs_misc_type", 0) @dataclass @@ -231,7 +235,7 @@ def __post_init__(self): if self.params is not None: self.startFrame = getInteger(self.params[1]) self.endFrame = getInteger(self.params[2]) - self.type = self.getEnumValue("csTransitionType", 0) + self.type = self.getEnumValue("cs_transition_type", 0) @dataclass @@ -250,7 +254,7 @@ def __post_init__(self): self.startFrame = getInteger(self.params[1]) self.endFrame = getInteger(self.params[2]) self.textId = getInteger(self.params[0]) - self.type = self.getEnumValue("csTextType", 3) + self.type = self.getEnumValue("cs_text_type", 3) self.altTextId1 = (getInteger(self.params[4]),) self.altTextId2 = (getInteger(self.params[5]),) @@ -281,7 +285,7 @@ def __post_init__(self): if self.params is not None: self.startFrame = getInteger(self.params[1]) self.endFrame = getInteger(self.params[2]) - self.ocarinaActionId = self.getEnumValue("ocarinaSongActionId", 0) + self.ocarinaActionId = self.getEnumValue("ocarina_song_action_id", 0) self.messageId = getInteger(self.params[3]) @@ -372,7 +376,7 @@ def __post_init__(self): if self.params is not None: self.startFrame = getInteger(self.params[1]) self.endFrame = getInteger(self.params[2]) - self.seqId = self.getEnumValue("seqId", 0, self.isLegacy) + self.seqId = self.getEnumValue("seq_id", 0, self.isLegacy) @dataclass @@ -402,7 +406,7 @@ def __post_init__(self): if self.params is not None: self.startFrame = getInteger(self.params[1]) self.endFrame = getInteger(self.params[2]) - self.seqPlayer = self.getEnumValue("csFadeOutSeqPlayer", 0) + self.seqPlayer = self.getEnumValue("cs_fade_out_seq_player", 0) @dataclass @@ -461,7 +465,7 @@ class CutsceneCmdDestination(CutsceneCmdBase): def __post_init__(self): if self.params is not None: - self.id = self.getEnumValue("csDestination", 0) + self.id = self.getEnumValue("cs_destination", 0) self.startFrame = getInteger(self.params[1]) @@ -496,26 +500,14 @@ class Cutscene: class CutsceneObjectFactory: """This class contains functions to create new Blender objects""" - def getNewObject(self, name: str, data, selectObject: bool, parentObj: Object) -> Object: - newObj = bpy.data.objects.new(name=name, object_data=data) - bpy.context.view_layer.active_layer_collection.collection.objects.link(newObj) - if selectObject: - newObj.select_set(True) - bpy.context.view_layer.objects.active = newObj - newObj.parent = parentObj - newObj.location = [0.0, 0.0, 0.0] - newObj.rotation_euler = [0.0, 0.0, 0.0] - newObj.scale = [1.0, 1.0, 1.0] - return newObj - def getNewEmptyObject(self, name: str, selectObject: bool, parentObj: Object): - return self.getNewObject(name, None, selectObject, parentObj) + return get_new_object(name, None, selectObject, parent=parentObj) def getNewArmatureObject(self, name: str, selectObject: bool, parentObj: Object): newArmatureData = bpy.data.armatures.new(name) newArmatureData.display_type = "STICK" newArmatureData.show_names = True - newArmatureObject = self.getNewObject(name, newArmatureData, selectObject, parentObj) + newArmatureObject = get_new_object(name, newArmatureData, selectObject, parent=parentObj) return newArmatureObject def getNewCutsceneObject(self, name: str, frameCount: int, parentObj: Object): @@ -527,13 +519,15 @@ def getNewCutsceneObject(self, name: str, frameCount: int, parentObj: Object): def getNewActorCueListObject(self, name: str, commandType: str, parentObj: Object): newActorCueListObj = self.getNewEmptyObject(name, False, parentObj) newActorCueListObj.ootEmptyType = f"CS {'Player' if 'Player' in name else 'Actor'} Cue List" - cmdEnum = ootData.enumData.enumByKey["csCmd"] + cmdEnum = game_data.z64.enums.enumByKey["cs_cmd"] if commandType == "Player": commandType = "player_cue" - index = cmdEnum.itemByKey[commandType].index if commandType in cmdEnum.itemByKey else int(commandType, base=16) - item = cmdEnum.itemByIndex.get(index) + index = ( + cmdEnum.item_by_key[commandType].index if commandType in cmdEnum.item_by_key else int(commandType, base=16) + ) + item = cmdEnum.item_by_index.get(index) if item is not None: newActorCueListObj.ootCSMotionProperty.actorCueListProp.commandType = item.key @@ -566,11 +560,11 @@ def getNewActorCueObject( item = None if isPlayer: - playerEnum = ootData.enumData.enumByKey["csPlayerCueId"] + playerEnum = game_data.z64.enums.enumByKey["cs_player_cue_id"] if isinstance(actionID, int): - item = playerEnum.itemByIndex.get(actionID) + item = playerEnum.item_by_index.get(actionID) else: - item = playerEnum.itemByKey.get(actionID) + item = playerEnum.item_by_key.get(actionID) if item is not None: newActorCueObj.ootCSMotionProperty.actorCueProp.playerCueID = item.key @@ -591,7 +585,7 @@ def getNewCameraObject( self, name: str, displaySize: float, clipStart: float, clipEnd: float, alpha: float, parentObj: Object ): newCamera = bpy.data.cameras.new(name) - newCameraObj = self.getNewObject(name, newCamera, False, parentObj) + newCameraObj = get_new_object(name, newCamera, False, parent=parentObj) newCameraObj.data.display_size = displaySize newCameraObj.data.clip_start = clipStart newCameraObj.data.clip_end = clipEnd diff --git a/fast64_internal/oot/cutscene/constants.py b/fast64_internal/z64/cutscene/constants.py similarity index 89% rename from fast64_internal/oot/cutscene/constants.py rename to fast64_internal/z64/cutscene/constants.py index b3525246a..d07464571 100644 --- a/fast64_internal/oot/cutscene/constants.py +++ b/fast64_internal/z64/cutscene/constants.py @@ -1,4 +1,4 @@ -from ..oot_constants import ootData +from ...game_data import game_data from .classes import ( CutsceneCmdActorCueList, CutsceneCmdActorCue, @@ -47,19 +47,6 @@ ("Object", "Object", "Reference to Blender object representing cutscene", "", 2), ] -# order here sets order on the UI -ootEnumCSListType = [ - ("TextList", "Text List", "Textbox", "ALIGN_BOTTOM", 0), - ("MiscList", "Misc List", "Misc", "OPTIONS", 7), - ("RumbleList", "Rumble List", "Rumble Controller", "OUTLINER_OB_FORCE_FIELD", 8), - ("Transition", "Transition", "Transition", "COLORSET_10_VEC", 1), - ("LightSettingsList", "Light Settings List", "Lighting", "LIGHT_SUN", 2), - ("TimeList", "Time List", "Time", "TIME", 3), - ("StartSeqList", "Start Seq List", "Play BGM", "PLAY", 4), - ("StopSeqList", "Stop Seq List", "Stop BGM", "SNAP_FACE", 5), - ("FadeOutSeqList", "Fade-Out Seq List", "Fade BGM", "IPO_EASE_IN_OUT", 6), -] - csListTypeToIcon = { "TextList": "ALIGN_BOTTOM", "Transition": "COLORSET_10_VEC", @@ -122,8 +109,11 @@ ("eyeOrAT", "Eye/AT Point", "Single Eye/AT point (not recommended)"), ] +# Note: `CS_CMD_UNIMPLEMENTED_16` is an unused actor cue ootEnumCSActorCueListCommandType = [ - item for item in ootData.enumData.ootEnumCsCmd if "actor_cue" in item[0] or "player_cue" in item[0] + item + for item in game_data.z64.enums.enum_cs_cmd + if "actor_cue" in item[0] or "player_cue" in item[0] or item[0] == "unimplemented_16" ] ootEnumCSActorCueListCommandType.sort() ootEnumCSActorCueListCommandType.insert(0, ("Custom", "Custom", "Custom")) @@ -207,14 +197,14 @@ ] ootCSSingleCommands = [ - "CS_BEGIN_CUTSCENE", - "CS_END", + "CS_HEADER", + "CS_END_OF_SCRIPT", "CS_TRANSITION", "CS_DESTINATION", ] ootCSListAndSingleCommands = ootCSSingleCommands + ootCSListCommands -ootCSListAndSingleCommands.remove("CS_BEGIN_CUTSCENE") +ootCSListAndSingleCommands.remove("CS_HEADER") ootCutsceneCommandsC = ootCSSingleCommands + ootCSListCommands + ootCSListEntryCommands cmdToClass = { diff --git a/fast64_internal/oot/cutscene/importer/__init__.py b/fast64_internal/z64/cutscene/importer/__init__.py similarity index 100% rename from fast64_internal/oot/cutscene/importer/__init__.py rename to fast64_internal/z64/cutscene/importer/__init__.py diff --git a/fast64_internal/oot/cutscene/importer/classes.py b/fast64_internal/z64/cutscene/importer/classes.py similarity index 88% rename from fast64_internal/oot/cutscene/importer/classes.py rename to fast64_internal/z64/cutscene/importer/classes.py index ce76b694f..3e0e73eaa 100644 --- a/fast64_internal/oot/cutscene/importer/classes.py +++ b/fast64_internal/z64/cutscene/importer/classes.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import Optional, TYPE_CHECKING from bpy.types import Object, Armature -from ....utility import PluginError +from ....utility import PluginError, deselectAllObjects, get_include_data from ..motion.utility import setupCutscene, getBlenderPosition, getInteger if TYPE_CHECKING: @@ -69,9 +69,33 @@ def getCmdParams(self, data: str, cmdName: str, paramNumber: int): return params def getNewCutscene(self, csData: str, name: str): - params = self.getCmdParams(csData, "CS_BEGIN_CUTSCENE", Cutscene.paramNumber) + params = self.getCmdParams(csData, "CS_HEADER", Cutscene.paramNumber) return Cutscene(name, getInteger(params[0]), getInteger(params[1])) + def parse_cs_lines(self, cs_name: str, cs_data: list[str]): + """ + Returns a list of (stripped) lines containing each cutscene command + + Parameters: + - `cs_name`: the name of the cutscene we're reading + - `cs_data`: the raw data of the cutscene (with newlines, identatio, etc...) + """ + parsed_cs_lines: list[str] = [] + + for line in cs_data: + line = line.strip() + cs_cmd = line.split("(")[0] + + if cs_cmd == "": + continue + + if cs_cmd not in ootCutsceneCommandsC: + print(f"WARNING: unknown command found: {repr(cs_cmd)}") + + parsed_cs_lines.append(line) + + return parsed_cs_lines + def getParsedCutscenes(self): """Returns the parsed commands read from every cutscene we can find""" @@ -104,8 +128,8 @@ def getParsedCutscenes(self): fileLines.append(line.strip()) # parse cutscenes - csData = [] - cutsceneList: list[list[str]] = [] + cs_data_map: dict[str, list[str]] = {} + cs_list_map: dict[str, list[str]] = {} foundCutscene = False for line in fileLines: if not line.startswith("//") and not line.startswith("/*"): @@ -113,52 +137,59 @@ def getParsedCutscenes(self): # split with "[" just in case the array has a set size csName = line.split(" ")[1].split("[")[0] if csName in existingCutsceneNames: - continue + print(f"WARNING: Cutscene '{csName}' already exists in this blend's data.") foundCutscene = True + cs_data_map[csName] = [] + print(f"INFO: Found cutscene '{csName}' in the file data.") if foundCutscene: - sLine = line.strip() - csCmd = sLine.split("(")[0] - if "CutsceneData " not in line and "};" not in line and csCmd not in ootCutsceneCommandsC: - if len(csData) > 0: - csData[-1] += line + next_line = fileLines[fileLines.index(line) + 1] - if len(csData) == 0 or sLine.startswith("CS_") and not sLine.startswith("CS_FLOAT"): - if self.csName is None or self.csName == csName: - csData.append(line) - - if "};" in line: + if "#include" in next_line: + cs_data_map[csName] = get_include_data(next_line).split("\n") foundCutscene = False - cutsceneList.append(csData) - csData = [] + else: + s_line = line.strip() + cs_cmd = s_line.split("(")[0] - if len(cutsceneList) == 0: + if "CutsceneData " not in line and "};" not in line and cs_cmd not in ootCutsceneCommandsC: + if len(cs_data_map[csName]) > 0: + cs_data_map[csName][-1] += line + + if s_line.startswith("CS_") and not s_line.startswith("CS_FLOAT"): + if self.csName is None or self.csName == cs_name: + cs_data_map[csName].append(line) + + if "};" in line: + foundCutscene = False + + for cs_name, cs_data in cs_data_map.items(): + cs_list_map[cs_name] = self.parse_cs_lines(cs_name, cs_data) + + if len(cs_list_map) == 0: print("INFO: Found no cutscenes in this file!") return None # parse the commands from every cutscene we found parsedCutscenes: list[ParsedCutscene] = [] - for cutscene in cutsceneList: + for cs_name, cutscene in cs_list_map.items(): cmdListFound = False curCmdPrefix = None parsedCS = [] parsedData = "" - csName = None for line in cutscene: curCmd = line.strip().split("(")[0] index = cutscene.index(line) + 1 nextCmd = cutscene[index].strip().split("(")[0] if index < len(cutscene) else None line = line.strip() - if "CutsceneData" in line: - csName = line.split(" ")[1][:-2] # NOTE: ``CS_UNK_DATA()`` are commands that are completely useless, so we're ignoring those - if csName is not None and not "CS_UNK_DATA" in curCmd: + if "CS_UNK_DATA" not in curCmd: if curCmd in ootCutsceneCommandsC: line = line.removesuffix(",") + "\n" - if curCmd in ootCSSingleCommands and curCmd != "CS_END": + if curCmd in ootCSSingleCommands and curCmd != "CS_END_OF_SCRIPT": parsedData += line if not cmdListFound and curCmd in ootCSListCommands: @@ -177,17 +208,21 @@ def getParsedCutscenes(self): if curCmdPrefix in curCmd: parsedData += line elif not cmdListFound and curCmd in ootCSListEntryCommands: - print(f"{csName}, command:\n{line}") - raise PluginError(f"ERROR: Found a list entry outside a list inside ``{csName}``!") + print(f"{cs_name}, command:\n{line}") + raise PluginError(f"ERROR: Found a list entry outside a list inside ``{cs_name}``!") - if cmdListFound and nextCmd == "CS_END" or nextCmd in ootCSListAndSingleCommands: + if cmdListFound and nextCmd == "CS_END_OF_SCRIPT" or nextCmd in ootCSListAndSingleCommands: cmdListFound = False parsedCS.append(parsedData) parsedData = "" elif not "CutsceneData" in curCmd and not "};" in curCmd: print(f"WARNING: Unknown command found: ``{curCmd}``") cmdListFound = False - parsedCutscenes.append(ParsedCutscene(csName, parsedCS)) + + if cs_name is not None and len(parsedCS) > 0: + parsedCutscenes.append(ParsedCutscene(cs_name, parsedCS)) + else: + raise PluginError("ERROR: Something wrong happened during the parsing of the cutscene.") return parsedCutscenes @@ -200,6 +235,9 @@ def getCutsceneList(self): # if it's none then there's no cutscene in the file return None + if len(parsedCutscenes) == 0: + raise PluginError("ERROR: No cutscene was found.") + cutsceneList: list[Cutscene] = [] # for each cutscene from the list returned by getParsedCutscenes(), @@ -213,7 +251,7 @@ def getCutsceneList(self): cmdListName = cmdListData.strip().split("(")[0] # create a new cutscene data - if cmdListName == "CS_BEGIN_CUTSCENE": + if cmdListName == "CS_HEADER": cutscene = self.getNewCutscene(data, parsedCS.csName) # if we have a cutscene, create and add the commands data in it @@ -486,6 +524,9 @@ def setCutsceneData(self, csNumber): # if it's none then there's no cutscene in the file return csNumber + if len(cutsceneList) == 0: + raise PluginError("ERROR: No cutscene was found.") + for i, cutscene in enumerate(cutsceneList, csNumber): print(f'Found Cutscene "{cutscene.name}"! Importing...') self.validateCameraData(cutscene) @@ -544,7 +585,7 @@ def setCutsceneData(self, csNumber): # Init camera + preview objects and setup the scene setupCutscene(csObj) - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() print("Success!") # ``csNumber`` makes sure there's no duplicates diff --git a/fast64_internal/oot/cutscene/importer/functions.py b/fast64_internal/z64/cutscene/importer/functions.py similarity index 100% rename from fast64_internal/oot/cutscene/importer/functions.py rename to fast64_internal/z64/cutscene/importer/functions.py diff --git a/fast64_internal/oot/cutscene/motion/operators.py b/fast64_internal/z64/cutscene/motion/operators.py similarity index 76% rename from fast64_internal/oot/cutscene/motion/operators.py rename to fast64_internal/z64/cutscene/motion/operators.py index d5d5668e7..9a47a513d 100644 --- a/fast64_internal/oot/cutscene/motion/operators.py +++ b/fast64_internal/z64/cutscene/motion/operators.py @@ -3,8 +3,10 @@ from bpy.types import Object, Operator, Context, Armature from bpy.utils import register_class, unregister_class from bpy.props import StringProperty, EnumProperty, BoolProperty +import mathutils +from dataclasses import dataclass from ....utility import PluginError -from ...oot_constants import ootData +from ....game_data import game_data from ..classes import CutsceneObjectFactory from ..constants import ootEnumCSActorCueListCommandType from ..preview import initFirstFrame, setupCompositorNodes @@ -290,6 +292,126 @@ def execute(self, context): return {"CANCELLED"} +@dataclass +class SavedBone: + name: str + head: mathutils.Vector + tail: mathutils.Vector + shotPointFrame: int + shotPointViewAngle: float + shotPointRoll: int + + @staticmethod + def from_bone(bone: bpy.types.Bone): + return SavedBone( + bone.name, + bone.head, + bone.tail, + bone.ootCamShotPointProp.shotPointFrame, + bone.ootCamShotPointProp.shotPointViewAngle, + bone.ootCamShotPointProp.shotPointRoll, + ) + + def to_edit_bone(self, edit_bone: bpy.types.EditBone): + edit_bone.name = self.name + edit_bone.head = self.head + edit_bone.tail = self.tail + + def to_bone(self, bone: bpy.types.Bone): + bone.name = self.name + bone.ootCamShotPointProp.shotPointFrame = self.shotPointFrame + bone.ootCamShotPointProp.shotPointViewAngle = self.shotPointViewAngle + bone.ootCamShotPointProp.shotPointRoll = self.shotPointRoll + + +class CutsceneCmdMoveBone(Operator): + bl_idname = "object.fast64_cs_move_bone" + bl_label = "Move bone" + bl_options = {"REGISTER", "UNDO"} + + direction: EnumProperty( + items=( + ("UP", "Up", "Up"), + ("DOWN", "Down", "Down"), + ) + ) + + @classmethod + def poll(cls, context: Context): + return ( + context.object is not None + and context.object.type == "ARMATURE" + and context.object.mode in {"OBJECT", "EDIT"} + and context.active_bone is not None + ) + + def execute(self, context): + assert context.object is not None + assert context.active_bone is not None + armature = context.object.data + assert isinstance(armature, bpy.types.Armature) + + if context.object.mode == "EDIT": + prev_mode = "EDIT" + bpy.ops.object.mode_set(mode="OBJECT") + else: + prev_mode = "OBJECT" + + saved_bones = [SavedBone.from_bone(_b) for _b in armature.bones.values()] + + target_bone_name = context.active_bone.name + + i_bone_to_move = None + for i, sb in enumerate(saved_bones): + if sb.name == target_bone_name: + i_bone_to_move = i + break + assert i_bone_to_move is not None + + if (i_bone_to_move == 0 and self.direction == "UP") or ( + i_bone_to_move == len(saved_bones) - 1 and self.direction == "DOWN" + ): + # Can't move bone further + return {"FINISHED"} + + if self.direction == "UP": + assert i_bone_to_move != 0 + saved_bones_new_order = ( + saved_bones[: i_bone_to_move - 1] + + [saved_bones[i_bone_to_move], saved_bones[i_bone_to_move - 1]] + + saved_bones[i_bone_to_move + 1 :] + ) + elif self.direction == "DOWN": + assert i_bone_to_move != len(saved_bones) - 1 + saved_bones_new_order = ( + saved_bones[:i_bone_to_move] + + [saved_bones[i_bone_to_move + 1], saved_bones[i_bone_to_move]] + + saved_bones[i_bone_to_move + 2 :] + ) + else: + assert False, self.direction + + bpy.ops.object.mode_set(mode="EDIT") + while armature.edit_bones: + armature.edit_bones.remove(armature.edit_bones[0]) + for sb in saved_bones_new_order: + edit_bone = armature.edit_bones.new(sb.name) + sb.to_edit_bone(edit_bone) + + # If head==tail the edit_bone disappears when leaving edit mode + assert edit_bone.head != edit_bone.tail, edit_bone + + bpy.ops.object.mode_set(mode="OBJECT") + for sb, bone in zip(saved_bones_new_order, armature.bones): + sb.to_bone(bone) + + armature.bones[target_bone_name].select = True + armature.bones.active = armature.bones[target_bone_name] + + bpy.ops.object.mode_set(mode=prev_mode) + return {"FINISHED"} + + class OOT_SearchActorCueCmdTypeEnumOperator(Operator): bl_idname = "object.oot_search_actorcue_cmdtype_enum_operator" bl_label = "Select Command Type" @@ -318,7 +440,7 @@ class OOT_SearchPlayerCueIdEnumOperator(Operator): bl_property = "playerCueID" bl_options = {"REGISTER", "UNDO"} - playerCueID: EnumProperty(items=ootData.enumData.ootEnumCsPlayerCueId, default="cueid_none") + playerCueID: EnumProperty(items=game_data.z64.enums.enum_cs_player_cue_id, default="cueid_none") objName: StringProperty() def execute(self, context): @@ -342,6 +464,7 @@ def invoke(self, context, event): CutsceneCmdCreateCameraShot, CutsceneCmdCreatePlayerCueList, CutsceneCmdCreateActorCueList, + CutsceneCmdMoveBone, OOT_SearchActorCueCmdTypeEnumOperator, OOT_SearchPlayerCueIdEnumOperator, ) diff --git a/fast64_internal/oot/cutscene/motion/panels.py b/fast64_internal/z64/cutscene/motion/panels.py similarity index 97% rename from fast64_internal/oot/cutscene/motion/panels.py rename to fast64_internal/z64/cutscene/motion/panels.py index a2018fed4..35a7a52d4 100644 --- a/fast64_internal/oot/cutscene/motion/panels.py +++ b/fast64_internal/z64/cutscene/motion/panels.py @@ -5,7 +5,7 @@ class OOT_CSMotionCameraShotPanel(OOT_Panel): bl_label = "Cutscene Motion Camera Shot Controls" - bl_idname = "OOT_PT_camera_shot_panel" + bl_idname = "Z64_PT_camera_shot_panel" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" diff --git a/fast64_internal/oot/cutscene/motion/preview.py b/fast64_internal/z64/cutscene/motion/preview.py similarity index 100% rename from fast64_internal/oot/cutscene/motion/preview.py rename to fast64_internal/z64/cutscene/motion/preview.py diff --git a/fast64_internal/oot/cutscene/motion/properties.py b/fast64_internal/z64/cutscene/motion/properties.py similarity index 94% rename from fast64_internal/oot/cutscene/motion/properties.py rename to fast64_internal/z64/cutscene/motion/properties.py index d18a48282..fbc0de3ec 100644 --- a/fast64_internal/oot/cutscene/motion/properties.py +++ b/fast64_internal/z64/cutscene/motion/properties.py @@ -3,9 +3,9 @@ from bpy.types import PropertyGroup, Object, UILayout, Armature, Bone, Scene, EditBone from bpy.props import IntProperty, StringProperty, PointerProperty, EnumProperty, FloatProperty from bpy.utils import register_class, unregister_class -from ...oot_upgrade import upgradeCutsceneMotion -from ...oot_utility import getEnumName -from ...oot_constants import ootData +from ...upgrade import upgradeCutsceneMotion +from ...utility import getEnumName +from ....game_data import game_data from ..constants import ootEnumCSMotionCamMode, ootEnumCSActorCueListCommandType from .operators import ( @@ -13,6 +13,7 @@ CutsceneCmdCreateActorCuePreview, OOT_SearchActorCueCmdTypeEnumOperator, CutsceneCmdAddBone, + CutsceneCmdMoveBone, OOT_SearchPlayerCueIdEnumOperator, ) @@ -86,7 +87,7 @@ class CutsceneCmdActorCueProperty(PropertyGroup): get=lambda self: getNextCuesStartFrame(self), ) - playerCueID: EnumProperty(items=ootData.enumData.ootEnumCsPlayerCueId, default="cueid_none") + playerCueID: EnumProperty(items=game_data.z64.enums.enum_cs_player_cue_id, default="cueid_none") cueActionID: StringProperty( name="Action ID", default="0x0001", description="Actor action. Meaning is unique for each different actor." ) @@ -116,7 +117,7 @@ def draw_props(self, layout: UILayout, labelPrefix: str, isDummy: bool, objName: split = box.split(factor=0.5) searchOp = split.operator(OOT_SearchPlayerCueIdEnumOperator.bl_idname, icon="VIEWZOOM", text=label) searchOp.objName = objName - split.label(text=getEnumName(ootData.enumData.ootEnumCsPlayerCueId, self.playerCueID)) + split.label(text=getEnumName(game_data.z64.enums.enum_cs_player_cue_id, self.playerCueID)) if not isPlayer or self.playerCueID == "Custom": split = box.split(factor=0.5) @@ -229,6 +230,10 @@ def draw_props(self, layout: UILayout): for propName in ["shotPointFrame", "shotPointViewAngle", "shotPointRoll"]: row.prop(self, propName) + row = box.row() + row.operator(CutsceneCmdMoveBone.bl_idname, text="Move Up", icon="TRIA_UP").direction = "UP" + row.operator(CutsceneCmdMoveBone.bl_idname, text="Move Down", icon="TRIA_DOWN").direction = "DOWN" + class OOTCutsceneMotionProperty(PropertyGroup): actorCueListProp: PointerProperty(type=CutsceneCmdActorCueListProperty) diff --git a/fast64_internal/oot/cutscene/motion/utility.py b/fast64_internal/z64/cutscene/motion/utility.py similarity index 98% rename from fast64_internal/oot/cutscene/motion/utility.py rename to fast64_internal/z64/cutscene/motion/utility.py index fa8b7c06d..de0a73144 100644 --- a/fast64_internal/oot/cutscene/motion/utility.py +++ b/fast64_internal/z64/cutscene/motion/utility.py @@ -4,7 +4,7 @@ from bpy.types import Object, Bone, Context, EditBone, Armature from mathutils import Vector from ....utility import yUpToZUp -from ...oot_utility import ootParseRotation +from ...utility import ootParseRotation class BoneData: @@ -91,9 +91,11 @@ def getInteger(number: str): number = number.removeprefix("0x") # ``"0" * (8 - len(number)`` adds the missing zeroes (if necessary) to have a 8 digit hex number - return unpack("!i", bytes.fromhex("0" * (8 - len(number)) + number))[0] + value = unpack("!i", bytes.fromhex("0" * (8 - len(number)) + number))[0] else: - return int(number) + value = int(number) + + return value def getRotation(data: str): @@ -225,7 +227,6 @@ def getCameraShotBoneData(shotObj: Object, runChecks: bool): print("Camera armature bones are not allowed to have parent bones") return None boneDataList.append(BoneData(shotObj, bone)) - boneDataList.sort(key=lambda b: b.name) if runChecks: if boneDataList is None: diff --git a/fast64_internal/z64/cutscene/operators.py b/fast64_internal/z64/cutscene/operators.py new file mode 100644 index 000000000..040651ef3 --- /dev/null +++ b/fast64_internal/z64/cutscene/operators.py @@ -0,0 +1,214 @@ +import bpy + +from bpy.path import abspath +from bpy.ops import object +from bpy.props import StringProperty, EnumProperty, IntProperty +from bpy.types import Scene, Operator, Object +from bpy.utils import register_class, unregister_class +from ...utility import PluginError, ExportUtils, raisePluginError +from ...game_data import game_data +from ..collection_utility import getCollection +from .constants import ootEnumCSTextboxType +from .importer import importCutsceneData +from ..exporter.cutscene import Cutscene + + +class OOTCSTextAdd(Operator): + bl_idname = "object.oot_cstextbox_add" + bl_label = "Add CS Textbox" + bl_options = {"REGISTER", "UNDO"} + + collectionType: StringProperty() + textboxType: EnumProperty(items=ootEnumCSTextboxType) + listIndex: IntProperty() + objName: StringProperty() + + def execute(self, context): + collection = getCollection(self.objName, self.collectionType, self.listIndex) + newTextboxElement = collection.add() + newTextboxElement.textboxType = self.textboxType + return {"FINISHED"} + + +class OOTCSListAdd(Operator): + bl_idname = "object.oot_cslist_add" + bl_label = "Add CS List" + bl_options = {"REGISTER", "UNDO"} + + collectionType: StringProperty() + listType: EnumProperty(items=lambda self, context: game_data.z64.get_enum("cs_list_type")) + objName: StringProperty() + + def execute(self, context): + collection = getCollection(self.objName, self.collectionType, None) + newList = collection.add() + newList.listType = self.listType + return {"FINISHED"} + + +class OOT_ImportCutscene(Operator): + bl_idname = "object.oot_import_cutscenes" + bl_label = "Import Cutscenes" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + try: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + + path = abspath(context.scene.ootCutsceneImportPath) + csName = context.scene.ootCSImportName if len(context.scene.ootCSImportName) > 0 else None + context.scene.ootCSNumber = importCutsceneData(path, None, csName) + + self.report({"INFO"}, "Successfully imported cutscenes") + return {"FINISHED"} + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + +class OOT_ExportCutscene(Operator): + bl_idname = "object.oot_export_cutscene" + bl_label = "Export Cutscene" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + with ExportUtils() as export_utils: + try: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + + if context.scene.fast64.oot.export_cutscene_obj is not None: + cs_obj = context.scene.fast64.oot.export_cutscene_obj + else: + cs_obj = context.view_layer.objects.active + + if cs_obj is None or cs_obj.type != "EMPTY" or cs_obj.ootEmptyType != "Cutscene": + raise PluginError("You must select a cutscene object") + + if cs_obj.parent is not None: + raise PluginError("Cutscene object must not be parented to anything") + + Cutscene.export(cs_obj) + + self.report({"INFO"}, "Successfully exported cutscene") + return {"FINISHED"} + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + +class OOT_ExportAllCutscenes(Operator): + bl_idname = "object.oot_export_all_cutscenes" + bl_label = "Export All Cutscenes" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + with ExportUtils() as export_utils: + try: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + + cs_obj_list: list[Object] = [] + + for obj in context.view_layer.objects: + if obj.type == "EMPTY" and obj.ootEmptyType == "Cutscene": + if obj.parent is not None: + print(f"Parent: {obj.parent.name}, Object: {obj.name}") + raise PluginError("Cutscene object must not be parented to anything") + + cs_obj_list.append(obj) + + for count, cs_obj in enumerate(cs_obj_list, 1): + # skip the includes if this isn't the first cutscene + # skip the #endif directive if this isn't the last cutscene + Cutscene.export(cs_obj, count > 1, count < len(cs_obj_list)) + + if count == 0: + raise PluginError("Could not find any cutscenes to export") + + self.report({"INFO"}, "Successfully exported " + str(count) + " cutscenes") + return {"FINISHED"} + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + +class OOT_SearchCSDestinationEnumOperator(Operator): + bl_idname = "object.oot_search_cs_dest_enum_operator" + bl_label = "Choose Destination" + bl_property = "csDestination" + bl_options = {"REGISTER", "UNDO"} + + csDestination: EnumProperty(items=game_data.z64.enums.enum_cs_destination, default="cutscene_map_ganon_horse") + objName: StringProperty() + + def execute(self, context): + obj = bpy.data.objects[self.objName] + obj.ootCutsceneProperty.csDestination = self.csDestination + + context.region.tag_redraw() + self.report({"INFO"}, "Selected: " + self.csDestination) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + +class OOT_SearchCSSeqOperator(Operator): + bl_idname = "object.oot_search_cs_seq_enum_operator" + bl_label = "Search Music Sequence" + bl_property = "seqId" + bl_options = {"REGISTER", "UNDO"} + + seqId: EnumProperty(items=game_data.z64.enums.enum_seq_id, default="general_sfx") + itemIndex: IntProperty() + listType: StringProperty() + + def execute(self, context): + csProp = context.view_layer.objects.active.ootCutsceneProperty + for elem in csProp.csLists: + if elem.listType == self.listType: + elem.seqList[self.itemIndex].csSeqID = self.seqId + break + context.region.tag_redraw() + self.report({"INFO"}, "Selected: " + self.seqId) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {"RUNNING_MODAL"} + + +oot_cutscene_classes = ( + OOTCSTextAdd, + OOTCSListAdd, + OOT_ImportCutscene, + OOT_ExportCutscene, + OOT_ExportAllCutscenes, + OOT_SearchCSDestinationEnumOperator, + OOT_SearchCSSeqOperator, +) + + +def cutscene_ops_register(): + for cls in oot_cutscene_classes: + register_class(cls) + + Scene.ootCutsceneExportPath = StringProperty(name="File", subtype="FILE_PATH") + Scene.ootCutsceneImportPath = StringProperty(name="File", subtype="FILE_PATH") + Scene.ootCSNumber = IntProperty(default=1, min=0) + Scene.ootCSImportName = StringProperty( + name="CS Name", description="Used to import a single cutscene, can be ``None``" + ) + + +def cutscene_ops_unregister(): + for cls in reversed(oot_cutscene_classes): + unregister_class(cls) + + del Scene.ootCSImportName + del Scene.ootCSNumber + del Scene.ootCutsceneImportPath + del Scene.ootCutsceneExportPath diff --git a/fast64_internal/z64/cutscene/panels.py b/fast64_internal/z64/cutscene/panels.py new file mode 100644 index 000000000..81a9ecd0d --- /dev/null +++ b/fast64_internal/z64/cutscene/panels.py @@ -0,0 +1,77 @@ +from bpy.utils import register_class, unregister_class +from bpy.types import Scene +from bpy.props import BoolProperty +from ...utility import prop_split +from ...panels import OOT_Panel +from .operators import OOT_ExportCutscene, OOT_ExportAllCutscenes, OOT_ImportCutscene + + +class OoT_PreviewSettingsPanel(OOT_Panel): + bl_idname = "Z64_PT_preview_settings" + bl_label = "CS Preview Settings" + + def draw(self, context): + context.scene.ootPreviewSettingsProperty.draw_props(self.layout) + + +class OOT_CutscenePanel(OOT_Panel): + bl_idname = "Z64_PT_export_cutscene" + bl_label = "Cutscene Exporter" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + + def draw(self, context): + layout = self.layout + + export_box = layout.box().column() + export_box.label(text="Cutscene Exporter") + + prop_split(export_box, context.scene, "ootCutsceneExportPath", "Export To") + prop_split(export_box, context.scene.fast64.oot, "export_cutscene_obj", "CS Object") + + cs_obj = context.scene.fast64.oot.export_cutscene_obj + + if cs_obj is None: + cs_obj = context.view_layer.objects.active + + label = None + if cs_obj is None or cs_obj.type != "EMPTY" or cs_obj.ootEmptyType != "Cutscene": + label = "Select a cutscene object" + + if cs_obj is not None and cs_obj.parent is not None: + label = "Cutscene object must not be parented to anything" + + export_op_layout = export_box.column() + + if label is not None: + export_box.label(text=label) + export_op_layout.enabled = False + + export_op_layout.operator(OOT_ExportCutscene.bl_idname) + export_box.operator(OOT_ExportAllCutscenes.bl_idname) + + import_box = layout.box().column() + import_box.label(text="Cutscene Importer") + prop_split(import_box, context.scene, "ootCSImportName", "Import") + prop_split(import_box, context.scene, "ootCutsceneImportPath", "From") + + if len(context.scene.ootCSImportName) == 0: + import_box.label(text="All Cutscenes will be imported.") + + import_box.operator(OOT_ImportCutscene.bl_idname) + + +oot_cutscene_panel_classes = ( + OoT_PreviewSettingsPanel, + OOT_CutscenePanel, +) + + +def cutscene_panels_register(): + for cls in oot_cutscene_panel_classes: + register_class(cls) + + +def cutscene_panels_unregister(): + for cls in oot_cutscene_panel_classes: + unregister_class(cls) diff --git a/fast64_internal/oot/cutscene/preview.py b/fast64_internal/z64/cutscene/preview.py similarity index 93% rename from fast64_internal/oot/cutscene/preview.py rename to fast64_internal/z64/cutscene/preview.py index 9d3a28830..b3a4f8c40 100644 --- a/fast64_internal/oot/cutscene/preview.py +++ b/fast64_internal/z64/cutscene/preview.py @@ -1,11 +1,15 @@ import bpy from math import isclose +from typing import TYPE_CHECKING from bpy.types import Scene, Object, Node from bpy.app.handlers import persistent from ...utility import gammaInverse, hexOrDecInt from .motion.utility import getCutsceneCamera +if TYPE_CHECKING: + from .properties import OOTCutscenePreviewSettingsProperty, OOTCutscenePreviewProperty + def getLerp(max: float, min: float, val: float): # from ``Environment_LerpWeight()`` in decomp @@ -112,13 +116,14 @@ def initFirstFrame(csObj: Object, useNodeFeatures: bool, defaultCam: Object): def processCurrentFrame(csObj: Object, curFrame: float, useNodeFeatures: bool, cameraObjects: Object): """Execute the actions of each command to create the preview for the current frame""" # this function was partially adapted from ``z_demo.c`` + previewProp: "OOTCutscenePreviewProperty" = csObj.ootCutsceneProperty.preview + preview_settings: "OOTCutscenePreviewSettingsProperty" = bpy.context.scene.ootPreviewSettingsProperty if curFrame == 0: initFirstFrame(csObj, useNodeFeatures, cameraObjects[1]) if useNodeFeatures: - previewProp = csObj.ootCutsceneProperty.preview - for transitionCmd in csObj.ootCutsceneProperty.preview.transitionList: + for transitionCmd in previewProp.transitionList: startFrame = transitionCmd.startFrame endFrame = transitionCmd.endFrame frameCur = curFrame @@ -168,7 +173,7 @@ def processCurrentFrame(csObj: Object, curFrame: float, useNodeFeatures: bool, c color[3] = alpha bpy.context.scene.node_tree.nodes["CSTrans_RGB"].outputs[0].default_value = color - for miscCmd in csObj.ootCutsceneProperty.preview.miscList: + for miscCmd in previewProp.miscList: startFrame = miscCmd.startFrame endFrame = miscCmd.endFrame @@ -177,10 +182,9 @@ def processCurrentFrame(csObj: Object, curFrame: float, useNodeFeatures: bool, c if curFrame == startFrame: if miscCmd.type == "set_locked_viewpoint" and not None in cameraObjects: - bpy.context.scene.camera = cameraObjects[int(csObj.ootCutsceneProperty.preview.isFixedCamSet)] - csObj.ootCutsceneProperty.preview.isFixedCamSet ^= True - - elif miscCmd.type == "stop_cutscene": + bpy.context.scene.camera = cameraObjects[int(previewProp.isFixedCamSet)] + previewProp.isFixedCamSet ^= True + elif not preview_settings.ignore_cs_misc_stop and miscCmd.type == "stop_cutscene": # stop the playback and set the frame to 0 bpy.ops.screen.animation_cancel() bpy.context.scene.frame_set(bpy.context.scene.frame_start) @@ -219,7 +223,7 @@ def processCurrentFrame(csObj: Object, curFrame: float, useNodeFeatures: bool, c @persistent def cutscenePreviewFrameHandler(scene: Scene): """Preview frame handler, executes each frame when the cutscene is played""" - previewSettings = scene.ootPreviewSettingsProperty + previewSettings: "OOTCutscenePreviewSettingsProperty" = scene.ootPreviewSettingsProperty csObj: Object = previewSettings.ootCSPreviewCSObj if csObj is None or not csObj.type == "EMPTY" and not csObj.ootEmptyType == "Cutscene": diff --git a/fast64_internal/oot/cutscene/properties.py b/fast64_internal/z64/cutscene/properties.py similarity index 90% rename from fast64_internal/oot/cutscene/properties.py rename to fast64_internal/z64/cutscene/properties.py index f2f6c3c58..86243c78e 100644 --- a/fast64_internal/oot/cutscene/properties.py +++ b/fast64_internal/z64/cutscene/properties.py @@ -2,9 +2,10 @@ from bpy.props import StringProperty, EnumProperty, IntProperty, BoolProperty, CollectionProperty, PointerProperty from bpy.utils import register_class, unregister_class from ...utility import PluginError, prop_split -from ..oot_utility import OOTCollectionAdd, drawCollectionOps, getEnumName -from ..oot_constants import ootData -from ..oot_upgrade import upgradeCutsceneSubProps, upgradeCSListProps, upgradeCutsceneProperty +from ...game_data import game_data +from ..collection_utility import OOTCollectionAdd, drawCollectionOps +from ..utility import getEnumName +from ..upgrade import upgradeCutsceneSubProps, upgradeCSListProps, upgradeCutsceneProperty from .operators import OOTCSTextAdd, OOT_SearchCSDestinationEnumOperator, OOTCSListAdd, OOT_SearchCSSeqOperator from .motion.preview import previewFrameHandler from .motion.utility import getCutsceneCamera @@ -18,7 +19,6 @@ from .constants import ( ootEnumCSTextboxType, - ootEnumCSListType, ootEnumCSTextboxTypeIcons, ootCSSubPropToName, csListTypeToIcon, @@ -113,13 +113,13 @@ class OOTCSTextProperty(OOTCutsceneCommon, PropertyGroup): # subprops textID: StringProperty(name="", default="0x0000") ocarinaAction: EnumProperty( - name="Ocarina Action", items=ootData.enumData.ootEnumOcarinaSongActionId, default="teach_minuet" + name="Ocarina Action", items=game_data.z64.enums.enum_ocarina_song_action_id, default="teach_minuet" ) ocarinaActionCustom: StringProperty(default="OCARINA_ACTION_CUSTOM") topOptionTextID: StringProperty(name="", default="0x0000") bottomOptionTextID: StringProperty(name="", default="0x0000") ocarinaMessageId: StringProperty(name="", default="0x0000") - csTextType: EnumProperty(name="Text Type", items=ootData.enumData.ootEnumCsTextType, default="normal") + csTextType: EnumProperty(name="Text Type", items=game_data.z64.enums.enum_cs_text_type, default="normal") csTextTypeCustom: StringProperty(default="CS_TEXT_CUSTOM") def getName(self): @@ -152,10 +152,10 @@ class OOTCSTimeProperty(OOTCutsceneCommon, PropertyGroup): class OOTCSSeqProperty(OOTCutsceneCommon, PropertyGroup): attrName = "seqList" subprops = ["csSeqID", "startFrame", "endFrame"] - csSeqID: EnumProperty(name="Seq ID", items=ootData.enumData.ootEnumSeqId, default="general_sfx") + csSeqID: EnumProperty(name="Seq ID", items=game_data.z64.enums.enum_seq_id, default="general_sfx") csSeqIDCustom: StringProperty(default="NA_BGM_CUSTOM") csSeqPlayer: EnumProperty( - name="Seq Player", items=ootData.enumData.ootEnumCsFadeOutSeqPlayer, default="fade_out_fanfare" + name="Seq Player", items=game_data.z64.enums.enum_cs_fade_out_seq_player, default="fade_out_fanfare" ) csSeqPlayerCustom: StringProperty(default="CS_FADE_OUT_CUSTOM") @@ -171,7 +171,7 @@ def filterName(self, name, listProp): class OOTCSMiscProperty(OOTCutsceneCommon, PropertyGroup): attrName = "miscList" subprops = ["csMiscType", "startFrame", "endFrame"] - csMiscType: EnumProperty(name="Type", items=ootData.enumData.ootEnumCsMiscType, default="rain") + csMiscType: EnumProperty(name="Type", items=game_data.z64.enums.enum_cs_misc_type, default="rain") csMiscTypeCustom: StringProperty(default="CS_MISC_CUSTOM") @@ -189,7 +189,7 @@ class OOTCSRumbleProperty(OOTCutsceneCommon, PropertyGroup): class OOTCSListProperty(PropertyGroup): expandTab: BoolProperty(default=True) - listType: EnumProperty(items=ootEnumCSListType) + listType: EnumProperty(items=lambda self, context: game_data.z64.get_enum("cs_list_type")) textList: CollectionProperty(type=OOTCSTextProperty) lightSettingsList: CollectionProperty(type=OOTCSLightSettingsProperty) timeList: CollectionProperty(type=OOTCSTimeProperty) @@ -197,14 +197,15 @@ class OOTCSListProperty(PropertyGroup): miscList: CollectionProperty(type=OOTCSMiscProperty) rumbleList: CollectionProperty(type=OOTCSRumbleProperty) - transitionType: EnumProperty(items=ootData.enumData.ootEnumCsTransitionType, default="gray_fill_in") + transitionType: EnumProperty(items=game_data.z64.enums.enum_cs_transition_type, default="gray_fill_in") transitionTypeCustom: StringProperty(default="CS_TRANS_CUSTOM") transitionStartFrame: IntProperty(name="", default=0, min=0) transitionEndFrame: IntProperty(name="", default=1, min=0) def draw_props(self, layout: UILayout, listIndex: int, objName: str, collectionType: str): box = layout.box().column() - enumName = getEnumName(ootEnumCSListType, self.listType) + enum_cs_list_type = game_data.z64.get_enum("cs_list_type") + enumName = getEnumName(enum_cs_list_type, self.listType) # Draw current command tab box.prop( @@ -263,7 +264,7 @@ def draw_props(self, layout: UILayout, listIndex: int, objName: str, collectionT addOp.objName = objName else: addOp = box.operator( - OOTCollectionAdd.bl_idname, text="Add item to " + getEnumName(ootEnumCSListType, self.listType) + OOTCollectionAdd.bl_idname, text="Add item to " + getEnumName(enum_cs_list_type, self.listType) ) addOp.option = len(data) addOp.collectionType = collectionType + "." + attrName @@ -277,7 +278,7 @@ def draw_props(self, layout: UILayout, listIndex: int, objName: str, collectionT p.draw_props(box, self, listIndex, i, objName, collectionType, enumName.removesuffix(" List")) if len(data) == 0: - box.label(text="No items in " + getEnumName(ootEnumCSListType, self.listType)) + box.label(text="No items in " + getEnumName(enum_cs_list_type, self.listType)) class OOTCutsceneCommandBase: @@ -322,6 +323,8 @@ class OOTCutscenePreviewSettingsProperty(PropertyGroup): default="link_adult", ) + ignore_cs_misc_stop: BoolProperty(name="Ignore 'Stop Cutscene' Command", default=False) + # internal only ootCSPreviewNodesReady: BoolProperty(default=False) ootCSPreviewCSObj: PointerProperty(type=Object) @@ -350,18 +353,19 @@ def draw_props(self, layout: UILayout): prop_split(previewBox, self, "previewPlayerAge", "Player Age for Preview") previewBox.prop(self, "useWidescreen") previewBox.prop(self, "useOpaqueCamBg") + previewBox.prop(self, "ignore_cs_misc_stop") class OOTCutsceneProperty(PropertyGroup): csEndFrame: IntProperty(name="End Frame", min=0, default=100) csUseDestination: BoolProperty(name="Cutscene Destination (Scene Change)") csDestination: EnumProperty( - name="Destination", items=ootData.enumData.ootEnumCsDestination, default="cutscene_map_ganon_horse" + name="Destination", items=game_data.z64.enums.enum_cs_destination, default="cutscene_map_ganon_horse" ) csDestinationCustom: StringProperty(default="CS_DEST_CUSTOM") csDestinationStartFrame: IntProperty(name="Start Frame", min=0, default=99) csLists: CollectionProperty(type=OOTCSListProperty, name="Cutscene Lists") - menuTab: EnumProperty(items=ootEnumCSListType) + menuTab: EnumProperty(items=lambda self, context: game_data.z64.get_enum("cs_list_type")) preview: PointerProperty(type=OOTCutscenePreviewProperty) @@ -407,7 +411,7 @@ def draw_props(self, layout: UILayout, obj: Object): boxRow = searchBox.row() searchOp = boxRow.operator(OOT_SearchCSDestinationEnumOperator.bl_idname, icon="VIEWZOOM", text="") searchOp.objName = obj.name - boxRow.label(text=getEnumName(ootData.enumData.ootEnumCsDestination, self.csDestination)) + boxRow.label(text=getEnumName(game_data.z64.enums.enum_cs_destination, self.csDestination)) if self.csDestination == "Custom": prop_split(searchBox.column(), self, "csDestinationCustom", "Cutscene Destination Custom") diff --git a/fast64_internal/oot/cutscene_docs.md b/fast64_internal/z64/cutscene_docs.md similarity index 97% rename from fast64_internal/oot/cutscene_docs.md rename to fast64_internal/z64/cutscene_docs.md index e22acb573..5e4c49d75 100644 --- a/fast64_internal/oot/cutscene_docs.md +++ b/fast64_internal/z64/cutscene_docs.md @@ -8,16 +8,16 @@ - "Seq": Sequence, it refers to the audio (most of the time it's the background music) ### Commands -More detailed informations and commands' parameters can be found [here](https://github.com/zeldaret/oot/blob/master/include/z64cutscene_commands.h) +More detailed informations and commands' parameters can be found [here](https://github.com/zeldaret/oot/blob/main/include/z64cutscene_commands.h) -- ``CS_BEGIN_CUTSCENE``: defines the beginning of a cutscene script -- ``CS_END``: defines the end of a cutscene script +- ``CS_HEADER``: defines the length and the total number of command entries for a cutscene script +- ``CS_END_OF_SCRIPT``: defines the end of a command list in a cutscene script - ``CS_CAM_POINT``: defines a single camera point, it can be used with any of the "eye" or "at" camera commands - ``CS_CAM_EYE``: defines a single eye point, this feature is not used in the final game and lacks polish - ``CS_CAM_EYE_SPLINE``: declares a list of "eye" camera points that forms a spline - ``CS_CAM_AT_SPLINE``: declares a list of "at" camera points that forms a spline - ``CS_CAM_EYE_SPLINE_REL_TO_PLAYER`` and ``CS_CAM_AT_SPLINE_REL_TO_PLAYER``: same as the 2 above except these are relative to the player's position and yaw. -- ``CS_MISC_LIST``: declares a list of various miscellaneous commands, they're all self-explanatory, you can find the list [here](https://github.com/zeldaret/oot/blob/master/include/z64cutscene.h#L167-L204) +- ``CS_MISC_LIST``: declares a list of various miscellaneous commands, they're all self-explanatory, you can find the list [here](https://github.com/zeldaret/oot/blob/f7a270655bd974242a286856bd56b490a998ac44/include/z64cutscene.h#L156-L193) - ``CS_MISC``: defines a single misc command - ``CS_LIGHT_SETTING_LIST``: declares a list of light settings commands - ``CS_LIGHT_SETTING``: changes the light settings to the specified index, the lighting is defined in the scene diff --git a/fast64_internal/oot/exporter/__init__.py b/fast64_internal/z64/exporter/__init__.py similarity index 95% rename from fast64_internal/oot/exporter/__init__.py rename to fast64_internal/z64/exporter/__init__.py index e85ef12fc..f88205c18 100644 --- a/fast64_internal/oot/exporter/__init__.py +++ b/fast64_internal/z64/exporter/__init__.py @@ -4,8 +4,8 @@ from mathutils import Matrix from bpy.types import Object from ...f3d.f3d_gbi import DLFormat, TextureExportSettings -from ..oot_model_classes import OOTModel -from ..oot_f3d_writer import writeTextureArraysNew, writeTextureArraysExisting1D +from ..model_classes import OOTModel +from ..f3d_writer import writeTextureArraysNew, writeTextureArraysExisting1D from .scene import Scene from .decomp_edit import Files @@ -19,7 +19,7 @@ writeFile, ) -from ..oot_utility import ( +from ..utility import ( ExportInfo, OOTObjectCategorizer, ootDuplicateHierarchy, @@ -72,10 +72,10 @@ def create_scene(originalSceneObj: Object, transform: Matrix, exportInfo: Export sceneName = f"{toAlnum(exportInfo.name)}_scene" newScene = Scene.new( sceneName, + originalSceneObj, sceneObj, transform, - exportInfo.useMacros, - exportInfo.saveTexturesAsPNG, + exportInfo, OOTModel(f"{sceneName}_dl", DLFormat.Static, False), ) newScene.validateScene() diff --git a/fast64_internal/oot/exporter/actor.py b/fast64_internal/z64/exporter/actor.py similarity index 100% rename from fast64_internal/oot/exporter/actor.py rename to fast64_internal/z64/exporter/actor.py diff --git a/fast64_internal/oot/animation/exporter/__init__.py b/fast64_internal/z64/exporter/animation/__init__.py similarity index 100% rename from fast64_internal/oot/animation/exporter/__init__.py rename to fast64_internal/z64/exporter/animation/__init__.py diff --git a/fast64_internal/oot/animation/exporter/classes.py b/fast64_internal/z64/exporter/animation/classes.py similarity index 62% rename from fast64_internal/oot/animation/exporter/classes.py rename to fast64_internal/z64/exporter/animation/classes.py index 0f60ac998..ef3be8b44 100644 --- a/fast64_internal/oot/animation/exporter/classes.py +++ b/fast64_internal/z64/exporter/animation/classes.py @@ -1,3 +1,5 @@ +import bpy + from ....utility import CData, toAlnum @@ -6,8 +8,9 @@ def convertToUnsignedShort(value: int) -> int: class OOTAnimation: - def __init__(self, name): + def __init__(self, name, filename: str): self.name = toAlnum(name) + self.filename = filename self.segmentID = None self.indices = {} self.values = [] @@ -22,7 +25,16 @@ def indicesName(self): def toC(self): data = CData() - data.source += '#include "ultra64.h"\n#include "global.h"\n\n' + + data.header = f"#ifndef {self.filename.upper()}_H\n" + f"#define {self.filename.upper()}_H\n\n" + + if bpy.context.scene.fast64.oot.is_globalh_present(): + data.header += '#include "ultra64.h"\n' + '#include "global.h"\n\n' + elif bpy.context.scene.fast64.oot.is_z64sceneh_present(): + data.header += '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "z64animation.h"\n\n' + else: + data.header += '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "animation.h"\n\n' + data.source = f'#include "{self.filename}.h"\n\n' # values data.source += "s16 " + self.valuesName() + "[" + str(len(self.values)) + "] = {\n" @@ -68,6 +80,7 @@ def toC(self): + " };\n\n" ) + data.header += "\n#endif\n" return data @@ -84,14 +97,32 @@ def toC(self, isCustomExport: bool): data = CData() animHeaderData = CData() - data.source += '#include "ultra64.h"\n#include "global.h"\n\n' - animHeaderData.source += '#include "ultra64.h"\n#include "global.h"\n\n' + data.header = f"#ifndef {self.dataName().upper()}_H\n" + f"#define {self.dataName().upper()}_H\n\n" + + animHeaderData.header = f"#ifndef {self.headerName.upper()}_H\n" + f"#define {self.headerName.upper()}_H\n\n" + + if bpy.context.scene.fast64.oot.is_globalh_present(): + data.header = '#include "ultra64.h"\n' + '#include "global.h"\n\n' + animHeaderData.header = '#include "ultra64.h"\n' + '#include "global.h"\n\n' + elif bpy.context.scene.fast64.oot.is_z64sceneh_present(): + data.header = '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "z64animation.h"\n\n' + animHeaderData.header = ( + '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "z64animation.h"\n\n' + ) + else: + data.header = '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "animation.h"\n\n' + animHeaderData.header = ( + '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "animation.h"\n\n' + ) + + data.source = f'#include "{self.dataName()}.h"\n\n' + animHeaderData.source = f'#include "{self.headerName}.h"\n' # TODO: handle custom import? if isCustomExport: - animHeaderData.source += f'#include "{self.dataName()}.h"\n' + animHeaderData.source += f'#include "{self.dataName()}.h"\n\n' else: - animHeaderData.source += f'#include "assets/misc/link_animetion/{self.dataName()}.h"\n' + animHeaderData.source += f'#include "assets/misc/link_animetion/{self.dataName()}.h"\n\n' # data data.header += f"extern s16 {self.dataName()}[];\n" @@ -113,4 +144,6 @@ def toC(self, isCustomExport: bool): f"LinkAnimationHeader {self.headerName} = {{\n\t{{ {str(self.frameCount)} }}, {self.dataName()} \n}};\n\n" ) + data.header += "\n#endif\n" + animHeaderData.header += "\n#endif\n" return data, animHeaderData diff --git a/fast64_internal/oot/animation/exporter/functions.py b/fast64_internal/z64/exporter/animation/functions.py similarity index 98% rename from fast64_internal/oot/animation/exporter/functions.py rename to fast64_internal/z64/exporter/animation/functions.py index 78585ae2c..dc462123b 100644 --- a/fast64_internal/oot/animation/exporter/functions.py +++ b/fast64_internal/z64/exporter/animation/functions.py @@ -2,7 +2,7 @@ import mathutils import bpy from ....utility import PluginError, toAlnum -from ...skeleton.exporter import ootConvertArmatureToSkeletonWithoutMesh +from ..skeleton import ootConvertArmatureToSkeletonWithoutMesh from .classes import OOTAnimation, OOTLinkAnimation from ....utility_anim import ( @@ -14,7 +14,7 @@ stashActionInArmature, ) -from ...oot_utility import ( +from ...utility import ( checkForStartBone, getStartBone, getSortedChildren, @@ -211,14 +211,14 @@ def ootConvertLinkAnimationData(anim, armatureObj, convertTransformMatrix, *, fr return frameData -def ootExportNonLinkAnimation(armatureObj, convertTransformMatrix, skeletonName): +def ootExportNonLinkAnimation(armatureObj, convertTransformMatrix, skeletonName, filename: str): if armatureObj.animation_data is None or armatureObj.animation_data.action is None: raise PluginError("No active animation selected.") anim = armatureObj.animation_data.action stashActionInArmature(armatureObj, anim) - ootAnim = OOTAnimation(toAlnum(skeletonName + anim.name.capitalize() + "Anim")) + ootAnim = OOTAnimation(toAlnum(skeletonName + anim.name.capitalize() + "Anim"), filename) skeleton = ootConvertArmatureToSkeletonWithoutMesh(armatureObj, convertTransformMatrix, skeletonName) diff --git a/fast64_internal/oot/exporter/collision/__init__.py b/fast64_internal/z64/exporter/collision/__init__.py similarity index 70% rename from fast64_internal/oot/exporter/collision/__init__.py rename to fast64_internal/z64/exporter/collision/__init__.py index 3a1ad4949..da8f34b28 100644 --- a/fast64_internal/oot/exporter/collision/__init__.py +++ b/fast64_internal/z64/exporter/collision/__init__.py @@ -1,12 +1,32 @@ -import math +import bpy +import ctypes +from pathlib import Path from dataclasses import dataclass from mathutils import Matrix, Vector from bpy.types import Mesh, Object from bpy.ops import object from typing import Optional -from ....utility import PluginError, CData, indent -from ...oot_utility import convertIntTo2sComplement + +from ....utility import ( + PluginError, + CData, + toAlnum, + unhideAllAndGetHiddenState, + restoreHiddenState, + cleanupDuplicatedObjects, + indent, +) + +from ...utility import ( + OOTObjectCategorizer, + convertIntTo2sComplement, + ootDuplicateHierarchy, + ootGetPath, + ootGetObjectPath, +) + +from ...collision.properties import OOTCollisionExportSettings from ..utility import Utility from .polygons import CollisionPoly, CollisionPolygons from .surface import SurfaceType, SurfaceTypes @@ -89,8 +109,9 @@ def getCollisionData(dataHolder: Optional[Object], transform: Matrix, useMacros: raise PluginError(f"'{meshObj.name}' must have a material associated with it.") meshObj.data.calc_loop_triangles() - for face in meshObj.data.loop_triangles: - colProp = meshObj.material_slots[face.material_index].material.ootCollisionProperty + for i, face in enumerate(meshObj.data.loop_triangles): + material = meshObj.material_slots[face.material_index].material + colProp = material.ootCollisionProperty # get bounds and vertices data planePoint = transform @ meshObj.data.vertices[face.vertices[0]].co @@ -150,44 +171,23 @@ def getCollisionData(dataHolder: Optional[Object], transform: Matrix, useMacros: indices[1], indices[2] = indices[2], indices[1] # get surface type and collision poly data - useConveyor = colProp.conveyorOption != "None" - conveyorSpeed = int(Utility.getPropValue(colProp, "conveyorSpeed"), base=16) if useConveyor else 0 - shouldKeepMomentum = colProp.conveyorKeepMomentum if useConveyor else False - surfaceType = SurfaceType( - colProp.cameraID, - colProp.exitID, - Utility.getPropValue(colProp, "floorProperty"), - 0, # unused? - Utility.getPropValue(colProp, "wallSetting"), - Utility.getPropValue(colProp, "floorSetting"), - colProp.decreaseHeight, - colProp.eponaBlock, - Utility.getPropValue(colProp, "sound"), - Utility.getPropValue(colProp, "terrain"), - colProp.lightingSetting, - int(colProp.echo, base=16), - colProp.hookshotable, - conveyorSpeed + (4 if shouldKeepMomentum else 0), - int(colProp.conveyorRotation / (2 * math.pi) * 0x3F) if useConveyor else 0, - colProp.isWallDamage, - useMacros, - ) + surfaceType = SurfaceType.new(colProp, useMacros, material) if surfaceType not in colPolyFromSurfaceType: colPolyFromSurfaceType[surfaceType] = [] - colPolyFromSurfaceType[surfaceType].append( - CollisionPoly( - indices, - colProp.ignoreCameraCollision, - colProp.ignoreActorCollision, - colProp.ignoreProjectileCollision, - useConveyor, - normal, - distance, - useMacros, - ) + new_col_poly = CollisionPoly( + indices, + colProp.ignoreCameraCollision, + colProp.ignoreActorCollision, + colProp.ignoreProjectileCollision, + colProp.conveyorOption == "Land", + normal, + ctypes.c_short(distance).value, + useMacros, ) + new_col_poly.index_to_obj = {i: meshObj} + colPolyFromSurfaceType[surfaceType].append(new_col_poly) count = 0 for surface, colPolyList in colPolyFromSurfaceType.items(): @@ -207,6 +207,8 @@ class CollisionHeader: name: str minBounds: tuple[int, int, int] maxBounds: tuple[int, int, int] + filename: Optional[str] + settings: Optional[OOTCollisionExportSettings] vertices: CollisionVertices collisionPoly: CollisionPolygons surfaceType: SurfaceTypes @@ -221,6 +223,8 @@ def new( transform: Matrix, useMacros: bool, includeChildren: bool, + filename: Optional[str] = None, + settings: Optional[OOTCollisionExportSettings] = None, ): # Ideally everything would be separated but this is complicated since it's all tied together colBounds, vertexList, polyList, surfaceTypeList = CollisionUtility.getCollisionData( @@ -231,6 +235,8 @@ def new( name, colBounds[0], colBounds[1], + filename, + settings, CollisionVertices(f"{sceneName}_vertices", vertexList), CollisionPolygons(f"{sceneName}_polygons", polyList), SurfaceTypes(f"{sceneName}_polygonTypes", surfaceTypeList), @@ -238,6 +244,51 @@ def new( WaterBoxes.new(f"{sceneName}_waterBoxes", dataHolder, transform, useMacros), ) + @staticmethod + def export(original_obj: Object, transform: Matrix, settings: OOTCollisionExportSettings): + """Exports collision data as C files, this should be called to do a separate export from the scene.""" + name = toAlnum(original_obj.name) + filename = settings.filename if settings.isCustomFilename else f"{name}_collision" + exportPath = ootGetObjectPath( + settings.customExport, bpy.path.abspath(settings.exportPath), settings.folder, True + ) + + if bpy.context.scene.exportHiddenGeometry: + hiddenState = unhideAllAndGetHiddenState(bpy.context.scene) + + # Don't remove ignore_render, as we want to resuse this for collision + obj, _ = ootDuplicateHierarchy(original_obj, None, True, OOTObjectCategorizer()) + + if bpy.context.scene.exportHiddenGeometry: + restoreHiddenState(hiddenState) + + # write file + if not obj.ignore_collision: + # create the collision header + col_header = CollisionHeader.new( + f"{name}_collisionHeader", + name, + obj, + transform, + bpy.context.scene.fast64.oot.useDecompFeatures, + settings.includeChildren, + ) + + filedata = col_header.get_file(filename, settings) + base_path = Path( + ootGetPath(exportPath, settings.customExport, "assets/objects/", settings.folder, True, True) + ).resolve() + + header_path = base_path / f"{filename}.h" + header_path.write_text(filedata.header, encoding="utf-8", newline="\n") + + source_path = base_path / f"{filename}.c" + source_path.write_text(filedata.source, encoding="utf-8", newline="\n") + else: + raise PluginError("ERROR: exporting collision with ignore collision enabled!") + + cleanupDuplicatedObjects([obj]) + def getCmd(self): """Returns the collision header scene command""" @@ -282,9 +333,10 @@ def getC(self): colPolyPtrLine = f"ARRAY_COUNT({self.collisionPoly.name}), {self.collisionPoly.name}" # build the C data of the collision header + headerData.append(colData) # .h - headerData.header = f"extern {varName};\n" + headerData.header += f"extern {varName};\n" # .c headerData.source += ( @@ -304,5 +356,43 @@ def getC(self): + "\n};\n\n" ) - headerData.append(colData) return headerData + + def get_file(self, filename: str, settings: OOTCollisionExportSettings): + filedata = CData() + + if bpy.context.scene.fast64.oot.is_globalh_present(): + includes = [ + '#include "ultra64.h"', + '#include "z64.h"', + '#include "macros.h"', + ] + elif bpy.context.scene.fast64.oot.is_z64sceneh_present(): + includes = [ + '#include "ultra64.h"', + '#include "z64math.h"', + '#include "z64bgcheck.h"', + '#include "array_count.h"', + ] + else: + includes = [ + '#include "ultra64.h"', + '#include "z_math.h"', + '#include "bgcheck.h"', + '#include "array_count.h"', + ] + + filedata.header = ( + f"#ifndef {filename.upper()}_H\n" + f"#define {filename.upper()}_H\n\n" + "\n".join(includes) + "\n\n" + ) + filedata.source = f'#include "{filename}.h"\n' + + if not settings.customExport: + filedata.source += f'#include "{settings.folder}.h"\n\n' + else: + filedata.source += "\n" + + filedata.append(self.getC()) + filedata.header += "\n#endif\n" + + return filedata diff --git a/fast64_internal/oot/exporter/collision/camera.py b/fast64_internal/z64/exporter/collision/camera.py similarity index 99% rename from fast64_internal/oot/exporter/collision/camera.py rename to fast64_internal/z64/exporter/collision/camera.py index 012defcee..f4f2eccfb 100644 --- a/fast64_internal/oot/exporter/collision/camera.py +++ b/fast64_internal/z64/exporter/collision/camera.py @@ -4,7 +4,7 @@ from mathutils import Quaternion, Matrix from bpy.types import Object from ....utility import PluginError, CData, indent -from ...oot_utility import getObjectList +from ...utility import getObjectList from ...collision.constants import decomp_compat_map_CameraSType from ...collision.properties import OOTCameraPositionProperty from ..utility import Utility diff --git a/fast64_internal/oot/exporter/collision/polygons.py b/fast64_internal/z64/exporter/collision/polygons.py similarity index 50% rename from fast64_internal/oot/exporter/collision/polygons.py rename to fast64_internal/z64/exporter/collision/polygons.py index 3339d42cc..6543da57f 100644 --- a/fast64_internal/oot/exporter/collision/polygons.py +++ b/fast64_internal/z64/exporter/collision/polygons.py @@ -1,7 +1,9 @@ from dataclasses import dataclass, field from typing import Optional from mathutils import Vector -from ....utility import PluginError, CData, indent +from bpy.types import Object + +from ....utility import PluginError, CData, hexOrDecInt, indent @dataclass @@ -12,12 +14,77 @@ class CollisionPoly: ignoreCamera: bool ignoreEntity: bool ignoreProjectile: bool - enableConveyor: bool + isLandConveyor: bool normal: Vector dist: int useMacros: bool type: Optional[int] = field(init=False, default=None) + index_to_obj: Optional[dict[int, Object]] = field(init=False, default=None) + + @staticmethod + def from_data(poly_data: list[str], not_zapd_assets: bool): + if not_zapd_assets: + third_vtx_macro = "COLPOLY_VTX_INDEX(" if "COLPOLY_VTX_INDEX(" in poly_data[3] else "COLPOLY_VTX(" + + # format: [ [vtxId, flags], [vtxId, flags], [vtxId, flags] ] (str) + vtx = [ + poly_data[1].removeprefix("COLPOLY_VTX(").removesuffix(")").split(","), + poly_data[2].removeprefix("COLPOLY_VTX(").removesuffix(")").split(","), + poly_data[3].removeprefix(third_vtx_macro).removesuffix(")").split(","), + ] + + new_poly = CollisionPoly( + [hexOrDecInt(vtx[0][0]), hexOrDecInt(vtx[1][0]), hexOrDecInt(vtx[2][0])], + "COLPOLY_IGNORE_CAMERA" in vtx[0][1], + "COLPOLY_IGNORE_ENTITY" in vtx[0][1], + "COLPOLY_IGNORE_PROJECTILES" in vtx[0][1], + "COLPOLY_IS_FLOOR_CONVEYOR" in vtx[1][1], + Vector( + ( + float(poly_data[4].removeprefix("COLPOLY_SNORMAL(").removesuffix(")")), + float(poly_data[5].removeprefix("COLPOLY_SNORMAL(").removesuffix(")")), + float(poly_data[6].removeprefix("COLPOLY_SNORMAL(").removesuffix(")")), + ) + ), + hexOrDecInt(poly_data[7]), + not_zapd_assets, + ) + else: + + def get_normal(value: int): + return int.from_bytes(value.to_bytes(2, "big", signed=value < 0x8000), "big", signed=True) / 0x7FFF + + vtx1 = hexOrDecInt(poly_data[1]) + vtx2 = hexOrDecInt(poly_data[2]) + vtx3 = hexOrDecInt(poly_data[3]) + + # format: [ [vtxId, flags], [vtxId, flags], [vtxId, flags] ] (int) + vtx = [ + [vtx1 & 0x1FFF, (vtx1 >> 13) & 7], + [vtx2 & 0x1FFF, (vtx2 >> 13) & 7], + [vtx3 & 0x1FFF, (vtx3 >> 13) & 7], + ] + + new_poly = CollisionPoly( + [vtx[0][0], vtx[1][0], vtx[2][0]], + ((vtx[0][1] >> (1 << 0)) & 1) == 1, + ((vtx[0][1] >> (1 << 1)) & 1) == 1, + ((vtx[0][1] >> (1 << 2)) & 1) == 1, + ((vtx[1][1] >> (1 << 0)) & 1) == 1, + Vector( + ( + get_normal(hexOrDecInt(poly_data[4])), + get_normal(hexOrDecInt(poly_data[5])), + get_normal(hexOrDecInt(poly_data[6])), + ) + ), + hexOrDecInt(poly_data[7]), + not_zapd_assets, + ) + + new_poly.type = hexOrDecInt(poly_data[0]) + return new_poly def __post_init__(self): for i, val in enumerate(self.normal): @@ -42,7 +109,7 @@ def getFlags_vIB(self): """Returns the value of ``flags_vIB``""" vtxId = self.indices[1] & 0x1FFF - if self.enableConveyor: + if self.isLandConveyor: flags = "COLPOLY_IS_FLOOR_CONVEYOR" if self.useMacros else "(1 << 0)" else: flags = "COLPOLY_IGNORE_NONE" if self.useMacros else "0" diff --git a/fast64_internal/oot/exporter/collision/surface.py b/fast64_internal/z64/exporter/collision/surface.py similarity index 58% rename from fast64_internal/oot/exporter/collision/surface.py rename to fast64_internal/z64/exporter/collision/surface.py index f5bf0d571..dc128bfb7 100644 --- a/fast64_internal/oot/exporter/collision/surface.py +++ b/fast64_internal/z64/exporter/collision/surface.py @@ -1,5 +1,14 @@ -from dataclasses import dataclass +import bpy +import math + +from dataclasses import dataclass, field +from bpy.types import Material +from typing import Optional + from ....utility import CData, indent +from ....game_data import game_data +from ...collision.properties import OOTMaterialCollisionProperty +from ..utility import Utility @dataclass(unsafe_hash=True) @@ -22,11 +31,68 @@ class SurfaceType: lightSetting: int echo: int canHookshot: bool - conveyorSpeed: int + conveyorSpeed: int | str conveyorDirection: int isWallDamage: bool # unk27 useMacros: bool + data_material: Optional[Material] = field(init=False, default=None) + + @staticmethod + def new(col_props: OOTMaterialCollisionProperty, use_macros: bool, material: Optional[Material] = None): + use_conveyor = col_props.conveyorOption != "None" + if use_conveyor: + if col_props.conveyorSpeed == "Custom": + conveyor_speed = col_props.conveyorSpeedCustom + else: + conveyor_speed = int(col_props.conveyorSpeed, base=16) + (4 if col_props.conveyorKeepMomentum else 0) + else: + conveyor_speed = 0 + + new_type = SurfaceType( + col_props.cameraID, + col_props.exitID, + Utility.getPropValue(col_props, "floorProperty"), + 0, # unused? + Utility.getPropValue(col_props, "wallSetting"), + Utility.getPropValue(col_props, "floorSetting"), + col_props.decreaseHeight, + col_props.eponaBlock, + Utility.getPropValue(col_props, "sound"), + Utility.getPropValue(col_props, "terrain"), + col_props.lightingSetting, + int(col_props.echo, base=16), + col_props.hookshotable, + conveyor_speed, + int(col_props.conveyorRotation / (2 * math.pi) * 0x3F) if use_conveyor else 0, + col_props.isWallDamage, + use_macros, + ) + + new_type.data_material = material + return new_type + + @staticmethod + def from_hex(surface0: int, surface1: int): + return SurfaceType( + ((surface0 >> 0) & 0xFF), + ((surface0 >> 8) & 0x1F), + game_data.z64.enums.enumByKey["floor_type"].item_by_index[((surface0 >> 13) & 0x1F)].id, + ((surface0 >> 18) & 0x07), + game_data.z64.enums.enumByKey["wall_type"].item_by_index[((surface0 >> 21) & 0x1F)].id, + game_data.z64.enums.enumByKey["floor_property"].item_by_index[((surface0 >> 26) & 0x0F)].id, + ((surface0 >> 30) & 1) > 0, + ((surface0 >> 31) & 1) > 0, + game_data.z64.enums.enumByKey["surface_material"].item_by_index[((surface1 >> 0) & 0x0F)].id, + game_data.z64.enums.enumByKey["floor_effect"].item_by_index[((surface1 >> 4) & 0x03)].id, + ((surface1 >> 6) & 0x1F), + ((surface1 >> 11) & 0x3F), + ((surface1 >> 17) & 1) > 0, + game_data.z64.enums.enumByKey["conveyor_speed"].item_by_index[((surface1 >> 18) & 0x07)].id, + ((surface1 >> 21) & 0x3F), + ((surface1 >> 27) & 1) > 0, + bpy.context.scene.fast64.oot.useDecompFeatures, + ) def getIsSoftC(self): return "1" if self.isSoft else "0" diff --git a/fast64_internal/oot/exporter/collision/vertex.py b/fast64_internal/z64/exporter/collision/vertex.py similarity index 100% rename from fast64_internal/oot/exporter/collision/vertex.py rename to fast64_internal/z64/exporter/collision/vertex.py diff --git a/fast64_internal/oot/exporter/collision/waterbox.py b/fast64_internal/z64/exporter/collision/waterbox.py similarity index 70% rename from fast64_internal/oot/exporter/collision/waterbox.py rename to fast64_internal/z64/exporter/collision/waterbox.py index 15d158239..4b8ccfbe8 100644 --- a/fast64_internal/oot/exporter/collision/waterbox.py +++ b/fast64_internal/z64/exporter/collision/waterbox.py @@ -1,8 +1,13 @@ +import bpy +import re + +from mathutils import Vector from dataclasses import dataclass from mathutils import Matrix from bpy.types import Object -from ...oot_utility import getObjectList -from ....utility import CData, checkIdentityRotation, indent + +from ....utility import CData, hexOrDecInt, checkIdentityRotation, indent, yUpToZUp +from ...utility import getObjectList from ..utility import Utility @@ -55,6 +60,56 @@ def new( useMacros, ) + @staticmethod + def from_data(raw_data: str, not_zapd_assets: bool): + if not_zapd_assets: + regex = r"(.*?)\s*,\s*WATERBOX_PROPERTIES\((.*?)\)" + else: + regex = r"(.*?)\s*,\s*(0x.*),?" + + match = re.search(regex, raw_data, re.DOTALL) + assert match is not None + + pos_scale = [hexOrDecInt(value) for value in match.group(1).split(",")] + + if not_zapd_assets: + params = match.group(2).split(",") + properties = [ + hexOrDecInt(params[0]), + hexOrDecInt(params[1]), + params[2], + params[3], + ] + else: + params = hexOrDecInt(match.group(2)) + properties = [ + ((params >> 0) & 0xFF), # bgCamIndex + ((params >> 8) & 0x1F), # lightIndex + str(((params >> 13) & 0x3F)), # room + str(((params >> 19) & 1) == 1).lower(), # setFlag19 + ] + + return WaterBox( + properties[0], + properties[1], + properties[2], + properties[3], + pos_scale[0], + pos_scale[1], + pos_scale[2], + pos_scale[3], + pos_scale[4], + not_zapd_assets, + ) + + def get_blender_position(self): + pos = [self.xMin, self.ySurface, self.zMin] + return yUpToZUp @ Vector([value / bpy.context.scene.ootBlenderScale for value in pos]) + + def get_blender_scale(self) -> list[int]: + scale = [self.xLength, self.zLength] + return [value / bpy.context.scene.ootBlenderScale for value in scale] + def getProperties(self): """Returns the waterbox properties""" diff --git a/fast64_internal/oot/exporter/cutscene/__init__.py b/fast64_internal/z64/exporter/cutscene/__init__.py similarity index 58% rename from fast64_internal/oot/exporter/cutscene/__init__.py rename to fast64_internal/z64/exporter/cutscene/__init__.py index f1cca53b6..3713489c9 100644 --- a/fast64_internal/oot/exporter/cutscene/__init__.py +++ b/fast64_internal/z64/exporter/cutscene/__init__.py @@ -1,10 +1,12 @@ import bpy from dataclasses import dataclass, field +from pathlib import Path from typing import Optional from bpy.types import Object + from ....utility import PluginError, CData, indent -from ...oot_utility import getCustomProperty +from ...utility import getCustomProperty from ...scene.properties import OOTSceneHeaderProperty from .data import CutsceneData @@ -38,6 +40,43 @@ def new(name: Optional[str], csObj: Optional[Object], useMacros: bool, motionOnl data = CutsceneData.new(csObj, useMacros, motionOnly) return Cutscene(name, data, data.totalEntries, data.frameCount, useMacros, motionOnly) + return None + + @staticmethod + def export(cs_obj: Object, skip_includes: bool = False, skip_endif: bool = False): + """Exports cutscene data as C files, this should be called to do a separate export from the scene.""" + + # create the cutscene + cutscene = Cutscene.new( + cs_obj.name.removeprefix("Cutscene."), + cs_obj, + bpy.context.scene.fast64.oot.useDecompFeatures, + bpy.context.scene.fast64.oot.exportMotionOnly, + ) + + # write file + source_path = Path(bpy.context.scene.ootCutsceneExportPath).resolve() + + if source_path.suffix != ".c": + raise PluginError("ERROR: output file must end with '.c'") + + header_path = source_path.with_suffix(".h") + filedata = cutscene.get_file(source_path.stem, skip_includes, skip_endif) + + if not skip_includes: + # export the data in writing mode (single cutscene or first cutscene of the multiple export) + + header_path.write_text(filedata.header, encoding="utf-8", newline="\n") + source_path.write_text(filedata.source, encoding="utf-8", newline="\n") + else: + # export the data in append mode (the other cutscenes of the multiple export) + + with header_path.open("a", encoding="utf-8", newline="\n") as file: + file.write(filedata.header) + + with source_path.open("a", encoding="utf-8", newline="\n") as file: + file.write(filedata.source) + def getC(self): """Returns the cutscene data""" @@ -83,10 +122,10 @@ def getC(self): csData.source = ( declarationBase + " = {\n" - + (indent + f"CS_BEGIN_CUTSCENE({self.totalEntries}, {self.frameCount}),\n") + + (indent + f"CS_HEADER({self.totalEntries}, {self.frameCount}),\n") + (self.data.destination.getCmd() if self.data.destination is not None else "") + "".join(entry.getCmd() for curList in dataListNames for entry in getattr(self.data, curList)) - + (indent + "CS_END(),\n") + + (indent + "CS_END_OF_SCRIPT(),\n") + "};\n\n" ) @@ -94,6 +133,51 @@ def getC(self): else: raise PluginError("ERROR: CutsceneData not initialised!") + def get_file(self, filename: str, skip_includes: bool, skip_endif: bool): + filedata = CData() + + if not skip_includes: + if bpy.context.scene.fast64.oot.is_globalh_present(): + includes = [ + '#include "ultra64.h"', + '#include "z64.h"', + '#include "macros.h"', + '#include "command_macros_base.h"', + '#include "z64cutscene_commands.h"', + ] + elif bpy.context.scene.fast64.oot.is_z64sceneh_present(): + includes = [ + '#include "ultra64.h"', + '#include "sequence.h"', + '#include "z64math.h"', + '#include "z64cutscene.h"', + '#include "z64cutscene_commands.h"', + '#include "z64ocarina.h"', + '#include "z64player.h"', + ] + else: + includes = [ + '#include "ultra64.h"', + '#include "sequence.h"', + '#include "math.h"', + '#include "cutscene.h"', + '#include "cutscene_commands.h"', + '#include "ocarina.h"', + '#include "player.h"', + ] + + filedata.header = ( + f"#ifndef {filename.upper()}_H\n" + f"#define {filename.upper()}_H\n\n" + "\n".join(includes) + "\n\n" + ) + filedata.source = f'#include "{filename}.h"\n\n' + + filedata.append(self.getC()) + + if not skip_endif: + filedata.header += "\n#endif\n" + + return filedata + @dataclass class SceneCutscene: @@ -104,7 +188,7 @@ class SceneCutscene: @staticmethod def new(props: OOTSceneHeaderProperty, headerIndex: int, useMacros: bool): csObj: Object = props.csWriteObject - cutsceneObjects: list[Object] = [csObj for csObj in props.extraCutscenes] + cutsceneObjects: list[Object] = [extraCS.csObject for extraCS in props.extraCutscenes] entries: list[Cutscene] = [] if headerIndex > 0 and len(cutsceneObjects) > 0: diff --git a/fast64_internal/oot/exporter/cutscene/actor_cue.py b/fast64_internal/z64/exporter/cutscene/actor_cue.py similarity index 93% rename from fast64_internal/oot/exporter/cutscene/actor_cue.py rename to fast64_internal/z64/exporter/cutscene/actor_cue.py index 7a38c3f0d..0c1313141 100644 --- a/fast64_internal/oot/exporter/cutscene/actor_cue.py +++ b/fast64_internal/z64/exporter/cutscene/actor_cue.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from ....utility import PluginError, indent -from ...oot_constants import ootData +from ....game_data import game_data from ...cutscene.motion.utility import getRotation, getInteger from .common import CutsceneCmdBase @@ -46,7 +46,7 @@ def getCmd(self): + "".join(f"{rot}, " for rot in self.rot) + "".join(f"{pos}, " for pos in self.startPos) + "".join(f"{pos}, " for pos in self.endPos) - + "0.0f, 0.0f, 0.0f),\n" + + "CS_FLOAT(0, 0.0f), CS_FLOAT(0, 0.0f), CS_FLOAT(0, 0.0f)),\n" ) @@ -74,7 +74,7 @@ def from_params(params: list[str], isPlayer: bool): commandType = commandType.removeprefix("0x") commandType = "0x" + "0" * (4 - len(commandType)) + commandType else: - commandType = ootData.enumData.enumByKey["csCmd"].itemById[commandType].key + commandType = game_data.z64.enums.enumByKey["cs_cmd"].item_by_id[commandType].key entryTotal = getInteger(params[1].strip()) return CutsceneCmdActorCueList(None, None, isPlayer, commandType, entryTotal) diff --git a/fast64_internal/oot/exporter/cutscene/camera.py b/fast64_internal/z64/exporter/cutscene/camera.py similarity index 96% rename from fast64_internal/oot/exporter/cutscene/camera.py rename to fast64_internal/z64/exporter/cutscene/camera.py index fa9ddc469..105998ee0 100644 --- a/fast64_internal/oot/exporter/cutscene/camera.py +++ b/fast64_internal/z64/exporter/cutscene/camera.py @@ -1,3 +1,5 @@ +import struct + from dataclasses import dataclass, field from ....utility import PluginError, indent from ...cutscene.motion.utility import getInteger @@ -32,8 +34,9 @@ def getCmd(self): if len(self.pos) == 0: raise PluginError("ERROR: Pos list is empty!") + viewAngle_ieee = f"0x{struct.unpack(' 0: + header = f"{self.header}\n\n" + + return header + "".join(section.to_c() for section in self.sections) class SpecUtility: @@ -297,4 +324,4 @@ def add_segments(exportInfo: "ExportInfo", scene: "Scene", sceneFile: "SceneFile specFile.append(SpecEntry(roomCmds)) # finally, write the spec file - writeFile(exportPath, specFile.to_c()) + get_spec_path(exportPath).write_text(specFile.to_c(), encoding="utf-8", newline="\n") diff --git a/fast64_internal/oot/exporter/file.py b/fast64_internal/z64/exporter/file.py similarity index 90% rename from fast64_internal/oot/exporter/file.py rename to fast64_internal/z64/exporter/file.py index 892702cd3..914b7a62a 100644 --- a/fast64_internal/oot/exporter/file.py +++ b/fast64_internal/z64/exporter/file.py @@ -60,8 +60,7 @@ def getSourceWithSceneInclude(self, sceneInclude: str, source: str): def setIncludeData(self): """Adds includes at the beginning of each file to write""" - sceneInclude = f'#include "{self.name}.h"\n\n\n' - csInclude = sceneInclude[:-2] + '#include "z64cutscene.h"\n' + '#include "z64cutscene_commands.h"\n\n\n' + sceneInclude = f'#include "{self.name}.h"\n\n' for roomData in self.roomList.values(): roomData.roomMain = self.getSourceWithSceneInclude(sceneInclude, roomData.roomMain) @@ -70,9 +69,7 @@ def setIncludeData(self): roomData.roomModelInfo = self.getSourceWithSceneInclude(sceneInclude, roomData.roomModelInfo) roomData.roomModel = self.getSourceWithSceneInclude(sceneInclude, roomData.roomModel) - self.sceneMain = self.getSourceWithSceneInclude( - sceneInclude if not self.hasCutscenes() else csInclude, self.sceneMain - ) + self.sceneMain = self.getSourceWithSceneInclude(sceneInclude, self.sceneMain) if not self.singleFileExport: self.sceneCollision = self.getSourceWithSceneInclude(sceneInclude, self.sceneCollision) @@ -82,7 +79,7 @@ def setIncludeData(self): if self.hasCutscenes(): for i in range(len(self.sceneCutscenes)): - self.sceneCutscenes[i] = self.getSourceWithSceneInclude(csInclude, self.sceneCutscenes[i]) + self.sceneCutscenes[i] = self.getSourceWithSceneInclude(sceneInclude, self.sceneCutscenes[i]) def write(self): """Writes the scene files""" diff --git a/fast64_internal/oot/exporter/room/__init__.py b/fast64_internal/z64/exporter/room/__init__.py similarity index 93% rename from fast64_internal/oot/exporter/room/__init__.py rename to fast64_internal/z64/exporter/room/__init__.py index 3fe3d6c2e..38d297c2c 100644 --- a/fast64_internal/oot/exporter/room/__init__.py +++ b/fast64_internal/z64/exporter/room/__init__.py @@ -5,8 +5,9 @@ from ....utility import PluginError, CData, indent from ....f3d.f3d_gbi import ScrollMethod, TextureExportSettings from ...room.properties import OOTRoomHeaderProperty -from ...oot_object import addMissingObjectsToAllRoomHeaders -from ...oot_model_classes import OOTModel, OOTGfxFormatter +from ...object import addMissingObjectsToAllRoomHeaders +from ...model_classes import OOTModel, OOTGfxFormatter +from ...utility import ExportInfo from ..file import RoomFile from ..utility import Utility, altHeaderList from .header import RoomAlternateHeader, RoomHeader @@ -29,12 +30,13 @@ def new( name: str, transform: Matrix, sceneObj: Object, + original_room_obj: Object, roomObj: Object, roomShapeType: str, model: OOTModel, roomIndex: int, sceneName: str, - saveTexturesAsPNG: bool, + exportInfo: ExportInfo, ): i = 0 mainHeaderProps = roomObj.ootRoomHeader @@ -90,10 +92,20 @@ def new( headers.extend([altHeader.childNight, altHeader.adultDay, altHeader.adultNight]) if len(altHeader.cutscenes) > 0: headers.extend(altHeader.cutscenes) - addMissingObjectsToAllRoomHeaders(roomObj, headers) + + if exportInfo.auto_add_room_objects: + addMissingObjectsToAllRoomHeaders(original_room_obj, headers) roomShape = RoomShapeUtility.create_shape( - sceneName, name, roomShapeType, model, transform, sceneObj, roomObj, saveTexturesAsPNG, mainHeaderProps + sceneName, + name, + roomShapeType, + model, + transform, + sceneObj, + roomObj, + exportInfo.saveTexturesAsPNG, + mainHeaderProps, ) return Room(name, roomIndex, mainHeader, altHeader, roomShape, hasAlternateHeaders) diff --git a/fast64_internal/oot/exporter/room/header.py b/fast64_internal/z64/exporter/room/header.py similarity index 79% rename from fast64_internal/oot/exporter/room/header.py rename to fast64_internal/z64/exporter/room/header.py index bcc275f73..13bb619fd 100644 --- a/fast64_internal/oot/exporter/room/header.py +++ b/fast64_internal/z64/exporter/room/header.py @@ -1,11 +1,14 @@ +import bpy + from dataclasses import dataclass, field from typing import Optional from mathutils import Matrix from bpy.types import Object from ....utility import CData, indent -from ...oot_utility import getObjectList -from ...oot_constants import ootData +from ....game_data import game_data +from ...utility import getObjectList from ...room.properties import OOTRoomHeaderProperty +from ...actor.properties import OOTActorProperty from ..utility import Utility from ..actor import Actor @@ -96,7 +99,7 @@ def new(name: str, props: Optional[OOTRoomHeaderProperty]): if objProp.objectKey == "Custom": objectList.append(objProp.objectIDCustom) else: - objectList.append(ootData.objectData.objectsByKey[objProp.objectKey].id) + objectList.append(game_data.z64.objects.objects_by_key[objProp.objectKey].id) return RoomObjects(name, objectList) def getDefineName(self): @@ -136,6 +139,25 @@ class RoomActors: name: str actorList: list[Actor] + @staticmethod + def get_rotation_values(actorProp: OOTActorProperty, blender_rot_values: list[int]): + # Figure out which rotation to export, Blender's or the override + custom = ( + "_custom" if not bpy.context.scene.fast64.oot.use_new_actor_panel or actorProp.actor_id == "Custom" else "" + ) + rot_values = [getattr(actorProp, f"rot_{rot}{custom}") for rot in ["x", "y", "z"]] + export_rot_values = [f"DEG_TO_BINANG({(rot * (180 / 0x8000)):.3f})" for rot in blender_rot_values] + + if not bpy.context.scene.fast64.oot.use_new_actor_panel or actorProp.actor_id == "Custom": + export_rot_values = rot_values if actorProp.rot_override else export_rot_values + else: + for i, rot in enumerate(["X", "Y", "Z"]): + if actorProp.is_rotation_used(f"{rot}Rot"): + export_rot_values[i] = rot_values[i] + + assert len(export_rot_values) == 3 + return export_rot_values + @staticmethod def new( name: str, @@ -148,7 +170,7 @@ def new( actorList: list[Actor] = [] actorObjList = getObjectList(sceneObj.children, "EMPTY", "Actor", parentObj=roomObj, room_index=room_index) for obj in actorObjList: - actorProp = obj.ootActorProperty + actorProp: OOTActorProperty = obj.ootActorProperty if not Utility.isCurrentHeaderValid(actorProp.headerSettings, headerIndex): continue @@ -157,30 +179,31 @@ def new( # any data loss as Blender saves the index of the element in the Actor list used for the EnumProperty # and not the identifier as defined by the first element of the tuple. Therefore, we need to check if # the current Actor has the ID `None` to avoid export issues. - if actorProp.actorID != "None": + if actorProp.actor_id != "None": pos, rot, _, _ = Utility.getConvertedTransform(transform, sceneObj, obj, True) actor = Actor() - if actorProp.actorID == "Custom": - actor.id = actorProp.actorIDCustom + if actorProp.actor_id == "Custom": + actor.id = actorProp.actor_id_custom else: - actor.id = actorProp.actorID + actor.id = actorProp.actor_id - if actorProp.rotOverride: - actor.rot = ", ".join([actorProp.rotOverrideX, actorProp.rotOverrideY, actorProp.rotOverrideZ]) - else: - actor.rot = ", ".join(f"DEG_TO_BINANG({(r * (180 / 0x8000)):.3f})" for r in rot) + actor.rot = ", ".join(RoomActors.get_rotation_values(actorProp, rot)) actor.name = ( - ootData.actorData.actorsByID[actorProp.actorID].name.replace( - f" - {actorProp.actorID.removeprefix('ACTOR_')}", "" + game_data.z64.actors.actorsByID[actorProp.actor_id].name.replace( + f" - {actorProp.actor_id.removeprefix('ACTOR_')}", "" ) - if actorProp.actorID != "Custom" + if actorProp.actor_id != "Custom" else "Custom Actor" ) actor.pos = pos - actor.params = actorProp.actorParam + actor.params = ( + actorProp.params + if bpy.context.scene.fast64.oot.use_new_actor_panel and actorProp.actor_id != "Custom" + else actorProp.params_custom + ) actorList.append(actor) return RoomActors(name, actorList) diff --git a/fast64_internal/oot/exporter/room/shape.py b/fast64_internal/z64/exporter/room/shape.py similarity index 99% rename from fast64_internal/oot/exporter/room/shape.py rename to fast64_internal/z64/exporter/room/shape.py index 96278f917..e08f141d2 100644 --- a/fast64_internal/oot/exporter/room/shape.py +++ b/fast64_internal/z64/exporter/room/shape.py @@ -8,13 +8,13 @@ from ....f3d.f3d_gbi import SPDisplayList, SPEndDisplayList, GfxListTag, GfxList, DLFormat from ....f3d.f3d_writer import TriangleConverterInfo, saveStaticModel, getInfoDict from ...room.properties import OOTRoomHeaderProperty, OOTBGProperty -from ...oot_model_classes import OOTModel +from ...model_classes import OOTModel from ..utility import Utility from bpy.types import Object from mathutils import Matrix, Vector from ....f3d.occlusion_planes.exporter import addOcclusionQuads, OcclusionPlaneCandidatesList -from ...oot_utility import ( +from ...utility import ( CullGroup, checkUniformScale, ootConvertTranslation, diff --git a/fast64_internal/z64/exporter/scene/__init__.py b/fast64_internal/z64/exporter/scene/__init__.py new file mode 100644 index 000000000..c9a576dcc --- /dev/null +++ b/fast64_internal/z64/exporter/scene/__init__.py @@ -0,0 +1,451 @@ +import bpy + +from dataclasses import dataclass +from mathutils import Matrix +from bpy.types import Object +from typing import Optional + +from ....game_data import game_data +from ....utility import PluginError, CData, indent +from ....f3d.f3d_gbi import TextureExportSettings, ScrollMethod +from ...scene.properties import OOTSceneHeaderProperty +from ...model_classes import OOTModel, OOTGfxFormatter +from ...utility import ExportInfo, is_hackeroot +from ..file import SceneFile +from ..utility import Utility, altHeaderList +from ..collision import CollisionHeader +from .animated_mats import SceneAnimatedMaterial +from .header import SceneAlternateHeader, SceneHeader +from .rooms import RoomEntries + + +def get_anm_mat_target_name(scene_obj: Object, alt_prop: OOTSceneHeaderProperty, name: str, header_index: int): + """ + This function tries to find the name of the animated material array we want to use. + It can return either a string, if using another header's data, or None, in which case we will export for the current header. + """ + + animated_materials = None + + # start by checking if we should try to find the right header's name + if alt_prop.reuse_anim_mat: + # little map for convenience + header_map = { + "Child Night": ("childNightHeader", 1), + "Adult Day": ("adultDayHeader", 2), + "Adult Night": ("adultNightHeader", 3), + } + + # initial values + target_prop = alt_prop + index = None + + # search as long as we don't find an entry + while target_prop.reuse_anim_mat: + # if it's not none it means this at least the second iteration + if index is not None: + # infinite loops can happen if you set header A to reuse header B and header B to reuse header A + assert ( + target_prop.internal_anim_mat_header != alt_prop.internal_anim_mat_header + ), f"infinite loop in {repr(scene_obj.name)}'s Animated Materials" + + if target_prop.internal_anim_mat_header == "Child Day": + target_prop = scene_obj.ootSceneHeader + index = 0 + elif target_prop.internal_anim_mat_header == "Cutscene": + index = cs_index = alt_prop.reuse_anim_mat_cs_index + assert ( + cs_index >= game_data.z64.cs_index_start and cs_index != header_index + ), "invalid cutscene index (Animated Material)" + + cs_index -= game_data.z64.cs_index_start + assert cs_index < len( + scene_obj.ootAlternateSceneHeaders.cutsceneHeaders + ), f"CS Header No. {index} don't exist (Animated Material)" + + target_prop = scene_obj.ootAlternateSceneHeaders.cutsceneHeaders[cs_index] + else: + prop_name, index = header_map[target_prop.internal_anim_mat_header] + target_prop = getattr(scene_obj.ootAlternateSceneHeaders, prop_name) + + if target_prop.usePreviousHeader: + index -= 1 + + if prop_name == "childNightHeader": + target_prop = scene_obj.ootSceneHeader + elif prop_name == "adultDayHeader": + target_prop = scene_obj.ootAlternateSceneHeaders.childNightHeader + elif prop_name == "adultNightHeader": + target_prop = scene_obj.ootAlternateSceneHeaders.adultDayHeader + + assert index is not None + animated_materials = f"{name}_header{index:02}_AnimatedMaterial" + + return animated_materials + + +@dataclass +class Scene: + """This class defines a scene""" + + name: str + model: OOTModel + mainHeader: Optional[SceneHeader] + altHeader: Optional[SceneAlternateHeader] + rooms: Optional[RoomEntries] + colHeader: Optional[CollisionHeader] + hasAlternateHeaders: bool + + @staticmethod + def new( + name: str, + original_scene_obj: Object, + sceneObj: Object, + transform: Matrix, + exportInfo: ExportInfo, + model: OOTModel, + ): + i = 0 + use_mat_anim = ( + "mat_anim" in sceneObj.ootSceneHeader.sceneTableEntry.drawConfig + or bpy.context.scene.ootSceneExportSettings.customExport + ) + + # process collisions first so we can use it for animated materials (dumb but works!) + 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, + use_mat_anim, + None, + colHeader, + ) + + if mainHeader.infos is not None: + model.draw_config = mainHeader.infos.drawConfig + except Exception as exc: + raise PluginError(f"In main scene header: {exc}") from exc + + hasAlternateHeaders = False + altHeader = SceneAlternateHeader(f"{name}_alternateHeaders") + altProp = sceneObj.ootAlternateSceneHeaders + + for i, header in enumerate(altHeaderList, 1): + altP: OOTSceneHeaderProperty = getattr(altProp, f"{header}Header") + + if altP.usePreviousHeader: + continue + + try: + target_name = get_anm_mat_target_name(sceneObj, altP, name, i) + + setattr( + altHeader, + header, + SceneHeader.new( + f"{name}_header{i:02}", + altP, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + target_name, + colHeader, + ), + ) + 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, game_data.z64.cs_index_start): + try: + target_name = get_anm_mat_target_name(sceneObj, csHeader, name, i) + + altHeader.cutscenes.append( + SceneHeader.new( + f"{name}_header{i:02}", + csHeader, + sceneObj, + transform, + i, + exportInfo.useMacros, + use_mat_anim, + target_name, + colHeader, + ) + ) + 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, + ) + + hasAlternateHeaders = True if len(altHeader.cutscenes) > 0 else hasAlternateHeaders + altHeader = altHeader if hasAlternateHeaders else None + return Scene(name, model, mainHeader, altHeader, rooms, colHeader, hasAlternateHeaders) + + def validateRoomIndices(self): + """Checks if there are multiple rooms with the same room index""" + + for i, room in enumerate(self.rooms.entries): + if i != room.roomIndex: + return False + return True + + def validateScene(self): + """Performs safety checks related to the scene data""" + + if not len(self.rooms.entries) > 0: + raise PluginError("ERROR: This scene does not have any rooms!") + + if not self.validateRoomIndices(): + raise PluginError("ERROR: Room indices do not have a consecutive list of indices.") + + def getSceneHeaderFromIndex(self, headerIndex: int) -> SceneHeader | None: + """Returns the scene header based on the header index""" + + if headerIndex == 0: + return self.mainHeader + + for i, header in enumerate(altHeaderList, 1): + if headerIndex == i: + return getattr(self.altHeader, header) + + for i, csHeader in enumerate(self.altHeader.cutscenes, game_data.z64.cs_index_start): + if headerIndex == i: + return csHeader + + return None + + def getCmdList(self, curHeader: SceneHeader, hasAltHeaders: bool): + """Returns the scene's commands list""" + + cmdListData = CData() + listName = f"SceneCmd {curHeader.name}" + + # .h + cmdListData.header = f"extern {listName}[]" + ";\n" + + # .c + cmdListData.source = ( + (f"{listName}[]" + " = {\n") + + (Utility.getAltHeaderListCmd(self.altHeader.name) if hasAltHeaders else "") + + self.colHeader.getCmd() + + self.rooms.getCmd() + + curHeader.infos.getCmds(curHeader.lighting) + + curHeader.lighting.getCmd() + + curHeader.path.getCmd() + + (curHeader.transitionActors.getCmd() if len(curHeader.transitionActors.entries) > 0 else "") + + curHeader.spawns.getCmd() + + curHeader.entranceActors.getCmd() + + (curHeader.exits.getCmd() if len(curHeader.exits.exitList) > 0 else "") + + (curHeader.cutscene.getCmd() if len(curHeader.cutscene.entries) > 0 else "") + + (curHeader.anim_mat.get_cmd() if curHeader.anim_mat is not None else "") + + Utility.getEndCmd() + + "};\n\n" + ) + + return cmdListData + + def getSceneMainC(self): + """Returns the main informations of the scene as ``CData``""" + + sceneC = CData() + headers: list[tuple[SceneHeader, str]] = [] + altHeaderPtrs = None + + if self.hasAlternateHeaders: + headers = [ + (self.altHeader.childNight, "Child Night"), + (self.altHeader.adultDay, "Adult Day"), + (self.altHeader.adultNight, "Adult Night"), + ] + + for i, csHeader in enumerate(self.altHeader.cutscenes): + headers.append((csHeader, f"Cutscene No. {i + 1}")) + + altHeaderPtrs = "\n".join( + 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) + ) + + headers.insert(0, (self.mainHeader, "Child Day (Default)")) + for i, (curHeader, headerDesc) in enumerate(headers): + if curHeader is not None: + sceneC.source += "/**\n * " + f"Header {headerDesc}\n" + "*/\n" + sceneC.append(self.getCmdList(curHeader, i == 0 and self.hasAlternateHeaders)) + + if i == 0: + if self.hasAlternateHeaders and altHeaderPtrs is not None: + altHeaderListName = f"SceneCmd* {self.altHeader.name}[]" + sceneC.header += f"extern {altHeaderListName};\n" + sceneC.source += altHeaderListName + " = {\n" + altHeaderPtrs + "\n};\n\n" + + # Write the room segment list + sceneC.append(self.rooms.getC(self.mainHeader.infos.useDummyRoomList)) + + sceneC.append(curHeader.getC()) + + return sceneC + + def getSceneCutscenesC(self): + """Returns the cutscene informations of the scene as ``CData``""" + + csDataList: list[CData] = [] + headers: list[SceneHeader] = [ + self.mainHeader, + ] + + if self.altHeader is not None: + headers.extend( + [ + self.altHeader.childNight, + self.altHeader.adultDay, + self.altHeader.adultNight, + ] + ) + headers.extend(self.altHeader.cutscenes) + + for curHeader in headers: + if curHeader is not None: + for csEntry in curHeader.cutscene.entries: + csDataList.append(csEntry.getC()) + + return csDataList + + def getSceneTexturesC(self, textureExportSettings: TextureExportSettings): + """ + Writes the textures and material setup displaylists that are shared between multiple rooms + (is written to the scene) + """ + + return self.model.to_c(textureExportSettings, OOTGfxFormatter(ScrollMethod.Vertex)).all() + + def getNewSceneFile(self, path: str, isSingleFile: bool, textureExportSettings: TextureExportSettings): + """Returns a new scene file containing the C data""" + + sceneMainData = self.getSceneMainC() + sceneCollisionData = self.colHeader.getC() + sceneCutsceneData = self.getSceneCutscenesC() + sceneTexturesData = self.getSceneTexturesC(textureExportSettings) + + if bpy.context.scene.fast64.oot.is_globalh_present(): + includes = [ + '#include "ultra64.h"', + '#include "macros.h"', + '#include "z64.h"', + ] + elif bpy.context.scene.fast64.oot.is_z64sceneh_present(): + includes = [ + '#include "ultra64.h"', + '#include "romfile.h"', + '#include "array_count.h"', + '#include "sequence.h"', + '#include "z64actor_profile.h"', + '#include "z64bgcheck.h"', + '#include "z64camera.h"', + '#include "z64cutscene.h"', + '#include "z64cutscene_commands.h"', + '#include "z64environment.h"', + '#include "z64math.h"', + '#include "z64object.h"', + '#include "z64ocarina.h"', + '#include "z64path.h"', + '#include "z64player.h"', + '#include "z64room.h"', + '#include "z64scene.h"', + ] + else: + includes = [ + '#include "ultra64.h"', + '#include "romfile.h"', + '#include "array_count.h"', + '#include "sequence.h"', + '#include "actor_profile.h"', + '#include "bgcheck.h"', + '#include "camera.h"', + '#include "cutscene.h"', + '#include "cutscene_commands.h"', + '#include "environment.h"', + '#include "z_math.h"', + '#include "object.h"', + '#include "ocarina.h"', + '#include "path.h"', + '#include "player.h"', + '#include "room.h"', + '#include "scene.h"', + ] + + if is_hackeroot(): + includes.extend( + [ + '#include "event_manager.h"', + '#include "animated_materials.h"', + '#include "save.h"', + ] + ) + + backwards_compatibility = [ + "// For older decomp versions", + "#ifndef SCENE_CMD_PLAYER_ENTRY_LIST", + "#define SCENE_CMD_PLAYER_ENTRY_LIST(length, playerEntryList) \\", + indent + "{ SCENE_CMD_ID_SPAWN_LIST, length, CMD_PTR(playerEntryList) }", + "#undef SCENE_CMD_SPAWN_LIST", + "#define SCENE_CMD_SPAWN_LIST(spawnList) \\", + indent + "{ SCENE_CMD_ID_ENTRANCE_LIST, 0, CMD_PTR(spawnList) }", + "#endif\n", + "#ifndef BLEND_RATE_AND_FOG_NEAR", + "#define BLEND_RATE_AND_FOG_NEAR(blendRate, fogNear) (s16)((((blendRate) / 4) << 10) | (fogNear))", + "#endif\n\n", + ] + + return SceneFile( + self.name, + sceneMainData.source, + sceneCollisionData.source, + [cs.source for cs in sceneCutsceneData], + sceneTexturesData.source, + { + room.roomIndex: room.getNewRoomFile(path, isSingleFile, textureExportSettings) + for room in self.rooms.entries + }, + isSingleFile, + path, + ( + f"#ifndef {self.name.upper()}_H\n" + + f"#define {self.name.upper()}_H\n\n" + + ("\n".join(includes) + "\n\n") + + "\n".join(backwards_compatibility) + + (SceneAnimatedMaterial.mat_seg_num_macro if "AnimatedMaterial" in sceneMainData.header else "") + + sceneMainData.header + + "".join(cs.header for cs in sceneCutsceneData) + + sceneCollisionData.header + + sceneTexturesData.header + ), + ) diff --git a/fast64_internal/oot/exporter/scene/actors.py b/fast64_internal/z64/exporter/scene/actors.py similarity index 70% rename from fast64_internal/oot/exporter/scene/actors.py rename to fast64_internal/z64/exporter/scene/actors.py index 8f7735b6e..ba4f2f6d8 100644 --- a/fast64_internal/oot/exporter/scene/actors.py +++ b/fast64_internal/z64/exporter/scene/actors.py @@ -1,15 +1,19 @@ +import bpy + from dataclasses import dataclass, field from typing import Optional from mathutils import Matrix from bpy.types import Object -from ....utility import PluginError, CData, indent -from ...oot_utility import getObjectList -from ...oot_constants import ootData + +from ....utility import PluginError, CData, hexOrDecInt, indent +from ....game_data import game_data +from ...utility import getObjectList, getEvalParams +from ...actor.properties import OOTActorProperty from ..utility import Utility from ..actor import Actor -@dataclass +@dataclass(unsafe_hash=True) class TransitionActor(Actor): """Defines a Transition Actor""" @@ -59,10 +63,8 @@ def new(name: str, sceneObj: Object, transform: Matrix, headerIndex: int): entries: list[TransitionActor] = [] for obj in actorObjList: transActorProp = obj.ootTransitionActorProperty - if ( - Utility.isCurrentHeaderValid(transActorProp.actor.headerSettings, headerIndex) - and transActorProp.actor.actorID != "None" - ): + actorProp: OOTActorProperty = transActorProp.actor + if Utility.isCurrentHeaderValid(actorProp.headerSettings, headerIndex) and actorProp.actor_id != "None": pos, rot, _, _ = Utility.getConvertedTransform(transform, sceneObj, obj, True) transActor = TransitionActor() @@ -76,27 +78,67 @@ def new(name: str, sceneObj: Object, transform: Matrix, headerIndex: int): front = (fromIndex, Utility.getPropValue(transActorProp, "cameraTransitionFront")) back = (toIndex, Utility.getPropValue(transActorProp, "cameraTransitionBack")) - if transActorProp.actor.actorID == "Custom": - transActor.id = transActorProp.actor.actorIDCustom + if actorProp.actor_id == "Custom": + transActor.id = actorProp.actor_id_custom else: - transActor.id = transActorProp.actor.actorID + transActor.id = actorProp.actor_id transActor.name = ( - ootData.actorData.actorsByID[transActorProp.actor.actorID].name.replace( - f" - {transActorProp.actor.actorID.removeprefix('ACTOR_')}", "" + game_data.z64.actors.actorsByID[actorProp.actor_id].name.replace( + f" - {actorProp.actor_id.removeprefix('ACTOR_')}", "" ) - if transActorProp.actor.actorID != "Custom" + if actorProp.actor_id != "Custom" else "Custom Actor" ) transActor.pos = pos transActor.rot = f"DEG_TO_BINANG({(rot[1] * (180 / 0x8000)):.3f})" # TODO: Correct axis? - transActor.params = transActorProp.actor.actorParam + transActor.params = ( + actorProp.params + if bpy.context.scene.fast64.oot.use_new_actor_panel and actorProp.actor_id != "Custom" + else actorProp.params_custom + ) transActor.roomFrom, transActor.cameraFront = front transActor.roomTo, transActor.cameraBack = back entries.append(transActor) return SceneTransitionActors(name, entries) + @staticmethod + def from_data(raw_data: str, not_zapd_assets: bool): + actor_list = [] + + if not_zapd_assets: + entries = raw_data.removeprefix("{").removesuffix(",},").split(",},{") + else: + entries = raw_data.split("},") + + for entry in entries: + if entry == "": + continue + + params = entry.replace("{", "").replace("}", "").split(",") + + # trailing commas + for p in params: + if p == "": + params.remove(p) + + assert len(params) == 10 + trans_actor = TransitionActor() + trans_actor.name = "(unset)" + trans_actor.id = params[4] + trans_actor.pos = [hexOrDecInt(params[5]), hexOrDecInt(params[6]), hexOrDecInt(params[7])] + trans_actor.rot = getEvalParams(params[8]) if "DEG_TO_BINANG" in params[8] else params[8] + trans_actor.params = params[9] + trans_actor.roomFrom = hexOrDecInt(params[0]) + trans_actor.roomTo = hexOrDecInt(params[2]) + trans_actor.isRoomTransition = trans_actor.roomFrom != trans_actor.roomTo + trans_actor.cameraFront = params[1] + trans_actor.cameraBack = params[3] + actor_list.append(trans_actor) + + return SceneTransitionActors("(unset)", actor_list) + def getCmd(self): """Returns the transition actor list scene command""" @@ -145,25 +187,27 @@ def new(name: str, sceneObj: Object, transform: Matrix, headerIndex: int): actorObjList = getObjectList(sceneObj.children_recursive, "EMPTY", "Entrance") for obj in actorObjList: entranceProp = obj.ootEntranceProperty - if ( - Utility.isCurrentHeaderValid(entranceProp.actor.headerSettings, headerIndex) - and entranceProp.actor.actorID != "None" - ): + actorProp: OOTActorProperty = entranceProp.actor + if Utility.isCurrentHeaderValid(actorProp.headerSettings, headerIndex) and actorProp.actor_id != "None": pos, rot, _, _ = Utility.getConvertedTransform(transform, sceneObj, obj, True) entranceActor = EntranceActor() entranceActor.name = ( - ootData.actorData.actorsByID[entranceProp.actor.actorID].name.replace( - f" - {entranceProp.actor.actorID.removeprefix('ACTOR_')}", "" + game_data.z64.actors.actorsByID[actorProp.actor_id].name.replace( + f" - {actorProp.actor_id.removeprefix('ACTOR_')}", "" ) - if entranceProp.actor.actorID != "Custom" + if actorProp.actor_id != "Custom" else "Custom Actor" ) - entranceActor.id = "ACTOR_PLAYER" if not entranceProp.customActor else entranceProp.actor.actorIDCustom + entranceActor.id = "ACTOR_PLAYER" if not entranceProp.customActor else actorProp.actor_id_custom entranceActor.pos = pos entranceActor.rot = ", ".join(f"DEG_TO_BINANG({(r * (180 / 0x8000)):.3f})" for r in rot) - entranceActor.params = entranceProp.actor.actorParam + entranceActor.params = ( + actorProp.params + if bpy.context.scene.fast64.oot.use_new_actor_panel and not entranceProp.customActor + else actorProp.params_custom + ) if entranceProp.tiedRoom is not None: entranceActor.roomIndex = entranceProp.tiedRoom.ootRoomHeader.roomIndex else: @@ -185,7 +229,7 @@ def getCmd(self): """Returns the spawn list scene command""" name = self.name if len(self.entries) > 0 else "NULL" - return indent + f"SCENE_CMD_SPAWN_LIST({len(self.entries)}, {name}),\n" + return indent + f"SCENE_CMD_PLAYER_ENTRY_LIST({len(self.entries)}, {name}),\n" def getC(self): """Returns the spawn actor array""" @@ -214,7 +258,7 @@ class SceneSpawns(Utility): def getCmd(self): """Returns the entrance list scene command""" - return indent + f"SCENE_CMD_ENTRANCE_LIST({self.name if len(self.entries) > 0 else 'NULL'}),\n" + return indent + f"SCENE_CMD_SPAWN_LIST({self.name if len(self.entries) > 0 else 'NULL'}),\n" def getC(self): """Returns the spawn array""" 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..7b13fb256 --- /dev/null +++ b/fast64_internal/z64/exporter/scene/animated_mats.py @@ -0,0 +1,894 @@ +import bpy +import re + +from dataclasses import dataclass +from bpy.types import Object +from typing import Optional, Any +from pathlib import Path + +from ....game_data import game_data +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 ..collision.surface import SurfaceType +from ..collision import CollisionHeader + +from ...animated_mats.properties import ( + Z64_AnimatedMatColorParams, + Z64_AnimatedMatTexScrollParams, + Z64_AnimatedMatTexCycleParams, + Z64_AnimatedMatTexTimedCycleParams, + Z64_AnimatedMatTextureParams, + Z64_AnimatedMatMultiTextureParams, + Z64_AnimatedMatSurfaceSwapParams, + Z64_AnimatedMatColorSwitchParams, + Z64_AnimatedMaterial, + Z64_AnimatedMaterialExportSettings, + Z64_AnimatedMaterialImportSettings, +) + +from ...importer.scene_header import parse_animated_material + + +class AnimatedMatColorParams: + def __init__( + self, + props: Z64_AnimatedMatColorParams, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + is_draw_color = type == "anim_mat_type_color" + is_draw_color_cycle = type == "anim_mat_type_color_cycle" + is_col_or_cycle = is_draw_color or is_draw_color_cycle + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.use_macros = use_macros + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}ColorParams{self.header_suffix}" + self.frame_length = len(props.keyframes) if is_draw_color else props.keyframe_length + self.prim_colors: list[tuple[int, int, int, int, int]] = [] + self.env_colors: list[tuple[int, int, int, int]] = [] + self.frames: list[int] = [] + + for keyframe in props.keyframes: + prim = exportColor(keyframe.prim_color[0:3]) + [scaleToU8(keyframe.prim_color[3])] + self.prim_colors.append((prim[0], prim[1], prim[2], prim[3], keyframe.prim_lod_frac)) + + if not is_col_or_cycle 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.duration if is_draw_color_cycle else keyframe.frame_num) + + if not is_col_or_cycle and keyframe.frame_num > self.frame_length: + raise PluginError("ERROR: the frame number cannot be higher than the total frame count!") + + self.frame_count = len(self.frames) + + if not is_col_or_cycle: + 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) + + if is_draw_color_cycle: + assert len(self.frames) == len(self.prim_colors) + + if props.use_env_color: + assert len(self.frames) == len(self.prim_colors) == len(self.env_colors) + + def to_c(self, all_externs: bool = True): + data = CData() + prim_array_name = f"{self.base_name}ColorPrimColor{self.header_suffix}" + env_array_name = f"{self.base_name}ColorEnvColor{self.header_suffix}" + frames_array_name = f"{self.base_name}ColorKeyFrames{self.header_suffix}" + params_name = f"AnimatedMatColorParams {self.name}" + + if len(self.env_colors) == 0: + env_array_name = "NULL" + + if len(self.frames) == 0: + frames_array_name = "NULL" + + # .h + if all_externs: + data.header = ( + f"extern F3DPrimColor {prim_array_name}[];\n" + + (f"extern F3DEnvColor {env_array_name}[];\n" if len(self.env_colors) > 0 else "") + + (f"extern u16 {frames_array_name}[];\n" if len(self.frames) > 0 else "") + + f"extern {params_name};\n" + ) + + # .c + length = f"ARRAY_COUNT({frames_array_name})" if self.use_macros else self.frame_count + data.source = ( + ( + (f"F3DPrimColor {prim_array_name}[]" + " = {\n" + indent) + + f"\n{indent}".join( + "{ " + f"{entry[0]}, {entry[1]}, {entry[2]}, {entry[3]}, {entry[4]}" + " }," + for entry in self.prim_colors + ) + + "\n};\n\n" + ) + + ( + ( + (f"F3DEnvColor {env_array_name}[]" + " = {\n" + indent) + + f"\n{indent}".join( + "{ " + f"{entry[0]}, {entry[1]}, {entry[2]}, {entry[3]}" + " }," for entry in self.env_colors + ) + + "\n};\n\n" + ) + if len(self.env_colors) > 0 + else "" + ) + + ( + ( + (f"u16 {frames_array_name}[]" + " = {\n" + indent) + + f"\n{indent}".join(f"{entry}," for entry in self.frames) + + "\n};\n\n" + ) + if len(self.frames) > 0 + else "" + ) + + ( + (params_name + " = {\n") + + (indent + f"{self.frame_length},\n") + + (indent + f"{length},\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: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.texture_1 = ( + "{ " + + f"{props.texture_1.step_x}, {props.texture_1.step_y}, {props.texture_1.width}, {props.texture_1.height}" + + " }," + ) + self.texture_2: Optional[str] = None + + if "two_tex" in type: + self.name = f"{self.base_name}{suffix}TwoTexScrollParams{self.header_suffix}" + self.texture_2 = ( + "{ " + + f"{props.texture_2.step_x}, {props.texture_2.step_y}, {props.texture_2.width}, {props.texture_2.height}" + + " }," + ) + else: + self.name = f"{self.base_name}{suffix}TexScrollParams{self.header_suffix}" + + def to_c(self, all_externs: bool = True): + data = CData() + params_name = f"AnimatedMatTexScrollParams {self.name}[]" + + # .h + if all_externs: + data.header = f"extern {params_name};\n" + + # .c + data.source = f"{params_name}" + " = {\n" + indent + self.texture_1 + + if self.texture_2 is not None: + data.source += "\n" + indent + self.texture_2 + + data.source += "\n};\n\n" + + return data + + +class AnimatedMatTexCycleParams: + def __init__( + self, + props: Z64_AnimatedMatTexCycleParams, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.use_macros = use_macros + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}TexCycleParams{self.header_suffix}" + self.textures: list[str] = [] + self.texture_indices: list[int] = [] + + for texture in props.textures: + self.textures.append(texture.symbol) + + for keyframe in props.keyframes: + assert keyframe.texture_index < len(self.textures), "ERROR: invalid AnimatedMatTexCycle texture index" + self.texture_indices.append(keyframe.texture_index) + + self.frame_length = len(self.texture_indices) + assert len(self.textures) > 0, "you need at least one texture symbol (Animated Material)" + assert len(self.texture_indices) > 0, "you need at least one texture index (Animated Material)" + + def to_c(self, all_externs: bool = True): + data = CData() + texture_array_name = f"{self.base_name}CycleTextures{self.header_suffix}" + texture_indices_array_name = f"{self.base_name}CycleTextureIndices{self.header_suffix}" + params_name = f"AnimatedMatTexCycleParams {self.name}" + + # .h + if all_externs: + data.header = ( + f"extern TexturePtr {texture_array_name}[];\n" + + f"extern u8 {texture_indices_array_name}[];\n" + + f"extern {params_name};\n" + ) + + # .c + data.source = ( + ( + (f"TexturePtr {texture_array_name}[]" + " = {\n") + + indent + + f",\n{indent}".join(texture for texture in self.textures) + + "\n};\n\n" + ) + + ( + (f"u8 {texture_indices_array_name}[]" + " = {\n") + + indent + + ", ".join(f"{index}" for index in self.texture_indices) + + "\n};\n\n" + ) + + ( + (params_name + " = {\n") + + indent + + f"{self.frame_length}, {texture_array_name}, {texture_indices_array_name}," + + "\n};\n\n" + ) + ) + + return data + + +class AnimatedMatTexTimedCycleParams: + def __init__( + self, + props: Z64_AnimatedMatTexTimedCycleParams, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}TexTimedCycleParams{self.header_suffix}" + self.use_macros = use_macros + self.entries: dict[str, int] = {} # entries["texture_symbol"] = duration + + for keyframe in props.keyframes: + self.entries[keyframe.symbol] = keyframe.duration + + assert len(self.entries) > 1, "ERROR: this type requires at least two entries" + + def to_c(self, all_externs: bool = True): + data = CData() + array_name = f"{self.base_name}TexTimedCycleKeyframes{self.header_suffix}" + params_name = f"AnimatedMatTexTimedCycleParams {self.name}" + + # .h + if all_externs: + data.header = f"extern AnimatedMatTexTimedCycleKeyframe {array_name}[];\n" + f"extern {params_name};\n" + + # .c + length = f"ARRAY_COUNT({array_name})" if self.use_macros else f"{len(self.entries)}" + data.source = ( + (f"AnimatedMatTexTimedCycleKeyframe {array_name}[]" + " = {\n") + + indent + + f"\n{indent}".join("{ " + f"{symbol}, {duration}" + " }," for symbol, duration in self.entries.items()) + + "\n};\n\n" + ) + ((params_name + " = {\n") + indent + f"{length}, {array_name}" + "\n};\n\n") + + return data + + +class AnimatedMatTextureParams: + def __init__( + self, + props: Z64_AnimatedMatTextureParams, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}TextureParams{self.header_suffix}" + self.texture_1 = props.texture_1 + self.texture_2 = props.texture_2 + assert len(self.texture_1) > 0 + assert len(self.texture_2) > 0 + + def to_c(self, all_externs: bool = True): + data = CData() + params_name = f"AnimatedMatTextureParams {self.name}" + + # .h + if all_externs: + data.header = f"extern {params_name};\n" + + # .c + data.source = params_name + " = {\n" + indent + "{ " + f"{self.texture_1}, {self.texture_2}" + " }" + "\n};\n\n" + + return data + + +class AnimatedMatMultiTextureParams: + def __init__( + self, + props: Z64_AnimatedMatMultiTextureParams, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}TextureParams{self.header_suffix}" + + self.min_prim_alpha: int = props.min_prim_alpha + self.max_prim_alpha: int = props.max_prim_alpha + self.min_env_alpha: int = props.min_env_alpha + self.max_env_alpha: int = props.max_env_alpha + self.speed: int = props.speed + self.use_texture_refs: bool = props.use_texture_refs + self.texture_1: str = props.texture_1 + self.texture_2: str = props.texture_2 + self.segment_1: int = props.segment_1 + self.segment_2: int = props.segment_2 + + def to_c(self, all_externs: bool = True): + data = CData() + params_name = f"AnimatedMatMultiTextureParams {self.name}" + + # .h + if all_externs: + data.header = f"extern {params_name};\n" + + # .c + data.source = ( + params_name + + " = {\n" + + indent + + f"{self.min_prim_alpha}, " + + f"{self.max_prim_alpha}, " + + f"{self.min_env_alpha}, " + + f"{self.max_env_alpha}, " + + f"{self.speed}, " + + ( + f"{self.texture_1}, {self.texture_2}, {self.segment_1}, {self.segment_2}," + if self.use_texture_refs + else "NULL, NULL, 0, 0" + ) + + "\n};\n\n" + ) + + return data + + +class AnimatedMatEventParams: + def __init__( + self, + props, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.segment_num = segment_num + self.type = type + + def to_c(self, all_externs: bool = True): + return CData() + + +class AnimatedMatSurfaceSwapParams: + def __init__( + self, + props: Z64_AnimatedMatSurfaceSwapParams, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}SurfaceSwapParams{self.header_suffix}" + self.surface_type = SurfaceType.new(props.col_settings, use_macros) + + ignore_cam = props.col_settings.ignoreCameraCollision + ignore_entity = props.col_settings.ignoreActorCollision + ignore_proj = props.col_settings.ignoreProjectileCollision + + if ignore_proj or ignore_entity or ignore_cam: + flag1 = ("COLPOLY_IGNORE_PROJECTILES" if use_macros else "(1 << 2)") if ignore_proj else "" + flag2 = ("COLPOLY_IGNORE_ENTITY" if use_macros else "(1 << 1)") if ignore_entity else "" + flag3 = ("COLPOLY_IGNORE_CAMERA" if use_macros else "(1 << 0)") if ignore_cam else "" + self.flags_a = "(" + " | ".join(flag for flag in [flag1, flag2, flag3] if len(flag) > 0) + ")" + else: + self.flags_a = "COLPOLY_IGNORE_NONE" if use_macros else "0" + + if props.col_settings.conveyorOption == "Land": + self.flags_b = "COLPOLY_IS_FLOOR_CONVEYOR" if use_macros else "(1 << 0)" + else: + self.flags_b = "COLPOLY_IGNORE_NONE" if use_macros else "0" + + self.multitexture: Optional[AnimatedMatMultiTextureParams] = None + if props.use_multitexture: + self.multitexture = AnimatedMatMultiTextureParams( + props.multitexture_params, + self.segment_num, + self.base_name, + "anim_mat_type_multitexture", + index, + use_macros, + col_header, + suffix, + ) + + self.meshes: list[int] = [] + self.surface_index = -1 + + if props.use_tris: + assert len(props.meshes) > 0, "ERROR: this context requires at least one entry" + + # TODO: find a less dumb way to get the index + for entry in col_header.collisionPoly.polyList: + if entry.index_to_obj is not None: + index = list(entry.index_to_obj.keys())[-1] + mesh_obj = list(entry.index_to_obj.values())[-1] + + for item in props.meshes: + if mesh_obj is item.mesh_obj: + self.meshes.append(index) + break + else: + assert props.material is not None, "ERROR: this context requires a material to be set" + + # TODO: find a less dumb way to get the index + for i, entry in enumerate(col_header.surfaceType.surfaceTypeList): + if entry.data_material is props.material: + self.surface_index = i + break + + assert self.surface_index >= 0, "ERROR: surface index not found, is the selected material assigned?" + + def to_c(self, all_externs: bool = True): + data = CData() + + params_name = f"AnimatedMatSurfaceSwapParams {self.name}" + data.append(self.multitexture.to_c() if self.multitexture is not None else CData()) + + # .h + if all_externs: + data.header += f"extern {params_name};\n" + + # .c + indices = (", ".join(f"{index}" for index in self.meshes) + ", ") if len(self.meshes) > 0 else "" + data.source += ( + params_name + + " = {\n" + + f"{self.surface_type.getEntryC()}\n" + + (indent + f"{self.surface_index},\n") + + (indent + f"{self.flags_a},\n") + + (indent + f"{self.flags_b},\n") + + (indent + f"{'&' + self.multitexture.name if self.multitexture is not None else 'NULL'},\n") + + (indent + "{ " f"{indices}" + "-1" + " },") + + "\n};\n\n" + ) + + return data + + +class AnimatedMatColorSwitchParams: + def __init__( + self, + props: Z64_AnimatedMatColorSwitchParams, + segment_num: int, + type: str, + base_name: str, + index: int, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + color_1 = props.color_1 + color_2 = props.color_2 + self.segment_num = segment_num + self.type = type + self.base_name = base_name + self.use_macros = use_macros + self.header_suffix = f"_{index:02}" + self.name = f"{self.base_name}{suffix}ColorSwitchParams{self.header_suffix}" + self.prim_colors: list[tuple[int, int, int, int, int]] = [] + self.env_colors: list[tuple[int, int, int, int]] = [(-1, -1, -1, -1), (-1, -1, -1, -1)] + self.use_env_colors: list[bool] = [self.bool_to_c(color_1.use_env_color), self.bool_to_c(color_2.use_env_color)] + + prim = exportColor(color_1.prim_color[0:3]) + [scaleToU8(color_1.prim_color[3])] + self.prim_colors.append((prim[0], prim[1], prim[2], prim[3], color_1.prim_lod_frac)) + if color_1.use_env_color: + self.env_colors[0] = tuple(exportColor(color_1.env_color[0:3]) + [scaleToU8(color_1.env_color[3])]) + + prim = exportColor(color_2.prim_color[0:3]) + [scaleToU8(color_2.prim_color[3])] + self.prim_colors.append((prim[0], prim[1], prim[2], prim[3], color_2.prim_lod_frac)) + if color_2.use_env_color: + self.env_colors[1] = tuple(exportColor(color_2.env_color[0:3]) + [scaleToU8(color_2.env_color[3])]) + + assert len(self.prim_colors) == 2, "ERROR: unexpected prim color list length" + + def bool_to_c(self, value: bool): + return "true" if value else "false" + + def to_c(self, all_externs: bool = True): + data = CData() + params_name = f"AnimatedMatColorSwitchParams {self.name}" + + # .h + if all_externs: + data.header = f"extern {params_name};\n" + + # .c + data.source = ( + (params_name + " = {\n") + + indent + + "{ " + + f", ".join( + "{ " + f"{entry[0]}, {entry[1]}, {entry[2]}, {entry[3]}, {entry[4]}" + " }" + for entry in self.prim_colors + ) + + " },\n" + + indent + + "{ " + + f", ".join("{ " + f"{entry[0]}, {entry[1]}, {entry[2]}, {entry[3]}" + " }" for entry in self.env_colors) + + " },\n" + + indent + + "{ " + + f"{self.use_env_colors[0]}, {self.use_env_colors[1]}" + + " },\n" + + "};\n\n" + ) + + return data + + +class AnimatedMaterial: + def __init__( + self, + props: Z64_AnimatedMaterial, + base_name: str, + use_macros: bool, + col_header: CollisionHeader, + suffix: str = "", + ): + self.name = base_name + self.entries = [] + self.event_map: dict[int, tuple[str, str, CData]] = {} # type index to event data + self.cam_type = ( + game_data.z64.get_enum_value("anim_mats_cam_type", props.cam_type) + if props.cam_type != "Custom" + else props.cam_type_custom + ) + self.cam_on_event = "true" if props.cam_on_event and self.cam_type != "anim_mat_camera_type_none" else "false" + + if len(props.entries) == 0: + return + + type_list_map: dict[str, tuple[Any, Optional[str]]] = { + "anim_mat_type_tex_scroll": (AnimatedMatTexScrollParams, "tex_scroll_params"), + "anim_mat_type_two_tex_scroll": (AnimatedMatTexScrollParams, "tex_scroll_params"), + "anim_mat_type_color": (AnimatedMatColorParams, "color_params"), + "anim_mat_type_color_lerp": (AnimatedMatColorParams, "color_params"), + "anim_mat_type_color_nonlinear_interp": (AnimatedMatColorParams, "color_params"), + "anim_mat_type_tex_cycle": (AnimatedMatTexCycleParams, "tex_cycle_params"), + "anim_mat_type_color_cycle": (AnimatedMatColorParams, "color_params"), + "anim_mat_type_tex_timed_cycle": (AnimatedMatTexTimedCycleParams, "tex_timed_cycle_params"), + "anim_mat_type_texture": (AnimatedMatTextureParams, "texture_params"), + "anim_mat_type_multitexture": (AnimatedMatMultiTextureParams, "multitexture_params"), + "anim_mat_type_event": (AnimatedMatEventParams, None), + "anim_mat_type_surface_swap": (AnimatedMatSurfaceSwapParams, "surface_params"), + "anim_mat_type_oscillating_two_tex": (AnimatedMatTexScrollParams, "tex_scroll_params"), + "anim_mat_type_color_switch": (AnimatedMatColorSwitchParams, "color_switch_params"), + } + + for i, item in enumerate(props.entries): + type = item.type if item.type != "Custom" else item.type_custom + if type != "Custom" and type != "anim_mat_type_none": + class_def, prop_name = type_list_map[type] + props = getattr(item, prop_name) if type != "anim_mat_type_event" else None + self.entries.append( + class_def(props, item.segment_num, type, base_name, i, use_macros, col_header, suffix) + ) + script_data = item.events.export(base_name, i) + if script_data is not None: + data_name, script_name = item.events.get_symbols(base_name, i) + self.event_map[i] = (data_name, script_name, script_data) + + def to_c(self, all_externs: bool = True): + data = CData() + + is_extended = is_hackeroot() + + for i, entry in enumerate(self.entries): + data.append(entry.to_c(all_externs)) + + if is_extended and len(self.event_map) > 0 and i in self.event_map: + _, _, event_data = self.event_map[i] + if all_externs: + data.header += event_data.header + + data.source += event_data.source + + array_name = f"AnimatedMaterial {self.name}[]" + + # .h + data.header += f"extern {array_name};\n" + + # .c + data.source += array_name + " = {\n" + indent + + if len(self.entries) > 0: + entries = [] + for i, entry in enumerate(self.entries): + if not is_extended: + script_name = "" + elif len(self.event_map) > 0 and i in self.event_map: + _, script_name, _ = self.event_map[i] + script_name = f" &{script_name}," + else: + script_name = " NULL," + + entries.append( + f"MATERIAL_SEGMENT_NUM(0x{entry.segment_num:02X}), " + + f"{game_data.z64.get_enum_value('anim_mats_type', entry.type)}, " + + ( + f"{'&' if 'tex_scroll' not in entry.type else ''}{entry.name}," + if entry.type != "anim_mat_type_event" + else "NULL," + ) + + script_name + ) + + # 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, NULL }," if is_extended else "{ 0, 6, NULL }," + + data.source += "\n};\n" + return data + + +@dataclass +class SceneAnimatedMaterial: + """This class hosts Animated Materials data for scenes""" + + name: str + animated_material: Optional[AnimatedMaterial] + + # add a macro for the segment number for convenience (only if using animated materials) + mat_seg_num_macro = "\n".join( + [ + "// Animated Materials requires the segment number to be offset by 7", + "#ifndef MATERIAL_SEGMENT_NUM", + "#define MATERIAL_SEGMENT_NUM(n) ((n) - 7)", + "#endif\n", + "// The last entry also requires to be a negative number", + "#ifndef LAST_MATERIAL_SEGMENT_NUM", + "#define LAST_MATERIAL_SEGMENT_NUM(n) -MATERIAL_SEGMENT_NUM(n)", + "#endif\n\n", + ] + ) + + @staticmethod + def new(name: str, props: OOTSceneHeaderProperty, is_reuse: bool, use_macros: bool, col_header: CollisionHeader): + return SceneAnimatedMaterial( + name, AnimatedMaterial(props.animated_material, name, use_macros, col_header) if not is_reuse else None + ) + + @staticmethod + def export(): + """Exports animated materials data as C files, this should be called to do a separate export from the scene.""" + + settings: Z64_AnimatedMaterialExportSettings = bpy.context.scene.fast64.oot.anim_mats_export_settings + export_obj: Object = settings.export_obj + name = toAlnum(export_obj.name) + assert name is not None + + # convert props + entries: list[AnimatedMaterial] = [ + AnimatedMaterial(item, f"{name}_AnimatedMaterial_{i:02}", "_") + for i, item in enumerate(export_obj.fast64.oot.animated_materials.items) + ] + assert len(entries) > 0, "The Animated Material list is empty!" + + filename = f"{name.lower()}_anim_mats" + + # create C data + data = CData() + data.header += f'#include "{settings.get_include_name()}"\n' + + if is_hackeroot(): + data.header += '#include "config.h"\n\n' + + if bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + data.header += "#if ENABLE_ANIMATED_MATERIALS\n\n" + else: + data.header += "\n" + + if not settings.is_custom_path: + data.source += f'#include "assets/objects/{settings.object_name}/{filename}.h"\n\n' + + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + data.source += "#if ENABLE_ANIMATED_MATERIALS\n\n" + + data.header += SceneAnimatedMaterial.mat_seg_num_macro + + for entry in entries: + c_data = entry.to_c(False) + c_data.source += "\n" + data.append(c_data) + + if is_hackeroot(): + if not settings.is_custom_path: + data.header += "\n" + else: + data.source = data.source[:-1] + + extra = "" + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + extra = "#endif\n" + + data.source += extra + + if not settings.is_custom_path: + data.header += extra + + # write C data + if settings.is_custom_path: + export_path = Path(settings.export_path) + export_path.mkdir(exist_ok=True) + else: + export_path = Path(bpy.context.scene.ootDecompPath) / "assets" / "objects" / settings.object_name + + export_path = export_path.resolve() + assert export_path.exists(), f"This path doesn't exist: {repr(export_path)}" + + if settings.is_custom_path: + c_path = export_path / f"{filename}.inc.c" + c_path.write_text(data.header + "\n" + data.source) + else: + h_path = export_path / f"{filename}.h" + h_path.write_text(data.header) + + c_path = export_path / f"{filename}.c" + c_path.write_text(data.source) + + @staticmethod + def from_data(): + """Imports animated materials data from C files, this should be called to do a separate import from the scene.""" + + settings: Z64_AnimatedMaterialImportSettings = bpy.context.scene.fast64.oot.anim_mats_import_settings + import_path = Path(settings.import_path).resolve() + + file_data = import_path.read_text() + array_names = [ + match.group(1) + for match in re.finditer(r"AnimatedMaterial\s([a-zA-Z0-9_]*)\[\]\s=\s\{", file_data, re.DOTALL) + ] + + new_obj = get_new_empty_object("Actor Animated Materials") + new_obj.ootEmptyType = "Animated Materials" + + for array_name in array_names: + parse_animated_material(new_obj.fast64.oot.animated_materials.items.add(), file_data, array_name) + + def get_cmd(self): + """Returns the animated material scene command""" + + if is_hackeroot(): + am = self.animated_material + assert am is not None + cmd = f"SCENE_CMD_ANIMATED_MATERIAL_LIST({self.name}, MATERIAL_CAM_PARAMS({am.cam_type}, {am.cam_on_event})),\n" + else: + cmd = f"SCENE_CMD_ANIMATED_MATERIAL_LIST({self.name}),\n" + + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + return "#if ENABLE_ANIMATED_MATERIALS\n" + indent + cmd + "#endif\n" + else: + return indent + cmd + + def to_c(self, is_scene: bool = True): + data = CData() + + if self.animated_material is not None: + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + data.source += "#if ENABLE_ANIMATED_MATERIALS\n" + data.header += "#if ENABLE_ANIMATED_MATERIALS\n" + + data.append(self.animated_material.to_c()) + + extra = "" + if is_hackeroot() and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs: + extra = "#endif\n" + + data.source += extra + "\n" + data.header += ("\n" if not is_scene else "") + extra + + return data + + +@dataclass +class ActorAnimatedMaterial: + """This class hosts Animated Materials data for actors""" + + name: str + entries: list[AnimatedMaterial] + + @staticmethod + def new(name: str, scene_obj: Object, header_index: int): + obj_list = getObjectList(scene_obj.children_recursive, "EMPTY", "Animated Materials") + entries: list[AnimatedMaterial] = [] + + for obj in obj_list: + entries.extend( + [AnimatedMaterial(item, name, header_index) for item in obj.fast64.oot.animated_materials.items] + ) + + return ActorAnimatedMaterial(name, entries) diff --git a/fast64_internal/oot/exporter/scene/general.py b/fast64_internal/z64/exporter/scene/general.py similarity index 54% rename from fast64_internal/oot/exporter/scene/general.py rename to fast64_internal/z64/exporter/scene/general.py index 2a1638dea..ec112a1d1 100644 --- a/fast64_internal/oot/exporter/scene/general.py +++ b/fast64_internal/z64/exporter/scene/general.py @@ -1,7 +1,12 @@ +import bpy +import re + from dataclasses import dataclass from bpy.types import Object -from ....utility import PluginError, CData, exportColor, ootGetBaseOrCustomLight, indent -from ...scene.properties import OOTSceneHeaderProperty, OOTLightProperty + +from ....utility import PluginError, CData, exportColor, ootGetBaseOrCustomLight, hexOrDecInt, indent +from ...scene.properties import OOTSceneHeaderProperty, OOTLightProperty, OOTLightGroupProperty +from ...utility import getEvalParamsInt from ..utility import Utility @@ -10,6 +15,7 @@ class EnvLightSettings: """This class defines the information of one environment light setting""" envLightMode: str + setting_name: str ambientColor: tuple[int, int, int] light1Color: tuple[int, int, int] light1Dir: tuple[int, int, int] @@ -20,9 +26,79 @@ class EnvLightSettings: zFar: int blendRate: int + @staticmethod + def from_data(raw_data: str, not_zapd_assets: bool): + lights: list[EnvLightSettings] = [] + split_str = ",},{" if not_zapd_assets else "},{" + entries = raw_data.removeprefix("{").removesuffix("},").split(split_str) + + for entry in entries: + if not_zapd_assets: + colors_and_dirs = [] + for match in re.finditer(r"(\{([0-9\-]*,[0-9\-]*,[0-9\-]*)\})", entry, re.DOTALL): + colors_and_dirs.append([hexOrDecInt(value) for value in match.group(2).split(",")]) + + if "BLEND_RATE_AND_FOG_NEAR" in entry: + blend_and_fogs = entry.replace(")", "").split("BLEND_RATE_AND_FOG_NEAR(")[-1].strip().split(",") + fog_near = hexOrDecInt(blend_and_fogs[1]) + z_far = hexOrDecInt(blend_and_fogs[2]) + blend_rate = getEvalParamsInt(blend_and_fogs[0]) + assert blend_rate is not None + blend_rate *= 4 + else: + blend_and_fogs = entry.split("},")[-1].split(",") + if blend_and_fogs[0].endswith(")"): + blend_split = blend_and_fogs[0].removeprefix("(").removesuffix(")").split("|") + else: + blend_split = blend_and_fogs[0].split("|") + blend_raw = blend_split[0] + fog_near = hexOrDecInt(blend_split[1]) + z_far = hexOrDecInt(blend_and_fogs[1]) + blend_rate = getEvalParamsInt(blend_raw) + assert blend_rate is not None + + if "/" in blend_raw: + blend_rate *= 4 + else: + split = entry.split(",") + + colors_and_dirs = [ + [hexOrDecInt(value) for value in split[0:3]], + [hexOrDecInt(value) for value in split[3:6]], + [hexOrDecInt(value) for value in split[6:9]], + [hexOrDecInt(value) for value in split[9:12]], + [hexOrDecInt(value) for value in split[12:15]], + [hexOrDecInt(value) for value in split[15:18]], + ] + + blend_rate = hexOrDecInt(split[18]) >> 10 + fog_near = hexOrDecInt(split[18]) & 0x3FF + z_far = hexOrDecInt(split[19]) + + lights.append( + EnvLightSettings( + "Custom", + "Custom Light Settings", + tuple(colors_and_dirs[0]), + tuple(colors_and_dirs[1]), + tuple(colors_and_dirs[2]), + tuple(colors_and_dirs[3]), + tuple(colors_and_dirs[4]), + tuple(colors_and_dirs[5]), + fog_near, + z_far, + blend_rate, + ) + ) + + return lights + def getBlendFogNear(self): """Returns the packed blend rate and fog near values""" + if bpy.context.scene.fast64.oot.useDecompFeatures: + return f"BLEND_RATE_AND_FOG_NEAR({self.blendRate}, {self.fogNear})" + return f"(({self.blendRate} << 10) | {self.fogNear})" def getColorValues(self, vector: tuple[int, int, int]): @@ -35,11 +111,9 @@ def getDirectionValues(self, vector: tuple[int, int, int]): return ", ".join(f"{v - 0x100 if v > 0x7F else v:5}" for v in vector) - def getEntryC(self, index: int): + def getEntryC(self): """Returns an environment light entry""" - isLightingCustom = self.envLightMode == "Custom" - vectors = [ (self.ambientColor, "Ambient Color", self.getColorValues), (self.light1Dir, "Diffuse0 Direction", self.getDirectionValues), @@ -54,21 +128,8 @@ def getEntryC(self, index: int): (f"{self.zFar}", "Fog Far"), ] - lightDescs = ["Dawn", "Day", "Dusk", "Night"] - - if not isLightingCustom and self.envLightMode == "LIGHT_MODE_TIME": - # TODO: Improve the lighting system. - # Currently Fast64 assumes there's only 4 possible settings for "Time of Day" lighting. - # This is not accurate and more complicated, - # for now we are doing ``index % 4`` to avoid having an OoB read in the list - # but this will need to be changed the day the lighting system is updated. - lightDesc = f"// {lightDescs[index % 4]} Lighting\n" - else: - isIndoor = not isLightingCustom and self.envLightMode == "LIGHT_MODE_SETTINGS" - lightDesc = f"// {'Indoor' if isIndoor else 'Custom'} No. {index + 1} Lighting\n" - lightData = ( - (indent + lightDesc) + (indent + f"// {self.setting_name}\n") + (indent + "{\n") + "".join( indent * 2 + f"{'{ ' + vecToC(vector) + ' },':26} // {desc}\n" for (vector, desc, vecToC) in vectors @@ -91,32 +152,46 @@ class SceneLighting: @staticmethod def new(name: str, props: OOTSceneHeaderProperty): envLightMode = Utility.getPropValue(props, "skyboxLighting") - lightList: list[OOTLightProperty] = [] + lightList: dict[str, OOTLightProperty] = {} settings: list[EnvLightSettings] = [] + is_custom = props.skyboxLighting == "Custom" - if envLightMode == "LIGHT_MODE_TIME": - todLights = props.timeOfDayLights - lightList = [todLights.dawn, todLights.day, todLights.dusk, todLights.night] - else: - lightList = props.lightList + if not is_custom and envLightMode == "LIGHT_MODE_TIME": + tod_lights: list[OOTLightGroupProperty] = [props.timeOfDayLights] + list(props.tod_lights) - for lightProp in lightList: - light1 = ootGetBaseOrCustomLight(lightProp, 0, True, True) - light2 = ootGetBaseOrCustomLight(lightProp, 1, True, True) - settings.append( - EnvLightSettings( - envLightMode, - exportColor(lightProp.ambient), - light1[0], - light1[1], - light2[0], - light2[1], - exportColor(lightProp.fogColor), - lightProp.fogNear, - lightProp.z_far, - lightProp.transitionSpeed, + for i, tod_light in enumerate(tod_lights): + for tod_type in ["Dawn", "Day", "Dusk", "Night"]: + setting_name = ( + f"Default Settings ({tod_type})" if i == 0 else f"Light Settings No. {i} ({tod_type})" + ) + lightList[setting_name] = getattr(tod_light, tod_type.lower()) + else: + is_indoor = not is_custom and envLightMode == "LIGHT_MODE_SETTINGS" + lightList = { + f"{'Indoor' if is_indoor else 'Custom'} No. {i + 1}": light for i, light in enumerate(props.lightList) + } + + for setting_name, lightProp in lightList.items(): + try: + light1 = ootGetBaseOrCustomLight(lightProp, 0, True, True) + light2 = ootGetBaseOrCustomLight(lightProp, 1, True, True) + settings.append( + EnvLightSettings( + envLightMode, + setting_name, + exportColor(lightProp.ambient), + light1[0], + light1[1], + light2[0], + light2[1], + exportColor(lightProp.fogColor), + lightProp.fogNear, + lightProp.z_far, + lightProp.transitionSpeed, + ) ) - ) + except Exception as exc: + raise PluginError(f"In light settings {setting_name}: {exc}") from exc return SceneLighting(name, envLightMode, settings) def getCmd(self): @@ -137,7 +212,7 @@ def getC(self): # .c lightSettingsC.source = ( - (lightName + " = {\n") + "".join(light.getEntryC(i) for i, light in enumerate(self.settings)) + "};\n\n" + (lightName + " = {\n") + "".join(light.getEntryC() for i, light in enumerate(self.settings)) + "};\n\n" ) return lightSettingsC @@ -154,6 +229,7 @@ class SceneInfos: drawConfig: str appendNullEntrance: bool useDummyRoomList: bool + title_card_name: str ### Skybox And Sound ### @@ -182,6 +258,7 @@ def new(props: OOTSceneHeaderProperty, sceneObj: Object): Utility.getPropValue(props.sceneTableEntry, "drawConfig"), props.appendNullEntrance, sceneObj.fast64.oot.scene.write_dummy_room_list, + Utility.getPropValue(props, "title_card_name"), Utility.getPropValue(props, "skyboxID"), Utility.getPropValue(props, "skyboxCloudiness"), Utility.getPropValue(props, "musicSeq"), @@ -235,7 +312,7 @@ def getC(self): """Returns a ``CData`` containing the C data of the exit array""" exitListC = CData() - listName = f"u16 {self.name}[{len(self.exitList)}]" + listName = f"s16 {self.name}[{len(self.exitList)}]" # .h exitListC.header = f"extern {listName};\n" diff --git a/fast64_internal/oot/exporter/scene/header.py b/fast64_internal/z64/exporter/scene/header.py similarity index 74% rename from fast64_internal/oot/exporter/scene/header.py rename to fast64_internal/z64/exporter/scene/header.py index 72bc8d32f..0e0a00437 100644 --- a/fast64_internal/oot/exporter/scene/header.py +++ b/fast64_internal/z64/exporter/scene/header.py @@ -2,12 +2,16 @@ 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 ..collision import CollisionHeader from .general import SceneLighting, SceneInfos, SceneExits from .actors import SceneTransitionActors, SceneEntranceActors, SceneSpawns from .pathways import ScenePathways +from .animated_mats import SceneAnimatedMaterial @dataclass @@ -24,11 +28,30 @@ class SceneHeader: spawns: Optional[SceneSpawns] path: Optional[ScenePathways] + # MM (or modded OoT) + anim_mat: Optional[SceneAnimatedMaterial] + @staticmethod def new( - name: str, props: OOTSceneHeaderProperty, sceneObj: Object, transform: Matrix, headerIndex: int, useMacros: bool + name: str, + props: OOTSceneHeaderProperty, + sceneObj: Object, + transform: Matrix, + headerIndex: int, + useMacros: bool, + use_mat_anim: bool, + target_name: Optional[str], + col_header: CollisionHeader, ): entranceActors = SceneEntranceActors.new(f"{name}_playerEntryList", sceneObj, transform, headerIndex) + + animated_materials = None + if use_mat_anim and not is_oot_features(): + final_name = target_name if target_name is not None else f"{name}_AnimMat" + animated_materials = SceneAnimatedMaterial.new( + final_name, props, target_name is not None, useMacros, col_header + ) + return SceneHeader( name, SceneInfos.new(props, sceneObj), @@ -39,6 +62,7 @@ def new( entranceActors, SceneSpawns(f"{name}_entranceList", entranceActors.entries), ScenePathways.new(f"{name}_pathway", sceneObj, transform, headerIndex), + animated_materials, ) def getC(self): @@ -67,6 +91,10 @@ def getC(self): if len(self.path.pathList) > 0: headerData.append(self.path.getC()) + # Write the animated material list, if used + if self.anim_mat is not None: + headerData.append(self.anim_mat.to_c()) + return headerData diff --git a/fast64_internal/oot/exporter/scene/pathways.py b/fast64_internal/z64/exporter/scene/pathways.py similarity index 98% rename from fast64_internal/oot/exporter/scene/pathways.py rename to fast64_internal/z64/exporter/scene/pathways.py index 331af8dbe..7feb19248 100644 --- a/fast64_internal/oot/exporter/scene/pathways.py +++ b/fast64_internal/z64/exporter/scene/pathways.py @@ -2,7 +2,7 @@ from mathutils import Matrix from bpy.types import Object from ....utility import PluginError, CData, indent -from ...oot_utility import getObjectList +from ...utility import getObjectList from ..utility import Utility diff --git a/fast64_internal/oot/exporter/scene/rooms.py b/fast64_internal/z64/exporter/scene/rooms.py similarity index 79% rename from fast64_internal/oot/exporter/scene/rooms.py rename to fast64_internal/z64/exporter/scene/rooms.py index 6948ef6bb..02a75d7f4 100644 --- a/fast64_internal/oot/exporter/scene/rooms.py +++ b/fast64_internal/z64/exporter/scene/rooms.py @@ -2,8 +2,8 @@ from mathutils import Matrix from bpy.types import Object from ....utility import PluginError, CData, indent -from ...oot_utility import getObjectList -from ...oot_model_classes import OOTModel +from ...utility import ExportInfo, getObjectList +from ...model_classes import OOTModel from ..room import Room @@ -13,16 +13,29 @@ class RoomEntries: entries: list[Room] @staticmethod - def new(name: str, sceneName: str, model: OOTModel, sceneObj: Object, transform: Matrix, saveTexturesAsPNG: bool): + def new( + name: str, + sceneName: str, + model: OOTModel, + original_scene_obj: Object, + sceneObj: Object, + transform: Matrix, + exportInfo: ExportInfo, + ): """Returns the room list from empty objects with the type 'Room'""" roomDict: dict[int, Room] = {} roomObjs = getObjectList(sceneObj.children_recursive, "EMPTY", "Room") + original_room_list = getObjectList(original_scene_obj.children_recursive, "EMPTY", "Room") + assert len(original_room_list) == len(roomObjs) + + roomObjs.sort(key=lambda obj: obj.ootRoomHeader.roomIndex) + original_room_list.sort(key=lambda obj: obj.ootRoomHeader.roomIndex) if len(roomObjs) == 0: raise PluginError("ERROR: The scene has no child empties with the 'Room' empty type.") - for roomObj in roomObjs: + for original_room_obj, roomObj in zip(original_room_list, roomObjs): roomHeader = roomObj.ootRoomHeader roomIndex = roomHeader.roomIndex @@ -34,6 +47,7 @@ def new(name: str, sceneName: str, model: OOTModel, sceneObj: Object, transform: roomName, transform, sceneObj, + original_room_obj, roomObj, roomHeader.roomShape, model.addSubModel( @@ -41,11 +55,12 @@ def new(name: str, sceneName: str, model: OOTModel, sceneObj: Object, transform: f"{roomName}_dl", model.DLFormat, None, + model.draw_config, ) ), roomIndex, sceneName, - saveTexturesAsPNG, + exportInfo, ) for i in range(min(roomDict.keys()), len(roomDict)): diff --git a/fast64_internal/oot/skeleton/exporter/__init__.py b/fast64_internal/z64/exporter/skeleton/__init__.py similarity index 100% rename from fast64_internal/oot/skeleton/exporter/__init__.py rename to fast64_internal/z64/exporter/skeleton/__init__.py diff --git a/fast64_internal/oot/skeleton/exporter/classes.py b/fast64_internal/z64/exporter/skeleton/classes.py similarity index 99% rename from fast64_internal/oot/skeleton/exporter/classes.py rename to fast64_internal/z64/exporter/skeleton/classes.py index 278218782..269492e0f 100644 --- a/fast64_internal/oot/skeleton/exporter/classes.py +++ b/fast64_internal/z64/exporter/skeleton/classes.py @@ -1,4 +1,5 @@ import mathutils + from ....f3d.f3d_writer import GfxList from ....utility import CData, toAlnum, writeXMLData import os diff --git a/fast64_internal/oot/skeleton/exporter/functions.py b/fast64_internal/z64/exporter/skeleton/functions.py similarity index 91% rename from fast64_internal/oot/skeleton/exporter/functions.py rename to fast64_internal/z64/exporter/skeleton/functions.py index 6254e5e2d..ec5653b71 100644 --- a/fast64_internal/oot/skeleton/exporter/functions.py +++ b/fast64_internal/z64/exporter/skeleton/functions.py @@ -1,11 +1,15 @@ -import mathutils, bpy, os +import mathutils +import bpy +import os + +from pathlib import Path from ....f3d.f3d_gbi import DLFormat, FMesh, TextureExportSettings, ScrollMethod from ....f3d.f3d_writer import getInfoDict -from ...oot_f3d_writer import ootProcessVertexGroup, writeTextureArraysNew, writeTextureArraysExisting -from ...oot_model_classes import OOTModel, OOTGfxFormatter -from ..constants import ootSkeletonImportDict -from ..properties import OOTSkeletonExportSettings -from ..utility import ootDuplicateArmatureAndRemoveRotations, getGroupIndices, ootRemoveSkeleton +from ...f3d_writer import ootProcessVertexGroup, writeTextureArraysNew, writeTextureArraysExisting +from ...model_classes import OOTModel, OOTGfxFormatter +from ...skeleton.constants import ootSkeletonImportDict +from ...skeleton.properties import OOTSkeletonExportSettings +from ...skeleton.utility import ootDuplicateArmatureAndRemoveRotations, getGroupIndices, ootRemoveSkeleton from .classes import OOTLimb, OOTSkeleton from ....utility import ( @@ -18,7 +22,7 @@ cleanupDuplicatedObjects, ) -from ...oot_utility import ( +from ...utility import ( checkEmptyName, checkForStartBone, getStartBone, @@ -387,12 +391,23 @@ def ootConvertArmatureToC( limbList[i].lodDL = lodLimbList[i].DL limbList[i].isFlex |= lodLimbList[i].isFlex + header_filename = Path(filename).parts[-1] data = CData() - data.source += '#include "ultra64.h"\n#include "global.h"\n' + + data.header = f"#ifndef {header_filename.upper()}_H\n" + f"#define {header_filename.upper()}_H\n\n" + + if bpy.context.scene.fast64.oot.is_globalh_present(): + data.header += '#include "ultra64.h"\n' + '#include "global.h"\n' + elif bpy.context.scene.fast64.oot.is_z64sceneh_present(): + data.header += '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "z64animation.h"\n' + else: + data.header += '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "animation.h"\n' + + data.source = f'#include "{header_filename}.h"\n\n' if not isCustomExport: - data.source += '#include "' + folderName + '.h"\n\n' + data.header += f'#include "{folderName}.h"\n\n' else: - data.source += "\n" + data.header += "\n" path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, True, True) includeDir = settings.customAssetIncludeDir if settings.isCustom else f"assets/objects/{folderName}" @@ -408,6 +423,7 @@ def ootConvertArmatureToC( textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D) data.append(textureArrayData) + data.header += "\n#endif\n" writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) if not isCustomExport: diff --git a/fast64_internal/oot/exporter/utility.py b/fast64_internal/z64/exporter/utility.py similarity index 98% rename from fast64_internal/oot/exporter/utility.py rename to fast64_internal/z64/exporter/utility.py index 16a76590f..297803bd2 100644 --- a/fast64_internal/oot/exporter/utility.py +++ b/fast64_internal/z64/exporter/utility.py @@ -2,7 +2,7 @@ from mathutils import Quaternion, Matrix from bpy.types import Object from ...utility import PluginError, indent -from ..oot_utility import ootConvertTranslation, ootConvertRotation +from ..utility import ootConvertTranslation, ootConvertRotation from ..actor.properties import OOTActorHeaderProperty diff --git a/fast64_internal/z64/f3d/operators.py b/fast64_internal/z64/f3d/operators.py new file mode 100644 index 000000000..891e10190 --- /dev/null +++ b/fast64_internal/z64/f3d/operators.py @@ -0,0 +1,242 @@ +import bpy +import os +import mathutils + +from bpy.types import Operator +from bpy.ops import object +from bpy.path import abspath +from bpy.utils import register_class, unregister_class +from mathutils import Matrix +from typing import Optional + +from ...utility import CData, PluginError, ExportUtils, raisePluginError, writeCData, toAlnum +from ...f3d.f3d_parser import importMeshC, getImportData +from ...f3d.f3d_gbi import DLFormat, TextureExportSettings, ScrollMethod, get_F3D_GBI +from ...f3d.f3d_writer import TriangleConverterInfo, removeDL, saveStaticModel, getInfoDict +from ..utility import ootGetObjectPath, ootGetObjectHeaderPath, getOOTScale +from ..model_classes import OOTF3DContext, ootGetIncludedAssetData +from ..texture_array import ootReadTextureArrays +from ..model_classes import OOTModel, OOTGfxFormatter +from ..f3d_writer import ootReadActorScale, writeTextureArraysNew, writeTextureArraysExisting +from .properties import OOTDLImportSettings, OOTDLExportSettings + +from ..utility import ( + OOTObjectCategorizer, + ootDuplicateHierarchy, + ootCleanupScene, + ootGetPath, + addIncludeFiles, + getOOTScale, +) + + +class OOTF3DGfxFormatter(OOTGfxFormatter): + def __init__(self, scrollMethod): + OOTGfxFormatter.__init__(self, scrollMethod) + + # override the function to give a custom name to the DL array + def drawToC(self, f3d, gfxList, layer: Optional[str] = None): + return gfxList.to_c(f3d, name_override=f"{gfxList.name}_{layer.lower()}_dl") + + +def ootConvertMeshToC( + originalObj: bpy.types.Object, + finalTransform: mathutils.Matrix, + DLFormat: DLFormat, + saveTextures: bool, + settings: OOTDLExportSettings, +): + folderName = settings.folder + exportPath = bpy.path.abspath(settings.customPath) + isCustomExport = settings.isCustom + removeVanillaData = settings.removeVanillaData + name = toAlnum(originalObj.name) + overlayName = settings.actorOverlayName + flipbookUses2DArray = settings.flipbookUses2DArray + flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None + + try: + obj, allObjs = ootDuplicateHierarchy(originalObj, None, False, OOTObjectCategorizer()) + + fModel = OOTModel(name, DLFormat, None) + triConverterInfo = TriangleConverterInfo(obj, None, fModel.f3d, finalTransform, getInfoDict(obj)) + fMeshes = saveStaticModel( + triConverterInfo, fModel, obj, finalTransform, fModel.name, not saveTextures, False, "oot" + ) + + # Since we provide a draw layer override, there should only be one fMesh. + for fMesh in fMeshes.values(): + fMesh.draw.name = name + + ootCleanupScene(originalObj, allObjs) + + except Exception as e: + ootCleanupScene(originalObj, allObjs) + raise Exception(str(e)) + + filename = settings.filename if settings.isCustomFilename else name + data = CData() + data.header = f"#ifndef {filename.upper()}_H\n" + f"#define {filename.upper()}_H\n\n" + '#include "ultra64.h"\n' + + if bpy.context.scene.fast64.oot.is_globalh_present(): + data.header += '#include "global.h"\n' + + data.source = f'#include "{filename}.h"\n\n' + if not isCustomExport: + data.header += f'#include "{folderName}.h"\n\n' + else: + data.header += "\n" + + path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, True) + includeDir = settings.customAssetIncludeDir if settings.isCustom else f"assets/objects/{folderName}" + exportData = fModel.to_c( + TextureExportSettings(False, saveTextures, includeDir, path), OOTF3DGfxFormatter(ScrollMethod.Vertex) + ) + + data.append(exportData.all()) + + if isCustomExport: + textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D) + data.append(textureArrayData) + + data.header += "\n#endif\n" + writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) + + if not isCustomExport: + writeTextureArraysExisting(bpy.context.scene.ootDecompPath, overlayName, False, flipbookArrayIndex2D, fModel) + addIncludeFiles(folderName, path, name) + if removeVanillaData: + headerPath = os.path.join(path, folderName + ".h") + sourcePath = os.path.join(path, folderName + ".c") + removeDL(sourcePath, headerPath, name) + + +class OOT_ImportDL(Operator): + # set bl_ properties + bl_idname = "object.oot_import_dl" + bl_label = "Import DL" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + # Called on demand (i.e. button press, menu item) + # Can also be called from operator search menu (Spacebar) + def execute(self, context): + obj = None + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + + try: + settings: OOTDLImportSettings = context.scene.fast64.oot.DLImportSettings + name = settings.name + folderName = settings.folder + importPath = abspath(settings.customPath) + isCustomImport = settings.isCustom + basePath = abspath(context.scene.ootDecompPath) if not isCustomImport else os.path.dirname(importPath) + removeDoubles = settings.removeDoubles + importNormals = settings.importNormals + drawLayer = settings.drawLayer + overlayName = settings.actorOverlayName + flipbookUses2DArray = settings.flipbookUses2DArray + flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None + + paths = [ + ootGetObjectPath(isCustomImport, importPath, folderName, True), + ootGetObjectHeaderPath(isCustomImport, importPath, folderName, True), + ] + + filedata = getImportData(paths) + f3dContext = OOTF3DContext(get_F3D_GBI(), [name], basePath) + + scale = None + if not isCustomImport: + filedata = ootGetIncludedAssetData(basePath, paths, filedata) + filedata + + if overlayName is not None: + ootReadTextureArrays(basePath, overlayName, name, f3dContext, False, flipbookArrayIndex2D) + if settings.autoDetectActorScale: + scale = ootReadActorScale(basePath, overlayName, False) + + if scale is None: + scale = getOOTScale(settings.actorScale) + + obj = importMeshC( + filedata, + name, + scale, + removeDoubles, + importNormals, + drawLayer, + f3dContext, + ) + obj.ootActorScale = scale / context.scene.ootBlenderScale + + self.report({"INFO"}, "Success!") + return {"FINISHED"} + + except Exception as e: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + raisePluginError(self, e) + return {"CANCELLED"} # must return a set + + +class OOT_ExportDL(Operator): + # set bl_ properties + bl_idname = "object.oot_export_dl" + bl_label = "Export DL" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + # Called on demand (i.e. button press, menu item) + # Can also be called from operator search menu (Spacebar) + def execute(self, context): + with ExportUtils() as export_utils: + obj = None + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + if len(context.selected_objects) == 0: + raise PluginError("Mesh not selected.") + obj = context.active_object + if obj.type != "MESH": + raise PluginError("Mesh not selected.") + + finalTransform = Matrix.Scale(getOOTScale(obj.ootActorScale), 4) + + try: + # exportPath, levelName = getPathAndLevel(context.scene.geoCustomExport, + # context.scene.geoExportPath, context.scene.geoLevelName, + # context.scene.geoLevelOption) + + saveTextures = context.scene.saveTextures + exportSettings = context.scene.fast64.oot.DLExportSettings + + ootConvertMeshToC( + obj, + finalTransform, + DLFormat.Static, + saveTextures, + exportSettings, + ) + + self.report({"INFO"}, "Success!") + return {"FINISHED"} + + except Exception as e: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + raisePluginError(self, e) + return {"CANCELLED"} # must return a set + + +oot_dl_writer_classes = ( + OOT_ImportDL, + OOT_ExportDL, +) + + +def f3d_ops_register(): + for cls in oot_dl_writer_classes: + register_class(cls) + + +def f3d_ops_unregister(): + for cls in reversed(oot_dl_writer_classes): + unregister_class(cls) diff --git a/fast64_internal/oot/f3d/panels.py b/fast64_internal/z64/f3d/panels.py similarity index 95% rename from fast64_internal/oot/f3d/panels.py rename to fast64_internal/z64/f3d/panels.py index 30b0bff20..3c10eded6 100644 --- a/fast64_internal/oot/f3d/panels.py +++ b/fast64_internal/z64/f3d/panels.py @@ -22,7 +22,7 @@ class OOT_DisplayListPanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and ( + return context.scene.gameEditorMode in {"OOT", "MM"} and ( context.object is not None and isinstance(context.object.data, Mesh) ) @@ -58,7 +58,7 @@ class OOT_MaterialPanel(Panel): @classmethod def poll(cls, context): - return context.material is not None and context.scene.gameEditorMode == "OOT" + return context.material is not None and context.scene.gameEditorMode in {"OOT", "MM"} def draw(self, context): layout = self.layout @@ -91,7 +91,7 @@ class OOT_DrawLayersPanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" + return context.scene.gameEditorMode in {"OOT", "MM"} def draw(self, context): world = context.scene.world @@ -102,8 +102,8 @@ def draw(self, context): class OOT_ExportDLPanel(OOT_Panel): - bl_idname = "OOT_PT_export_dl" - bl_label = "OOT DL Exporter" + bl_idname = "Z64_PT_export_dl" + bl_label = "DL Exporter" # called every frame def draw(self, context): diff --git a/fast64_internal/oot/f3d/properties.py b/fast64_internal/z64/f3d/properties.py similarity index 98% rename from fast64_internal/oot/f3d/properties.py rename to fast64_internal/z64/f3d/properties.py index d4c0e4772..e57fa50e3 100644 --- a/fast64_internal/oot/f3d/properties.py +++ b/fast64_internal/z64/f3d/properties.py @@ -1,11 +1,11 @@ -import bpy - from bpy.types import PropertyGroup, Object, World, Material, UILayout from bpy.props import PointerProperty, StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty from bpy.utils import register_class, unregister_class + 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): @@ -19,7 +19,6 @@ class OOTDLExportSettings(PropertyGroup): name="Use Custom Path", description="Determines whether or not to export to an explicitly specified folder" ) removeVanillaData: BoolProperty(name="Replace Vanilla DLs") - drawLayer: EnumProperty(name="Draw Layer", items=ootEnumDrawLayers) actorOverlayName: StringProperty(name="Overlay", default="") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) @@ -45,7 +44,6 @@ def draw_props(self, layout: UILayout): box = layout.box().column() prop_split(box, self, "flipbookArrayIndex2D", "Flipbook Index") - prop_split(layout, self, "drawLayer", "Export Draw Layer") layout.prop(self, "isCustom") layout.prop(self, "removeVanillaData") @@ -62,12 +60,13 @@ class OOTDLImportSettings(PropertyGroup): flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) autoDetectActorScale: BoolProperty(name="Auto Detect Actor Scale", default=True) - actorScale: FloatProperty(name="Actor Scale", min=0, default=100) + actorScale: FloatProperty(name="Actor Scale", min=0, default=10) def draw_props(self, layout: UILayout): prop_split(layout, self, "name", "DL") if self.isCustom: prop_split(layout, self, "customPath", "File") + prop_split(layout, self, "actorScale", "Actor Scale") else: prop_split(layout, self, "folder", "Object") prop_split(layout, self, "actorOverlayName", "Overlay (Optional)") diff --git a/fast64_internal/oot/oot_f3d_writer.py b/fast64_internal/z64/f3d_writer.py similarity index 96% rename from fast64_internal/oot/oot_f3d_writer.py rename to fast64_internal/z64/f3d_writer.py index 3c43b1ac4..458fb3aff 100644 --- a/fast64_internal/oot/oot_f3d_writer.py +++ b/fast64_internal/z64/f3d_writer.py @@ -1,9 +1,14 @@ -import bpy, os, re +import os +import re +import bpy + +from typing import Optional + from ..utility import CData, getGroupIndexFromname, readFile, writeFile from ..f3d.flipbook import flipbook_to_c, flipbook_2d_to_c, flipbook_data_to_c from ..f3d.f3d_material import createF3DMat, F3DMaterial_UpdateLock, update_preset_manual -from .oot_utility import replaceMatchContent, getOOTScale -from .oot_texture_array import TextureFlipbook +from .utility import replaceMatchContent, getOOTScale +from .texture_array import TextureFlipbook from ..f3d.f3d_writer import ( checkForF3dMaterialInFaces, @@ -12,7 +17,7 @@ saveMeshByFaces, ) -from .oot_model_classes import ( +from .model_classes import ( OOTTriangleConverterInfo, OOTModel, ootGetActorData, @@ -291,12 +296,12 @@ def writeTextureArraysExisting2D(data: str, flipbook: TextureFlipbook, flipbookA array2DMatch = re.search( r"(static\s*)?void\s*\*\s*" + re.escape(flipbook.name) - + r"\s*\[\s*\]\s*\[\s*[0-9a-fA-Fx]*\s*\]\s*=\s*\{(.*?)\}\s*;", + + r"\s*\[\s*\]\s*\[\s*[0-9a-zA-Z_]*\s*\]\s*=\s*\{(.*?)\}\s*;", newData, flags=re.DOTALL, ) - newArrayData = "{ " + flipbook_data_to_c(flipbook) + " }" + newArrayData = "{\n" + flipbook_data_to_c(flipbook) + " }" # build a list of arrays here # replace existing element if list is large enough @@ -333,7 +338,7 @@ def writeTextureArraysExisting2D(data: str, flipbook: TextureFlipbook, flipbookA # Note this does not work well with actors containing multiple "parts". (z_en_honotrap) -def ootReadActorScale(basePath: str, overlayName: str, isLink: bool) -> float: +def ootReadActorScale(basePath: str, overlayName: str, isLink: bool) -> Optional[float]: if not isLink: actorData = ootGetActorData(basePath, overlayName) else: @@ -353,4 +358,8 @@ def ootReadActorScale(basePath: str, overlayName: str, isLink: bool) -> float: scale = scale[:-1] return getOOTScale(1 / float(scale)) - return getOOTScale(100) + if isLink: + return getOOTScale(100.0) + + print("WARNING: auto-detection failed, defaulting to this panel's actor scale property value") + return None diff --git a/fast64_internal/oot/file_settings.py b/fast64_internal/z64/file_settings.py similarity index 66% rename from fast64_internal/oot/file_settings.py rename to fast64_internal/z64/file_settings.py index 01e4c5642..0b6201f1f 100644 --- a/fast64_internal/oot/file_settings.py +++ b/fast64_internal/z64/file_settings.py @@ -1,14 +1,16 @@ from bpy.utils import register_class, unregister_class -from bpy.props import StringProperty, FloatProperty, BoolProperty +from bpy.props import StringProperty, FloatProperty from bpy.types import Scene + +from ..game_data import game_data from ..utility import prop_split from ..render_settings import on_update_render_settings from ..panels import OOT_Panel class OOT_FileSettingsPanel(OOT_Panel): - bl_idname = "OOT_PT_file_settings" - bl_label = "OOT File Settings" + bl_idname = "Z64_PT_file_settings" + bl_label = "Workspace Settings" bl_options = set() # default to being open # called every frame @@ -19,17 +21,29 @@ def draw(self, context): prop_split(col, context.scene, "ootDecompPath", "Decomp Path") - prop_split(col, context.scene.fast64.oot, "oot_version", "OoT Version") + is_oot = game_data.z64.is_oot() + version = "oot_version" if is_oot else "mm_version" + prop_split(col, context.scene.fast64.oot, version, "Game Version") if context.scene.fast64.oot.oot_version == "Custom": prop_split(col, context.scene.fast64.oot, "oot_version_custom", "Custom Version") + is_decomp = context.scene.fast64.oot.feature_set == "default" + if is_oot: + prop_split(col, context.scene.fast64.oot, "feature_set", "Feature Set") + col.prop(context.scene.fast64.oot, "headerTabAffectsVisibility") - col.prop(context.scene.fast64.oot, "featureSet") - if context.scene.fast64.oot.featureSet != "HackerOOT": + if is_oot and is_decomp: + col.prop(context.scene.fast64.oot, "mm_features") + + if game_data.z64.is_mm() or is_decomp: col.prop(context.scene.fast64.oot, "useDecompFeatures") + col.prop(context.scene.fast64.oot, "exportMotionOnly") + if is_oot: + col.prop(context.scene.fast64.oot, "use_new_actor_panel") + oot_classes = (OOT_FileSettingsPanel,) diff --git a/fast64_internal/z64/hackeroot/operators.py b/fast64_internal/z64/hackeroot/operators.py new file mode 100644 index 000000000..d7a584e03 --- /dev/null +++ b/fast64_internal/z64/hackeroot/operators.py @@ -0,0 +1,31 @@ +import os + +from bpy.path import abspath +from bpy.types import Operator +from bpy.utils import register_class, unregister_class + +from ..exporter.decomp_edit.config import Config + + +class HackerOoT_ClearBootupScene(Operator): + bl_idname = "object.hackeroot_clear_bootup_scene" + bl_label = "Undo Boot To Scene" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + Config.clearBootupScene(os.path.join(abspath(context.scene.ootDecompPath), "include/config/config_debug.h")) + self.report({"INFO"}, "Success!") + return {"FINISHED"} + + +classes = (HackerOoT_ClearBootupScene,) + + +def hackeroot_ops_register(): + for cls in classes: + register_class(cls) + + +def hackeroot_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/z64/hackeroot/panels.py b/fast64_internal/z64/hackeroot/panels.py new file mode 100644 index 000000000..3871b3b1d --- /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.feature_set == "hackeroot": + 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..a3cbf8bb0 --- /dev/null +++ b/fast64_internal/z64/hackeroot/properties.py @@ -0,0 +1,490 @@ +import bpy + +from typing import Optional, TYPE_CHECKING + +from bpy.types import PropertyGroup, UILayout, Object +from bpy.utils import register_class, unregister_class +from bpy.props import PointerProperty, EnumProperty, StringProperty, BoolProperty, IntProperty, CollectionProperty + +from ...game_data import game_data +from ...utility import PluginError, CData, prop_split, indent +from ..collection_utility import drawCollectionOps, draw_utility_ops +from ..utility import is_oot_features, is_hackeroot +from .operators import HackerOoT_ClearBootupScene + +if TYPE_CHECKING: + from ..scene.properties import OOTBootupSceneOptions + + +def bool_to_c(value: bool): + return "true" if value else "false" + + +class HackerOoT_EventConditionProperty(PropertyGroup): + type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("event_condition"), default=1) + type_custom: StringProperty() + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "type", "Condition Type") + + if self.type == "Custom": + prop_split(layout, self, "type_custom", "Custom Condition Type") + + def export(self): + return game_data.z64.get_enum_value("event_condition", self.type) + + +class HackerOoT_EventFlagProperty(PropertyGroup): + type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("event_flag_type"), default=1) + type_custom: StringProperty() + value: StringProperty() + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "type", "Flag Type") + + if self.type == "Custom": + prop_split(layout, self, "type_custom", "Custom Flag Type") + + prop_split(layout, self, "value", "Flag Value") + + def export(self): + type_to_cmd = { + "event_flag_type_switch_flag": "EVENT_SWITCH_FLAG", + "event_flag_type_eventchkinf_flag": "EVENT_EVENTCHKINF_FLAG", + "event_flag_type_inf_flag": "EVENT_INF_FLAG", + "event_flag_type_collectible_flag": "EVENT_COLLECTIBLE_FLAG", + "event_flag_type_treasure_flag": "EVENT_TREASURE_FLAG", + "event_flag_type_tempclear_flag": "EVENT_TEMPCLEAR_FLAG", + "event_flag_type_clear_flag": "EVENT_CLEAR_FLAG", + } + + if self.type == "Custom": + return f"EVENT_FLAG({self.type_custom}, {self.value})" + + return f"{type_to_cmd[self.type]}({self.value})" + + +class HackerOoT_EventInventoryProperty(PropertyGroup): + type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("event_inv_type"), default=1) + + item_id: EnumProperty(items=lambda self, context: game_data.z64.get_enum("inventory_items"), default=1) + upgrade_type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("upgrade_type"), default=1) + quest_item: EnumProperty(items=lambda self, context: game_data.z64.get_enum("quest_items"), default=1) + equipment_item: EnumProperty(items=lambda self, context: game_data.z64.get_enum("equipment_items"), default=1) + item_id_custom: StringProperty() + upgrade_type_custom: StringProperty() + quest_item_custom: StringProperty() + equipment_item_custom: StringProperty() + scene_id: StringProperty() + + is_upgrade: BoolProperty() + obtained: BoolProperty(name="Must be obtained") + check_ammo: BoolProperty() + amount: IntProperty(min=0) + check_sword_health: BoolProperty() + sword_health: IntProperty(min=0) + upgrade_value: StringProperty() + gs_tokens: IntProperty(min=0) + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "type", "Inventory Type") + + if self.type == "Custom": + layout.label(text="Custom type is not supported yet.") + return + + if self.type == "event_inv_type_items": + layout.prop(self, "obtained") + + prop_split(layout, self, "item_id", "Item ID") + + if self.item_id == "Custom": + prop_split(layout, self, f"item_id_custom", "Item ID Custom") + + layout.prop(self, "check_ammo", text="Check Amount") + + if self.check_ammo: + prop_split(layout, self, "amount", "Amount") + elif self.type == "event_inv_type_equipment": + layout.prop(self, "obtained") + + if not self.is_upgrade and not self.check_sword_health: + prop_split(layout, self, "equipment_item", "Item ID") + + if self.equipment_item == "Custom": + prop_split(layout, self, f"equipment_item_custom", "Item ID Custom") + + layout.prop(self, "is_upgrade", text="Upgrade Item") + + if self.is_upgrade: + prop_split(layout, self, "upgrade_type", "Upgrade Type") + + if self.upgrade_type == "Custom": + prop_split(layout, self, f"upgrade_type_custom", "Upgrade Type Custom") + + prop_split(layout, self, "upgrade_value", "Upgrade Value") + else: + layout.prop(self, "check_sword_health", text="Check Sword Health") + + if self.check_sword_health: + prop_split(layout, self, "sword_health", "Sword Health") + elif self.type == "event_inv_type_quest": + layout.prop(self, "obtained") + prop_split(layout, self, "quest_item", "Quest Item") + + if self.quest_item == "Custom": + prop_split(layout, self, f"quest_item_custom", "Quest Item Custom") + elif self.type == "event_inv_type_gs_tokens": + prop_split(layout, self, "gs_tokens", "Gold Skulltula Tokens") + else: + layout.label(text="Not implemented yet.", icon="ERROR") + + def export(self, cond: str): + type_to_cmd = { + "event_inv_type_items": "EVENT_ITEM", + "event_inv_type_equipment": "EVENT_EQUIPMENT", + "event_inv_type_quest": "EVENT_QUEST_ITEM", + "event_inv_type_gs_tokens": "EVENT_GS_TOKEN", + } + + cmd = type_to_cmd[self.type] + + if self.type in {"event_inv_type_items", "event_inv_type_equipment"}: + prop_name = "item_id" if self.type == "event_inv_type_items" else "equipment_item" + enum_name = "inventory_items" if self.type == "event_inv_type_items" else "equipment_items" + + if getattr(self, prop_name) == "Custom": + item_id = getattr(self, f"{prop_name}_custom") + else: + item_id = game_data.z64.get_enum_value(enum_name, getattr(self, prop_name)) + + if self.type == "event_inv_type_items": + if self.check_ammo: + assert "none" not in cond.lower(), "ERROR: item amount events must have a condition" + return f"{cmd}_AMMO({cond}, {item_id}, {self.amount})" + else: + if self.check_sword_health: + assert "none" not in cond.lower(), "ERROR: sword health events must have a condition" + return f"{cmd}_BGS({cond}, {self.sword_health})" + + if self.is_upgrade: + assert "none" not in cond.lower(), "ERROR: upgrade events must have a condition" + if self.upgrade_type == "Custom": + item_id = self.upgrade_type_custom + else: + item_id = game_data.z64.get_enum_value("upgrade_type", self.upgrade_type) + return f"{cmd}_UPG({cond}, {item_id}, {self.upgrade_value})" + + return f"{cmd}({item_id}, {bool_to_c(self.obtained)})" + elif self.type == "event_inv_type_quest": + if self.quest_item == "Custom": + item_id = self.quest_item_custom + else: + item_id = game_data.z64.get_enum_value("quest_items", self.quest_item) + return f"{cmd}({item_id}, {bool_to_c(self.obtained)})" + elif self.type == "event_inv_type_gs_tokens": + assert "none" not in cond.lower(), "ERROR: tokens events must have a condition" + return f"{cmd}({cond}, {self.gs_tokens})" + + raise PluginError(f"ERROR: type {repr(self.type)} not implemented yet.") + + +class HackerOoT_EventGameProperty(PropertyGroup): + type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("event_game_type"), default=1) + condition: PointerProperty(type=HackerOoT_EventConditionProperty) + + is_adult: BoolProperty(default=False) + health: IntProperty(min=0) + rupees: IntProperty(min=0) + magic: IntProperty(min=0) + inventory: PointerProperty(type=HackerOoT_EventInventoryProperty) + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "type", "Game Type") + + if self.type == "Custom": + layout.label(text="Custom type is not supported yet.") + return + + is_bgs = self.inventory.type == "event_inv_type_equipment" and self.inventory.check_sword_health + is_upg = self.inventory.type == "event_inv_type_equipment" and self.inventory.is_upgrade + is_ammo = self.inventory.type == "event_inv_type_items" and self.inventory.check_ammo + is_gs = self.inventory.type == "event_inv_type_gs_tokens" + is_player = self.type in {"event_game_type_health", "event_game_type_rupees", "event_game_type_magic"} + if is_player or is_bgs or is_upg or is_ammo or is_gs: + self.condition.draw_props(layout) + + if self.type == "event_game_type_age": + layout.prop(self, "is_adult", text="Is Adult") + elif self.type == "event_game_type_health": + prop_split(layout, self, "health", "Health Amount") + elif self.type == "event_game_type_rupees": + prop_split(layout, self, "rupees", "Rupee Amount") + elif self.type == "event_game_type_magic": + prop_split(layout, self, "magic", "Magic Amount") + elif self.type == "event_game_type_inventory": + self.inventory.draw_props(layout) + + def export(self): + cond = self.condition.export() + + if self.type == "event_game_type_age": + age = "LINK_AGE_ADULT" if self.is_adult else "LINK_AGE_CHILD" + return f"EVENT_AGE({age})" + elif self.type == "event_game_type_health": + assert "none" not in cond.lower(), "ERROR: health events must have a condition" + return f"EVENT_HEALTH({cond}, {self.health})" + elif self.type == "event_game_type_rupees": + assert "none" not in cond.lower(), "ERROR: rupee events must have a condition" + return f"EVENT_RUPEES({cond}, {self.rupees})" + elif self.type == "event_game_type_magic": + assert "none" not in cond.lower(), "ERROR: magic events must have a condition" + return f"EVENT_MAGIC({cond}, {self.magic})" + elif self.type == "event_game_type_inventory": + return self.inventory.export(cond) + + raise PluginError(f"ERROR: type {repr(self.type)} not implemented yet.") + + +class HackerOoT_EventClockProperty(PropertyGroup): + condition: PointerProperty(type=HackerOoT_EventConditionProperty) + hour: IntProperty(min=0, max=24) + minute: IntProperty(min=0, max=59) + + def draw_props(self, layout: UILayout, name: Optional[str]): + if name is not None: + layout.label(text=name) + + self.condition.draw_props(layout) + prop_split(layout, self, "hour", "Hour") + prop_split(layout, self, "minute", "Minute") + + def export(self): + return f"{self.condition.export()}, {self.hour}, {self.minute}" + + +class HackerOoT_EventTimeProperty(PropertyGroup): + type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("event_time_type"), default=1) + type_custom: StringProperty() + clock_1: PointerProperty(type=HackerOoT_EventClockProperty) + clock_2: PointerProperty(type=HackerOoT_EventClockProperty) + + is_night: BoolProperty(default=False) + is_clock: BoolProperty() + is_range: BoolProperty() + + def draw_props(self, layout: UILayout): + prop_split(layout, self, "type", "Time Type") + is_custom = self.type == "Custom" + + if is_custom: + prop_split(layout, self, "type_custom", "Custom Time Type") + + if is_custom: + self.clock_1.draw_props(layout.box().column(), "Clock 1") + self.clock_2.draw_props(layout.box().column(), "Clock 2") + layout.prop(self, "is_clock", text="Is Clock") + layout.prop(self, "is_range", text="Is Range") + + if not self.is_range: + layout.prop(self, "is_night", text="Is Night") + else: + if self.type in {"event_time_type_clock", "event_time_type_conditional"}: + if self.type == "event_time_type_clock": + box_1 = layout + name = None + else: + box_1 = layout.box().column() + name = "Clock 1" + + self.clock_1.draw_props(box_1, name) + + if self.type == "event_time_type_conditional": + self.clock_2.draw_props(layout.box().column(), "Clock 2") + + def export(self): + type_to_cmd = { + "event_time_type_clock": "EVENT_TIME_CLOCK", + "event_time_type_conditional": "EVENT_TIME_CONDITIONAL", + "event_time_type_day": "EVENT_TIME_DAY", + "event_time_type_night": "EVENT_TIME_NIGHT", + } + + if self.type == "Custom": + range_or_night = self.is_range if self.is_range else self.is_night + return f"EVENT_TIME({self.type_custom}, {bool_to_c(self.is_clock)}, {range_or_night}, {self.clock_1.export()}, {self.clock_2.export()})" + + cmd = type_to_cmd[self.type] + + if self.type == "event_time_type_clock": + return f"{cmd}({self.clock_1.export()})" + + if self.type == "event_time_type_conditional": + return f"{cmd}({self.clock_1.export()}, {self.clock_2.export()})" + + return f"{cmd}()" + + +class HackerOoT_EventItemProperty(PropertyGroup): + type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("event_type"), default=2) + flag: PointerProperty(type=HackerOoT_EventFlagProperty) + game: PointerProperty(type=HackerOoT_EventGameProperty) + time: PointerProperty(type=HackerOoT_EventTimeProperty) + + # ui only props + show_item: BoolProperty(default=False) + + def draw_props( + self, layout: UILayout, owner: Object, collec_type: str, index: int, header_index: int, parent_index: int + ): + layout.prop( + self, + "show_item", + text=f"Trigger Event No. {index + 1}", + icon="TRIA_DOWN" if self.show_item else "TRIA_RIGHT", + ) + + if self.show_item: + drawCollectionOps(layout, index, collec_type, header_index, owner.name, collection_index=parent_index) + prop_split(layout, self, "type", "Event Type") + + if self.type == "Custom": + layout.label(text="Custom type is not supported yet.") + return + + if self.type == "event_type_flag": + self.flag.draw_props(layout) + elif self.type == "event_type_game": + self.game.draw_props(layout) + elif self.type == "event_type_time": + self.time.draw_props(layout) + + def export(self): + if self.type == "event_type_flag": + return self.flag.export() + elif self.type == "event_type_game": + return self.game.export() + elif self.type == "event_type_time": + return self.time.export() + + raise PluginError(f"ERROR: type {repr(self.type)} not implemented yet.") + + +class HackerOoT_EventProperty(PropertyGroup): + entries: CollectionProperty(type=HackerOoT_EventItemProperty) + action_type: EnumProperty(items=lambda self, context: game_data.z64.get_enum("event_action_type"), default=1) + action_type_custom: StringProperty() + + # ui only props + show_entries: BoolProperty(default=False) + + def draw_props( + self, layout: UILayout, owner: Object, collec_type: str, header_index: int, collection_index: int = 0 + ): + layout.prop( + self, "show_entries", text=f"Trigger Events", icon="TRIA_DOWN" if self.show_entries else "TRIA_RIGHT" + ) + + if self.show_entries: + if is_oot_features() and not is_hackeroot(): + layout.label(text="This requires HackerOoT features.", icon="ERROR") + return + + prop_split(layout, self, "action_type", "Draw When") + + if self.action_type == "Custom": + prop_split(layout, self, "action_type_custom", "Draw When Custom") + + for i, entry in enumerate(self.entries): + entry.draw_props(layout.box().column(), owner, collec_type, i, header_index, collection_index) + + draw_utility_ops( + layout.row(), + len(self.entries), + collec_type, + header_index, + owner.name, + collection_index, + ask_for_amount=True, + do_copy=False, + ) + + if is_hackeroot() and len(self.entries) == 0: + layout.label(text="This animated material will always draw.", icon="QUESTION") + + def get_symbols(self, base_name: str, index: int): + data_name = f"{base_name}_EventData_{index:02}" + script_name = f"{base_name}_EventScriptEntry_{index:02}" + return data_name, script_name + + def export(self, base_name: str, index: int): + if len(self.entries) == 0 or not is_hackeroot(): + return None + + data = CData() + + cmd_data = "".join(indent + f"{entry.export()},\n" for entry in self.entries) + indent + "EVENT_END(),\n" + data_name, script_name = self.get_symbols(base_name, index) + + # .h + data.header = f"extern EventData {data_name}[];\n" + f"extern EventScriptEntry {script_name};\n" + + if self.action_type == "Custom": + action_type = self.action_type_custom + else: + action_type = game_data.z64.get_enum_value("event_action_type", self.action_type) + + # .c + data.source = ( + f"EventData {data_name}[]" + + " = {\n" + + cmd_data + + "};\n\n" + + f"EventScriptEntry {script_name}" + + " = {\n" + + indent + + f"{data_name}, {action_type},\n" + + "};\n\n" + ) + + return data + + +class HackerOoTSettings(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 = ( + HackerOoT_EventConditionProperty, + HackerOoT_EventFlagProperty, + HackerOoT_EventInventoryProperty, + HackerOoT_EventGameProperty, + HackerOoT_EventClockProperty, + HackerOoT_EventTimeProperty, + HackerOoT_EventItemProperty, + HackerOoT_EventProperty, + 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/oot/importer/__init__.py b/fast64_internal/z64/importer/__init__.py similarity index 100% rename from fast64_internal/oot/importer/__init__.py rename to fast64_internal/z64/importer/__init__.py diff --git a/fast64_internal/oot/importer/actor.py b/fast64_internal/z64/importer/actor.py similarity index 67% rename from fast64_internal/oot/importer/actor.py rename to fast64_internal/z64/importer/actor.py index 557c2d646..505510b72 100644 --- a/fast64_internal/oot/importer/actor.py +++ b/fast64_internal/z64/importer/actor.py @@ -2,9 +2,11 @@ import bpy from ...utility import parentObject, hexOrDecInt +from ..exporter.scene.actors import SceneTransitionActors from ..scene.properties import OOTSceneHeaderProperty -from ..oot_utility import setCustomProperty, getEvalParams -from ..oot_constants import ootEnumCamTransition, ootData +from ..utility import setCustomProperty, getEvalParams, getEvalParamsInt +from ...game_data import game_data +from ..constants import ootEnumCamTransition from .classes import SharedSceneData from .constants import actorsWithRotAsParam @@ -24,62 +26,51 @@ def parseTransActorList( sharedSceneData: SharedSceneData, headerIndex: int, ): - transitionActorList = getDataMatch(sceneData, transActorListName, "TransitionActorEntry", "transition actor list") - - regex = r"(?:\{(.*?)\}\s*,)|(?:\{([a-zA-Z0-9\-_.,{}\s]*[^{]*)\},)" - for actorMatch in re.finditer(regex, transitionActorList): - actorMatch = actorMatch.group(0).replace(" ", "").replace("\n", "").replace("{", "").replace("}", "") - - params = [value.strip() for value in actorMatch.split(",") if value.strip() != ""] - - position = tuple([hexOrDecInt(value) for value in params[5:8]]) - - rotY = getEvalParams(params[8]) if "DEG_TO_BINANG" in params[8] else params[8] - rotation = tuple([0, hexOrDecInt(rotY), 0]) - - roomIndexFront = hexOrDecInt(params[0]) - camFront = params[1] - roomIndexBack = hexOrDecInt(params[2]) - camBack = params[3] - actorID = params[4] - actorParam = params[9] - - actorHash = ( - roomIndexFront, - camFront, - roomIndexBack, - camBack, - actorID, - position, - rotation, - actorParam, - ) - if not sharedSceneData.addHeaderIfItemExists(actorHash, "Transition Actor", headerIndex): - actorObj = createEmptyWithTransform(position, [0, 0, 0] if actorID in actorsWithRotAsParam else rotation) + transitionActorList = getDataMatch( + sceneData, transActorListName, "TransitionActorEntry", "transition actor list", strip=True + ) + scene_trans_actors = SceneTransitionActors.from_data(transitionActorList, sharedSceneData.not_zapd_assets) + + for i, actor in enumerate(scene_trans_actors.entries): + if not sharedSceneData.addHeaderIfItemExists((i, actor), "Transition Actor", headerIndex): + rotation = tuple([0, hexOrDecInt(actor.rot), 0]) + + actorObj = createEmptyWithTransform(actor.pos, [0, 0, 0] if actor.id in actorsWithRotAsParam else rotation) actorObj.ootEmptyType = "Transition Actor" - actorObj.name = "Transition " + getDisplayNameFromActorID(params[4]) + actorObj.name = "Transition " + getDisplayNameFromActorID(actor.id) transActorProp = actorObj.ootTransitionActorProperty - sharedSceneData.transDict[actorHash] = actorObj + sharedSceneData.transDict[actor] = actorObj + + # make sure the room is valid + fromRoom = roomObjs[actor.roomFrom] if actor.roomFrom >= 0 else None + toRoom = roomObjs[actor.roomTo] if actor.roomTo >= 0 else None + transActorProp.isRoomTransition = actor.isRoomTransition + + if actor.isRoomTransition: + if fromRoom is not None: + parentObject(fromRoom, actorObj) + else: + # make it obvious to the user that this transition actor has an issue + actorObj.name = f"Invalid Front Room Index - {actorObj.name}" - fromRoom = roomObjs[roomIndexFront] - toRoom = roomObjs[roomIndexBack] - if roomIndexFront != roomIndexBack: - parentObject(fromRoom, actorObj) transActorProp.fromRoom = fromRoom transActorProp.toRoom = toRoom - transActorProp.isRoomTransition = True else: - transActorProp.isRoomTransition = False + # that side should always be valid + assert toRoom is not None parentObject(toRoom, actorObj) - setCustomProperty(transActorProp, "cameraTransitionFront", camFront, ootEnumCamTransition) - setCustomProperty(transActorProp, "cameraTransitionBack", camBack, ootEnumCamTransition) + setCustomProperty(transActorProp, "cameraTransitionFront", actor.cameraFront, ootEnumCamTransition) + setCustomProperty(transActorProp, "cameraTransitionBack", actor.cameraBack, ootEnumCamTransition) actorProp = transActorProp.actor - setCustomProperty(actorProp, "actorID", actorID, ootData.actorData.ootEnumActorID) - actorProp.actorParam = actorParam - handleActorWithRotAsParam(actorProp, actorID, rotation) + setCustomProperty(actorProp, "actor_id", actor.id, game_data.z64.actors.ootEnumActorID) + if actorProp.actor_id != "Custom": + actorProp.params = actor.params + else: + actorProp.params_custom = actor.params + handleActorWithRotAsParam(actorProp, actor.id, rotation) unsetAllHeadersExceptSpecified(actorProp.headerSettings, headerIndex) @@ -112,7 +103,7 @@ def parseActorInfo(actorMatch: re.Match, nestedBrackets: bool) -> tuple[str, lis ) rotation = tuple( [ - hexOrDecInt(getEvalParams(value.strip())) + hexOrDecInt(getEvalParamsInt(value.strip())) for value in actorMatch.group(3).split(",") if value.strip() != "" ] @@ -125,7 +116,7 @@ def parseActorInfo(actorMatch: re.Match, nestedBrackets: bool) -> tuple[str, lis rotation = tuple([hexOrDecInt(value) for value in params[4:7]]) actorParam = params[7] - return actorID, position, rotation, actorParam + return actorID, position, rotation, actorParam.removesuffix(",") def parseSpawnList( @@ -149,13 +140,17 @@ def parseSpawnList( spawnObj = createEmptyWithTransform(position, [0, 0, 0] if actorID in actorsWithRotAsParam else rotation) spawnObj.ootEmptyType = "Entrance" spawnObj.name = "Entrance" + spawnObj.empty_display_type = "CONE" spawnProp = spawnObj.ootEntranceProperty spawnProp.tiedRoom = roomObjs[roomIndex] spawnProp.spawnIndex = spawnIndex spawnProp.customActor = actorID != "ACTOR_PLAYER" actorProp = spawnProp.actor - setCustomProperty(actorProp, "actorID", actorID, ootData.actorData.ootEnumActorID) - actorProp.actorParam = actorParam + setCustomProperty(actorProp, "actor_id", actorID, game_data.z64.actors.ootEnumActorID) + if actorProp.actor_id != "Custom": + actorProp.params = actorParam + else: + actorProp.params_custom = actorParam handleActorWithRotAsParam(actorProp, actorID, rotation) unsetAllHeadersExceptSpecified(actorProp.headerSettings, headerIndex) @@ -178,7 +173,7 @@ def getActorRegex(actorList: list[str]): def parseActorList( roomObj: bpy.types.Object, sceneData: str, actorListName: str, sharedSceneData: SharedSceneData, headerIndex: int ): - actorList = getDataMatch(sceneData, actorListName, "ActorEntry", "actor list") + actorList = getDataMatch(sceneData, actorListName, "ActorEntry", "actor list", strip=True) regex, nestedBrackets = getActorRegex(actorList) for actorMatch in re.finditer(regex, actorList, flags=re.DOTALL): @@ -192,8 +187,11 @@ def parseActorList( actorObj.name = getDisplayNameFromActorID(actorID) actorProp = actorObj.ootActorProperty - setCustomProperty(actorProp, "actorID", actorID, ootData.actorData.ootEnumActorID) - actorProp.actorParam = actorParam + setCustomProperty(actorProp, "actor_id", actorID, game_data.z64.actors.ootEnumActorID) + if actorProp.actor_id != "Custom": + actorProp.params = actorParam + else: + actorProp.params_custom = actorParam handleActorWithRotAsParam(actorProp, actorID, rotation) unsetAllHeadersExceptSpecified(actorProp.headerSettings, headerIndex) diff --git a/fast64_internal/oot/importer/classes.py b/fast64_internal/z64/importer/classes.py similarity index 83% rename from fast64_internal/oot/importer/classes.py rename to fast64_internal/z64/importer/classes.py index 87f19d28c..1c32e6cde 100644 --- a/fast64_internal/oot/importer/classes.py +++ b/fast64_internal/z64/importer/classes.py @@ -1,5 +1,5 @@ from ...utility import PluginError -from ..oot_utility import getHeaderSettings +from ..utility import getHeaderSettings from .constants import headerNames @@ -7,6 +7,7 @@ class SharedSceneData: def __init__( self, scenePath: str, + scene_name: str, includeMesh: bool, includeCollision: bool, includeActors: bool, @@ -16,6 +17,10 @@ def __init__( includePaths: bool, includeWaterBoxes: bool, includeCutscenes: bool, + includeAnimatedMats: bool, + is_single_file: bool, + is_fast64_data: bool, + not_zapd_assets: bool, ): self.actorDict = {} # actor hash : blender object self.entranceDict = {} # actor hash : blender object @@ -23,6 +28,7 @@ def __init__( self.pathDict = {} # path hash : blender object self.scenePath = scenePath + self.scene_name = scene_name self.includeMesh = includeMesh self.includeCollision = includeCollision self.includeActors = includeActors @@ -32,6 +38,10 @@ 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 def addHeaderIfItemExists(self, hash, itemType: str, headerIndex: int): if itemType == "Actor": diff --git a/fast64_internal/oot/importer/constants.py b/fast64_internal/z64/importer/constants.py similarity index 100% rename from fast64_internal/oot/importer/constants.py rename to fast64_internal/z64/importer/constants.py diff --git a/fast64_internal/oot/importer/room_header.py b/fast64_internal/z64/importer/room_header.py similarity index 79% rename from fast64_internal/oot/importer/room_header.py rename to fast64_internal/z64/importer/room_header.py index 36fa422d6..1d3cb6251 100644 --- a/fast64_internal/oot/importer/room_header.py +++ b/fast64_internal/z64/importer/room_header.py @@ -1,12 +1,14 @@ import bpy import re -from ...utility import hexOrDecInt -from ..oot_utility import setCustomProperty -from ..oot_model_classes import OOTF3DContext +from pathlib import Path + +from ...utility import PluginError, hexOrDecInt +from ..utility import setCustomProperty +from ..model_classes import OOTF3DContext from ..room.properties import OOTRoomHeaderProperty -from ..oot_constants import ootData, ootEnumLinkIdle, ootEnumRoomBehaviour -from .utility import getDataMatch, stripName +from ...game_data import game_data +from .utility import getDataMatch, stripName, parse_commands_data from .classes import SharedSceneData from .constants import headerNames from .actor import parseActorList @@ -14,12 +16,12 @@ def parseObjectList(roomHeader: OOTRoomHeaderProperty, sceneData: str, objectListName: str): - objectData = getDataMatch(sceneData, objectListName, "s16", "object list") + objectData = getDataMatch(sceneData, objectListName, "s16", "object list", strip=True) objects = [value.strip() for value in objectData.split(",") if value.strip() != ""] for object in objects: objectProp = roomHeader.objectList.add() - objByID = ootData.objectData.objectsByID.get(object) + objByID = game_data.z64.objects.objects_by_id.get(object) if objByID is not None: objectProp.objectKey = objByID.key @@ -37,6 +39,18 @@ def parseRoomCommands( sharedSceneData: SharedSceneData, headerIndex: int, ): + # we need to access the header in `loadMultiBlock()` for the new assets system + if not sharedSceneData.is_fast64_data and sharedSceneData.not_zapd_assets: + header_path = Path(sharedSceneData.scenePath).resolve() / f"{sharedSceneData.scene_name}.h" + if not header_path.exists(): + raise PluginError("ERROR: scene file header not found!") + sceneData += header_path.read_text() + + header_path = Path(sharedSceneData.scenePath).resolve() / f"{roomName}.h" + if not header_path.exists(): + raise PluginError("ERROR: room file header not found!") + sceneData += header_path.read_text() + if roomObj is None: # Name set in parseRoomList() roomObj = bpy.data.objects.new(roomCommandsName, None) @@ -59,17 +73,16 @@ def parseRoomCommands( roomHeader = cutsceneHeaders[headerIndex - 4] commands = getDataMatch(sceneData, roomCommandsName, ["SceneCmd", "SCmdBase"], "scene commands") - 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(",")] + cmd_map = parse_commands_data(commands) + for command, args in cmd_map.items(): if command == "SCENE_CMD_ALTERNATE_HEADER_LIST": altHeadersListName = stripName(args[0]) parseAlternateRoomHeaders(roomObj, roomIndex, sharedSceneData, sceneData, altHeadersListName, f3dContext) elif command == "SCENE_CMD_ECHO_SETTINGS": roomHeader.echo = args[0] elif command == "SCENE_CMD_ROOM_BEHAVIOR": - setCustomProperty(roomHeader, "roomBehaviour", args[0], ootEnumRoomBehaviour) - setCustomProperty(roomHeader, "linkIdleMode", args[1], ootEnumLinkIdle) + setCustomProperty(roomHeader, "roomBehaviour", args[0], game_data.z64.get_enum("room_type")) + setCustomProperty(roomHeader, "linkIdleMode", args[1], game_data.z64.get_enum("environment_type")) roomHeader.showInvisibleActors = args[2] == "true" or args[2] == "1" roomHeader.disableWarpSongs = args[3] == "true" or args[3] == "1" elif command == "SCENE_CMD_SKYBOX_DISABLES": @@ -122,9 +135,10 @@ def parseAlternateRoomHeaders( ): altHeadersData = getDataMatch(sceneData, altHeadersListName, ["SceneCmd*", "SCmdBase*"], "alternate header list") altHeadersList = [value.strip() for value in altHeadersData.split(",") if value.strip() != ""] + room_name = roomObj.name.split(".")[0] for i in range(len(altHeadersList)): if not (altHeadersList[i] == "NULL" or altHeadersList[i] == "0"): parseRoomCommands( - roomObj.name, roomObj, sceneData, altHeadersList[i], roomIndex, f3dContext, sharedSceneData, i + 1 + room_name, roomObj, sceneData, altHeadersList[i], roomIndex, f3dContext, sharedSceneData, i + 1 ) diff --git a/fast64_internal/oot/importer/room_shape.py b/fast64_internal/z64/importer/room_shape.py similarity index 94% rename from fast64_internal/oot/importer/room_shape.py rename to fast64_internal/z64/importer/room_shape.py index bff6d7c42..58a1bd59b 100644 --- a/fast64_internal/oot/importer/room_shape.py +++ b/fast64_internal/z64/importer/room_shape.py @@ -5,9 +5,9 @@ from ...utility import parentObject, hexOrDecInt, yUpToZUp from ...f3d.f3d_parser import importMeshC -from ..oot_model_classes import OOTF3DContext +from ..model_classes import OOTF3DContext from ..room.properties import OOTRoomHeaderProperty -from ..oot_constants import ootEnumRoomShapeType +from ..constants import ootEnumRoomShapeType from .classes import SharedSceneData from .utility import getDataMatch, stripName @@ -20,7 +20,7 @@ def parseMeshHeader( sharedSceneData: SharedSceneData, ): roomHeader = roomObj.ootRoomHeader - meshData = getDataMatch(sceneData, meshHeaderName, "", "mesh header", False) + meshData = getDataMatch(sceneData, meshHeaderName, "", "mesh header", False, strip=True) meshData = meshData.replace("{", "").replace("}", "") meshParams = [value.strip() for value in meshData.split(",") if value.strip() != ""] @@ -79,15 +79,15 @@ def parseMeshList( sharedSceneData: SharedSceneData, ): roomHeader = roomObj.ootRoomHeader - meshEntryData = getDataMatch(sceneData, meshListName, "", "mesh list", roomShape != 1) + meshEntryData = getDataMatch(sceneData, meshListName, "", "mesh list", roomShape != 1, strip=True) if roomShape == 2: - matchPattern = r"\{\s*\{(.*?),(.*?),(.*?)\}\s*,(.*?),(.*?),(.*?)\}\s*," + matchPattern = r"\{\s*\{(.*?),(.*?),(.*?)\}\s*,(.*?),(.*?),(.*?),?\}\s*,?" searchItems = re.finditer(matchPattern, meshEntryData, flags=re.DOTALL) elif roomShape == 1: searchItems = [meshEntryData] else: - matchPattern = r"\{(.*?),(.*?)\}\s*," + matchPattern = r"\{(.*?),(.*?),?\}\s*,?" searchItems = re.finditer(matchPattern, meshEntryData, flags=re.DOTALL) for entryMatch in searchItems: @@ -96,7 +96,7 @@ def parseMeshList( transparentDL = entryMatch.group(6).strip() position = yUpToZUp @ mathutils.Vector( [ - hexOrDecInt(entryMatch.group(value).strip()) / bpy.context.scene.ootBlenderScale + hexOrDecInt(entryMatch.group(value).strip().removesuffix(",")) / bpy.context.scene.ootBlenderScale for value in range(1, 4) ] ) diff --git a/fast64_internal/oot/importer/scene.py b/fast64_internal/z64/importer/scene.py similarity index 72% rename from fast64_internal/oot/importer/scene.py rename to fast64_internal/z64/importer/scene.py index 38541609a..621696846 100644 --- a/fast64_internal/oot/importer/scene.py +++ b/fast64_internal/z64/importer/scene.py @@ -3,19 +3,21 @@ import bpy import mathutils -from ...utility import readFile, hexOrDecInt +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 from ...f3d.flipbook import TextureFlipbook -from ..oot_model_classes import OOTF3DContext +from ..model_classes import OOTF3DContext from ..exporter.decomp_edit.scene_table import SceneTableUtility from ..scene.properties import OOTImportSceneSettingsProperty -from ..oot_constants import ootEnumDrawConfig +from ..cutscene.importer import importCutsceneData from .scene_header import parseSceneCommands from .classes import SharedSceneData -from ..cutscene.importer import importCutsceneData -from ..oot_utility import ( +from ..utility import ( getSceneDirFromLevelName, setCustomProperty, sceneNameFromID, @@ -103,44 +105,53 @@ def parseScene( sceneName, False, True, + True, ) - filePath = os.path.join(sceneFolderPath, f"{sceneName}_scene.c") - sceneData = readFile(filePath) - # roomData = "" - # sceneFolderFiles = [f for f in listdir(sceneFolderPath) if isfile(join(sceneFolderPath, f))] - # for sceneFile in sceneFolderFiles: - # if re.search(rf"{sceneName}_room_[0-9]+\.c", sceneFile): - # roomPath = os.path.join(sceneFolderPath, sceneFile) - # roomData += readFile(roomPath) + 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(): + file_path = Path(sceneFolderPath).resolve() / f"{sceneName}_scene_main.c" + is_single_file = False + + if not file_path.exists(): + raise PluginError("ERROR: scene not found!") + + sceneData = file_path.read_text() - # sceneData += roomData + if not is_single_file: + # get the other scene files for non-single file fast64 exports + for file in file_path.parent.rglob("*.c"): + if "_scene_main.c" not in str(file) and "_room_" not in str(file): + sceneData += file.read_text() if bpy.context.mode != "OBJECT": bpy.context.mode = "OBJECT" - # set scene default registers (see sDefaultDisplayList) - f3dContext = OOTF3DContext(get_F3D_GBI(), [], bpy.path.abspath(bpy.context.scene.ootDecompPath)) - f3dContext.mat().prim_color = (0.5, 0.5, 0.5, 0.5) - f3dContext.mat().env_color = (0.5, 0.5, 0.5, 0.5) + if game_data.z64.is_oot(): + sceneCommandsName = f"{sceneName}_sceneCommands" + else: + sceneCommandsName = f"{sceneName}Commands" - parseMatrices(sceneData, f3dContext, 1 / bpy.context.scene.ootBlenderScale) - f3dContext.addMatrix("&gMtxClear", mathutils.Matrix.Scale(1 / bpy.context.scene.ootBlenderScale, 4)) + not_zapd_assets = False - if not settings.isCustomDest: - drawConfigName = SceneTableUtility.get_draw_config(sceneName) - drawConfigData = readFile(os.path.join(importPath, "src/code/z_scene_table.c")) - parseDrawConfig(drawConfigName, sceneData, drawConfigData, f3dContext) + # fast64 naming + if sceneCommandsName not in sceneData: + not_zapd_assets = True + sceneCommandsName = f"{sceneName}_scene_header00" - bpy.context.space_data.overlay.show_relationship_lines = False - bpy.context.space_data.overlay.show_curve_normals = True - bpy.context.space_data.overlay.normals_length = 2 + # newer assets system naming + if game_data.z64.is_oot() and sceneCommandsName not in sceneData: + not_zapd_assets = True + sceneCommandsName = f"{sceneName}_scene" - sceneCommandsName = f"{sceneName}_sceneCommands" - if sceneCommandsName not in sceneData: - sceneCommandsName = f"{sceneName}_scene_header00" # fast64 naming sharedSceneData = SharedSceneData( sceneFolderPath, + f"{sceneName}_scene" if game_data.z64.is_oot() else sceneName, settings.includeMesh, settings.includeCollision, settings.includeActors, @@ -150,8 +161,34 @@ def parseScene( settings.includePaths, settings.includeWaterBoxes, settings.includeCutscenes, + settings.includeAnimatedMats, + is_single_file, + f"{sceneName}_scene_header00" in sceneData, + not_zapd_assets, ) + # set scene default registers (see sDefaultDisplayList) + f3dContext = OOTF3DContext(get_F3D_GBI(), [], bpy.path.abspath(bpy.context.scene.ootDecompPath)) + f3dContext.mat().prim_color = (0.5, 0.5, 0.5, 0.5) + f3dContext.mat().env_color = (0.5, 0.5, 0.5, 0.5) + + # disable TLUTs only if we're trying to import a scene from the new assets system + f3dContext.ignore_tlut = sharedSceneData.not_zapd_assets and not sharedSceneData.is_fast64_data + + parseMatrices(sceneData, f3dContext, 1 / bpy.context.scene.ootBlenderScale) + f3dContext.addMatrix("&gMtxClear", mathutils.Matrix.Scale(1 / bpy.context.scene.ootBlenderScale, 4)) + f3dContext.addMatrix("&gIdentityMtx", mathutils.Matrix.Scale(1 / bpy.context.scene.ootBlenderScale, 4)) + + if not settings.isCustomDest: + drawConfigName = SceneTableUtility.get_draw_config(sceneName) + 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 + bpy.context.space_data.overlay.show_curve_normals = True + bpy.context.space_data.overlay.normals_length = 2 + if settings.includeCutscenes: bpy.context.scene.ootCSNumber = importCutsceneData(None, sceneData) @@ -163,7 +200,7 @@ def parseScene( sceneObj.ootSceneHeader.sceneTableEntry, "drawConfig", SceneTableUtility.get_draw_config(sceneName), - ootEnumDrawConfig, + game_data.z64.get_enum("drawConfig"), ) if bpy.context.scene.fast64.oot.headerTabAffectsVisibility: diff --git a/fast64_internal/z64/importer/scene_collision.py b/fast64_internal/z64/importer/scene_collision.py new file mode 100644 index 000000000..e53936873 --- /dev/null +++ b/fast64_internal/z64/importer/scene_collision.py @@ -0,0 +1,450 @@ +import math +import re +import bpy +import mathutils + +from random import random +from bpy.types import Material + +from ...game_data import game_data +from ...utility import PluginError, parentObject, hexOrDecInt, get_include_data, yUpToZUp +from ..exporter.collision.surface import SurfaceType +from ..exporter.collision.polygons import CollisionPoly +from ..exporter.collision.waterbox import WaterBox +from ..collision.properties import OOTMaterialCollisionProperty +from ..f3d_writer import getColliderMat +from ..utility import setCustomProperty, ootParseRotation +from .utility import getDataMatch, getBits, checkBit, createCurveFromPoints, stripName +from .classes import SharedSceneData + +from ..collision.constants import ( + ootEnumWallSetting, + ootEnumCollisionTerrain, + ootEnumCollisionSound, + ootEnumCameraCrawlspaceSType, + enum_conveyor_speed, +) + + +def parseCrawlSpaceData( + setting: str, sceneData: str, posDataName: str, index: int, count: int, objName: str, orderIndex: str +): + camPosData = getDataMatch(sceneData, posDataName, "Vec3s", "camera position list", strip=True) + camPosList = [value.replace("{", "").strip() for value in camPosData.split("},") if value.strip() != ""] + posData = [camPosList[index : index + count][i] for i in range(0, count, 3)] + + points = [] + for posDataItem in posData: + points.append([hexOrDecInt(value.strip()) for value in posDataItem.split(",") if value.strip() != ""]) + + # name is important for alphabetical ordering + curveObj = createCurveFromPoints(points, objName) + curveObj.show_name = True + crawlProp = curveObj.ootSplineProperty + crawlProp.splineType = "Crawlspace" + crawlProp.index = orderIndex + setCustomProperty(crawlProp, "camSType", "CAM_SET_CRAWLSPACE", ootEnumCameraCrawlspaceSType) + + return curveObj + + +def parseCamDataList(sceneObj: bpy.types.Object, camDataListName: str, sceneData: str): + camMatchData = getDataMatch(sceneData, camDataListName, ["CamData", "BgCamInfo"], "camera data list", strip=True) + camDataList = [value.replace("{", "").strip() for value in camMatchData.split("},") if value.strip() != ""] + + # orderIndex used for naming cameras in alphabetical order + orderIndex = 0 + for camEntry in camDataList: + setting, count, posDataName = [value.strip() for value in camEntry.split(",") if value.strip() != ""] + index = None + + objName = f"{sceneObj.name}_camPos_{format(orderIndex, '03')}" + + if posDataName != "NULL" and posDataName != "0": + index = hexOrDecInt(posDataName[posDataName.index("[") + 1 : -1]) + posDataName = posDataName[1 : posDataName.index("[")] # remove '&' and '[n]' + + if setting == "CAM_SET_CRAWLSPACE" or setting == "0x001E": + obj = parseCrawlSpaceData(setting, sceneData, posDataName, index, hexOrDecInt(count), objName, orderIndex) + else: + obj = parseCamPosData(setting, sceneData, posDataName, index, objName, orderIndex) + + parentObject(sceneObj, obj) + orderIndex += 1 + + +def parseCamPosData(setting: str, sceneData: str, posDataName: str, index: int, objName: str, orderIndex: str): + camera = bpy.data.cameras.new("Camera") + camObj = bpy.data.objects.new(objName, camera) + bpy.context.scene.collection.objects.link(camObj) + camProp = camObj.ootCameraPositionProperty + setCustomProperty(camProp, "camSType", setting, game_data.z64.get_enum("camera_setting_type")) + camProp.hasPositionData = posDataName != "NULL" and posDataName != "0" + camProp.index = orderIndex + + # name is important for alphabetical ordering + camObj.name = objName + + if index is None: + camObj.location = [0, 0, 0] + return camObj + + camPosData = getDataMatch(sceneData, posDataName, "Vec3s", "camera position list", strip=True) + camPosList = [value.replace("{", "").strip() for value in camPosData.split("},") if value.strip() != ""] + + posData = camPosList[index : index + 3] + position = yUpToZUp @ mathutils.Vector( + [ + hexOrDecInt(value.strip()) / bpy.context.scene.ootBlenderScale + for value in posData[0].split(",") + if value.strip() != "" + ] + ) + + # camera faces opposite direction + rotation = ( + yUpToZUp.to_quaternion() + @ mathutils.Euler( + ootParseRotation([hexOrDecInt(value.strip()) for value in posData[1].split(",") if value.strip() != ""]) + ).to_quaternion() + @ mathutils.Quaternion((0, 1, 0), math.radians(180.0)) + ).to_euler() + + fov, bgImageOverrideIndex, unknown = [value.strip() for value in posData[2].split(",") if value.strip() != ""] + + camObj.location = position + camObj.rotation_euler = rotation + camObj.show_name = True + + camProp = camObj.ootCameraPositionProperty + camProp.bgImageOverrideIndex = hexOrDecInt(bgImageOverrideIndex) + + fovValue = hexOrDecInt(fov) + fovValue = int.from_bytes(fovValue.to_bytes(2, "big", signed=fovValue < 0x8000), "big", signed=True) + if fovValue > 360: + fovValue *= 0.01 # see CAM_DATA_SCALED() macro + camObj.data.angle = math.radians(fovValue) + + return camObj + + +def parseWaterBoxes( + sceneObj: bpy.types.Object, + roomObjs: list[bpy.types.Object], + sceneData: str, + waterBoxListName: str, + sharedSceneData: SharedSceneData, +): + waterBoxListData = getDataMatch(sceneData, waterBoxListName, "WaterBox", "water box list", strip=True) + waterBoxList = [value.replace("{", "").strip() for value in waterBoxListData.split("},") if value.strip() != ""] + + # orderIndex used for naming cameras in alphabetical order + for orderIndex, waterBoxData in enumerate(waterBoxList): + objName = f"{sceneObj.name}_waterBox_{format(orderIndex, '03')}" + waterbox = WaterBox.from_data(waterBoxData, sharedSceneData.not_zapd_assets) + + topCorner = waterbox.get_blender_position() + dimensions = waterbox.get_blender_scale() + height = 1000 / bpy.context.scene.ootBlenderScale # just to add volume + + location = mathutils.Vector([0, 0, 0]) + scale = [dimensions[0] / 2, dimensions[1] / 2, height / 2] + location.x = topCorner[0] + scale[0] # x + location.y = topCorner[1] - scale[1] # -z + location.z = topCorner.z - scale[2] # y + + waterBoxObj = bpy.data.objects.new(objName, None) + bpy.context.scene.collection.objects.link(waterBoxObj) + waterBoxObj.location = location + waterBoxObj.scale = scale + waterBoxProp = waterBoxObj.ootWaterBoxProperty + + waterBoxObj.show_name = True + waterBoxObj.ootEmptyType = "Water Box" + roomIndex = hexOrDecInt(waterbox.roomIndexC) + waterBoxProp.lighting = waterbox.lightIndex + waterBoxProp.camera = waterbox.bgCamIndex + waterBoxProp.flag19 = waterbox.setFlag19C == "true" + + # 0x3F = -1 in 6bit value + parent = roomObjs[roomIndex] if roomObjs is not None and len(roomObjs) > 0 and roomIndex != 0x3F else sceneObj + parentObject(parent, waterBoxObj) + + +def parseSurfaceParams( + surface_type: SurfaceType, collision_poly: CollisionPoly, col_props: OOTMaterialCollisionProperty +): + col_props.eponaBlock = surface_type.isHorseBlocked + col_props.decreaseHeight = surface_type.isSoft + setCustomProperty(col_props, "floorSetting", surface_type.floorProperty, game_data.z64.get_enum("floor_property")) + setCustomProperty(col_props, "wallSetting", surface_type.wallType, ootEnumWallSetting) + setCustomProperty(col_props, "floorProperty", surface_type.floorType, game_data.z64.get_enum("floor_type")) + col_props.exitID = surface_type.exitIndex + col_props.cameraID = surface_type.bgCamIndex + col_props.isWallDamage = surface_type.isWallDamage + + col_props.conveyorRotation = (surface_type.conveyorDirection / 0x3F) * (2 * math.pi) + col_props.conveyorSpeed = "Custom" + col_props.conveyorSpeedCustom = str(surface_type.conveyorSpeed) + setCustomProperty(col_props, "conveyorSpeed", surface_type.conveyorSpeed, enum_conveyor_speed) + + if isinstance(surface_type.conveyorSpeed, int): + speed_int = surface_type.conveyorSpeed + else: + speed_int = int(surface_type.conveyorSpeed, 16) + + if col_props.conveyorRotation == 0 and speed_int == 0: + col_props.conveyorOption = "None" + elif collision_poly.isLandConveyor: + col_props.conveyorOption = "Land" + else: + col_props.conveyorOption = "Water" + + col_props.hookshotable = surface_type.canHookshot + col_props.echo = str(surface_type.echo) + col_props.lightingSetting = surface_type.lightSetting + setCustomProperty(col_props, "terrain", str(surface_type.floorEffect), ootEnumCollisionTerrain) + setCustomProperty(col_props, "sound", str(surface_type.material), ootEnumCollisionSound) + + col_props.ignoreCameraCollision = collision_poly.ignoreCamera + col_props.ignoreActorCollision = collision_poly.ignoreEntity + col_props.ignoreProjectileCollision = collision_poly.ignoreProjectile + + +def parseSurfaces(surfaceList: list[str]): + surfaces: list[SurfaceType] = [] + + # TODO: temporary fix to get the enums import properly + # a proper fix would be cleaning up current enums to use the names from decomp + new_names_to_old_names = { + "FLOOR_TYPE_0": "0x00", + "FLOOR_TYPE_1": "0x01", + "FLOOR_TYPE_2": "0x02", + "FLOOR_TYPE_3": "0x03", + "FLOOR_TYPE_4": "0x04", + "FLOOR_TYPE_5": "0x05", + "FLOOR_TYPE_6": "0x06", + "FLOOR_TYPE_7": "0x07", + "FLOOR_TYPE_8": "0x08", + "FLOOR_TYPE_9": "0x09", + "FLOOR_TYPE_10": "0x0A", + "FLOOR_TYPE_11": "0x0B", + "WALL_TYPE_0": "0x00", + "WALL_TYPE_1": "0x01", + "WALL_TYPE_2": "0x02", + "WALL_TYPE_3": "0x03", + "WALL_TYPE_4": "0x04", + "WALL_TYPE_5": "0x05", + "WALL_TYPE_6": "0x06", + "WALL_TYPE_7": "0x07", + "FLOOR_PROPERTY_0": "0x00", + "FLOOR_PROPERTY_5": "0x05", + "FLOOR_PROPERTY_6": "0x06", + "FLOOR_PROPERTY_8": "0x08", + "FLOOR_PROPERTY_9": "0x09", + "FLOOR_PROPERTY_11": "0x0B", + "FLOOR_PROPERTY_12": "0x0C", + "SURFACE_MATERIAL_DIRT": "0x00", + "SURFACE_MATERIAL_SAND": "0x01", + "SURFACE_MATERIAL_STONE": "0x02", + "SURFACE_MATERIAL_JABU": "0x03", + "SURFACE_MATERIAL_WATER_SHALLOW": "0x04", + "SURFACE_MATERIAL_WATER_DEEP": "0x05", + "SURFACE_MATERIAL_TALL_GRASS": "0x06", + "SURFACE_MATERIAL_LAVA": "0x07", + "SURFACE_MATERIAL_GRASS": "0x08", + "SURFACE_MATERIAL_BRIDGE": "0x09", + "SURFACE_MATERIAL_WOOD": "0x0A", + "SURFACE_MATERIAL_DIRT_SOFT": "0x0B", + "SURFACE_MATERIAL_ICE": "0x0C", + "SURFACE_MATERIAL_CARPET": "0x0D", + "FLOOR_EFFECT_0": "0x00", + "FLOOR_EFFECT_1": "0x01", + "FLOOR_EFFECT_2": "0x02", + "CONVEYOR_SPEED_DISABLED": "0x00", + "CONVEYOR_SPEED_SLOW": "0x01", + "CONVEYOR_SPEED_MEDIUM": "0x02", + "CONVEYOR_SPEED_FAST": "0x03", + } + + for surfaceData in surfaceList: # SurfaceType + if "SURFACETYPE0" in surfaceData: + split = surfaceData.removeprefix("SURFACETYPE0(").split("SURFACETYPE1(") + surface0 = split[0].replace(")", "").split(",") + surface1 = split[1].replace(")", "").split(",") + + surface = SurfaceType( + hexOrDecInt(surface0[0]), # bgCamIndex + hexOrDecInt(surface0[1]), # exitIndex + new_names_to_old_names.get(surface0[2], surface0[2]), # floorType + hexOrDecInt(surface0[3]), # unk18 + new_names_to_old_names.get(surface0[4], surface0[4]), # wallType + new_names_to_old_names.get(surface0[5], surface0[5]), # floorProperty + surface0[6] == "true", # isSoft + surface0[7] == "true", # isHorseBlocked + new_names_to_old_names.get(surface1[0], surface1[0]), # material + new_names_to_old_names.get(surface1[1], surface1[1]), # floorEffect + hexOrDecInt(surface1[2]), # lightSetting + hexOrDecInt(surface1[3]), # echo + surface1[4] == "true", # canHookshot + new_names_to_old_names.get(surface1[5], surface1[5]), # conveyorSpeed + hexOrDecInt(surface1[6].removeprefix("CONVEYOR_DIRECTION_FROM_BINANG(").removesuffix(")")), + surface1[7] == "true", # unk27 + bpy.context.scene.fast64.oot.useDecompFeatures, + ) + else: + params = [hexOrDecInt(value.strip()) for value in surfaceData.split(",")] + surface = SurfaceType.from_hex(params[0], params[1]) + + surface.floorType = new_names_to_old_names.get(surface.floorType, surface.floorType) + surface.wallType = new_names_to_old_names.get(surface.wallType, surface.wallType) + surface.floorProperty = new_names_to_old_names.get(surface.floorProperty, surface.floorProperty) + surface.material = new_names_to_old_names.get(surface.material, surface.material) + surface.floorEffect = new_names_to_old_names.get(surface.floorEffect, surface.floorEffect) + surface.conveyorSpeed = new_names_to_old_names.get(surface.conveyorSpeed, surface.conveyorSpeed) + + surfaces.append(surface) + + return surfaces + + +def parseVertices(vertexList: list[str]): + vertices = [] + for vertexData in vertexList: + vertex = [hexOrDecInt(value.strip()) / bpy.context.scene.ootBlenderScale for value in vertexData.split(",")] + position = yUpToZUp @ mathutils.Vector(vertex) + vertices.append(position) + + return vertices + + +def parsePolygon(polygonData: list[str], sharedSceneData: SharedSceneData): + assert len(polygonData) == 8 + return CollisionPoly.from_data(polygonData, sharedSceneData.not_zapd_assets) + + +def parseCollisionHeader( + sceneObj: bpy.types.Object, + roomObjs: list[bpy.types.Object], + sceneData: str, + collisionHeaderName: str, + sharedSceneData: SharedSceneData, +): + match = re.search( + rf"CollisionHeader\s*{re.escape(collisionHeaderName)}\s*=\s*\{{\s*\{{(.*?)\}}\s*,\s*\{{(.*?)\}}\s*,(.*?)\}}\s*;", + sceneData, + flags=re.DOTALL, + ) + + if not match: + match = re.search( + rf"CollisionHeader\s*{re.escape(collisionHeaderName)}\s*=\s*\{{(.*?)\}}\s*;", + sceneData, + flags=re.DOTALL, + ) + if not match: + raise PluginError(f"Could not find collision header {collisionHeaderName}.") + + if "#include" in match.group(1): + params = get_include_data(match.group(1)).splitlines() + otherParams = [value.strip().split(",")[0] for value in params[10:-1]] + else: + params = [value.strip() for value in match.group(1).split(",")] + otherParams = [value.strip() for value in params[6:]] + else: + otherParams = [value.strip() for value in match.group(3).split(",")] + + vertexListName = stripName(otherParams[1]) + polygonListName = stripName(otherParams[3]) + surfaceTypeListName = stripName(otherParams[4]) + camDataListName = stripName(otherParams[5]) + waterBoxListName = stripName(otherParams[7]) + + if sharedSceneData.includeCollision: + parseCollision(sceneObj, vertexListName, polygonListName, surfaceTypeListName, sceneData, sharedSceneData) + if sharedSceneData.includeCameras and camDataListName != "NULL" and camDataListName != "0": + parseCamDataList(sceneObj, camDataListName, sceneData) + if sharedSceneData.includeWaterBoxes and waterBoxListName != "NULL" and waterBoxListName != "0": + parseWaterBoxes(sceneObj, roomObjs, sceneData, waterBoxListName, sharedSceneData) + + +def parseCollision( + sceneObj: bpy.types.Object, + vertexListName: str, + polygonListName: str, + surfaceTypeListName: str, + sceneData: str, + sharedSceneData: SharedSceneData, +): + vertMatchData = getDataMatch(sceneData, vertexListName, "Vec3s", "vertex list", strip=True) + polyMatchData = getDataMatch(sceneData, polygonListName, "CollisionPoly", "polygon list", strip=True) + + surfMatchData = ( + getDataMatch(sceneData, surfaceTypeListName, "SurfaceType", "surface type list") + .replace("\n", "") + .replace(" ", "") + ) + + if sharedSceneData.is_fast64_data: + poly_regex = r"\{([0-9\-]*),(COLPOLY_VTX\([0-9\-]*,[a-zA-Z0-9\-_|\s]*\)),(COLPOLY_VTX\([0-9\-]*,[a-zA-Z0-9\-_|\s]*\)),(COLPOLY_VTX_INDEX\([0-9]*\)),\{(COLPOLY_SNORMAL\([0-9.\-e]*\)),(COLPOLY_SNORMAL\([0-9.\-e]*\)),(COLPOLY_SNORMAL\([0-9.\-e]*\)),?\},?([0-9\-]*),?\}" + elif sharedSceneData.not_zapd_assets: + poly_regex = r"\{([0-9\-]*),\{(COLPOLY_VTX\([0-9\-]*,[a-zA-Z0-9\-_|\s]*\)),(COLPOLY_VTX\([0-9\-]*,[a-zA-Z0-9\-_|\s]*\)),(COLPOLY_VTX\([0-9]*,[0-9]*\)),\},\{(COLPOLY_SNORMAL\([0-9.\-]*\)),(COLPOLY_SNORMAL\([0-9.\-]*\)),(COLPOLY_SNORMAL\([0-9.\-]*\)),\},([0-9\-]*),\}" + else: + poly_regex = r"\{(0x[0-9a-fA-F]*),\s*(0x[0-9a-fA-F]*),\s*(0x[0-9a-fA-F]*),\s*(0x[0-9a-fA-F]*),\s*(0x[0-9a-fA-F]*),\s*(0x[0-9a-fA-F]*),\s*(0x[0-9a-fA-F]*),\s*(0x[0-9a-fA-F]*)\}" + + vertexList = [value.replace("{", "").strip() for value in vertMatchData.split("},") if value.strip() != ""] + polygonList = [list(match.groups()) for match in re.finditer(poly_regex, polyMatchData, re.DOTALL)] + surfaceList = [value.replace("{", "").strip() for value in surfMatchData.split("},") if value.strip() != ""] + + surface_map: dict[int, SurfaceType] = {} + collision_list: list[CollisionPoly] = [] + + surfaces = parseSurfaces(surfaceList) + vertices = parseVertices(vertexList) + + for polygonData in polygonList: + collision_poly = parsePolygon(polygonData, sharedSceneData) + + # it's impossible that this is set None but doesn't hurt to make sure + assert collision_poly.type is not None + + if collision_poly.type not in surface_map: + surface_map[collision_poly.type] = surfaces[collision_poly.type] + + collision_list.append(collision_poly) + + collisionName = f"{sceneObj.name}_collision" + mesh = bpy.data.meshes.new(collisionName) + obj = bpy.data.objects.new(collisionName, mesh) + bpy.context.scene.collection.objects.link(obj) + + triData = [] + triMatData = [] + material_map: dict[int, Material] = {} + + # create the materials from the surface types + for poly_type, _ in surface_map.items(): + randomColor = mathutils.Color((1, 1, 1)) + randomColor.hsv = (random(), 0.5, 0.5) + collisionMat = getColliderMat(f"oot_collision_mat_{poly_type}", randomColor[:] + (0.5,)) + mesh.materials.append(collisionMat) + material_map[poly_type] = collisionMat + + # create the triangles based on the collision data + for collision_poly in collision_list: + assert collision_poly.type is not None + collision = material_map[collision_poly.type].ootCollisionProperty + + # ideally this would be above but we need the surface type and the collision poly + parseSurfaceParams(surface_map[collision_poly.type], collision_poly, collision) + + triData.append(collision_poly.indices) + triMatData += [collision_poly.type] + mesh.from_pydata(vertices=vertices, edges=[], faces=triData) + + for i in range(len(mesh.polygons)): + mesh.polygons[i].material_index = triMatData[i] + + obj.ignore_render = True + + parentObject(sceneObj, obj) diff --git a/fast64_internal/z64/importer/scene_header.py b/fast64_internal/z64/importer/scene_header.py new file mode 100644 index 000000000..45367a9da --- /dev/null +++ b/fast64_internal/z64/importer/scene_header.py @@ -0,0 +1,608 @@ +import math +import re +import bpy +import mathutils + +from pathlib import Path +from typing import Optional + +from ...game_data import game_data +from ...utility import PluginError, get_new_empty_object, parentObject, hexOrDecInt, gammaInverse +from ...f3d.f3d_parser import parseMatrices +from ..exporter.scene.general import EnvLightSettings +from ..model_classes import OOTF3DContext +from ..scene.properties import OOTSceneHeaderProperty, OOTLightProperty +from ..utility import setCustomProperty, is_hackeroot, getEnumIndex +from .constants import headerNames +from .utility import getDataMatch, stripName +from .classes import SharedSceneData +from .room_header import parseRoomCommands +from .actor import parseTransActorList, parseSpawnList, parseEntranceList +from .scene_collision import parseCollisionHeader +from .scene_pathways import parsePathList +from ..animated_mats.properties import Z64_AnimatedMaterial + +from ..constants import ( + ootEnumAudioSessionPreset, + ootEnumCameraMode, + ootEnumMapLocation, + ootEnumNaviHints, + ootEnumSkyboxLighting, +) + + +def parseColor(values: tuple[int, int, int]) -> tuple[float, float, float]: + return tuple(gammaInverse([value / 0xFF for value in values])) + + +def parseDirection(index: int, values: tuple[int, int, int]) -> tuple[float, float, float] | int: + if tuple(values) == (0, 0, 0): + return "Zero" + elif index == 0 and tuple(values) == (0x49, 0x49, 0x49): + return "Default" + elif index == 1 and tuple(values) == (0xB7, 0xB7, 0xB7): + return "Default" + else: + direction = mathutils.Vector( + [int.from_bytes(value.to_bytes(1, "big", signed=value < 127), "big", signed=True) / 127 for value in values] + ) + + return ( + mathutils.Euler((0, 0, math.pi)).to_quaternion() + @ (mathutils.Euler((math.pi / 2, 0, 0)).to_quaternion() @ direction).rotation_difference( + mathutils.Vector((0, 0, 1)) + ) + ).to_euler() + + +def parseLight( + lightHeader: OOTLightProperty, index: int, rotation: mathutils.Euler, color: mathutils.Vector, desc: str +) -> bpy.types.Object | None: + setattr(lightHeader, f"useCustomDiffuse{index}", rotation != "Zero" and rotation != "Default") + + if rotation == "Zero" or rotation == "Default": + setattr(lightHeader, f"zeroDiffuse{index}", rotation == "Zero") + setattr(lightHeader, f"diffuse{index}", color + (1,)) + return None + else: + light = bpy.data.lights.new(f"{desc} Diffuse {index} Light", "SUN") + lightObj = bpy.data.objects.new(f"{desc} Diffuse {index}", light) + bpy.context.scene.collection.objects.link(lightObj) + setattr(lightHeader, f"diffuse{index}Custom", lightObj.data) + lightObj.rotation_euler = rotation + lightObj.data.color = color + lightObj.data.type = "SUN" + return lightObj + + +def set_light_props( + parent_obj: bpy.types.Object, + light_props: OOTLightProperty, + header_index: int, + index: int, + light_entry: EnvLightSettings, + desc: str, +): + ambient_col = parseColor(light_entry.ambientColor) + diffuse0_dir = parseDirection(0, light_entry.light1Dir) + diffuse0_col = parseColor(light_entry.light1Color) + diffuse1_dir = parseDirection(1, light_entry.light2Dir) + diffuse1_col = parseColor(light_entry.light2Color) + fog_col = parseColor(light_entry.fogColor) + + light_props.ambient = ambient_col + (1,) + + lightObj0 = parseLight(light_props, 0, diffuse0_dir, diffuse0_col, desc) + lightObj1 = parseLight(light_props, 1, diffuse1_dir, diffuse1_col, desc) + + if lightObj0 is not None: + parentObject(parent_obj, lightObj0) + lightObj0.location = [4 + header_index * 2, 0, -index * 2] + if lightObj1 is not None: + parentObject(parent_obj, lightObj1) + lightObj1.location = [4 + header_index * 2, 2, -index * 2] + + light_props.fogColor = fog_col + (1,) + light_props.fogNear = light_entry.fogNear + light_props.z_far = light_entry.zFar + light_props.transitionSpeed = light_entry.blendRate + + +def parseLightList( + sceneObj: bpy.types.Object, + sceneHeader: OOTSceneHeaderProperty, + sceneData: str, + lightListName: str, + headerIndex: int, + sharedSceneData: SharedSceneData, +): + lightData = getDataMatch(sceneData, lightListName, ["LightSettings", "EnvLightSettings"], "light list", strip=True) + lightList = EnvLightSettings.from_data(lightData, sharedSceneData.not_zapd_assets) + + sceneHeader.tod_lights.clear() + sceneHeader.lightList.clear() + + lights_empty = None + if len(lightList) > 0: + lights_empty = get_new_empty_object( + f"{sceneObj.name} Lights (header {headerIndex})", do_select=False, parent=sceneObj + ) + lights_empty.ootEmptyType = "None" + + parent_obj = lights_empty if lights_empty is not None else sceneObj + + custom_value = None + if sceneHeader.skyboxLighting == "Custom": + # try to convert the custom value to an int + try: + custom_value = hexOrDecInt(sceneHeader.skyboxLightingCustom) + except: + custom_value = None + + # for older decomps, make sure it's using the right thing for convenience + if custom_value is not None and custom_value <= 1: + sceneHeader.skyboxLighting = "LIGHT_MODE_TIME" if custom_value == 0 else "LIGHT_MODE_SETTINGS" + + for i, lightEntry in enumerate(lightList): + if sceneHeader.skyboxLighting == "LIGHT_MODE_TIME": + new_tod_light = sceneHeader.tod_lights.add() if i > 0 else None + + settings_name = "Default Settings" if i == 0 else f"Light Settings {i}" + sub_lights_empty = get_new_empty_object( + f"(Header {headerIndex}) {settings_name}", do_select=False, parent=parent_obj + ) + sub_lights_empty.ootEmptyType = "None" + + for tod_type in ["Dawn", "Day", "Dusk", "Night"]: + desc = f"{settings_name} ({tod_type})" + + if i == 0: + set_light_props( + sub_lights_empty, + getattr(sceneHeader.timeOfDayLights, tod_type.lower()), + headerIndex, + i, + lightEntry, + desc, + ) + else: + assert new_tod_light is not None + set_light_props( + sub_lights_empty, getattr(new_tod_light, tod_type.lower()), headerIndex, i, lightEntry, desc + ) + else: + settings_name = "Indoor" if sceneHeader.skyboxLighting != "Custom" else "Custom" + desc = f"{settings_name} {i}" + + # indoor and custom modes shares the same properties + set_light_props(parent_obj, sceneHeader.lightList.add(), headerIndex, i, lightEntry, desc) + + +def parseExitList(sceneHeader: OOTSceneHeaderProperty, sceneData: str, exitListName: str): + exitData = getDataMatch(sceneData, exitListName, ["u16", "s16"], "exit list", strip=True) + + # see also start position list + exitList = [value.strip() for value in exitData.split(",") if value.strip() != ""] + for exit in exitList: + exitProp = sceneHeader.exitList.add() + exitProp.exitIndex = "Custom" + exitProp.exitIndexCustom = exit + + +def parseRoomList( + sceneObj: bpy.types.Object, + sceneData: str, + roomListName: str, + f3dContext: OOTF3DContext, + sharedSceneData: SharedSceneData, + headerIndex: int, +): + roomList = getDataMatch(sceneData, roomListName, "RomFile", "room list", strip=True) + index = 0 + roomObjs = [] + use_macros = "ROM_FILE" in roomList + + if use_macros: + regex = r"ROM_FILE\((.*?)\)" + else: + regex = rf"\{{([\(\)\sA-Za-z0-9\_]*),([\(\)\sA-Za-z0-9\_]*)\}}\s*," + + # Assumption that alternate scene headers all use the same room list. + for roomMatch in re.finditer(regex, roomList, flags=re.DOTALL): + if use_macros: + roomName = roomMatch.group(1) + else: + roomName = roomMatch.group(1).strip().replace("SegmentRomStart", "") + if "(u32)" in roomName: + roomName = roomName[5:].strip()[1:] # includes leading underscore + elif "(uintptr_t)" in roomName: + roomName = roomName[11:].strip()[1:] + else: + roomName = roomName[1:] + + file_path = Path(sharedSceneData.scenePath) / f"{roomName}.c" + + if not file_path.exists(): + file_path = Path(sharedSceneData.scenePath).resolve() / f"{roomName}_main.c" + + if not file_path.exists(): + raise PluginError("ERROR: scene not found!") + + roomData = file_path.read_text() + + if not sharedSceneData.is_single_file: + # get the other room files for non-single file fast64 exports + for file in file_path.parent.rglob("*.c"): + if roomName in str(file) and f"{roomName}_main" not in str(file): + roomData += file.read_text() + + parseMatrices(roomData, f3dContext, 1 / bpy.context.scene.ootBlenderScale) + + roomCommandsName = f"{roomName}Commands" + + # fast64 naming + if roomCommandsName not in roomData: + roomCommandsName = f"{roomName}_header00" + + # newer assets system naming + if roomCommandsName not in roomData: + roomCommandsName = roomName + + # Assumption that any shared textures are stored after the CollisionHeader. + # This is done to avoid including large collision data in regex searches. + try: + collisionHeaderIndex = sceneData.index("CollisionHeader ") + except: + collisionHeaderIndex = 0 + sharedRoomData = sceneData[collisionHeaderIndex:] + roomObj = parseRoomCommands( + roomName, + None, + sharedRoomData + roomData, + roomCommandsName, + index, + f3dContext, + sharedSceneData, + headerIndex, + ) + parentObject(sceneObj, roomObj) + index += 1 + roomObjs.append(roomObj) + + return roomObjs + + +def parseAlternateSceneHeaders( + sceneObj: bpy.types.Object, + roomObjs: list[bpy.types.Object], + sceneData: str, + altHeadersListName: str, + f3dContext: OOTF3DContext, + sharedSceneData: SharedSceneData, +): + altHeadersData = getDataMatch(sceneData, altHeadersListName, ["SceneCmd*", "SCmdBase*"], "alternate header list") + altHeadersList = [value.strip() for value in altHeadersData.split(",") if value.strip() != ""] + + for i in range(len(altHeadersList)): + if not (altHeadersList[i] == "NULL" or altHeadersList[i] == "0"): + parseSceneCommands( + sceneObj.name, sceneObj, roomObjs, altHeadersList[i], sceneData, f3dContext, i + 1, sharedSceneData + ) + + +def parse_animated_material(anim_mat: Z64_AnimatedMaterial, scene_data: str, list_name: str): + anim_mat_type_to_struct = { + 0: "AnimatedMatTexScrollParams", + 1: "AnimatedMatTexScrollParams", + 2: "AnimatedMatColorParams", + 3: "AnimatedMatColorParams", + 4: "AnimatedMatColorParams", + 5: "AnimatedMatTexCycleParams", + } + + struct_to_regex = { + "AnimatedMatTexScrollParams": r"\{\s?(0?x?\-?\d+),\s?(0?x?\-?\d+),\s?(0?x?\-?\d+),\s?(0?x?\-?\d+)\s?\}", + "AnimatedMatColorParams": r"(\d+)(\,\n?\s*)?(\d+)(\,\n?\s*)?([a-zA-Z0-9_]*)(\,\n?\s*)?([a-zA-Z0-9_]*)(\,\n?\s*)?([a-zA-Z0-9_]*)", + "AnimatedMatTexCycleParams": r"(\d+)(\,\n?\s*)?([a-zA-Z0-9_]*)(\,\n?\s*)?([a-zA-Z0-9_]*)", + } + + data_match = getDataMatch(scene_data, list_name, "AnimatedMaterial", "animated material") + anim_mat_data = data_match.strip().split("\n") + + for data in anim_mat_data: + data = data.replace("{", "").replace("}", "").removesuffix(",").strip() + + split = data.split(", ") + + type_num = int(split[1], base=0) + + if type_num == 6: + continue + + raw_segment = split[0] + + if "MATERIAL_SEGMENT_NUM" in raw_segment: + raw_segment = raw_segment.removesuffix(")").split("(")[1] + + segment = int(raw_segment, base=0) + data_ptr = split[2].removeprefix("&") + + is_array = type_num in {0, 1} + struct_name = anim_mat_type_to_struct[type_num] + regex = struct_to_regex[struct_name] + data_match = getDataMatch(scene_data, data_ptr, struct_name, "animated params", is_array, False) + params_data: list[list[str]] | list[str] = [] + + if is_array: + params_data = [ + [match.group(1), match.group(2), match.group(3), match.group(4)] + for match in re.finditer(regex, data_match, re.DOTALL) + ] + else: + match = re.search(regex, data_match, re.DOTALL) + assert match is not None + + params_data = [match.group(1), match.group(3), match.group(5)] + if struct_name == "AnimatedMatColorParams": + params_data.extend([match.group(7), match.group(9)]) + + entry = anim_mat.entries.add() + entry.segment_num = segment + enum_type = entry.user_type = game_data.z64.enums.enum_anim_mats_type[type_num + 1][0] + entry.on_type_set(getEnumIndex(game_data.z64.get_enum("anim_mats_type"), enum_type)) + + if struct_name == "AnimatedMatTexScrollParams": + entry.tex_scroll_params.texture_1.set_from_data(params_data[0]) + + if len(params_data) > 1: + entry.tex_scroll_params.texture_2.set_from_data(params_data[1]) + elif struct_name == "AnimatedMatColorParams": + entry.color_params.keyframe_length = int(params_data[0], base=0) + + prim_match = getDataMatch(scene_data, params_data[2], "F3DPrimColor", "animated material prim color", True) + prim_data = prim_match.strip().replace(" ", "").replace("}", "").replace("{", "").split("\n") + + use_env_color = params_data[3] != "NULL" + use_frame_indices = params_data[4] != "NULL" + + env_data = [None] * len(prim_data) + if use_env_color: + env_match = getDataMatch(scene_data, params_data[3], "F3DEnvColor", "animated material env color", True) + env_data = env_match.strip().replace(" ", "").replace("}", "").replace("{", "").split("\n") + + frame_data = [None] * len(prim_data) + if use_frame_indices: + frame_match = getDataMatch( + scene_data, params_data[4], "u16", "animated material color frame data", True + ) + frame_data = ( + frame_match.strip() + .replace(" ", "") + .replace(",\n", ",") + .replace(",", "\n") + .removesuffix("\n") + .split("\n") + ) + + assert len(prim_data) == len(env_data) == len(frame_data) + + for prim_color_raw, env_color_raw, frame in zip(prim_data, env_data, frame_data): + prim_color = [hexOrDecInt(elem) for elem in prim_color_raw.split(",") if len(elem) > 0] + + color_entry = entry.color_params.keyframes.add() + + if use_frame_indices: + assert frame is not None + color_entry.frame_num = int(frame, base=0) + + color_entry.prim_lod_frac = prim_color[4] + color_entry.prim_color = parseColor(prim_color[0:3]) + (1,) + + if use_env_color: + assert env_color_raw is not None + env_color = [hexOrDecInt(elem) for elem in env_color_raw.split(",") if len(elem) > 0] + color_entry.env_color = parseColor(env_color[0:3]) + (1,) + elif struct_name == "AnimatedMatTexCycleParams": + entry.tex_cycle_params.keyframe_length = int(params_data[0], base=0) + textures: list[str] = [] + frames: list[int] = [] + + data_match = getDataMatch(scene_data, params_data[1], "TexturePtr", "animated material texture ptr", True) + for texture_ptr in data_match.replace(" ", "").replace("\n", "").split(","): + if len(texture_ptr) > 0: + textures.append(texture_ptr.strip()) + + data_match = getDataMatch(scene_data, params_data[2], "u8", "animated material frame data", True) + for frame_num in data_match.replace(",", "\n").strip().split("\n"): + frames.append(int(frame_num.strip(), base=0)) + + for symbol in textures: + cycle_entry = entry.tex_cycle_params.textures.add() + cycle_entry.symbol = symbol + + for frame_num in frames: + cycle_entry = entry.tex_cycle_params.keyframes.add() + cycle_entry.texture_index = frame_num + + +def parseSceneCommands( + sceneName: Optional[str], + sceneObj: Optional[bpy.types.Object], + roomObjs: Optional[list[bpy.types.Object]], + sceneCommandsName: str, + sceneData: str, + f3dContext: OOTF3DContext, + headerIndex: int, + sharedSceneData: SharedSceneData, +): + if sceneObj is None: + sceneObj = bpy.data.objects.new(sceneCommandsName, None) + bpy.context.scene.collection.objects.link(sceneObj) + sceneObj.empty_display_type = "SPHERE" + sceneObj.ootEmptyType = "Scene" + sceneObj.name = sceneName + + if headerIndex == 0: + sceneHeader = sceneObj.ootSceneHeader + 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 - (game_data.z64.cs_index_start - 1): + cutsceneHeaders.add() + sceneHeader = cutsceneHeaders[headerIndex - game_data.z64.cs_index_start] + + commands = getDataMatch(sceneData, sceneCommandsName, ["SceneCmd", "SCmdBase"], "scene commands") + entranceList = None + # 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 + raw_cmds = commands.strip().replace(" ", "").split("\n") + for raw_cmd in raw_cmds: + cmd_match = re.search(r"(SCENE\_CMD\_[a-zA-Z0-9\_]*)", raw_cmd, re.DOTALL) + assert cmd_match is not None + command = cmd_match.group(1) + args = raw_cmd.removeprefix(f"{command}(").removesuffix("),").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")) + + 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 + + setCustomProperty(sceneHeader, "musicSeq", enum_id, game_data.z64.get_enum("musicSeq")) + command_list.remove(command) + elif command == "SCENE_CMD_ROOM_LIST": + # Delay until actor cutscenes are processed + delayed_commands[command] = args + command_list.remove(command) + elif command == "SCENE_CMD_TRANSITION_ACTOR_LIST": + if sharedSceneData.includeActors: + # This must be handled after rooms, so that room objs can be referenced + delayed_commands[command] = args + command_list.remove(command) + elif game_data.z64.is_oot() and command == "SCENE_CMD_MISC_SETTINGS": + setCustomProperty(sceneHeader, "cameraMode", args[0], ootEnumCameraMode) + setCustomProperty(sceneHeader, "mapLocation", args[1], ootEnumMapLocation) + command_list.remove(command) + elif command == "SCENE_CMD_COL_HEADER": + # Delay until after rooms are processed + delayed_commands[command] = args + command_list.remove(command) + elif command in {"SCENE_CMD_ENTRANCE_LIST", "SCENE_CMD_SPAWN_LIST"}: + if sharedSceneData.includeActors: + # Delay until after rooms are processed + delayed_commands["SCENE_CMD_SPAWN_LIST"] = args + command_list.remove(command) + elif command == "SCENE_CMD_SPECIAL_FILES": + if game_data.z64.is_oot(): + setCustomProperty(sceneHeader, "naviCup", args[0], ootEnumNaviHints) + setCustomProperty(sceneHeader, "globalObject", args[1], game_data.z64.get_enum("globalObject")) + command_list.remove(command) + elif command == "SCENE_CMD_PATH_LIST": + if sharedSceneData.includePaths: + pathListName = stripName(args[0]) + parsePathList(sceneObj, sceneData, pathListName, headerIndex, sharedSceneData) + command_list.remove(command) + elif command in {"SCENE_CMD_SPAWN_LIST", "SCENE_CMD_PLAYER_ENTRY_LIST"}: + if sharedSceneData.includeActors: + # This must be handled after entrance list, so that entrance list and room list can be referenced + delayed_commands["SCENE_CMD_PLAYER_ENTRY_LIST"] = args + command_list.remove(command) + elif command == "SCENE_CMD_SKYBOX_SETTINGS": + args_index = 0 + if game_data.z64.is_mm(): + sceneHeader.skybox_texture_id = args[args_index] + args_index += 1 + setCustomProperty(sceneHeader, "skyboxID", args[args_index], game_data.z64.get_enum("skybox")) + setCustomProperty( + sceneHeader, "skyboxCloudiness", args[args_index + 1], game_data.z64.get_enum("skybox_config") + ) + setCustomProperty(sceneHeader, "skyboxLighting", args[args_index + 2], ootEnumSkyboxLighting) + command_list.remove(command) + elif command == "SCENE_CMD_EXIT_LIST": + exitListName = stripName(args[0]) + parseExitList(sceneHeader, sceneData, exitListName) + command_list.remove(command) + elif command == "SCENE_CMD_ENV_LIGHT_SETTINGS": + if sharedSceneData.includeLights: + if not (args[1] == "NULL" or args[1] == "0" or args[1] == "0x00"): + lightsListName = stripName(args[1]) + parseLightList(sceneObj, sceneHeader, sceneData, lightsListName, headerIndex, sharedSceneData) + command_list.remove(command) + elif command == "SCENE_CMD_CUTSCENE_DATA": + if sharedSceneData.includeCutscenes: + sceneHeader.writeCutscene = True + sceneHeader.csWriteType = "Object" + csObjName = f"Cutscene.{args[0]}" + try: + sceneHeader.csWriteObject = bpy.data.objects[csObjName] + except: + print(f"ERROR: Cutscene ``{csObjName}`` do not exist!") + command_list.remove(command) + elif command == "SCENE_CMD_ALTERNATE_HEADER_LIST": + # Delay until after rooms are processed + delayed_commands[command] = args + command_list.remove(command) + elif command == "SCENE_CMD_END": + command_list.remove(command) + + # handle Majora's Mask (or modded OoT) exclusive commands + elif game_data.z64.is_mm() or is_hackeroot(): + if command == "SCENE_CMD_ANIMATED_MATERIAL_LIST": + if sharedSceneData.includeAnimatedMats: + parse_animated_material(sceneHeader.animated_material, sceneData, stripName(args[0])) + command_list.remove(command) + + if "SCENE_CMD_ROOM_LIST" in delayed_commands: + args = delayed_commands["SCENE_CMD_ROOM_LIST"] + # Assumption that all scenes use the same room list. + if headerIndex == 0: + if roomObjs is not None: + raise PluginError("Attempting to parse a room list while room objs already loaded.") + roomListName = stripName(args[1]) + roomObjs = parseRoomList(sceneObj, sceneData, roomListName, f3dContext, sharedSceneData, headerIndex) + delayed_commands.pop("SCENE_CMD_ROOM_LIST") + else: + raise PluginError("ERROR: no room command found for this scene!") + + # any other delayed command requires rooms to be processed + for command, args in delayed_commands.items(): + if command == "SCENE_CMD_TRANSITION_ACTOR_LIST" and sharedSceneData.includeActors: + transActorListName = stripName(args[1]) + parseTransActorList(roomObjs, sceneData, transActorListName, sharedSceneData, headerIndex) + elif command == "SCENE_CMD_COL_HEADER": + # Assumption that all scenes use the same collision. + if headerIndex == 0: + collisionHeaderName = args[0][1:] # remove '&' + parseCollisionHeader(sceneObj, roomObjs, sceneData, collisionHeaderName, sharedSceneData) + elif command == "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_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_ALTERNATE_HEADER_LIST": + parseAlternateSceneHeaders(sceneObj, roomObjs, sceneData, stripName(args[0]), 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/oot/importer/scene_pathways.py b/fast64_internal/z64/importer/scene_pathways.py similarity index 89% rename from fast64_internal/oot/importer/scene_pathways.py rename to fast64_internal/z64/importer/scene_pathways.py index 55e435553..578317a82 100644 --- a/fast64_internal/oot/importer/scene_pathways.py +++ b/fast64_internal/z64/importer/scene_pathways.py @@ -13,11 +13,13 @@ def parsePath( sharedSceneData: SharedSceneData, orderIndex: int, ): - pathData = getDataMatch(sceneData, pathName, "Vec3s", "path") + pathData = getDataMatch(sceneData, pathName, "Vec3s", "path", strip=True) pathPointsEntries = [value.replace("{", "").strip() for value in pathData.split("},") if value.strip() != ""] pathPointsInfo = [] for pathPoint in pathPointsEntries: - pathPointsInfo.append(tuple([hexOrDecInt(value.strip()) for value in pathPoint.split(",")])) + pathPointsInfo.append( + tuple([hexOrDecInt(value.strip()) for value in pathPoint.split(",") if value.strip() != ""]) + ) pathPoints = tuple(pathPointsInfo) if sharedSceneData.addHeaderIfItemExists(pathPoints, "Curve", headerIndex): @@ -40,7 +42,7 @@ def parsePathList( headerIndex: int, sharedSceneData: SharedSceneData, ): - pathData = getDataMatch(sceneData, pathListName, "Path", "path list") + pathData = getDataMatch(sceneData, pathListName, "Path", "path list", strip=True) pathList = [value.replace("{", "").strip() for value in pathData.split("},") if value.strip() != ""] for i, pathEntry in enumerate(pathList): numPoints, pathName = [value.strip() for value in pathEntry.split(",")] diff --git a/fast64_internal/oot/importer/utility.py b/fast64_internal/z64/importer/utility.py similarity index 60% rename from fast64_internal/oot/importer/utility.py rename to fast64_internal/z64/importer/utility.py index 8eb975813..d409a87d1 100644 --- a/fast64_internal/oot/importer/utility.py +++ b/fast64_internal/z64/importer/utility.py @@ -2,10 +2,13 @@ import bpy import mathutils -from ...utility import PluginError, hexOrDecInt, removeComments, yUpToZUp +from pathlib import Path + +from ...utility import PluginError, hexOrDecInt, removeComments, get_include_data, yUpToZUp from ..actor.properties import OOTActorProperty, OOTActorHeaderProperty -from ..oot_utility import ootParseRotation +from ..utility import ootParseRotation from .constants import headerNames, actorsWithRotAsParam +from .classes import SharedSceneData def checkBit(value: int, index: int) -> bool: @@ -47,14 +50,19 @@ def getDisplayNameFromActorID(actorID: str): def handleActorWithRotAsParam(actorProp: OOTActorProperty, actorID: str, rotation: list[int]): if actorID in actorsWithRotAsParam: - actorProp.rotOverride = True - actorProp.rotOverrideX = hex(rotation[0]) - actorProp.rotOverrideY = hex(rotation[1]) - actorProp.rotOverrideZ = hex(rotation[2]) + if actorProp.actor_id != "Custom": + actorProp.rot_x = hex(rotation[0]) + actorProp.rot_y = hex(rotation[1]) + actorProp.rot_z = hex(rotation[2]) + else: + actorProp.rot_override = True + actorProp.rot_x_custom = hex(rotation[0]) + actorProp.rot_y_custom = hex(rotation[1]) + actorProp.rot_z_custom = hex(rotation[2]) def getDataMatch( - sceneData: str, name: str, dataType: str | list[str], errorMessageID: str, isArray: bool = True + sceneData: str, name: str, dataType: str | list[str], errorMessageID: str, isArray: bool = True, strip: bool = False ) -> str: arrayText = rf"\[[\s0-9A-Za-z_]*\]\s*" if isArray else "" @@ -69,18 +77,24 @@ def getDataMatch( match = re.search(regex, sceneData, flags=re.DOTALL) if not match: - raise PluginError(f"Could not find {errorMessageID} {name}.") + raise PluginError(f"ERROR: Could not find {errorMessageID} {name}. (regex used: '{regex}')") # return the match with comments removed - return removeComments(match.group(1)) + data_match = removeComments(match.group(1)) + + if "#include" in data_match: + data_match = removeComments(get_include_data(data_match)) + + if strip: + data_match = data_match.replace("\n", "").replace(" ", "") + + return data_match def stripName(name: str): if "&" in name: name = name[name.index("&") + 1 :].strip() - if name[0] == "(" and name[-1] == ")": - name = name[1:-1].strip() - return name + return name.removeprefix("(").removesuffix(")") def createCurveFromPoints(points: list[tuple[float, float, float]], name: str): @@ -108,3 +122,40 @@ def createCurveFromPoints(points: list[tuple[float, float, float]], name: str): curve.dimensions = "3D" return curveObj + + +def parse_commands_data(data: str): + lines = data.replace(" ", "").split("\n") + cmd_map: dict[str, list[str]] = {} + + if lines[-1] == "": + lines.pop() + + if lines[0] == "": + lines.pop(0) + + for line in lines: + match = re.search(r"SCENE\_CMD\_[a-zA-Z0-9\_]*", line, re.DOTALL) + + if match is not None: + cmd = match.group(0) + cmd_map[cmd] = line.removeprefix(f"{cmd}(").removesuffix("),").split(",") + else: + raise PluginError(f"ERROR: no command found! ({repr(line)})") + + return cmd_map + + +def get_array_count(shared_data: SharedSceneData, symbol: str): + header_path = Path(shared_data.scenePath).resolve() / f"{shared_data.scene_name}.h" + + if not header_path.exists(): + raise PluginError("ERROR: can't find scene header!") + + symbol = symbol.removeprefix("ARRAY_COUNT(").removesuffix(")") + match = re.search(rf"#define\s*LENGTH_{symbol}\s*([0-9]*)", header_path.read_text(), re.DOTALL) + + if match is None: + raise PluginError(f"ERROR: can't find array count for {repr(symbol)}") + + return hexOrDecInt(match.group(1)) diff --git a/fast64_internal/oot/oot_model_classes.py b/fast64_internal/z64/model_classes.py similarity index 91% rename from fast64_internal/oot/oot_model_classes.py rename to fast64_internal/z64/model_classes.py index ce3fe2f45..0d676e905 100644 --- a/fast64_internal/oot/oot_model_classes.py +++ b/fast64_internal/z64/model_classes.py @@ -1,11 +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, @@ -13,12 +20,12 @@ writeNonCITextureData, getTextureNamesFromImage, ) + from ..f3d.f3d_gbi import ( FModel, FMaterial, FImage, FImageKey, - FPaletteKey, GfxMatWriteMethod, SPDisplayList, GfxList, @@ -26,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: @@ -94,10 +103,33 @@ def ootGetLinkData(basePath: str) -> str: return actorData +# custom `SPDisplayList` so we can customize the C output +@dataclass(unsafe_hash=True) +class DynamicMaterialDL(SPDisplayList): + is_animated_material_sdc: bool + + def __post_init__(self): + self.default_formatting = False + + def to_c(self, static=True): + assert static + if ( + is_hackeroot() + and bpy.context.scene.fast64.oot.hackeroot_settings.export_ifdefs + and self.is_animated_material_sdc + ): + return ( + "#if ENABLE_ANIMATED_MATERIALS\n" + indent + f"gsSPDisplayList({self.displayList.name}),\n" + "#endif\n" + ) + else: + return indent + f"gsSPDisplayList({self.displayList.name}),\n" + + class OOTModel(FModel): - def __init__(self, name, DLFormat, drawLayerOverride): + def __init__(self, name, DLFormat, drawLayerOverride, draw_config: Optional[str] = None): self.drawLayerOverride = drawLayerOverride self.flipbooks: list[TextureFlipbook] = [] + self.draw_config = draw_config FModel.__init__(self, name, DLFormat, GfxMatWriteMethod.WriteAll) @@ -121,7 +153,7 @@ def getRenderMode(self, drawLayer): defaultRenderModes = create_or_get_world(bpy.context.scene).ootDefaultRenderModes cycle1 = getattr(defaultRenderModes, drawLayerUsed.lower() + "Cycle1") cycle2 = getattr(defaultRenderModes, drawLayerUsed.lower() + "Cycle2") - return [cycle1, cycle2] + return (cycle1, cycle2) def addFlipbookWithRepeatCheck(self, flipbook: TextureFlipbook): model = self.getFlipbookOwner() @@ -282,16 +314,25 @@ def onMaterialCommandsBuilt(self, fMaterial, material, drawLayer): # handle dynamic material calls gfxList = fMaterial.material matDrawLayer = getattr(material.ootMaterial, drawLayer.lower()) + for i in range(8, 14): - if getattr(matDrawLayer, "segment" + format(i, "X")): + if getattr(matDrawLayer, f"segment{i:X}"): + is_animated_material = False + + if self.draw_config is not None and "mat_anim" in self.draw_config: + is_animated_material = True + gfxList.commands.append( - SPDisplayList(GfxList("0x" + format(i, "X") + "000000", GfxListTag.Material, DLFormat.Static)) + DynamicMaterialDL( + GfxList(f"0x0{i:X}000000", GfxListTag.Material, DLFormat.Static), is_animated_material + ) ) + 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): @@ -332,6 +373,10 @@ def __init__(self, f3d, limbList, basePath): self.isBillboard = False self.flipbooks = {} # {(segment, draw layer) : TextureFlipbook} + # the new assets system extracts CI textures as PNGs with the TLUT already applied + # so we need to avoid reading TLUTs as the files don't exist outside the build folder + self.ignore_tlut = False + materialContext = createF3DMat(None, preset="oot_shaded_solid") # materialContext.f3d_mat.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE" F3DContext.__init__(self, f3d, basePath, materialContext) @@ -497,6 +542,14 @@ def handleApplyTLUT( else: super().handleApplyTLUT(material, texProp, tlut, index) + def applyTLUTToIndex(self, index): + if not self.ignore_tlut: + super().applyTLUTToIndex(index) + + def loadTLUTPal(self, name: str, dlData: str, count: int): + if not self.ignore_tlut: + super().loadTLUTPal(name, dlData, count) + def clearOOTFlipbookProperty(flipbookProp): flipbookProp.enable = False diff --git a/fast64_internal/oot/oot_object.py b/fast64_internal/z64/object.py similarity index 85% rename from fast64_internal/oot/oot_object.py rename to fast64_internal/z64/object.py index a4eb89f89..6bab6531b 100644 --- a/fast64_internal/oot/oot_object.py +++ b/fast64_internal/z64/object.py @@ -1,6 +1,6 @@ from bpy.types import Object from ..utility import ootGetSceneOrRoomHeader -from .oot_constants import ootData +from ..game_data import game_data from .exporter.room.header import RoomHeader @@ -18,12 +18,12 @@ def addMissingObjectsToRoomHeader(roomObj: Object, curHeader: RoomHeader, header """Adds missing objects to the object list""" if len(curHeader.actors.actorList) > 0: for roomActor in curHeader.actors.actorList: - actor = ootData.actorData.actorsByID.get(roomActor.id) + actor = game_data.z64.actors.actorsByID.get(roomActor.id) if actor is not None and actor.key != "player" and len(actor.tiedObjects) > 0: for objKey in actor.tiedObjects: if objKey not in ["obj_gameplay_keep", "obj_gameplay_field_keep", "obj_gameplay_dangeon_keep"]: - objID = ootData.objectData.objectsByKey[objKey].id - if not (objID in curHeader.objects.objectList): + objID = game_data.z64.objects.objects_by_key[objKey].id + if objID not in curHeader.objects.objectList: curHeader.objects.objectList.append(objID) addMissingObjectToProp(roomObj, headerIndex, objKey) diff --git a/fast64_internal/oot/props_panel_main.py b/fast64_internal/z64/props_panel_main.py similarity index 90% rename from fast64_internal/oot/props_panel_main.py rename to fast64_internal/z64/props_panel_main.py index 3323825b3..49c011b83 100644 --- a/fast64_internal/oot/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 .oot_utility import getSceneObj, getRoomObj +from .utility import getSceneObj, getRoomObj, is_oot_features from .scene.properties import OOTSceneProperties from .room.properties import OOTObjectProperty, OOTRoomHeaderProperty, OOTAlternateRoomHeaderProperty from .collision.properties import OOTWaterBoxProperty from .cutscene.properties import OOTCutsceneProperty +from .animated_mats.properties import Z64_AnimatedMaterialProperty from .cutscene.motion.properties import ( OOTCutsceneMotionProperty, CutsceneCmdActorCueListProperty, @@ -37,15 +38,16 @@ ("CS Actor Cue Preview", "CS Actor Cue Preview", "CS Actor Cue Preview"), ("CS Player Cue Preview", "CS Player Cue Preview", "CS Player Cue Preview"), ("CS Dummy Cue", "CS Dummy Cue", "CS Dummy Cue"), + ("Animated Materials", "Animated Materials", "Animated Materials"), # ('Camera Volume', 'Camera Volume', 'Camera Volume'), ] def drawSceneHeader(box: bpy.types.UILayout, obj: bpy.types.Object): objName = obj.name - obj.ootSceneHeader.draw_props(box, None, None, objName) + obj.ootSceneHeader.draw_props(box.box(), None, None, obj) if obj.ootSceneHeader.menuTab == "Alternate": - obj.ootAlternateSceneHeaders.draw_props(box, objName) + obj.ootAlternateSceneHeaders.draw_props(box.box(), obj) box.prop(obj.fast64.oot.scene, "write_dummy_room_list") @@ -117,7 +119,9 @@ class OOTObjectPanel(bpy.types.Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and (context.object is not None and context.object.type == "EMPTY") + return context.scene.gameEditorMode in {"OOT", "MM"} and ( + context.object is not None and context.object.type == "EMPTY" + ) def draw(self, context): prop_split(self.layout, context.scene, "gameEditorMode", "Game") @@ -135,7 +139,7 @@ def draw(self, context): if obj.ootEmptyType == "Actor": actorProp: OOTActorProperty = obj.ootActorProperty - actorProp.draw_props(box, altRoomProp, objName) + actorProp.draw_props(box, altRoomProp, obj) elif obj.ootEmptyType == "Transition Actor": transActorProp: OOTTransitionActorProperty = obj.ootTransitionActorProperty @@ -170,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 context.scene.fast64.oot.feature_set == "default": + 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", @@ -190,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(): @@ -198,7 +211,7 @@ def upgrade_changed_props(): if obj.type == "EMPTY": if obj.ootEmptyType == "Room": OOTObjectProperty.upgrade_object(obj) - if obj.ootEmptyType in {"Entrance", "Transition Actor"}: + if obj.ootEmptyType in {"Actor", "Entrance", "Transition Actor"}: OOTActorProperty.upgrade_object(obj) if obj.ootEmptyType == "Cutscene": OOTCutsceneProperty.upgrade_object(obj) diff --git a/fast64_internal/oot/room/operators.py b/fast64_internal/z64/room/operators.py similarity index 90% rename from fast64_internal/oot/room/operators.py rename to fast64_internal/z64/room/operators.py index 954f2ee32..e148ba540 100644 --- a/fast64_internal/oot/room/operators.py +++ b/fast64_internal/z64/room/operators.py @@ -3,7 +3,7 @@ from bpy.utils import register_class, unregister_class from bpy.props import EnumProperty, IntProperty, StringProperty from ...utility import ootGetSceneOrRoomHeader -from ..oot_constants import ootData +from ...game_data import game_data class OOT_SearchObjectEnumOperator(Operator): @@ -12,7 +12,7 @@ class OOT_SearchObjectEnumOperator(Operator): bl_property = "objectKey" bl_options = {"REGISTER", "UNDO"} - objectKey: EnumProperty(items=ootData.objectData.ootEnumObjectKey, default="obj_human") + objectKey: EnumProperty(items=game_data.z64.objects.ootEnumObjectKey, default="obj_human") headerIndex: IntProperty(default=0, min=0) index: IntProperty(default=0, min=0) objName: StringProperty() diff --git a/fast64_internal/oot/room/properties.py b/fast64_internal/z64/room/properties.py similarity index 88% rename from fast64_internal/oot/room/properties.py rename to fast64_internal/z64/room/properties.py index 1e9d643d2..6ad40e269 100644 --- a/fast64_internal/oot/room/properties.py +++ b/fast64_internal/z64/room/properties.py @@ -1,9 +1,13 @@ import bpy -from bpy.types import PropertyGroup, UILayout, Image, Object + +from bpy.types import PropertyGroup, UILayout, Image, Object, Context from bpy.utils import register_class, unregister_class + from ...utility import prop_split -from ..oot_utility import drawCollectionOps, onMenuTabChange, onHeaderMenuTabChange, drawEnumWithCustom, drawAddButton -from ..oot_upgrade import upgradeRoomHeaders +from ...game_data import game_data +from ..collection_utility import drawCollectionOps, drawAddButton +from ..utility import onMenuTabChange, onHeaderMenuTabChange, drawEnumWithCustom +from ..upgrade import upgradeRoomHeaders from .operators import OOT_SearchObjectEnumOperator from bpy.props import ( @@ -17,10 +21,7 @@ IntVectorProperty, ) -from ..oot_constants import ( - ootData, - ootEnumRoomBehaviour, - ootEnumLinkIdle, +from ..constants import ( ootEnumRoomShapeType, ootEnumHeaderMenu, ) @@ -36,21 +37,21 @@ class OOTObjectProperty(PropertyGroup): expandTab: BoolProperty(name="Expand Tab") - objectKey: EnumProperty(items=ootData.objectData.ootEnumObjectKey, default="obj_human") + objectKey: EnumProperty(items=game_data.z64.objects.ootEnumObjectKey, default="obj_human") objectIDCustom: StringProperty(default="OBJECT_CUSTOM") @staticmethod def upgrade_object(obj: Object): print(f"Processing '{obj.name}'...") - upgradeRoomHeaders(obj, ootData.objectData) + upgradeRoomHeaders(obj, game_data.z64.objects) def draw_props(self, layout: UILayout, headerIndex: int, index: int, objName: str): isLegacy = True if "objectID" in self else False if isLegacy: - objectName = ootData.objectData.ootEnumObjectIDLegacy[self["objectID"]][1] + objectName = game_data.z64.objects.ootEnumObjectIDLegacy[self["objectID"]][1] elif self.objectKey != "Custom": - objectName = ootData.objectData.objectsByKey[self.objectKey].name + objectName = game_data.z64.objects.objects_by_key[self.objectKey].name else: objectName = self.objectIDCustom @@ -92,11 +93,13 @@ class OOTRoomHeaderProperty(PropertyGroup): usePreviousHeader: BoolProperty(name="Use Previous Header", default=True) roomIndex: IntProperty(name="Room Index", default=0, min=0) - roomBehaviour: EnumProperty(items=ootEnumRoomBehaviour, default="0x00") + roomBehaviour: EnumProperty(items=lambda self, context: game_data.z64.get_enum("room_type"), default=1) roomBehaviourCustom: StringProperty(default="0x00") disableWarpSongs: BoolProperty(name="Disable Warp Songs") showInvisibleActors: BoolProperty(name="Show Invisible Actors") - linkIdleMode: EnumProperty(name="Link Idle Mode", items=ootEnumLinkIdle, default="0x00") + linkIdleMode: EnumProperty( + name="Link Idle Mode", items=lambda self, context: game_data.z64.get_enum("environment_type"), default=1 + ) linkIdleModeCustom: StringProperty(name="Link Idle Mode Custom", default="0x00") roomIsHot: BoolProperty( name="Use Hot Room Behavior", @@ -149,9 +152,9 @@ 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 - 4, "Room", None, objName) + drawCollectionOps(layout, headerIndex - game_data.z64.cs_index_start, "Room", None, objName) - if headerIndex is not None and headerIndex > 0 and headerIndex < 4: + 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 @@ -237,6 +240,13 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj drawAddButton(objBox, len(self.objectList), "Object", headerIndex, objName) +def update_cutscene_index(self: "OOTAlternateRoomHeaderProperty", context: Context): + if self.currentCutsceneIndex < game_data.z64.cs_index_start: + self.currentCutsceneIndex = game_data.z64.cs_index_start + + onHeaderMenuTabChange(self, context) + + class OOTAlternateRoomHeaderProperty(PropertyGroup): childNightHeader: PointerProperty(name="Child Night Header", type=OOTRoomHeaderProperty) adultDayHeader: PointerProperty(name="Adult Day Header", type=OOTRoomHeaderProperty) @@ -244,7 +254,7 @@ class OOTAlternateRoomHeaderProperty(PropertyGroup): cutsceneHeaders: CollectionProperty(type=OOTRoomHeaderProperty) headerMenuTab: EnumProperty(name="Header Menu", items=ootEnumHeaderMenu, update=onHeaderMenuTabChange) - currentCutsceneIndex: IntProperty(min=4, default=4, update=onHeaderMenuTabChange) + currentCutsceneIndex: IntProperty(default=1, update=update_cutscene_index) def draw_props(self, layout: UILayout, objName: str): headerSetup = layout.column() @@ -262,8 +272,8 @@ def draw_props(self, layout: UILayout, objName: str): prop_split(headerSetup, self, "currentCutsceneIndex", "Cutscene Index") drawAddButton(headerSetup, len(self.cutsceneHeaders), "Room", None, objName) index = self.currentCutsceneIndex - if index - 4 < len(self.cutsceneHeaders): - self.cutsceneHeaders[index - 4].draw_props(headerSetup, None, index, objName) + 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) else: headerSetup.label(text="No cutscene header for this index.", icon="QUESTION") diff --git a/fast64_internal/oot/scene/operators.py b/fast64_internal/z64/scene/operators.py similarity index 59% rename from fast64_internal/oot/scene/operators.py rename to fast64_internal/z64/scene/operators.py index db7d41a06..83c0968f1 100644 --- a/fast64_internal/oot/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,12 @@ 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 ..oot_utility import ExportInfo, RemoveInfo, sceneNameFromID -from ..oot_constants import ootEnumMusicSeq, ootEnumSceneID + +from ...utility import PluginError, ExportUtils, raisePluginError, ootGetSceneOrRoomHeader +from ..utility import ExportInfo, RemoveInfo, sceneNameFromID, is_hackeroot +from ..constants import ootEnumMusicSeq, ootEnumSceneID from ..importer import parseScene -from ..exporter.decomp_edit.config import Config + from ..exporter import SceneExport, Files @@ -87,17 +86,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.""" @@ -131,86 +119,88 @@ class OOT_ExportScene(Operator): bl_options = {"REGISTER", "UNDO", "PRESET"} def execute(self, context): - activeObj = None - try: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - activeObj = context.view_layer.objects.active - - obj = context.scene.ootSceneExportObj - if obj is None: - raise PluginError("Scene object input not set.") - elif obj.type != "EMPTY" or obj.ootEmptyType != "Scene": - raise PluginError("The input object is not an empty with the Scene type.") - - scaleValue = context.scene.ootBlenderScale - finalTransform = Matrix.Diagonal(Vector((scaleValue, scaleValue, scaleValue))).to_4x4() - - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - try: - settings = context.scene.ootSceneExportSettings - levelName = settings.name - option = settings.option - - bootOptions = context.scene.fast64.oot.bootupSceneOptions - hackerFeaturesEnabled = (context.scene.fast64.oot.featureSet == "HackerOOT") - - if settings.customExport: - isCustomExport = True - exportPath = bpy.path.abspath(settings.exportPath) - customSubPath = None - else: - if option == "Custom": - customSubPath = "assets/scenes/" + settings.subFolder + "/" - else: - levelName = sceneNameFromID(option) + with ExportUtils() as export_utils: + activeObj = None + try: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + activeObj = context.view_layer.objects.active + + obj = context.scene.ootSceneExportObj + if obj is None: + raise PluginError("Scene object input not set.") + elif obj.type != "EMPTY" or obj.ootEmptyType != "Scene": + raise PluginError("The input object is not an empty with the Scene type.") + + scaleValue = context.scene.ootBlenderScale + finalTransform = Matrix.Diagonal(Vector((scaleValue, scaleValue, scaleValue))).to_4x4() + + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + try: + settings = context.scene.ootSceneExportSettings + levelName = settings.name + option = settings.option + + bootOptions = context.scene.fast64.oot.bootupSceneOptions + is_hackeroot_features = is_hackeroot() + + if settings.customExport: + isCustomExport = True + exportPath = bpy.path.abspath(settings.exportPath) customSubPath = None - isCustomExport = False - exportPath = bpy.path.abspath(context.scene.ootDecompPath) - - exportInfo = ExportInfo( - isCustomExport, - exportPath, - customSubPath, - levelName, - option, - bpy.context.scene.saveTextures, - settings.singleFile, - context.scene.fast64.oot.useDecompFeatures if not hackerFeaturesEnabled else hackerFeaturesEnabled, - bootOptions if hackerFeaturesEnabled else None, - ) - - SceneExport.export( - obj, - finalTransform, - exportInfo, - ) - - self.report({"INFO"}, "Success!") - - # don't select the scene - for elem in context.selectable_objects: - elem.select_set(False) - - context.view_layer.objects.active = activeObj - if activeObj is not None: - activeObj.select_set(True) - - return {"FINISHED"} - - except Exception as e: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - # don't select the scene - for elem in context.selectable_objects: - elem.select_set(False) - context.view_layer.objects.active = activeObj - if activeObj is not None: - activeObj.select_set(True) - raisePluginError(self, e) - return {"CANCELLED"} + else: + if option == "Custom": + customSubPath = "assets/scenes/" + settings.subFolder + "/" + else: + levelName = sceneNameFromID(option) + customSubPath = None + isCustomExport = False + exportPath = bpy.path.abspath(context.scene.ootDecompPath) + + exportInfo = ExportInfo( + isCustomExport, + exportPath, + customSubPath, + levelName, + option, + bpy.context.scene.saveTextures, + settings.singleFile, + context.scene.fast64.oot.useDecompFeatures if not is_hackeroot_features else is_hackeroot_features, + bootOptions if is_hackeroot_features else None, + settings.auto_add_room_objects, + ) + + SceneExport.export( + obj, + finalTransform, + exportInfo, + ) + + self.report({"INFO"}, "Success!") + + # don't select the scene + for elem in context.selectable_objects: + elem.select_set(False) + + context.view_layer.objects.active = activeObj + if activeObj is not None: + activeObj.select_set(True) + + return {"FINISHED"} + + except Exception as e: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + # don't select the scene + for elem in context.selectable_objects: + elem.select_set(False) + context.view_layer.objects.active = activeObj + if activeObj is not None: + activeObj.select_set(True) + raisePluginError(self, e) + return {"CANCELLED"} class OOT_RemoveScene(Operator): @@ -252,7 +242,6 @@ def draw(self, context): classes = ( OOT_SearchMusicSeqEnumOperator, OOT_SearchSceneEnumOperator, - OOT_ClearBootupScene, OOT_ImportScene, OOT_ExportScene, OOT_RemoveScene, diff --git a/fast64_internal/oot/scene/panels.py b/fast64_internal/z64/scene/panels.py similarity index 73% rename from fast64_internal/oot/scene/panels.py rename to fast64_internal/z64/scene/panels.py index b5beb907a..3801381d7 100644 --- a/fast64_internal/oot/scene/panels.py +++ b/fast64_internal/z64/scene/panels.py @@ -1,30 +1,25 @@ -import bpy -import os - from bpy.types import UILayout from bpy.utils import register_class, unregister_class from ...panels import OOT_Panel -from ..oot_constants import ootEnumSceneID -from ..oot_utility import getEnumName +from ..constants import ootEnumSceneID +from ..utility import getEnumName from .properties import ( OOTExportSceneSettingsProperty, OOTImportSceneSettingsProperty, OOTRemoveSceneSettingsProperty, - OOTBootupSceneOptions, ) from .operators import ( OOT_ImportScene, OOT_ExportScene, OOT_RemoveScene, - OOT_ClearBootupScene, OOT_SearchSceneEnumOperator, ) class OOT_ExportScenePanel(OOT_Panel): - bl_idname = "OOT_PT_export_level" - bl_label = "OOT Scene Exporter" + bl_idname = "Z64_PT_export_level" + bl_label = "Scene Exporter" def drawSceneSearchOp(self, layout: UILayout, enumValue: str, opName: str): searchBox = layout.box().row() @@ -43,18 +38,6 @@ def draw(self, context): self.drawSceneSearchOp(exportBox, settings.option, "Export") settings.draw_props(exportBox) - if context.scene.fast64.oot.featureSet == "HackerOOT": - 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/oot/scene/properties.py b/fast64_internal/z64/scene/properties.py similarity index 71% rename from fast64_internal/oot/scene/properties.py rename to fast64_internal/z64/scene/properties.py index b48948e06..5338de3cb 100644 --- a/fast64_internal/oot/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -1,5 +1,7 @@ import bpy -from bpy.types import PropertyGroup, Object, Light, UILayout, Scene + +from typing import Optional +from bpy.types import PropertyGroup, Object, Light, UILayout, Scene, Context from bpy.props import ( EnumProperty, IntProperty, @@ -10,32 +12,26 @@ 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, getEnumIndex +from ..animated_mats.properties import Z64_AnimatedMaterial -from ..oot_utility import ( - onMenuTabChange, - onHeaderMenuTabChange, - drawCollectionOps, - drawEnumWithCustom, - drawAddButton, -) - -from ..oot_constants import ( +from ..constants import ( ootEnumMusicSeq, ootEnumSceneID, ootEnumGlobalObject, ootEnumNaviHints, - ootEnumSkybox, - ootEnumCloudiness, ootEnumSkyboxLighting, ootEnumMapLocation, ootEnumCameraMode, - ootEnumNightSeq, ootEnumAudioSessionPreset, ootEnumHeaderMenu, - ootEnumDrawConfig, ootEnumHeaderMenuComplete, ) @@ -44,6 +40,7 @@ ("Lighting", "Lighting", "Lighting"), ("Cutscene", "Cutscene", "Cutscene"), ("Exits", "Exits", "Exits"), + ("AnimMats", "Material Anim.", "Material Anim."), ] ootEnumSceneMenu = ootEnumSceneMenuAlternate + [ ("Alternate", "Alternate", "Alternate"), @@ -182,7 +179,14 @@ class OOTLightProperty(PropertyGroup): expandTab: BoolProperty(name="Expand Tab") def draw_props( - self, layout: UILayout, name: str, showExpandTab: bool, index: int, sceneHeaderIndex: int, objName: str + self, + layout: UILayout, + name: str, + showExpandTab: bool, + index: Optional[int], + sceneHeaderIndex: Optional[int], + objName: Optional[str], + collection_type: Optional[str], ): if showExpandTab: box = layout.box().column() @@ -193,28 +197,25 @@ def draw_props( expandTab = True if expandTab: - if index is not None: - drawCollectionOps(box, index, "Light", sceneHeaderIndex, objName) + if index is not None and collection_type is not None: + drawCollectionOps(box, index, collection_type, sceneHeaderIndex, objName) prop_split(box, self, "ambient", "Ambient Color") - if self.useCustomDiffuse0: - prop_split(box, self, "diffuse0Custom", "Diffuse 0") - box.label(text="Make sure light is not part of scene hierarchy.", icon="FILE_PARENT") - else: - prop_split(box, self, "diffuse0", "Diffuse 0") - box.prop(self, "useCustomDiffuse0") - - if self.useCustomDiffuse1: - prop_split(box, self, "diffuse1Custom", "Diffuse 1") - box.label(text="Make sure light is not part of scene hierarchy.", icon="FILE_PARENT") - else: - prop_split(box, self, "diffuse1", "Diffuse 1") - box.prop(self, "useCustomDiffuse1") + def draw_diffuse(index: int): + layout_diffuse = box.box() + layout_diffuse.prop(self, f"useCustomDiffuse{index}") + if self.useCustomDiffuse0: + layout_diffuse.label(text="Make sure light is not part of scene hierarchy.") + prop_split(layout_diffuse, self, f"diffuse{index}Custom", f"Diffuse {index} Object") + else: + prop_split(layout_diffuse, self, f"diffuse{index}", f"Diffuse{index} Color") + draw_diffuse(0) + draw_diffuse(1) prop_split(box, self, "fogColor", "Fog Color") prop_split(box, self, "fogNear", "Fog Near (Fog Far=1000)") prop_split(box, self, "z_far", "Z Far (Draw Distance)") - prop_split(box, self, "transitionSpeed", "Transition Speed") + prop_split(box, self, "transitionSpeed", "Blend Rate") class OOTLightGroupProperty(PropertyGroup): @@ -226,27 +227,37 @@ class OOTLightGroupProperty(PropertyGroup): night: PointerProperty(type=OOTLightProperty) defaultsSet: BoolProperty() - def draw_props(self, layout: UILayout): + def draw_props(self, layout: UILayout, index: Optional[int], header_index: int, obj_name: str): box = layout.column() - box.row().prop(self, "menuTab", expand=True) - if self.menuTab == "Dawn": - self.dawn.draw_props(box, "Dawn", False, None, None, None) - if self.menuTab == "Day": - self.day.draw_props(box, "Day", False, None, None, None) - if self.menuTab == "Dusk": - self.dusk.draw_props(box, "Dusk", False, None, None, None) - if self.menuTab == "Night": - self.night.draw_props(box, "Night", False, None, None, None) + + text = "Default Settings" if index is None else f"Light Settings No. {index + 1}" + box.prop(self, "expandTab", text=text, icon="TRIA_DOWN" if self.expandTab else "TRIA_RIGHT") + + if self.expandTab: + if index is not None: + drawCollectionOps(box, index, "ToD Light", header_index, obj_name) + + box.row().prop(self, "menuTab", expand=True) + + for tod_type in ["Dawn", "Day", "Dusk", "Night"]: + if self.menuTab == tod_type: + getattr(self, tod_type.lower()).draw_props( + box, tod_type, False, index, header_index, obj_name, None + ) 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( @@ -265,9 +276,11 @@ class OOTSceneHeaderProperty(PropertyGroup): naviCup: EnumProperty(name="Navi Hints", default="0x00", items=ootEnumNaviHints) naviCupCustom: StringProperty(name="Navi Hints Custom", default="0x00") - skyboxID: EnumProperty(name="Skybox", items=ootEnumSkybox, default="0x01") + skyboxID: EnumProperty(name="Skybox", items=lambda self, context: game_data.z64.get_enum("skybox"), default=2) skyboxIDCustom: StringProperty(name="Skybox ID", default="0") - skyboxCloudiness: EnumProperty(name="Cloudiness", items=ootEnumCloudiness, default="0x00") + skyboxCloudiness: EnumProperty( + name="Cloudiness", items=lambda self, context: game_data.z64.get_enum("skybox_config"), default=1 + ) skyboxCloudinessCustom: StringProperty(name="Cloudiness ID", default="0x00") skyboxLighting: EnumProperty( name="Skybox Lighting", @@ -286,13 +299,19 @@ class OOTSceneHeaderProperty(PropertyGroup): musicSeq: EnumProperty(name="Music Sequence", items=ootEnumMusicSeq, default="NA_BGM_FIELD_LOGIC") musicSeqCustom: StringProperty(name="Music Sequence ID", default="0x00") - nightSeq: EnumProperty(name="Nighttime SFX", items=ootEnumNightSeq, default="0x00") + nightSeq: EnumProperty( + name="Nighttime SFX", items=lambda self, context: game_data.z64.get_enum("nature_id"), default=1 + ) nightSeqCustom: StringProperty(name="Nighttime SFX ID", default="0x00") audioSessionPreset: EnumProperty(name="Audio Session Preset", items=ootEnumAudioSessionPreset, default="0x00") audioSessionPresetCustom: StringProperty(name="Audio Session Preset", default="0x00") + # ideally `timeOfDayLights` would be removed in favor of `tod_lights` + # but it's easier to keep it since we need at least one element in the collection timeOfDayLights: PointerProperty(type=OOTLightGroupProperty, name="Time Of Day Lighting") + tod_lights: CollectionProperty(type=OOTLightGroupProperty) lightList: CollectionProperty(type=OOTLightProperty, name="Lighting List") + exitList: CollectionProperty(type=OOTExitProperty, name="Exit List") writeCutscene: BoolProperty(name="Write Cutscene") @@ -315,7 +334,61 @@ class OOTSceneHeaderProperty(PropertyGroup): default=False, ) - def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, objName: str): + title_card_name: StringProperty( + name="Title Card", default="none", description="Segment name of the title card to use" + ) + + reuse_anim_mat: BoolProperty(default=False) + reuse_anim_mat_cs_index: IntProperty(min=game_data.z64.cs_index_start, default=game_data.z64.cs_index_start) + animated_material: PointerProperty(type=Z64_AnimatedMaterial) + + internal_anim_mat_header: StringProperty(default="Child Day") # used for the export + internal_header_index: IntProperty(min=1, default=1) # used for the UI + reuse_anim_mat_header: EnumProperty( + items=lambda self, context: self.get_anim_mat_header_list(), + set=lambda self, value: self.on_am_header_set(value), + get=lambda self: self.on_am_header_get(), + ) + + def get_anim_mat_header_list(self): + # all but child night + enum_am_headers_1 = ootEnumHeaderMenuComplete.copy() + enum_am_headers_1.pop(1) + + # all but adult day + enum_am_headers_2 = ootEnumHeaderMenuComplete.copy() + enum_am_headers_2.pop(2) + + # all but adult night + enum_am_headers_3 = ootEnumHeaderMenuComplete.copy() + enum_am_headers_3.pop(3) + + enum_am_headers_4 = ootEnumHeaderMenuComplete.copy() + + am_enum_map = { + 1: enum_am_headers_1, + 2: enum_am_headers_2, + 3: enum_am_headers_3, + 4: enum_am_headers_4, + } + + return am_enum_map[self.internal_header_index] + + def on_am_header_set(self, value): + enum = self.get_anim_mat_header_list() + self.internal_anim_mat_header = enum[value][0] + + def on_am_header_get(self): + index = getEnumIndex(self.get_anim_mat_header_list(), self.internal_anim_mat_header) + return index if index is not None else 0 + + def draw_props( + self, + layout: UILayout, + dropdownLabel: str, + headerIndex: int, + obj: Object, + ): from .operators import OOT_SearchMusicSeqEnumOperator # temp circular import fix if dropdownLabel is not None: @@ -323,18 +396,19 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj if not self.expandTab: return if headerIndex is not None and headerIndex > 3: - drawCollectionOps(layout, headerIndex - 4, "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 < 4: + 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": @@ -344,6 +418,9 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj drawEnumWithCustom(general, self, "naviCup", "Navi Hints", "") if headerIndex is None or headerIndex == 0: self.sceneTableEntry.draw_props(general) + prop_split(general, self, "title_card_name", "Title Card") + if bpy.context.scene.ootSceneExportSettings.customExport: + general.label(text="Custom Export Path enabled, title card will be ignored.", icon="INFO") general.prop(self, "appendNullEntrance") skyboxAndSound = layout.column() @@ -352,7 +429,7 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj drawEnumWithCustom(skyboxAndSound, self, "skyboxCloudiness", "Cloudiness", "") drawEnumWithCustom(skyboxAndSound, self, "musicSeq", "Music Sequence", "") musicSearch = skyboxAndSound.operator(OOT_SearchMusicSeqEnumOperator.bl_idname, icon="VIEWZOOM") - musicSearch.objName = objName + musicSearch.objName = obj.name musicSearch.headerIndex = headerIndex if headerIndex is not None else 0 drawEnumWithCustom(skyboxAndSound, self, "nightSeq", "Nighttime SFX", "") drawEnumWithCustom(skyboxAndSound, self, "audioSessionPreset", "Audio Session Preset", "") @@ -366,12 +443,17 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj lighting = layout.column() lighting.box().label(text="Lighting List") drawEnumWithCustom(lighting, self, "skyboxLighting", "Lighting Mode", "") + if self.skyboxLighting == "LIGHT_MODE_TIME": # Time of Day - self.timeOfDayLights.draw_props(lighting) + self.timeOfDayLights.draw_props(lighting.box(), None, headerIndex, obj.name) + + for i, tod_light in enumerate(self.tod_lights): + tod_light.draw_props(lighting.box(), i, headerIndex, obj.name) + drawAddButton(lighting, len(self.tod_lights), "ToD Light", headerIndex, obj.name) else: for i in range(len(self.lightList)): - self.lightList[i].draw_props(lighting, "Lighting " + str(i), True, i, headerIndex, objName) - drawAddButton(lighting, len(self.lightList), "Light", headerIndex, objName) + self.lightList[i].draw_props(lighting, f"Lighting {i}", True, i, headerIndex, obj.name, "Light") + drawAddButton(lighting, len(self.lightList), "Light", headerIndex, obj.name) elif menuTab == "Cutscene": cutscene = layout.column() @@ -388,18 +470,50 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj cutscene.label(text="Extra cutscenes (not in any header):") for i in range(len(self.extraCutscenes)): box = cutscene.box().column() - drawCollectionOps(box, i, "extraCutscenes", None, objName, True) + drawCollectionOps(box, i, "extraCutscenes", None, obj.name, True) box.prop(self.extraCutscenes[i], "csObject", text="CS obj") if len(self.extraCutscenes) == 0: - drawAddButton(cutscene, 0, "extraCutscenes", 0, objName) + drawAddButton(cutscene, 0, "extraCutscenes", 0, obj.name) elif menuTab == "Exits": exitBox = layout.column() exitBox.box().label(text="Exit List") for i in range(len(self.exitList)): - self.exitList[i].draw_props(exitBox, i, headerIndex, objName) + self.exitList[i].draw_props(exitBox, i, headerIndex, obj.name) + + drawAddButton(exitBox, len(self.exitList), "Exit", headerIndex, obj.name) + + elif menuTab == "AnimMats": + if headerIndex is not None: + layout.prop(self, "reuse_anim_mat", text="Use Existing Material Anim.") - drawAddButton(exitBox, len(self.exitList), "Exit", headerIndex, objName) + if "mat_anim" not in obj.ootSceneHeader.sceneTableEntry.drawConfig: + wrong_box = layout.box().column() + wrong_box.label(text="Wrong Draw Config", icon="ERROR") + + if bpy.context.scene.ootSceneExportSettings.customExport: + wrong_box.label(text="Make sure the `scene_table.h` entry is using") + wrong_box.label(text="the right draw config.") + else: + wrong_box.label(text="Make sure one of the 'Material Animated'") + wrong_box.label(text="draw configs is selected otherwise") + wrong_box.label(text="animated materials won't be exported.") + + if headerIndex is not None and headerIndex > 0 and self.reuse_anim_mat: + pass + prop_split(layout, self, "reuse_anim_mat_header", "Use Material Anim. from") + + if self.internal_anim_mat_header == "Cutscene": + prop_split(layout, self, "reuse_anim_mat_cs_index", "Cutscene Index") + else: + self.animated_material.draw_props(layout, obj, None, headerIndex) + + +def update_cutscene_index(self: "OOTAlternateSceneHeaderProperty", context: Context): + if self.currentCutsceneIndex < game_data.z64.cs_index_start: + self.currentCutsceneIndex = game_data.z64.cs_index_start + + onHeaderMenuTabChange(self, context) class OOTAlternateSceneHeaderProperty(PropertyGroup): @@ -409,30 +523,35 @@ class OOTAlternateSceneHeaderProperty(PropertyGroup): cutsceneHeaders: CollectionProperty(type=OOTSceneHeaderProperty) headerMenuTab: EnumProperty(name="Header Menu", items=ootEnumHeaderMenu, update=onHeaderMenuTabChange) - currentCutsceneIndex: IntProperty(min=4, default=4, update=onHeaderMenuTabChange) + currentCutsceneIndex: IntProperty(default=game_data.z64.cs_index_start, update=update_cutscene_index) - def draw_props(self, layout: UILayout, objName: str): + def draw_props(self, layout: UILayout, obj: Object): headerSetup = layout.column() - # headerSetup.box().label(text = "Alternate Headers") headerSetupBox = headerSetup.column() + menu_tab_map = { + "Child Night": ("childNightHeader", 1), + "Adult Day": ("adultDayHeader", 2), + "Adult Night": ("adultNightHeader", 3), + "Cutscene": ("cutsceneHeaders", game_data.z64.cs_index_start), + } + headerSetupBox.row().prop(self, "headerMenuTab", expand=True) - if self.headerMenuTab == "Child Night": - self.childNightHeader.draw_props(headerSetupBox, None, 1, objName) - elif self.headerMenuTab == "Adult Day": - self.adultDayHeader.draw_props(headerSetupBox, None, 2, objName) - elif self.headerMenuTab == "Adult Night": - self.adultNightHeader.draw_props(headerSetupBox, None, 3, objName) - elif self.headerMenuTab == "Cutscene": + attr_name, header_index = menu_tab_map[self.headerMenuTab] + + if header_index < 4: + getattr(self, attr_name).draw_props(headerSetupBox, None, header_index, obj) + else: prop_split(headerSetup, self, "currentCutsceneIndex", "Cutscene Index") - drawAddButton(headerSetup, len(self.cutsceneHeaders), "Scene", None, objName) + drawAddButton(headerSetup, len(self.cutsceneHeaders), "Scene", None, obj.name) index = self.currentCutsceneIndex - if index - 4 < len(self.cutsceneHeaders): - self.cutsceneHeaders[index - 4].draw_props(headerSetup, None, index, objName) + 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, obj) else: headerSetup.label(text="No cutscene header for this index.", icon="QUESTION") +# TODO: move to HackerOoT properties.py class OOTBootupSceneOptions(PropertyGroup): bootToScene: BoolProperty(default=False, name="Boot To Scene") overrideHeader: BoolProperty(default=False, name="Override Header") @@ -488,6 +607,7 @@ class OOTExportSceneSettingsProperty(PropertyGroup): description="Does not split the scene and rooms into multiple files.", ) option: EnumProperty(items=ootEnumSceneID, default="SCENE_DEKU_TREE") + auto_add_room_objects: BoolProperty(name="Auto-add Missing Room Objects", default=True) # keeping this on purpose, will be removed once old code is cleaned-up useNewExporter: BoolProperty(name="Use New Exporter", default=True) @@ -506,6 +626,7 @@ def draw_props(self, layout: UILayout): layout.prop(self, "singleFile") layout.prop(self, "customExport") + layout.prop(self, "auto_add_room_objects") # layout.prop(self, "useNewExporter") @@ -523,6 +644,7 @@ class OOTImportSceneSettingsProperty(PropertyGroup): includePaths: BoolProperty(name="Paths", default=True) includeWaterBoxes: BoolProperty(name="Water Boxes", default=True) includeCutscenes: BoolProperty(name="Cutscenes", default=False) + includeAnimatedMats: BoolProperty(name="Animated Materials", default=False) option: EnumProperty(items=ootEnumSceneID, default="SCENE_DEKU_TREE") def draw_props(self, layout: UILayout, sceneOption: str): @@ -541,6 +663,11 @@ def draw_props(self, layout: UILayout, sceneOption: str): includeButtons3.prop(self, "includePaths", toggle=1) includeButtons3.prop(self, "includeWaterBoxes", toggle=1) includeButtons3.prop(self, "includeCutscenes", toggle=1) + + includeButtons4 = col.row(align=True) + if not is_oot_features(): + includeButtons4.prop(self, "includeAnimatedMats", toggle=1) + col.prop(self, "isCustomDest") if self.isCustomDest: diff --git a/fast64_internal/oot/skeleton/constants.py b/fast64_internal/z64/skeleton/constants.py similarity index 100% rename from fast64_internal/oot/skeleton/constants.py rename to fast64_internal/z64/skeleton/constants.py diff --git a/fast64_internal/oot/skeleton/importer/__init__.py b/fast64_internal/z64/skeleton/importer/__init__.py similarity index 100% rename from fast64_internal/oot/skeleton/importer/__init__.py rename to fast64_internal/z64/skeleton/importer/__init__.py diff --git a/fast64_internal/oot/skeleton/importer/functions.py b/fast64_internal/z64/skeleton/importer/functions.py similarity index 75% rename from fast64_internal/oot/skeleton/importer/functions.py rename to fast64_internal/z64/skeleton/importer/functions.py index 84699c009..6ab8daf57 100644 --- a/fast64_internal/oot/skeleton/importer/functions.py +++ b/fast64_internal/z64/skeleton/importer/functions.py @@ -1,16 +1,30 @@ import re +import mathutils +import bpy +import math + +from pathlib import Path from typing import List -import mathutils, bpy, math + from ....f3d.f3d_gbi import F3D, get_F3D_GBI from ....f3d.f3d_parser import getImportData, parseF3D -from ....utility import hexOrDecInt, applyRotation, PluginError -from ...oot_f3d_writer import ootReadActorScale -from ...oot_model_classes import OOTF3DContext, ootGetIncludedAssetData -from ...oot_utility import OOTEnum, ootGetObjectPath, getOOTScale, ootGetObjectHeaderPath, ootGetEnums, ootStripComments -from ...oot_texture_array import ootReadTextureArrays +from ....utility import ( + PluginError, + hexOrDecInt, + applyRotation, + deselectAllObjects, + selectSingleObject, + get_include_data, + removeComments, +) +from ...f3d_writer import ootReadActorScale +from ...model_classes import OOTF3DContext, ootGetIncludedAssetData +from ...utility import OOTEnum, ootGetObjectPath, getOOTScale, ootGetObjectHeaderPath, ootGetEnums, ootStripComments +from ...texture_array import ootReadTextureArrays from ..constants import ootSkeletonImportDict from ..properties import OOTSkeletonImportSettings -from ..utility import ootGetLimb, ootGetLimbs, ootGetSkeleton, applySkeletonRestPose +from ..utility import ootGetLimb, ootGetLimbs, ootGetSkeleton, applySkeletonRestPose, get_anim_names +from ...tools.quick_import import quick_import_exec class OOTDLEntry: @@ -22,8 +36,7 @@ def __init__(self, dlName, limbIndex): def ootAddBone(armatureObj, boneName, parentBoneName, currentTransform, loadDL): if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") - bpy.context.view_layer.objects.active = armatureObj + selectSingleObject(armatureObj) bpy.ops.object.mode_set(mode="EDIT") bone = armatureObj.data.edit_bones.new(boneName) bone.use_connect = False @@ -64,30 +77,27 @@ def ootAddLimbRecursively( ): limbName = f3dContext.getLimbName(limbIndex) boneName = f3dContext.getBoneName(limbIndex) - matchResult = ootGetLimb(skeletonData, limbName, False) - - isLOD = matchResult.lastindex > 6 + limb_info = ootGetLimb(skeletonData, limbName, False) + assert limb_info is not None - if isLOD and useFarLOD: - dlName = matchResult.group(7) + if limb_info.is_lod and useFarLOD: + dlName = limb_info.far_dl_name else: - dlName = matchResult.group(6) + dlName = limb_info.dl_name # Animations override the root translation, so we just ignore importing them as well. if limbIndex == 0: translation = [0, 0, 0] else: translation = [ - hexOrDecInt(matchResult.group(1)), - hexOrDecInt(matchResult.group(2)), - hexOrDecInt(matchResult.group(3)), + hexOrDecInt(limb_info.translationX_str), + hexOrDecInt(limb_info.translationY_str), + hexOrDecInt(limb_info.translationZ_str), ] LIMB_DONE = 0xFF - nextChildIndexStr = matchResult.group(4) - nextChildIndex = ootEvaluateLimbExpression(nextChildIndexStr, enums) - nextSiblingIndexStr = matchResult.group(5) - nextSiblingIndex = ootEvaluateLimbExpression(nextSiblingIndexStr, enums) + nextChildIndex = ootEvaluateLimbExpression(limb_info.nextChildIndex_str, enums) + nextSiblingIndex = ootEvaluateLimbExpression(limb_info.nextSiblingIndex_str, enums) # str(limbIndex) + " " + str(translation) + " " + str(nextChildIndex) + " " + \ # str(nextSiblingIndex) + " " + str(dlName)) @@ -103,6 +113,8 @@ def ootAddLimbRecursively( if loadDL: f3dContext.dlList.append(OOTDLEntry(dlName, limbIndex)) + isLOD = limb_info.is_lod + if nextChildIndex != LIMB_DONE: isLOD |= ootAddLimbRecursively( nextChildIndex, skeletonData, obj, armatureObj, currentTransform, boneName, f3dContext, useFarLOD, enums @@ -213,9 +225,7 @@ def ootBuildSkeleton( armatureObj.location = bpy.context.scene.cursor.location # Set bone rotation mode. - bpy.ops.object.select_all(action="DESELECT") - armatureObj.select_set(True) - bpy.context.view_layer.objects.active = armatureObj + selectSingleObject(armatureObj) bpy.ops.object.mode_set(mode="POSE") for bone in armatureObj.pose.bones: bone.rotation_mode = "XYZ" @@ -223,7 +233,7 @@ def ootBuildSkeleton( # Apply mesh to armature. if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() obj.select_set(True) armatureObj.select_set(True) bpy.context.view_layer.objects.active = armatureObj @@ -262,30 +272,58 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) ootGetObjectHeaderPath(isCustomImport, importPath, folderName, True), ] + if isLink: + filepaths.append(ootGetObjectPath(isCustomImport, "", "gameplay_keep", True)) + filepaths.append(ootGetObjectHeaderPath(isCustomImport, "", "gameplay_keep", True)) + + if (Path(bpy.context.scene.ootDecompPath) / "assets/objects" / folderName).exists(): + filepaths.extend( + [ + ootGetObjectPath(isCustomImport, importPath, folderName, False), + ootGetObjectHeaderPath(isCustomImport, importPath, folderName, False), + ( + f"{bpy.context.scene.ootDecompPath}/include/z64player.h" + if bpy.context.scene.fast64.oot.is_z64sceneh_present() + else f"{bpy.context.scene.ootDecompPath}/include/player.h" + ), + ] + ) + + for path in filepaths: + p = Path(path) + if not p.exists(): + filepaths.remove(path) + removeDoubles = importSettings.removeDoubles importNormals = importSettings.importNormals + import_animations = importSettings.import_animations drawLayer = importSettings.drawLayer skeletonData = getImportData(filepaths) if overlayName is not None or isLink: skeletonData = ootGetIncludedAssetData(basePath, filepaths, skeletonData) + skeletonData - matchResult = ootGetSkeleton(skeletonData, skeletonName, False) - limbsName = matchResult.group(2) + skel_info = ootGetSkeleton(skeletonData, skeletonName, False) + assert skel_info is not None + if skel_info.uses_include: + ignore_tlut = True + else: + ignore_tlut = False - matchResult = ootGetLimbs(skeletonData, limbsName, False) - limbsData = matchResult.group(2) - limbList = [entry.strip()[1:] for entry in ootStripComments(limbsData).split(",") if entry.strip() != ""] + limbs_info = ootGetLimbs(skeletonData, skel_info.limbs_name, False) - f3dContext = OOTF3DContext(get_F3D_GBI(), limbList, basePath) + f3dContext = OOTF3DContext(get_F3D_GBI(), limbs_info.limb_list, basePath) f3dContext.mat().draw_layer.oot = drawLayer + f3dContext.ignore_tlut = ignore_tlut + + actorScale = None - if overlayName is not None and importSettings.autoDetectActorScale: + if overlayName is not None and importSettings.autoDetectActorScale and not importSettings.isCustom: actorScale = ootReadActorScale(basePath, overlayName, isLink) - else: + + if actorScale is None: actorScale = getOOTScale(importSettings.actorScale) - # print(limbList) isLOD, armatureObj = ootBuildSkeleton( skeletonName, overlayName, @@ -324,3 +362,14 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) applySkeletonRestPose(restPoseData, armatureObj) if isLOD: applySkeletonRestPose(restPoseData, LODArmatureObj) + + if import_animations: + if armatureObj is not None: + selectSingleObject(armatureObj) + + animation_names = get_anim_names(skeletonData, isLink) + animation_names = list(dict.fromkeys(animation_names)) + + # Call quick_import_exec for each animation name + for animation_name in animation_names: + quick_import_exec(bpy.context, animation_name) diff --git a/fast64_internal/oot/skeleton/operators.py b/fast64_internal/z64/skeleton/operators.py similarity index 64% rename from fast64_internal/oot/skeleton/operators.py rename to fast64_internal/z64/skeleton/operators.py index 440ea5f38..1032306be 100644 --- a/fast64_internal/oot/skeleton/operators.py +++ b/fast64_internal/z64/skeleton/operators.py @@ -4,9 +4,9 @@ from bpy.path import abspath from mathutils import Matrix from ...f3d.f3d_gbi import DLFormat -from ...utility import PluginError, raisePluginError -from ..oot_utility import getStartBone, getNextBone, getOOTScale -from .exporter import ootConvertArmatureToC, ootConvertArmatureToXML +from ...utility import PluginError, ExportUtils, raisePluginError +from ..utility import getStartBone, getNextBone, getOOTScale +from ..exporter.skeleton import ootConvertArmatureToC from .importer import ootImportSkeletonC from .properties import OOTSkeletonImportSettings, OOTSkeletonExportSettings @@ -94,53 +94,47 @@ class OOT_ExportSkeleton(Operator): # Called on demand (i.e. button press, menu item) # Can also be called from operator search menu (Spacebar) def execute(self, context): - armatureObj = None - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - armatureObj = context.active_object - if armatureObj.type != "ARMATURE": - raise PluginError("Armature not selected.") + with ExportUtils() as export_utils: + armatureObj = None + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + if len(context.selected_objects) == 0: + raise PluginError("Armature not selected.") + armatureObj = context.active_object + if armatureObj.type != "ARMATURE": + raise PluginError("Armature not selected.") - if len(armatureObj.children) == 0 or not isinstance(armatureObj.children[0].data, Mesh): - raise PluginError("Armature does not have any mesh children, or " + "has a non-mesh child.") + if len(armatureObj.children) == 0 or not isinstance(armatureObj.children[0].data, Mesh): + raise PluginError("Armature does not have any mesh children, or " + "has a non-mesh child.") - obj = armatureObj.children[0] - finalTransform = Matrix.Scale(getOOTScale(armatureObj.ootActorScale), 4) + obj = armatureObj.children[0] + finalTransform = Matrix.Scale(getOOTScale(armatureObj.ootActorScale), 4) - # Rotation must be applied before exporting skeleton. - # For some reason this does not work if done on the duplicate generated later, so we have to do it before then. - object.select_all(action="DESELECT") - armatureObj.select_set(True) - object.transform_apply(location=False, rotation=True, scale=True, properties=False) - object.select_all(action="DESELECT") + # Rotation must be applied before exporting skeleton. + # For some reason this does not work if done on the duplicate generated later, so we have to do it before then. + object.select_all(action="DESELECT") + armatureObj.select_set(True) + object.transform_apply(location=False, rotation=True, scale=True, properties=False) + object.select_all(action="DESELECT") - try: - exportSettings: OOTSkeletonExportSettings = context.scene.fast64.oot.skeletonExportSettings + try: + exportSettings: OOTSkeletonExportSettings = context.scene.fast64.oot.skeletonExportSettings - saveTextures = context.scene.saveTextures - drawLayer = armatureObj.ootDrawLayer + saveTextures = context.scene.saveTextures + drawLayer = armatureObj.ootDrawLayer - self.report({"INFO"}, f"ootConvertArmatureTo?? with featureSet = {context.scene.fast64.oot.featureSet}") - - if context.scene.fast64.oot.featureSet == "HM64": - ootConvertArmatureToXML( - armatureObj, finalTransform, DLFormat.Static, saveTextures, drawLayer, exportSettings, self.report - ) - else: ootConvertArmatureToC( armatureObj, finalTransform, DLFormat.Static, saveTextures, drawLayer, exportSettings ) - self.report({"INFO"}, "Success!") - return {"FINISHED"} + self.report({"INFO"}, "Success!") + return {"FINISHED"} - except Exception as e: - if context.mode != "OBJECT": - object.mode_set(mode="OBJECT") - raisePluginError(self, e) - return {"CANCELLED"} # must return a set + except Exception as e: + if context.mode != "OBJECT": + object.mode_set(mode="OBJECT") + raisePluginError(self, e) + return {"CANCELLED"} # must return a set oot_skeleton_classes = ( diff --git a/fast64_internal/oot/skeleton/panels.py b/fast64_internal/z64/skeleton/panels.py similarity index 91% rename from fast64_internal/oot/skeleton/panels.py rename to fast64_internal/z64/skeleton/panels.py index f598e7b2e..891bba7f0 100644 --- a/fast64_internal/oot/skeleton/panels.py +++ b/fast64_internal/z64/skeleton/panels.py @@ -17,7 +17,7 @@ class OOT_SkeletonPanel(Panel): @classmethod def poll(cls, context): return ( - context.scene.gameEditorMode == "OOT" + context.scene.gameEditorMode in {"OOT", "MM"} and hasattr(context, "object") and context.object is not None and isinstance(context.object.data, Armature) @@ -43,7 +43,7 @@ class OOT_BonePanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and context.bone is not None + return context.scene.gameEditorMode in {"OOT", "MM"} and context.bone is not None # called every frame def draw(self, context): @@ -53,8 +53,8 @@ def draw(self, context): class OOT_ExportSkeletonPanel(OOT_Panel): - bl_idname = "OOT_PT_export_skeleton" - bl_label = "OOT Skeleton Exporter" + bl_idname = "Z64_PT_export_skeleton" + bl_label = "Skeleton Exporter" # called every frame def draw(self, context): diff --git a/fast64_internal/oot/skeleton/properties.py b/fast64_internal/z64/skeleton/properties.py similarity index 97% rename from fast64_internal/oot/skeleton/properties.py rename to fast64_internal/z64/skeleton/properties.py index 4f081a2df..3e44795c3 100644 --- a/fast64_internal/oot/skeleton/properties.py +++ b/fast64_internal/z64/skeleton/properties.py @@ -117,21 +117,24 @@ class OOTSkeletonImportSettings(PropertyGroup): isCustom: BoolProperty(name="Use Custom Path") removeDoubles: BoolProperty(name="Remove Doubles On Import", default=True) importNormals: BoolProperty(name="Import Normals", default=True) + import_animations: BoolProperty(name="Import Animations", default=False) drawLayer: EnumProperty(name="Import Draw Layer", items=ootEnumDrawLayers) actorOverlayName: StringProperty(name="Overlay", default="ovl_En_GeldB") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) autoDetectActorScale: BoolProperty(name="Auto Detect Actor Scale", default=True) - actorScale: FloatProperty(name="Actor Scale", min=0, default=100) + actorScale: FloatProperty(name="Actor Scale", min=0, default=10) def draw_props(self, layout: UILayout): prop_split(layout, self, "drawLayer", "Import Draw Layer") layout.prop(self, "removeDoubles") layout.prop(self, "importNormals") + layout.prop(self, "import_animations") layout.prop(self, "isCustom") if self.isCustom: prop_split(layout, self, "name", "Skeleton") prop_split(layout, self, "customPath", "File") + prop_split(layout, self, "actorScale", "Actor Scale") else: prop_split(layout, self, "mode", "Mode") if self.mode == "Generic": @@ -172,7 +175,7 @@ def skeleton_props_register(): for cls in oot_skeleton_classes: register_class(cls) - Object.ootActorScale = FloatProperty(min=0, default=100) + Object.ootActorScale = FloatProperty(min=0, default=10) Object.ootSkeleton = PointerProperty(type=OOTSkeletonProperty) Bone.ootBone = PointerProperty(type=OOTBoneProperty) diff --git a/fast64_internal/oot/skeleton/utility.py b/fast64_internal/z64/skeleton/utility.py similarity index 64% rename from fast64_internal/oot/skeleton/utility.py rename to fast64_internal/z64/skeleton/utility.py index 0a565c355..e42a1c5f9 100644 --- a/fast64_internal/oot/skeleton/utility.py +++ b/fast64_internal/z64/skeleton/utility.py @@ -1,7 +1,9 @@ +import dataclasses import mathutils, bpy, os, re +from typing import Optional from ...utility_anim import armatureApplyWithMesh -from ..oot_model_classes import OOTVertexGroupInfo -from ..oot_utility import checkForStartBone, getStartBone, getNextBone, ootStripComments +from ..model_classes import OOTVertexGroupInfo +from ..utility import checkForStartBone, getStartBone, getNextBone, ootStripComments from ...utility import ( PluginError, @@ -13,16 +15,27 @@ getGroupNameFromIndex, attemptModifierApply, cleanupDuplicatedObjects, + get_include_data, + removeComments, yUpToZUp, + deselectAllObjects, + selectSingleObject, ) -def ootGetSkeleton(skeletonData, skeletonName, continueOnError): - # TODO: Does this handle non flex skeleton? +@dataclasses.dataclass +class SkeletonInfo: + start: int + end: int + limbs_name: str + uses_include: bool + + +def ootGetSkeleton(skeletonData: str, skeletonName: str, continueOnError: bool): matchResult = re.search( - "(Flex)?SkeletonHeader\s*" + r"(Flex)?SkeletonHeader\s*" + re.escape(skeletonName) - + "\s*=\s*\{\s*\{?\s*([^,\s]*)\s*,\s*([^,\s\}]*)\s*\}?\s*(,\s*([^,\s]*))?\s*\}\s*;\s*", + + r"\s*=\s*\{\s*\{?\s*([^,\s]*)\s*,?\s*([^,\s\}]*)\s*\}?\s*(,\s*([^,\s]*))?\s*\}\s*;\s*", skeletonData, ) if matchResult is None: @@ -30,12 +43,29 @@ def ootGetSkeleton(skeletonData, skeletonName, continueOnError): return None else: raise PluginError("Cannot find skeleton named " + skeletonName) - return matchResult + + if "#include" in matchResult.group(0): + uses_include = True + split = get_include_data(matchResult.group(3), strip=True).replace("{", "").replace(",}", "").split(",") + limbs_name = split[0] + else: + uses_include = False + limbs_name = matchResult.group(2) + + return SkeletonInfo(matchResult.start(0), matchResult.end(0), limbs_name, uses_include) + + +@dataclasses.dataclass +class LimbsInfo: + start: int + end: int + limb_list: list[str] + uses_include: bool def ootGetLimbs(skeletonData, limbsName, continueOnError): matchResult = re.search( - "(static\s*)?void\s*\*\s*" + re.escape(limbsName) + "\s*\[\s*[0-9]*\s*\]\s*=\s*\{([^\}]*)\}\s*;\s*", + r"(static\s*)?void\s*\*\s*" + re.escape(limbsName) + r"\s*\[\s*[0-9]*\s*\]\s*=\s*\{([^\}]*)\}\s*;\s*", skeletonData, re.DOTALL, ) @@ -44,31 +74,66 @@ def ootGetLimbs(skeletonData, limbsName, continueOnError): return None else: raise PluginError("Cannot find skeleton limbs named " + limbsName) - return matchResult + + if "#include" in matchResult.group(0): + uses_include = True + limbsData = removeComments(get_include_data(matchResult.group(2))) + else: + uses_include = False + limbsData = matchResult.group(2) + + limb_list = [entry.strip()[1:] for entry in ootStripComments(limbsData).split(",") if entry.strip() != ""] + + return LimbsInfo(matchResult.start(0), matchResult.end(0), limb_list, uses_include) + + +@dataclasses.dataclass +class LimbInfo: + start: int + end: int + translationX_str: str + translationY_str: str + translationZ_str: str + nextChildIndex_str: str + nextSiblingIndex_str: str + is_lod: bool + dl_name: str + far_dl_name: Optional[str] + uses_include: bool def ootGetLimb(skeletonData, limbName, continueOnError): - matchResult = re.search("([A-Za-z0-9\_]*)Limb\s*" + re.escape(limbName), skeletonData) + matchResultIni = re.search( + r"([A-Za-z0-9\_]*)Limb\s*" + re.escape(limbName) + r"\s*=\s*\{(.*?)\s*\}\s*;", + skeletonData, + re.DOTALL | re.MULTILINE, + ) - if matchResult is None: + if matchResultIni is None: if continueOnError: return None else: raise PluginError("Cannot find skeleton limb named " + limbName) - limbType = matchResult.group(1) + result = matchResultIni.group(2) + if "#include" in result: + uses_include = True + limb_data = removeComments(get_include_data(result)) + else: + uses_include = False + limb_data = result + + limbType = matchResultIni.group(1) if limbType == "Lod": - dlRegex = "\{\s*([^,\s]*)\s*,\s*([^,\s]*)\s*\}" + is_lod = True + dlRegex = r"\{\s*([^,\s]*)\s*,\s*([^,\s]*)\s*,?\}" else: - dlRegex = "([^,\s]*)" + is_lod = False + dlRegex = r"([^,\s]*)" matchResult = re.search( - "[A-Za-z0-9\_]*Limb\s*" - + re.escape(limbName) - + "\s*=\s*\{\s*\{\s*([^,\s]*)\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*\},\s*([^,]*)\s*,\s*([^,]*)\s*,\s*" - + dlRegex - + "\s*\}\s*;\s*", - skeletonData, + r"\{([^,\s]*),([^,\s]*),([^,\s]*),?\},([^,]*),([^,]*),\{?" + dlRegex, + limb_data.replace("\n", "").replace(" ", ""), re.DOTALL, ) @@ -77,7 +142,43 @@ def ootGetLimb(skeletonData, limbName, continueOnError): return None else: raise PluginError("Cannot handle skeleton limb named " + limbName + " of type " + limbType) - return matchResult + + translationX_str = matchResult.group(1) + translationY_str = matchResult.group(2) + translationZ_str = matchResult.group(3) + nextChildIndex_str = matchResult.group(4) + nextSiblingIndex_str = matchResult.group(5) + + dl_name = matchResult.group(6) + + if is_lod: + far_dl_name = matchResult.group(7) + else: + far_dl_name = None + + return LimbInfo( + matchResultIni.start(0), + matchResultIni.end(0), + translationX_str, + translationY_str, + translationZ_str, + nextChildIndex_str, + nextSiblingIndex_str, + is_lod, + dl_name, + far_dl_name, + uses_include, + ) + + +def get_anim_names(skeleton_data: str, is_link: bool): + """Extracts all animation names that start with 'AnimationHeader' from the given skeleton data.""" + struct_name = "AnimationHeader" + + if is_link: + struct_name = f"Link{struct_name}" + + return re.findall(rf"{struct_name}\s+(\w+)", skeleton_data) def getGroupIndexOfVert(vert, armatureObj, obj, rootGroupIndex): @@ -132,31 +233,29 @@ def ootRemoveSkeleton(filepath, objectName, skeletonName): skeletonDataH = readFile(headerPath) originalDataH = skeletonDataH - matchResult = ootGetSkeleton(skeletonDataC, skeletonName, True) - if matchResult is None: + skel_info = ootGetSkeleton(skeletonDataC, skeletonName, True) + + if skel_info is None: return - skeletonDataC = skeletonDataC[: matchResult.start(0)] + skeletonDataC[matchResult.end(0) :] - limbsName = matchResult.group(2) + skeletonDataC = skeletonDataC[: skel_info.start] + skeletonDataC[skel_info.end :] headerMatch = getDeclaration(skeletonDataH, skeletonName) if headerMatch is not None: skeletonDataH = skeletonDataH[: headerMatch.start(0)] + skeletonDataH[headerMatch.end(0) :] - matchResult = ootGetLimbs(skeletonDataC, limbsName, True) - if matchResult is None: + limbs_info = ootGetLimbs(skeletonDataC, skel_info.limbs_name, True) + if limbs_info is None: return - skeletonDataC = skeletonDataC[: matchResult.start(0)] + skeletonDataC[matchResult.end(0) :] - limbsData = matchResult.group(2) - limbList = [entry.strip()[1:] for entry in ootStripComments(limbsData).split(",") if entry.strip() != ""] + skeletonDataC = skeletonDataC[: limbs_info.start] + skeletonDataC[limbs_info.end :] - headerMatch = getDeclaration(skeletonDataH, limbsName) + headerMatch = getDeclaration(skeletonDataH, skel_info.limbs_name) if headerMatch is not None: skeletonDataH = skeletonDataH[: headerMatch.start(0)] + skeletonDataH[headerMatch.end(0) :] - for limb in limbList: - matchResult = ootGetLimb(skeletonDataC, limb, True) - if matchResult is not None: - skeletonDataC = skeletonDataC[: matchResult.start(0)] + skeletonDataC[matchResult.end(0) :] + for limb in limbs_info.limb_list: + limb_info = ootGetLimb(skeletonDataC, limb, True) + if limb_info is not None: + skeletonDataC = skeletonDataC[: limb_info.start] + skeletonDataC[limb_info.end :] headerMatch = getDeclaration(skeletonDataH, limb) if headerMatch is not None: skeletonDataH = skeletonDataH[: headerMatch.start(0)] + skeletonDataH[headerMatch.end(0) :] @@ -204,7 +303,7 @@ def ootRemoveRotationsFromArmature(armatureObj: bpy.types.Object) -> None: def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object): # Duplicate objects to apply scale / modifiers / linked data - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() for originalMeshObj in [obj for obj in originalArmatureObj.children if obj.type == "MESH"]: originalMeshObj.select_set(True) @@ -220,17 +319,15 @@ def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object try: for obj in meshObjs: - setOrigin(armatureObj, obj) + setOrigin(obj, armatureObj.location) - bpy.ops.object.select_all(action="DESELECT") - armatureObj.select_set(True) - bpy.context.view_layer.objects.active = armatureObj + selectSingleObject(armatureObj) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False) ootRemoveRotationsFromArmature(armatureObj) # Apply modifiers/data to mesh objs - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() for obj in meshObjs: obj.select_set(True) bpy.context.view_layer.objects.active = obj @@ -238,16 +335,13 @@ def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object bpy.ops.object.make_single_user(obdata=True) bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) for selectedObj in meshObjs: - bpy.ops.object.select_all(action="DESELECT") - selectedObj.select_set(True) - bpy.context.view_layer.objects.active = selectedObj + selectSingleObject(selectedObj) for modifier in selectedObj.modifiers: attemptModifierApply(modifier) # Apply new armature rest pose - bpy.ops.object.select_all(action="DESELECT") - bpy.context.view_layer.objects.active = armatureObj + selectSingleObject(armatureObj) bpy.ops.object.mode_set(mode="POSE") bpy.ops.pose.armature_apply() bpy.ops.object.mode_set(mode="OBJECT") @@ -263,8 +357,7 @@ def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object def applySkeletonRestPose(boneData: list[tuple[float, float, float]], armatureObj: bpy.types.Object): if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.object.select_all(action="DESELECT") - armatureObj.select_set(True) + selectSingleObject(armatureObj) bpy.ops.object.mode_set(mode="POSE") diff --git a/fast64_internal/oot/spline/panels.py b/fast64_internal/z64/spline/panels.py similarity index 92% rename from fast64_internal/oot/spline/panels.py rename to fast64_internal/z64/spline/panels.py index e899b3075..6e9b1c543 100644 --- a/fast64_internal/oot/spline/panels.py +++ b/fast64_internal/z64/spline/panels.py @@ -1,7 +1,7 @@ from bpy.types import Panel, Curve from bpy.utils import register_class, unregister_class from ...utility import prop_split -from ..oot_utility import getSceneObj, drawEnumWithCustom +from ..utility import getSceneObj, drawEnumWithCustom from ..actor.properties import OOTActorHeaderProperty from .properties import OOTSplineProperty @@ -16,7 +16,7 @@ class OOTSplinePanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and ( + return context.scene.gameEditorMode in {"OOT", "MM"} and ( context.object is not None and type(context.object.data) == Curve ) diff --git a/fast64_internal/oot/spline/properties.py b/fast64_internal/z64/spline/properties.py similarity index 97% rename from fast64_internal/oot/spline/properties.py rename to fast64_internal/z64/spline/properties.py index b0c32f27c..cd5d2a271 100644 --- a/fast64_internal/oot/spline/properties.py +++ b/fast64_internal/z64/spline/properties.py @@ -2,7 +2,7 @@ from bpy.props import EnumProperty, PointerProperty, StringProperty, IntProperty from bpy.utils import register_class, unregister_class from ...utility import prop_split -from ..oot_utility import drawEnumWithCustom +from ..utility import drawEnumWithCustom from ..collision.constants import ootEnumCameraCrawlspaceSType from ..actor.properties import OOTActorHeaderProperty from ..scene.properties import OOTAlternateSceneHeaderProperty diff --git a/fast64_internal/oot/oot_texture_array.py b/fast64_internal/z64/texture_array.py similarity index 98% rename from fast64_internal/oot/oot_texture_array.py rename to fast64_internal/z64/texture_array.py index 8803312ad..328f91e21 100644 --- a/fast64_internal/oot/oot_texture_array.py +++ b/fast64_internal/z64/texture_array.py @@ -1,8 +1,8 @@ import os, re from typing import Callable -from ..utility import hexOrDecInt +from ..utility import hexOrDecInt, removeComments -from .oot_model_classes import ( +from .model_classes import ( OOTF3DContext, TextureFlipbook, ootGetActorData, @@ -31,6 +31,8 @@ def ootReadTextureArrays( currentPaths = [os.path.join(basePath, f"src/code/z_player_lib.c")] actorData = ootGetIncludedAssetData(basePath, currentPaths, actorData) + actorData + actorData = removeComments(actorData) + # search for texture arrays # this is done first so that its easier to tell which gSPSegment calls refer to texture data. flipbookList = getTextureArrays(actorData, flipbookArrayIndex2D) diff --git a/fast64_internal/oot/tools/__init__.py b/fast64_internal/z64/tools/__init__.py similarity index 100% rename from fast64_internal/oot/tools/__init__.py rename to fast64_internal/z64/tools/__init__.py diff --git a/fast64_internal/oot/tools/operators.py b/fast64_internal/z64/tools/operators.py similarity index 78% rename from fast64_internal/oot/tools/operators.py rename to fast64_internal/z64/tools/operators.py index 5b40e36c3..34836d15b 100644 --- a/fast64_internal/oot/tools/operators.py +++ b/fast64_internal/z64/tools/operators.py @@ -1,13 +1,14 @@ 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 ..oot_utility import getNewPath +from ..utility import getNewPath from .quick_import import QuickImportAborted, quick_import_exec @@ -54,12 +55,12 @@ def execute(self, context): emptyObj = context.view_layer.objects.active emptyObj.ootEmptyType = "Transition Actor" emptyObj.name = "Door Actor" - emptyObj.ootTransitionActorProperty.actor.actorID = "ACTOR_DOOR_SHUTTER" - emptyObj.ootTransitionActorProperty.actor.actorParam = "0x0000" + emptyObj.ootTransitionActorProperty.actor.actor_id = "ACTOR_DOOR_SHUTTER" + emptyObj.ootTransitionActorProperty.actor.params = "0x0000" parentObject(cubeObj, emptyObj) - setOrigin(emptyObj, cubeObj) + setOrigin(cubeObj, emptyObj.location) return {"FINISHED"} @@ -79,7 +80,7 @@ def execute(self, context): object.mode_set(mode="OBJECT") object.select_all(action="DESELECT") - location = Vector(context.scene.cursor.location) + location = Vector() mesh.primitive_plane_add(size=2 * self.scale, enter_editmode=False, align="WORLD", location=location[:]) planeObj = context.view_layer.objects.active planeObj.name = "Floor" @@ -89,7 +90,7 @@ def execute(self, context): entranceObj = context.view_layer.objects.active entranceObj.ootEmptyType = "Entrance" entranceObj.name = "Entrance" - entranceObj.ootEntranceProperty.actor.actorParam = "0x0FFF" + entranceObj.ootEntranceProperty.actor.params = "0x0FFF" parentObject(planeObj, entranceObj) location += Vector([0, 0, 10]) @@ -107,6 +108,14 @@ def execute(self, context): sceneObj.name = "Scene" parentObject(sceneObj, roomObj) + object.camera_add(align="WORLD", location=Vector(), rotation=Vector()) + camera_obj = context.view_layer.objects.active + camera_obj.name = f"{sceneObj.name} Camera" + camera_props = camera_obj.ootCameraPositionProperty + camera_props.camSType = "CAM_SET_NORMAL0" + camera_props.hasPositionData = False + parentObject(sceneObj, camera_obj) + context.scene.ootSceneExportObj = sceneObj context.scene.fast64.renderSettings.ootSceneObject = sceneObj @@ -293,3 +302,54 @@ def execute(self, context: Context): self.report({"ERROR"}, e.message) return {"CANCELLED"} return {"FINISHED"} + + +class Z64_AddAnimatedMaterial(Operator): + bl_idname = "object.z64_add_animated_material" + bl_label = "Add Animated Materials" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Create a new Animated Material empty object." + + add_test_color: BoolProperty(default=False) + obj_name: StringProperty(default="Actor Animated Materials") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "obj_name", text="Name") + self.layout.prop(self, "add_test_color", text="Add Color Non-linear Interpolation Example") + + def execute(self, context: Context): + new_obj = get_new_empty_object(self.obj_name) + new_obj.ootEmptyType = "Animated Materials" + + if self.add_test_color: + am_props = new_obj.fast64.oot.animated_materials + new_am = am_props.items.add() + new_am_item = new_am.entries.add() + new_am_item.type = "anim_mat_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/oot/tools/panel.py b/fast64_internal/z64/tools/panel.py similarity index 89% rename from fast64_internal/oot/tools/panel.py rename to fast64_internal/z64/tools/panel.py index e72ac48a8..a710142be 100644 --- a/fast64_internal/oot/tools/panel.py +++ b/fast64_internal/z64/tools/panel.py @@ -9,12 +9,13 @@ OOT_AddPath, OOTClearTransformAndLock, OOTQuickImport, + Z64_AddAnimatedMaterial, ) class OoT_ToolsPanel(OOT_Panel): - bl_idname = "OOT_PT_tools" - bl_label = "OOT Tools" + bl_idname = "Z64_PT_tools" + bl_label = "Tools" def draw(self, context): col = self.layout.column() @@ -26,6 +27,7 @@ def draw(self, context): col.operator(OOT_AddPath.bl_idname) col.operator(OOTClearTransformAndLock.bl_idname) col.operator(OOTQuickImport.bl_idname) + col.operator(Z64_AddAnimatedMaterial.bl_idname) oot_operator_panel_classes = [ @@ -41,6 +43,7 @@ def draw(self, context): OOT_AddPath, OOTClearTransformAndLock, OOTQuickImport, + Z64_AddAnimatedMaterial, ] diff --git a/fast64_internal/oot/tools/quick_import.py b/fast64_internal/z64/tools/quick_import.py similarity index 74% rename from fast64_internal/oot/tools/quick_import.py rename to fast64_internal/z64/tools/quick_import.py index 0a51344ee..c3b215f17 100644 --- a/fast64_internal/oot/tools/quick_import.py +++ b/fast64_internal/z64/tools/quick_import.py @@ -1,9 +1,9 @@ -from pathlib import Path import os import re - import bpy +from pathlib import Path +from typing import Optional from ..f3d.properties import OOTDLImportSettings from ..skeleton.properties import OOTSkeletonImportSettings from ..animation.properties import OOTAnimImportSettingsProperty @@ -21,18 +21,25 @@ def get_found_defs(path: Path, sym_name: str, sym_def_pattern: re.Pattern[str]): for dirpath, _, filenames in os.walk(path): dirpath_p = Path(dirpath) + for filename in filenames: file_p = dirpath_p / filename + # Only look into C files if file_p.suffix != ".c": continue + source = file_p.read_text() + # Simple check to see if we should look into this file any further if sym_name not in source: continue + found_defs = sym_def_pattern.findall(source) - print(file_p, f"{found_defs=}") - all_found_defs[file_p] = found_defs + + if len(found_defs) > 0: + print(file_p, f"{found_defs=}") + all_found_defs[file_p] = found_defs return all_found_defs @@ -57,33 +64,37 @@ def quick_import_exec(context: bpy.types.Context, sym_name: str): sym_def_pattern = re.compile(rf"([^\s]+)\s+{sym_name}\s*(\[[^\]]*\])?\s*=") - base_dir_p = Path(context.scene.ootDecompPath) / context.scene.fast64.oot.get_extracted_path() - assets_objects_dir_p = base_dir_p / "assets" / "objects" - assets_scenes_dir_p = base_dir_p / "assets" / "scenes" - is_sym_object = True - all_found_defs = get_found_defs(assets_objects_dir_p, sym_name, sym_def_pattern) - if len(all_found_defs) == 0: - is_sym_object = False - all_found_defs = get_found_defs(assets_scenes_dir_p, sym_name, sym_def_pattern) - - found_dir_p = assets_objects_dir_p if is_sym_object else assets_scenes_dir_p all_found_defs: dict[Path, list[tuple[str, str]]] = dict() + found_dir_p: Optional[Path] = None + base_dir_p = Path(context.scene.ootDecompPath) - for dirpath, dirnames, filenames in os.walk(found_dir_p): - dirpath_p = Path(dirpath) - for filename in filenames: - file_p = dirpath_p / filename - # Only look into C files - if file_p.suffix != ".c": - continue - source = file_p.read_text() - # Simple check to see if we should look into this file any further - if sym_name not in source: - continue - found_defs = sym_def_pattern.findall(source) - print(file_p, f"{found_defs=}") - if found_defs: - all_found_defs[file_p] = found_defs + # this str cast completely useless, it's there to force linting to recognize a Path element + extracted_dir_p = base_dir_p / str(context.scene.fast64.oot.get_extracted_path()) + + assets_paths: list[tuple[Optional[Path], Path]] = [ + # objects + (extracted_dir_p / "assets" / "objects", base_dir_p / "assets" / "objects"), + # scenes + (extracted_dir_p / "assets" / "scenes", base_dir_p / "assets" / "scenes"), + # other assets embedded in actors (cutscenes for instance) + (None, base_dir_p / "src" / "overlays" / "actors"), + ] + + for extracted_path, path in assets_paths: + all_found_defs = get_found_defs(path, sym_name, sym_def_pattern) + + if len(all_found_defs) > 0: + found_dir_p = path + break + + if extracted_path is not None: + all_found_defs = get_found_defs(extracted_path, sym_name, sym_def_pattern) + + if len(all_found_defs) > 0: + found_dir_p = extracted_path + break + + assert found_dir_p is not None # Ideally if for example sym_name was gLinkAdultHookshotTipDL, # all_found_defs now contains: @@ -93,13 +104,15 @@ def quick_import_exec(context: bpy.types.Context, sym_name: str): if len(all_found_defs) == 0: raise QuickImportAborted(f"Couldn't find a definition of {sym_name}, is the OoT Version correct?") + if len(all_found_defs) > 1: raise QuickImportAborted( f"Found definitions of {sym_name} in several files: " + ", ".join(str(p.relative_to(found_dir_p)) for p in all_found_defs.keys()) ) - assert len(all_found_defs) == 1 + sym_file_p, sym_defs = list(all_found_defs.items())[0] + if len(sym_defs) > 1: raise QuickImportAborted(f"Found several definitions of {sym_name} in {sym_file_p.relative_to(found_dir_p)}") @@ -109,7 +122,7 @@ def quick_import_exec(context: bpy.types.Context, sym_name: str): folder_name = sym_file_p.relative_to(found_dir_p).parts[0] def raise_only_from_object(type: str): - if not is_sym_object: + if "objects" not in str(found_dir_p): raise QuickImportAborted( f"Can only import {type} from an object ({sym_name} found in {sym_file_p.relative_to(base_dir_p)})" ) @@ -144,6 +157,14 @@ def raise_only_from_object(type: str): settings.animName = sym_name settings.folderName = folder_name bpy.ops.object.oot_import_anim() + elif sym_def_type == "LinkAnimationHeader" and not is_array: + raise_only_from_object(sym_def_type) + settings: OOTAnimImportSettingsProperty = context.scene.fast64.oot.animImportSettings + settings.isCustom = False + settings.isLink = True + settings.animName = sym_name + settings.folderName = folder_name + bpy.ops.object.oot_import_anim() elif sym_def_type == "CutsceneData" and is_array: bpy.context.scene.ootCSNumber = importCutsceneData(f"{sym_file_p}", None, sym_name) else: diff --git a/fast64_internal/oot/oot_upgrade.py b/fast64_internal/z64/upgrade.py similarity index 80% rename from fast64_internal/oot/oot_upgrade.py rename to fast64_internal/z64/upgrade.py index 75e3c4fb1..9564d2c00 100644 --- a/fast64_internal/oot/oot_upgrade.py +++ b/fast64_internal/z64/upgrade.py @@ -5,9 +5,9 @@ import bpy from bpy.types import Object, CollectionProperty from ..utility import PluginError -from .data import OoT_ObjectData -from .oot_utility import getEvalParams -from .oot_constants import ootData +from ..data import Z64_ObjectData +from .utility import getEvalParams, get_actor_prop_from_obj +from ..game_data import game_data from .cutscene.constants import ootEnumCSMotionCamMode if TYPE_CHECKING: @@ -17,7 +17,7 @@ ##################################### # Room Header ##################################### -def upgradeObjectList(objList: CollectionProperty, objData: OoT_ObjectData): +def upgradeObjectList(objList: CollectionProperty, objData: Z64_ObjectData): """Transition to the XML object system""" for obj in objList: # In order to check whether the data in the current blend needs to be updated, @@ -31,12 +31,12 @@ def upgradeObjectList(objList: CollectionProperty, objData: OoT_ObjectData): if objectID == "Custom": obj.objectKey = objectID else: - obj.objectKey = objData.objectsByID[objectID].key + obj.objectKey = objData.objects_by_id[objectID].key del obj["objectID"] -def upgradeRoomHeaders(roomObj: Object, objData: OoT_ObjectData): +def upgradeRoomHeaders(roomObj: Object, objData: Z64_ObjectData): """Main upgrade logic for room headers""" altHeaders = roomObj.ootAlternateRoomHeaders for sceneLayer in [ @@ -180,12 +180,12 @@ def upgradeCutsceneSubProps(csListSubProp): subPropsToEnum = [ # TextBox - Cutscene_UpgradeData("ocarinaSongAction", "ocarinaAction", ootData.enumData.ootEnumOcarinaSongActionId), - Cutscene_UpgradeData("type", "csTextType", ootData.enumData.ootEnumCsTextType), + Cutscene_UpgradeData("ocarinaSongAction", "ocarinaAction", game_data.z64.enums.enum_ocarina_song_action_id), + Cutscene_UpgradeData("type", "csTextType", game_data.z64.enums.enum_cs_text_type), # Seq - Cutscene_UpgradeData("value", "csSeqID", ootData.enumData.ootEnumSeqId), + Cutscene_UpgradeData("value", "csSeqID", game_data.z64.enums.enum_seq_id), # Misc - Cutscene_UpgradeData("operation", "csMiscType", ootData.enumData.ootEnumCsMiscType), + Cutscene_UpgradeData("operation", "csMiscType", game_data.z64.enums.enum_cs_misc_type), ] transferOldDataToNew(csListSubProp, subPropsOldToNew) @@ -210,7 +210,7 @@ def upgradeCSListProps(csListProp): # both are enums but the item list is different (the old one doesn't have a "custom" entry) convertOldDataToEnumData( - csListProp, [Cutscene_UpgradeData("fxType", "transitionType", ootData.enumData.ootEnumCsTransitionType)] + csListProp, [Cutscene_UpgradeData("fxType", "transitionType", game_data.z64.enums.enum_cs_transition_type)] ) @@ -223,7 +223,7 @@ def upgradeCutsceneProperty(csProp: "OOTCutsceneProperty"): transferOldDataToNew(csProp, csPropOldToNew) convertOldDataToEnumData( - csProp, [Cutscene_UpgradeData("csTermIdx", "csDestination", ootData.enumData.ootEnumCsDestination)] + csProp, [Cutscene_UpgradeData("csTermIdx", "csDestination", game_data.z64.enums.enum_cs_destination)] ) @@ -242,8 +242,8 @@ def upgradeCutsceneMotion(csMotionObj: Object): if "actor_id" in legacyData: index = legacyData["actor_id"] if index >= 0: - cmdEnum = ootData.enumData.enumByKey["csCmd"] - cmdType = cmdEnum.itemByIndex.get(index) + cmdEnum = game_data.z64.enums.enumByKey["cs_cmd"] + cmdType = cmdEnum.item_by_index.get(index) if cmdType is not None: csMotionProp.actorCueListProp.commandType = cmdType.key else: @@ -263,10 +263,10 @@ def upgradeCutsceneMotion(csMotionObj: Object): del legacyData["start_frame"] if "action_id" in legacyData: - playerEnum = ootData.enumData.enumByKey["csPlayerCueId"] + playerEnum = game_data.z64.enums.enumByKey["cs_player_cue_id"] item = None if isPlayer: - item = playerEnum.itemByIndex.get(int(legacyData["action_id"], base=16)) + item = playerEnum.item_by_index.get(int(legacyData["action_id"], base=16)) if isPlayer and item is not None: csMotionProp.actorCueProp.playerCueID = item.key @@ -307,6 +307,61 @@ def upgradeCutsceneMotion(csMotionObj: Object): # Actors ##################################### def upgradeActors(actorObj: Object): + # parameters + actorProp = get_actor_prop_from_obj(actorObj) + isCustom = False + + if actorObj.ootEmptyType == "Entrance": + isCustom = actorObj.ootEntranceProperty.customActor + else: + if "actorID" in actorProp: + actorProp.actor_id = game_data.z64.actors.ootEnumActorID[actorProp["actorID"]][0] + del actorProp["actorID"] + + if "actorIDCustom" in actorProp: + actorProp.actor_id_custom = actorProp["actorIDCustom"] + del actorProp["actorIDCustom"] + + isCustom = actorProp.actor_id == "Custom" + + if "actorParam" in actorProp: + if not isCustom: + prop_name = "params" + + if getEvalParams(actorProp["actorParam"]) is None: + actorProp.actor_id_custom = actorProp.actor_id + actorProp.actor_id = "Custom" + prop_name = "params_custom" + else: + prop_name = "params_custom" + + setattr(actorProp, prop_name, actorProp["actorParam"]) + del actorProp["actorParam"] + + if actorObj.ootEmptyType == "Actor": + custom = "_custom" if actorProp.actor_id == "Custom" else "" + + if isCustom: + if "rotOverride" in actorProp: + actorProp.rot_override = actorProp["rotOverride"] + del actorProp["rotOverride"] + + for rot in {"X", "Y", "Z"}: + if actorProp.actor_id == "Custom" or actorProp.is_rotation_used(f"{rot}Rot"): + if f"rotOverride{rot}" in actorProp: + if getEvalParams(actorProp[f"rotOverride{rot}"]) is None: + custom = "_custom" + + if actorProp.actor_id != "Custom": + actorProp.actor_id_custom = actorProp.actor_id + actorProp.params_custom = actorProp.params + actorProp.actor_id = "Custom" + actorProp.rot_override = True + + setattr(actorProp, f"rot_{rot.lower()}{custom}", actorProp[f"rotOverride{rot}"]) + del actorProp[f"rotOverride{rot}"] + + # room stuff if actorObj.ootEmptyType == "Entrance": entranceProp = actorObj.ootEntranceProperty diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/z64/utility.py similarity index 78% rename from fast64_internal/oot/oot_utility.py rename to fast64_internal/z64/utility.py index 3173cc299..5e229acb0 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/z64/utility.py @@ -2,16 +2,18 @@ import math import os import re +import traceback -from ast import parse, Expression, Num, UnaryOp, USub, Invert, BinOp +from ast import parse, Expression, Constant, UnaryOp, USub, Invert, BinOp from mathutils import Vector from bpy.types import Object -from bpy.utils import register_class, unregister_class -from bpy.types import Object from typing import Callable, Optional, TYPE_CHECKING, List -from .oot_constants import ootSceneIDToName from dataclasses import dataclass +from ..game_data import game_data +from .constants import ootSceneIDToName + + from ..utility import ( PluginError, prop_split, @@ -21,13 +23,15 @@ setOrigin, applyRotation, cleanupDuplicatedObjects, - ootGetSceneOrRoomHeader, hexOrDecInt, binOps, + deselectAllObjects, + selectSingleObject, ) if TYPE_CHECKING: from .scene.properties import OOTBootupSceneOptions + from .actor.properties import OOTActorProperty def isPathObject(obj: bpy.types.Object) -> bool: @@ -295,6 +299,9 @@ class ExportInfo: hackerootBootOption: "OOTBootupSceneOptions" """ Options for setting the bootup scene in HackerOoT.""" + auto_add_room_objects: bool + """ Whether to enable the automatic room object addition feature """ + @dataclass class RemoveInfo: @@ -312,15 +319,15 @@ class RemoveInfo: class OOTObjectCategorizer: def __init__(self): - self.sceneObj = None - self.roomObjs = [] - self.actors = [] - self.transitionActors = [] - self.meshes = [] - self.entrances = [] - self.waterBoxes = [] - - def sortObjects(self, allObjs): + self.sceneObj: Optional[Object] = None + self.roomObjs: list[Object] = [] + self.actors: list[Object] = [] + self.transitionActors: list[Object] = [] + self.meshes: list[Object] = [] + self.entrances: list[Object] = [] + self.waterBoxes: list[Object] = [] + + def sortObjects(self, allObjs: list[Object]): for obj in allObjs: if obj.type == "EMPTY": if obj.ootEmptyType == "Actor": @@ -340,9 +347,11 @@ def sortObjects(self, allObjs): # This also sets all origins relative to the scene object. -def ootDuplicateHierarchy(obj, ignoreAttr, includeEmpties, objectCategorizer) -> tuple[Object, list[Object]]: +def ootDuplicateHierarchy( + obj: Object, ignoreAttr: Optional[str], includeEmpties: bool, objectCategorizer: OOTObjectCategorizer +) -> tuple[Object, list[Object]]: # Duplicate objects to apply scale / modifiers / linked data - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() ootSelectMeshChildrenOnly(obj, includeEmpties) obj.select_set(True) bpy.context.view_layer.objects.active = obj @@ -353,26 +362,24 @@ def ootDuplicateHierarchy(obj, ignoreAttr, includeEmpties, objectCategorizer) -> bpy.ops.object.make_single_user(obdata=True) objectCategorizer.sortObjects(allObjs) + meshObjs = objectCategorizer.meshes - bpy.ops.object.select_all(action="DESELECT") + deselectAllObjects() for selectedObj in meshObjs: selectedObj.select_set(True) bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) for selectedObj in meshObjs: - bpy.ops.object.select_all(action="DESELECT") - selectedObj.select_set(True) - bpy.context.view_layer.objects.active = selectedObj + selectSingleObject(selectedObj) for modifier in selectedObj.modifiers: attemptModifierApply(modifier) for selectedObj in meshObjs: - setOrigin(obj, selectedObj) + setOrigin(selectedObj, obj.location) if ignoreAttr is not None: for selectedObj in meshObjs: if getattr(selectedObj, ignoreAttr): for child in selectedObj.children: - bpy.ops.object.select_all(action="DESELECT") - child.select_set(True) + selectSingleObject(child) bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") selectedObj.parent.select_set(True) bpy.ops.object.parent_set(keep_transform=True) @@ -412,9 +419,7 @@ def ootDuplicateHierarchy(obj, ignoreAttr, includeEmpties, objectCategorizer) -> # This is a relative transform we care about so the 90 degrees # doesn't matter (since they're both right-handed). print("Applying transform") - bpy.ops.object.select_all(action="DESELECT") - tempObj.select_set(True) - bpy.context.view_layer.objects.active = tempObj + selectSingleObject(tempObj) bpy.ops.object.transform_apply() return tempObj, allObjs @@ -496,7 +501,9 @@ def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: st return filepath -def ootGetPath(exportPath, isCustomExport, subPath, folderName, makeIfNotExists, useFolderForCustom): +def ootGetPath( + exportPath, isCustomExport, subPath, folderName, makeIfNotExists, useFolderForCustom, is_import: bool = False +): if isCustomExport: path = bpy.path.abspath(os.path.join(exportPath, (folderName if useFolderForCustom else ""))) else: @@ -505,7 +512,7 @@ def ootGetPath(exportPath, isCustomExport, subPath, folderName, makeIfNotExists, path = bpy.path.abspath(os.path.join(os.path.join(bpy.context.scene.ootDecompPath, subPath), folderName)) if not os.path.exists(path): - if isCustomExport or makeIfNotExists: + if not is_import and isCustomExport or makeIfNotExists: os.makedirs(path) else: raise PluginError(path + " does not exist.") @@ -570,7 +577,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) @@ -586,7 +595,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): @@ -645,160 +654,6 @@ def getCutsceneName(obj): return name -def getCollectionFromIndex(obj, prop, subIndex, isRoom): - header = ootGetSceneOrRoomHeader(obj, subIndex, isRoom) - return getattr(header, prop) - - -# 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): - obj = bpy.data.objects[objName] - if collectionType == "Actor": - collection = obj.ootActorProperty.headerSettings.cutsceneHeaders - elif collectionType == "Transition Actor": - collection = obj.ootTransitionActorProperty.actor.headerSettings.cutsceneHeaders - elif collectionType == "Entrance": - collection = obj.ootEntranceProperty.actor.headerSettings.cutsceneHeaders - elif collectionType == "Room": - collection = obj.ootAlternateRoomHeaders.cutsceneHeaders - elif collectionType == "Scene": - collection = obj.ootAlternateSceneHeaders.cutsceneHeaders - elif collectionType == "Light": - collection = getCollectionFromIndex(obj, "lightList", subIndex, False) - elif collectionType == "Exit": - collection = getCollectionFromIndex(obj, "exitList", subIndex, False) - elif collectionType == "Object": - collection = getCollectionFromIndex(obj, "objectList", subIndex, True) - elif collectionType == "Curve": - collection = obj.ootSplineProperty.headerSettings.cutsceneHeaders - elif collectionType.startswith("CSHdr."): - # CSHdr.HeaderNumber[.ListType] - # Specifying ListType means uses subIndex - toks = collectionType.split(".") - assert len(toks) in [2, 3] - hdrnum = int(toks[1]) - collection = getCollectionFromIndex(obj, "csLists", hdrnum, False) - if len(toks) == 3: - collection = getattr(collection[subIndex], toks[2]) - elif collectionType.startswith("Cutscene."): - # Cutscene.ListType - toks = collectionType.split(".") - assert len(toks) == 2 - collection = obj.ootCutsceneProperty.csLists - collection = getattr(collection[subIndex], toks[1]) - elif collectionType == "Cutscene": - collection = obj.ootCutsceneProperty.csLists - elif collectionType == "extraCutscenes": - collection = obj.ootSceneHeader.extraCutscenes - elif collectionType == "BgImage": - collection = obj.ootRoomHeader.bgImageList - else: - raise PluginError("Invalid collection type: " + collectionType) - - return collection - - -def drawAddButton(layout, index, collectionType, subIndex, objName): - if subIndex is None: - subIndex = 0 - addOp = layout.operator(OOTCollectionAdd.bl_idname) - addOp.option = index - addOp.collectionType = collectionType - addOp.subIndex = subIndex - addOp.objName = objName - - -def drawCollectionOps(layout, index, collectionType, subIndex, objName, allowAdd=True, compact=False): - if subIndex is None: - subIndex = 0 - - if not compact: - buttons = layout.row(align=True) - else: - buttons = layout - - if allowAdd: - addOp = buttons.operator(OOTCollectionAdd.bl_idname, text="Add" if not compact else "", icon="ADD") - addOp.option = index + 1 - addOp.collectionType = collectionType - addOp.subIndex = subIndex - addOp.objName = objName - - 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 - - moveUp = buttons.operator(OOTCollectionMove.bl_idname, text="Up" if not compact else "", icon="TRIA_UP") - moveUp.option = index - moveUp.offset = -1 - moveUp.collectionType = collectionType - moveUp.subIndex = subIndex - moveUp.objName = objName - - moveDown = buttons.operator(OOTCollectionMove.bl_idname, text="Down" if not compact else "", icon="TRIA_DOWN") - moveDown.option = index - moveDown.offset = 1 - moveDown.collectionType = collectionType - moveDown.subIndex = subIndex - moveDown.objName = objName - - -class OOTCollectionAdd(bpy.types.Operator): - bl_idname = "object.oot_collection_add" - bl_label = "Add Item" - bl_options = {"REGISTER", "UNDO"} - - option: bpy.props.IntProperty() - collectionType: bpy.props.StringProperty(default="Actor") - subIndex: bpy.props.IntProperty(default=0) - objName: bpy.props.StringProperty() - - def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) - - collection.add() - collection.move(len(collection) - 1, self.option) - return {"FINISHED"} - - -class OOTCollectionRemove(bpy.types.Operator): - bl_idname = "object.oot_collection_remove" - bl_label = "Remove Item" - bl_options = {"REGISTER", "UNDO"} - - option: bpy.props.IntProperty() - collectionType: bpy.props.StringProperty(default="Actor") - subIndex: bpy.props.IntProperty(default=0) - objName: bpy.props.StringProperty() - - def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) - collection.remove(self.option) - return {"FINISHED"} - - -class OOTCollectionMove(bpy.types.Operator): - bl_idname = "object.oot_collection_move" - bl_label = "Move Item" - bl_options = {"REGISTER", "UNDO"} - - option: bpy.props.IntProperty() - offset: bpy.props.IntProperty() - subIndex: bpy.props.IntProperty(default=0) - objName: bpy.props.StringProperty() - - collectionType: bpy.props.StringProperty(default="Actor") - - def execute(self, context): - collection = getCollection(self.objName, self.collectionType, self.subIndex) - collection.move(self.option, self.option + self.offset) - return {"FINISHED"} - - def getHeaderSettings(actorObj: bpy.types.Object): itemType = actorObj.ootEmptyType if actorObj.type == "EMPTY": @@ -818,23 +673,6 @@ def getHeaderSettings(actorObj: bpy.types.Object): return headerSettings -oot_utility_classes = ( - OOTCollectionAdd, - OOTCollectionRemove, - OOTCollectionMove, -) - - -def oot_utility_register(): - for cls in oot_utility_classes: - register_class(cls) - - -def oot_utility_unregister(): - for cls in reversed(oot_utility_classes): - unregister_class(cls) - - def getActiveHeaderIndex() -> int: # All scenes/rooms should have synchronized tabs from property callbacks headerObjs = [obj for obj in bpy.data.objects if obj.ootEmptyType == "Scene" or obj.ootEmptyType == "Room"] @@ -917,6 +755,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": @@ -929,6 +778,11 @@ def callback(thisHeader, otherObj: bpy.types.Object): onHeaderPropertyChange(self, context, callback) + active_obj = context.view_layer.objects.active + if active_obj is not None and active_obj.ootEmptyType == "Scene": + # not using `self` is intended + on_alt_menu_tab_change(context.view_layer.objects.active.ootAlternateSceneHeaders, context) + def onHeaderPropertyChange(self, context: bpy.types.Context, callback: Callable[[any, bpy.types.Object], None]): if not bpy.context.scene.fast64.oot.headerTabAffectsVisibility or bpy.context.scene.ootActiveHeaderLock: @@ -951,16 +805,16 @@ def onHeaderPropertyChange(self, context: bpy.types.Context, callback: Callable[ bpy.context.scene.ootActiveHeaderLock = False -def getEvalParams(input: str): +def getEvalParamsInt(input: str): """Evaluates a string to an hexadecimal number""" # degrees to binary angle conversion if "DEG_TO_BINANG(" in input: input = input.strip().removeprefix("DEG_TO_BINANG(").removesuffix(")").strip() - return f"0x{round(float(input) * (0x8000 / 180)):X}" + return round(float(input) * (0x8000 / 180)) if input is None or "None" in input: - return "0x0" + return 0 # remove spaces input = input.strip() @@ -970,10 +824,10 @@ def getEvalParams(input: str): except Exception as e: raise ValueError(f"Could not parse {input} as an AST.") from e - def _eval(node): + def _eval(node) -> int: if isinstance(node, Expression): return _eval(node.body) - elif isinstance(node, Num): + elif isinstance(node, Constant): return node.n elif isinstance(node, UnaryOp): if isinstance(node.op, USub): @@ -983,11 +837,45 @@ def _eval(node): else: raise ValueError(f"Unsupported unary operator {node.op}") elif isinstance(node, BinOp): - return binOps[type(node.op)](_eval(node.left), _eval(node.right)) + return binOps[type(node.op)](int(_eval(node.left)), int(_eval(node.right))) else: raise ValueError(f"Unsupported AST node {node}") - return f"0x{_eval(node.body):X}" + try: + return _eval(node.body) + except: + print("WARNING: something wrong happened:", traceback.print_exc()) + return None + + +def getEvalParams(input: str): + num = getEvalParamsInt(input) + return f"0x{num:X}" if num is not None else None + + +def getShiftFromMask(mask: int): + """Returns the shift value from the mask""" + + # make sure the mask is a mask + binaryMask = f"{mask:016b}" + assert set(f"{mask:b}".rstrip("0")) == {"1"}, binaryMask + + # get the shift by subtracting the length of the mask + # converted in binary on 16 bits (since the mask can be on 16 bits) with + # that length but with the rightmost zeros stripped + return len(binaryMask) - len(binaryMask.rstrip("0")) + + +def getFormattedParams(mask: int, value: int, isBool: bool): + """Returns the parameter with the correct format""" + shift = getShiftFromMask(mask) + + if value == 0: + return None + elif not isBool: + return f"((0x{value:02X} << {shift}) & 0x{mask:04X})" if shift > 0 else f"(0x{value:02X} & 0x{mask:04X})" + else: + return f"(0x{value:02X} << {shift})" if shift > 0 else f"0x{value:02X}" def getNewPath(type: str, isClosedShape: bool): @@ -1100,3 +988,46 @@ def getObjectList( ret.append(obj) ret.sort(key=lambda o: o.name) return ret + + +def get_actor_prop_from_obj(actor_obj: Object) -> "OOTActorProperty": + """ + Returns the reference to `OOTActorProperty` + + Parameters: + - `actor_obj`: the Blender object to use to find the actor properties + """ + + actor_prop = None + + if actor_obj.ootEmptyType == "Actor": + actor_prop = actor_obj.ootActorProperty + elif actor_obj.ootEmptyType == "Transition Actor": + actor_prop = actor_obj.ootTransitionActorProperty.actor + elif actor_obj.ootEmptyType == "Entrance": + actor_prop = actor_obj.ootEntranceProperty.actor + else: + 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 bpy.context.scene.fast64.oot.feature_set == "default" + ) + + +def is_hackeroot(): + return game_data.z64.is_oot() and bpy.context.scene.fast64.oot.feature_set == "hackeroot" diff --git a/images/mat_inspector.png b/images/mat_inspector.png deleted file mode 100644 index 8b162106f..000000000 Binary files a/images/mat_inspector.png and /dev/null differ diff --git a/images/updater_after_check.png b/images/updater_after_check.png deleted file mode 100644 index f7cccbd85..000000000 Binary files a/images/updater_after_check.png and /dev/null differ diff --git a/images/updater_initially.png b/images/updater_initially.png deleted file mode 100644 index efdac1248..000000000 Binary files a/images/updater_initially.png and /dev/null differ diff --git a/images/updater_install_main.png b/images/updater_install_main.png deleted file mode 100644 index 7c74eba0e..000000000 Binary files a/images/updater_install_main.png and /dev/null differ diff --git a/images/updater_success_restart.png b/images/updater_success_restart.png deleted file mode 100644 index a2c26bf4f..000000000 Binary files a/images/updater_success_restart.png and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index ad3f17c34..199984355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,3 +25,6 @@ extend-exclude = ''' /addon_updater\.py | /addon_updater_ops\.py )$ ''' + +[tool.pyright] +reportInvalidTypeForm = 'none' \ No newline at end of file