diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index 65ce0b288..dc7f5a053 100644 --- a/fast64_internal/f3d/f3d_material.py +++ b/fast64_internal/f3d/f3d_material.py @@ -414,18 +414,20 @@ def ui_geo_mode(settings, dataHolder, layout, useDropdown): icon="TRIA_DOWN" if dataHolder.menu_geo else "TRIA_RIGHT", ) if not useDropdown or dataHolder.menu_geo: + disable_dependent = False # Don't disable dependent props in world defaults def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], isText: bool) -> UILayout: c = parent.column(align=True) if isText: c.label(text=textOrProp) + enable = True else: c.prop(settings, textOrProp) - if not getattr(settings, textOrProp): - return None + enable = getattr(settings, textOrProp) c = c.split(factor=0.1) c.label(text="") c = c.column(align=True) + c.enabled = enable or not disable_dependent return c isF3DEX3 = bpy.context.scene.f3d_type == "F3DEX3" @@ -436,6 +438,7 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], ccWarnings = True ccUse = all_combiner_uses(dataHolder) shadeInCC = ccUse["Shade"] or ccUse["Shade Alpha"] + disable_dependent = True if settings.set_rendermode: blendWarnings = True shadeInBlender = settings.does_blender_use_input("G_BL_A_SHADE") @@ -444,16 +447,15 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], inputGroup.prop(settings, "g_shade_smooth") c = indentGroup(inputGroup, "g_lighting", False) - if c is not None: - if ccWarnings and not shadeInCC and not settings.g_tex_gen: - c.label(text="Shade not used in CC, can disable lighting.", icon="INFO") - if isF3DEX3: - c.prop(settings, "g_packed_normals") - c.prop(settings, "g_lighting_specular") - c.prop(settings, "g_ambocclusion") - d = indentGroup(c, "g_tex_gen", False) - if d is not None: - d.prop(settings, "g_tex_gen_linear") + if ccWarnings and not shadeInCC and settings.g_lighting and not settings.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: shadeColorLabel = "Fresnel" @@ -463,11 +465,7 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], shadeColorLabel = "Lighting * vertex color" else: shadeColorLabel = "Lighting" - if lightFxPrereq: - c = indentGroup(inputGroup, f"Shade color = {shadeColorLabel}:", True) - c.prop(settings, "g_fresnel_color") - else: - inputGroup.column().label(text=f"Shade color = {shadeColorLabel}") + inputGroup.label(text=f"Shade color = {shadeColorLabel}") shadowMapInShadeAlpha = False if settings.g_fog: @@ -482,9 +480,11 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], else: shadeAlphaLabel = "Vtx alpha" c = indentGroup(inputGroup, f"Shade alpha = {shadeAlphaLabel}:", True) - if lightFxPrereq: - c.prop(settings, "g_lighttoalpha") - c.prop(settings, "g_fresnel_alpha") + 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") @@ -1751,7 +1751,7 @@ def update_node_values_of_material(material: Material, context): nodes = material.node_tree.nodes - if f3dMat.rdp_settings.g_tex_gen: + if f3dMat.rdp_settings.g_lighting and f3dMat.rdp_settings.g_tex_gen: if f3dMat.rdp_settings.g_tex_gen_linear: nodes["UV"].node_tree = bpy.data.node_groups["UV_EnvMap_Linear"] else: @@ -2100,7 +2100,8 @@ def update_tex_values_manual(material: Material, context, prop_path=None): elif texture_settings.mute: texture_settings.mute = False - isTexGen = f3dMat.rdp_settings.g_tex_gen # linear requires tex gen to be enabled as well + # linear requires tex gen to be enabled as well + isTexGen = f3dMat.rdp_settings.g_lighting and f3dMat.rdp_settings.g_tex_gen if f3dMat.scale_autoprop: if isTexGen: @@ -4070,91 +4071,73 @@ def rna_recursive_attr_expand(value, rna_path_step, level): return {"FINISHED"} -def convertToNewMat(material, oldMat): - material.f3d_mat.presetName = oldMat.pop("presetName", "Custom") - - material.f3d_mat.scale_autoprop = oldMat.pop("scale_autoprop", material.f3d_mat.scale_autoprop) - material.f3d_mat.uv_basis = oldMat.pop("uv_basis", material.f3d_mat.uv_basis) - - # Combiners - recursiveCopyOldPropertyGroup(oldMat.pop("combiner1", {}), material.f3d_mat.combiner1) - recursiveCopyOldPropertyGroup(oldMat.pop("combiner2", {}), material.f3d_mat.combiner2) - - # Texture animation - material.f3d_mat.menu_procAnim = oldMat.pop("menu_procAnim", material.f3d_mat.menu_procAnim) - recursiveCopyOldPropertyGroup(oldMat.pop("UVanim", {}), material.f3d_mat.UVanim0) - recursiveCopyOldPropertyGroup(oldMat.pop("UVanim_tex1", {}), material.f3d_mat.UVanim1) - - # material textures - material.f3d_mat.tex_scale = oldMat.pop("tex_scale", material.f3d_mat.tex_scale) - recursiveCopyOldPropertyGroup(oldMat.pop("tex0", {}), material.f3d_mat.tex0) - recursiveCopyOldPropertyGroup(oldMat.pop("tex1", {}), material.f3d_mat.tex1) - - # Should Set? - material.f3d_mat.set_prim = oldMat.pop("set_prim", material.f3d_mat.set_prim) - material.f3d_mat.set_lights = oldMat.pop("set_lights", material.f3d_mat.set_lights) - material.f3d_mat.set_env = oldMat.pop("set_env", material.f3d_mat.set_env) - material.f3d_mat.set_blend = oldMat.pop("set_blend", material.f3d_mat.set_blend) - material.f3d_mat.set_key = oldMat.pop("set_key", material.f3d_mat.set_key) - material.f3d_mat.set_k0_5 = oldMat.pop("set_k0_5", material.f3d_mat.set_k0_5) - material.f3d_mat.set_combiner = oldMat.pop("set_combiner", material.f3d_mat.set_combiner) - material.f3d_mat.use_default_lighting = oldMat.pop("use_default_lighting", material.f3d_mat.use_default_lighting) +def convertToNewMat(material): + old_to_new_props = { + "presetName": "presetName", + "scale_autoprop": "scale_autoprop", + "uv_basis": "uv_basis", + "combiner1": "combiner1", + "combiner2": "combiner2", + "menu_procAnim": "menu_procAnim", + "UVanim0": "UVanim", + "UVanim1": "UVanim_tex1", + "tex_scale": "tex_scale", + "tex0": "tex0", + "tex1": "tex1", + "set_prim": "set_prim", + "set_lights": "set_lights", + "set_env": "set_env", + "set_blend": "set_blend", + "set_key": "set_key", + "set_k0_5": "set_k0_5", + "set_combiner": "set_combiner", + "use_default_lighting": "use_default_lighting", + "blend_color": "blend_color", + "key_scale": "key_scale", + "key_width": "key_width", + "k0": "k0", + "k1": "k1", + "k2": "k2", + "k3": "k3", + "k4": "k4", + "k5": "k5", + "prim_lod_frac": "prim_lod_frac", + "prim_lod_min": "prim_lod_min", + "default_light_color": "default_light_color", + "ambient_light_color": "ambient_light_color", + "fog_color": "fog_color", + "fog_position": "fog_position", + "set_fog": "set_fog", + "use_global_fog": "use_global_fog", + "menu_geo": "menu_geo", + "menu_upper": "menu_upper", + "menu_lower": "menu_lower", + "menu_other": "menu_other", + "menu_lower_render": "menu_lower_render", + "rdp_settings": "rdp_settings", + } + for new, old in old_to_new_props.items(): + upgrade_old_prop(material.f3d_mat, new, material, old) # Colors - nodes = oldMat.node_tree.nodes + nodes = material.node_tree.nodes - if oldMat.mat_ver == 3: + if material.mat_ver == 3: prim = nodes["Primitive Color Output"].inputs[0].default_value env = nodes["Environment Color Output"].inputs[0].default_value else: prim = nodes["Primitive Color"].outputs[0].default_value env = nodes["Environment Color"].outputs[0].default_value - material.f3d_mat.blend_color = oldMat.pop("blend_color", material.f3d_mat.blend_color) material.f3d_mat.prim_color = prim material.f3d_mat.env_color = env if "Chroma Key Center" in nodes: material.f3d_mat.key_center = nodes["Chroma Key Center"].outputs[0].default_value - # Chroma - material.f3d_mat.key_scale = oldMat.pop("key_scale", material.f3d_mat.key_scale) - material.f3d_mat.key_width = oldMat.pop("key_width", material.f3d_mat.key_width) - - # Convert - material.f3d_mat.k0 = oldMat.pop("k0", material.f3d_mat.k0) - material.f3d_mat.k1 = oldMat.pop("k1", material.f3d_mat.k1) - material.f3d_mat.k2 = oldMat.pop("k2", material.f3d_mat.k2) - material.f3d_mat.k3 = oldMat.pop("k3", material.f3d_mat.k3) - material.f3d_mat.k4 = oldMat.pop("k4", material.f3d_mat.k4) - material.f3d_mat.k5 = oldMat.pop("k5", material.f3d_mat.k5) - - # Prim - material.f3d_mat.prim_lod_frac = oldMat.pop("prim_lod_frac", material.f3d_mat.prim_lod_frac) - material.f3d_mat.prim_lod_min = oldMat.pop("prim_lod_min", material.f3d_mat.prim_lod_min) - # lights - material.f3d_mat.default_light_color = oldMat.pop("default_light_color", material.f3d_mat.default_light_color) - material.f3d_mat.ambient_light_color = oldMat.pop("ambient_light_color", material.f3d_mat.ambient_light_color) for i in range(1, 8): - old_light = oldMat.pop(f"f3d_light{str(i)}", None) - # can be a broken property with V1 materials (IDPropertyGroup), thankfully this isnt typical to see when upgrading but - # this method is safer - if type(old_light) is Light: - setattr(material.f3d_mat, f"f3d_light{str(i)}", old_light) - - # Fog Properties - material.f3d_mat.fog_color = oldMat.pop("fog_color", material.f3d_mat.fog_color) - material.f3d_mat.fog_position = oldMat.pop("fog_position", material.f3d_mat.fog_position) - material.f3d_mat.set_fog = oldMat.pop("set_fog", material.f3d_mat.set_fog) - material.f3d_mat.use_global_fog = oldMat.pop("use_global_fog", material.f3d_mat.use_global_fog) - - # geometry mode - material.f3d_mat.menu_geo = oldMat.pop("menu_geo", material.f3d_mat.menu_geo) - material.f3d_mat.menu_upper = oldMat.pop("menu_upper", material.f3d_mat.menu_upper) - material.f3d_mat.menu_lower = oldMat.pop("menu_lower", material.f3d_mat.menu_lower) - material.f3d_mat.menu_other = oldMat.pop("menu_other", material.f3d_mat.menu_other) - material.f3d_mat.menu_lower_render = oldMat.pop("menu_lower_render", material.f3d_mat.menu_lower_render) - recursiveCopyOldPropertyGroup(oldMat.pop("rdp_settings", {}), material.f3d_mat.rdp_settings) + light_attr = f"f3d_light{str(i)}" + upgrade_old_prop(material.f3d_mat, light_attr, material, light_attr) class F3DMaterialProperty(PropertyGroup): diff --git a/fast64_internal/f3d_material_converter.py b/fast64_internal/f3d_material_converter.py index 6c1fabdd0..fffb903da 100644 --- a/fast64_internal/f3d_material_converter.py +++ b/fast64_internal/f3d_material_converter.py @@ -148,7 +148,7 @@ def convertF3DtoNewVersion( material.is_f3d, material.f3d_update_flag = False, True # Convert before node tree changes, as old materials store some values in the actual nodes if material.mat_ver <= 3: - convertToNewMat(material, material) + convertToNewMat(material) node_tree_copy(f3d_node_tree, material.node_tree) diff --git a/fast64_internal/oot/__init__.py b/fast64_internal/oot/__init__.py index 22a324464..1e9ed07ae 100644 --- a/fast64_internal/oot/__init__.py +++ b/fast64_internal/oot/__init__.py @@ -76,6 +76,19 @@ class OOT_Properties(bpy.types.PropertyGroup): animImportSettings: bpy.props.PointerProperty(type=OOTAnimImportSettingsProperty) collisionExportSettings: bpy.props.PointerProperty(type=OOTCollisionExportSettings) + useDecompFeatures: bpy.props.BoolProperty( + name="Use decomp for export", description="Use names and macros from decomp when exporting", default=True + ) + + 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." + ), + default=False, + ) + oot_classes = (OOT_Properties,) diff --git a/fast64_internal/oot/collision/exporter/to_c/collision.py b/fast64_internal/oot/collision/exporter/to_c/collision.py index dd4b1e7d7..4120776be 100644 --- a/fast64_internal/oot/collision/exporter/to_c/collision.py +++ b/fast64_internal/oot/collision/exporter/to_c/collision.py @@ -5,7 +5,7 @@ from ...exporter import OOTCollision, OOTCameraData from ...properties import OOTCollisionExportSettings from ..classes import OOTCameraData, OOTCameraPosData, OOTCrawlspaceData -from ..functions import exportCollisionCommon +from ....exporter.collision import CollisionHeader from .....utility import ( PluginError, @@ -270,7 +270,6 @@ def ootCollisionToC(collision): def exportCollisionToC( originalObj: bpy.types.Object, transformMatrix: mathutils.Matrix, exportSettings: OOTCollisionExportSettings ): - includeChildren = exportSettings.includeChildren name = toAlnum(originalObj.name) isCustomExport = exportSettings.customExport folderName = exportSettings.folder @@ -289,26 +288,34 @@ def exportCollisionToC( restoreHiddenState(hiddenState) try: - exportCollisionCommon(collision, obj, transformMatrix, includeChildren, name) - ootCleanupScene(originalObj, allObjs) + 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: - ootCleanupScene(originalObj, allObjs) raise Exception(str(e)) - - collisionC = ootCollisionToC(collision) - - data = CData() - data.source += '#include "ultra64.h"\n#include "z64.h"\n#include "macros.h"\n' - if not isCustomExport: - data.source += '#include "' + folderName + '.h"\n\n' - else: - data.source += "\n" - - data.append(collisionC) - - path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, True) - filename = exportSettings.filename if exportSettings.isCustomFilename else f"{name}_collision" - writeCData(data, os.path.join(path, f"{filename}.h"), os.path.join(path, f"{filename}.c")) - - if not isCustomExport: - addIncludeFiles(folderName, path, name) + finally: + ootCleanupScene(originalObj, allObjs) diff --git a/fast64_internal/oot/collision/operators.py b/fast64_internal/oot/collision/operators.py index cc744e4af..38fe2a5df 100644 --- a/fast64_internal/oot/collision/operators.py +++ b/fast64_internal/oot/collision/operators.py @@ -1,4 +1,4 @@ -from bpy.types import Operator, Mesh +from bpy.types import Operator from bpy.utils import register_class, unregister_class from bpy.ops import object from mathutils import Matrix diff --git a/fast64_internal/oot/cutscene/exporter/functions.py b/fast64_internal/oot/cutscene/exporter/functions.py index 4512f1512..9d901b704 100644 --- a/fast64_internal/oot/cutscene/exporter/functions.py +++ b/fast64_internal/oot/cutscene/exporter/functions.py @@ -44,6 +44,6 @@ def getNewCutsceneExport(csName: str, motionOnly: bool): # this allows us to change the exporter's variables to get what we need return CutsceneExport( getCutsceneObjects(csName), - bpy.context.scene.fast64.oot.hackerFeaturesEnabled or bpy.context.scene.useDecompFeatures, + bpy.context.scene.fast64.oot.hackerFeaturesEnabled or bpy.context.scene.fast64.oot.useDecompFeatures, motionOnly, ) diff --git a/fast64_internal/oot/cutscene/operators.py b/fast64_internal/oot/cutscene/operators.py index bf02ff845..29f727dd4 100644 --- a/fast64_internal/oot/cutscene/operators.py +++ b/fast64_internal/oot/cutscene/operators.py @@ -8,12 +8,12 @@ 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, getCutsceneName +from ..oot_utility import getCollection from ..oot_constants import ootData -from ..scene.exporter.to_c import getCutsceneC from .constants import ootEnumCSTextboxType, ootEnumCSListType from .importer import importCutsceneData from .exporter import getNewCutsceneExport +from ..exporter.cutscene import Cutscene def checkGetFilePaths(context: Context): @@ -180,10 +180,11 @@ def execute(self, context): cpath, hpath, headerfilename = checkGetFilePaths(context) csdata = ootCutsceneIncludes(headerfilename) - if context.scene.exportMotionOnly: + if context.scene.fast64.oot.exportMotionOnly: + # TODO: improve this csdata.append(insertCutsceneData(cpath, activeObj.name.removeprefix("Cutscene."))) else: - csdata.append(getCutsceneC(getCutsceneName(activeObj))) + csdata.append(Cutscene(activeObj, context.scene.fast64.oot.useDecompFeatures).getC()) writeCData(csdata, hpath, cpath) self.report({"INFO"}, "Successfully exported cutscene") @@ -213,10 +214,10 @@ def execute(self, context): print(f"Parent: {obj.parent.name}, Object: {obj.name}") raise PluginError("Cutscene object must not be parented to anything") - if context.scene.exportMotionOnly: + if context.scene.fast64.oot.exportMotionOnly: raise PluginError("ERROR: Not implemented yet.") else: - csdata.append(getCutsceneC(getCutsceneName(obj))) + csdata.append(Cutscene(obj, context.scene.fast64.oot.useDecompFeatures).getC()) count += 1 if count == 0: diff --git a/fast64_internal/oot/cutscene/panels.py b/fast64_internal/oot/cutscene/panels.py index 76685144a..44f3de3e5 100644 --- a/fast64_internal/oot/cutscene/panels.py +++ b/fast64_internal/oot/cutscene/panels.py @@ -27,11 +27,6 @@ def draw(self, context): exportBox = layout.box() exportBox.label(text="Cutscene Exporter") - col = exportBox.column() - if not context.scene.fast64.oot.hackerFeaturesEnabled: - col.prop(context.scene, "useDecompFeatures") - col.prop(context.scene, "exportMotionOnly") - prop_split(exportBox, context.scene, "ootCutsceneExportPath", "Export To") activeObj = context.view_layer.objects.active @@ -66,25 +61,10 @@ def draw(self, context): def cutscene_panels_register(): - Scene.useDecompFeatures = BoolProperty( - name="Use Decomp for Export", description="Use names and macros from decomp when exporting", default=True - ) - - Scene.exportMotionOnly = BoolProperty( - name="Export Motion Data Only", - description=( - "Export everything or only the camera and actor motion data.\n" - + "This will insert the data into the cutscene." - ), - ) - for cls in oot_cutscene_panel_classes: register_class(cls) def cutscene_panels_unregister(): - del Scene.exportMotionOnly - del Scene.useDecompFeatures - for cls in oot_cutscene_panel_classes: unregister_class(cls) diff --git a/fast64_internal/oot/exporter/__init__.py b/fast64_internal/oot/exporter/__init__.py new file mode 100644 index 000000000..e85ef12fc --- /dev/null +++ b/fast64_internal/oot/exporter/__init__.py @@ -0,0 +1,137 @@ +import bpy +import os + +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 .scene import Scene +from .decomp_edit import Files + +from ...utility import ( + PluginError, + checkObjectReference, + unhideAllAndGetHiddenState, + restoreHiddenState, + toAlnum, + readFile, + writeFile, +) + +from ..oot_utility import ( + ExportInfo, + OOTObjectCategorizer, + ootDuplicateHierarchy, + ootCleanupScene, + getSceneDirFromLevelName, + ootGetPath, +) + + +def writeTextureArraysExistingScene(fModel: OOTModel, exportPath: str, sceneInclude: str): + drawConfigPath = os.path.join(exportPath, "src/code/z_scene_table.c") + drawConfigData = readFile(drawConfigPath) + newData = drawConfigData + + if f'#include "{sceneInclude}"' not in newData: + additionalIncludes = f'#include "{sceneInclude}"\n' + else: + additionalIncludes = "" + + for flipbook in fModel.flipbooks: + if flipbook.exportMode == "Array": + newData = writeTextureArraysExisting1D(newData, flipbook, additionalIncludes) + else: + raise PluginError("Scenes can only use array flipbooks.") + + if newData != drawConfigData: + writeFile(drawConfigPath, newData) + + +class SceneExport: + """This class is the main exporter class, it handles generating the C data and writing the files""" + + @staticmethod + def create_scene(originalSceneObj: Object, transform: Matrix, exportInfo: ExportInfo) -> Scene: + """Returns and creates scene data""" + # init + if originalSceneObj.type != "EMPTY" or originalSceneObj.ootEmptyType != "Scene": + raise PluginError(f'{originalSceneObj.name} is not an empty with the "Scene" empty type.') + + if bpy.context.scene.exportHiddenGeometry: + hiddenState = unhideAllAndGetHiddenState(bpy.context.scene) + + # Don't remove ignore_render, as we want to reuse this for collision + sceneObj, allObjs = ootDuplicateHierarchy(originalSceneObj, None, True, OOTObjectCategorizer()) + + if bpy.context.scene.exportHiddenGeometry: + restoreHiddenState(hiddenState) + + try: + sceneName = f"{toAlnum(exportInfo.name)}_scene" + newScene = Scene.new( + sceneName, + sceneObj, + transform, + exportInfo.useMacros, + exportInfo.saveTexturesAsPNG, + OOTModel(f"{sceneName}_dl", DLFormat.Static, False), + ) + newScene.validateScene() + + except Exception as e: + raise Exception(str(e)) + finally: + ootCleanupScene(originalSceneObj, allObjs) + + return newScene + + @staticmethod + def export(originalSceneObj: Object, transform: Matrix, exportInfo: ExportInfo): + """Main function""" + # circular import fixes + from .decomp_edit.config import Config + + checkObjectReference(originalSceneObj, "Scene object") + scene = SceneExport.create_scene(originalSceneObj, transform, exportInfo) + + isCustomExport = exportInfo.isCustomExportPath + exportPath = exportInfo.exportPath + sceneName = exportInfo.name + + exportSubdir = "" + if exportInfo.customSubPath is not None: + exportSubdir = exportInfo.customSubPath + if not isCustomExport and exportInfo.customSubPath is None: + exportSubdir = os.path.dirname(getSceneDirFromLevelName(sceneName)) + + sceneInclude = exportSubdir + "/" + sceneName + "/" + path = ootGetPath(exportPath, isCustomExport, exportSubdir, sceneName, True, True) + textureExportSettings = TextureExportSettings(False, exportInfo.saveTexturesAsPNG, sceneInclude, path) + + sceneFile = scene.getNewSceneFile(path, exportInfo.isSingleFile, textureExportSettings) + + if not isCustomExport: + writeTextureArraysExistingScene(scene.model, exportPath, sceneInclude + sceneName + "_scene.h") + else: + textureArrayData = writeTextureArraysNew(scene.model, None) + sceneFile.sceneTextures += textureArrayData.source + sceneFile.header += textureArrayData.header + + sceneFile.write() + for room in scene.rooms.entries: + room.roomShape.copy_bg_images(path) + + if not isCustomExport: + Files.add_scene_edits(exportInfo, scene, sceneFile) + + hackerootBootOption = exportInfo.hackerootBootOption + if hackerootBootOption is not None and hackerootBootOption.bootToScene: + Config.setBootupScene( + os.path.join(exportPath, "include/config/config_debug.h") + if not isCustomExport + else os.path.join(path, "config_bootup.h"), + f"ENTR_{sceneName.upper()}_{hackerootBootOption.spawnIndex}", + hackerootBootOption, + ) diff --git a/fast64_internal/oot/exporter/actor.py b/fast64_internal/oot/exporter/actor.py new file mode 100644 index 000000000..612437c65 --- /dev/null +++ b/fast64_internal/oot/exporter/actor.py @@ -0,0 +1,31 @@ +from ...utility import indent + +# this file is not inside the room folder since the scene data can have actors too + + +class Actor: + """Defines an Actor""" + + def __init__(self): + self.name = str() + self.id = str() + self.pos: list[int] = [] + self.rot = str() + self.params = str() + + def getActorEntry(self): + """Returns a single actor entry""" + + posData = "{ " + ", ".join(f"{round(p)}" for p in self.pos) + " }" + rotData = "{ " + self.rot + " }" + + actorInfos = [self.id, posData, rotData, self.params] + infoDescs = ["Actor ID", "Position", "Rotation", "Parameters"] + + return ( + indent + + (f"// {self.name}\n" + indent if self.name != "" else "") + + "{\n" + + ",\n".join((indent * 2) + f"/* {desc:10} */ {info}" for desc, info in zip(infoDescs, actorInfos)) + + ("\n" + indent + "},\n") + ) diff --git a/fast64_internal/oot/exporter/collision/__init__.py b/fast64_internal/oot/exporter/collision/__init__.py new file mode 100644 index 000000000..d2ef96e7d --- /dev/null +++ b/fast64_internal/oot/exporter/collision/__init__.py @@ -0,0 +1,308 @@ +import math + +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 Utility +from .polygons import CollisionPoly, CollisionPolygons +from .surface import SurfaceType, SurfaceTypes +from .camera import BgCamInformations +from .waterbox import WaterBoxes +from .vertex import CollisionVertex, CollisionVertices + + +@dataclass +class CollisionUtility: + """This class hosts different functions used to convert mesh data""" + + @staticmethod + def updateBounds(position: tuple[int, int, int], colBounds: list[tuple[int, int, int]]): + """This is used to update the scene's boundaries""" + + if len(colBounds) == 0: + colBounds.append([position[0], position[1], position[2]]) + colBounds.append([position[0], position[1], position[2]]) + return + + minBounds = colBounds[0] + maxBounds = colBounds[1] + for i in range(3): + if position[i] < minBounds[i]: + minBounds[i] = position[i] + if position[i] > maxBounds[i]: + maxBounds[i] = position[i] + + @staticmethod + def getVertexIndex(vertexPos: tuple[int, int, int], vertexList: list[CollisionVertex]): + """Returns the index of a CollisionVertex based on position data, returns None if no match found""" + + for i in range(len(vertexList)): + if vertexList[i].pos == vertexPos: + return i + return None + + @staticmethod + def getMeshObjects( + dataHolder: Object, curTransform: Matrix, transformFromMeshObj: dict[Object, Matrix], includeChildren: bool + ): + """Returns and updates a dictionnary containing mesh objects associated with their correct transforms""" + + if includeChildren: + for obj in dataHolder.children: + newTransform = curTransform @ obj.matrix_local + + if obj.type == "MESH" and not obj.ignore_collision: + transformFromMeshObj[obj] = newTransform + + if len(obj.children) > 0: + CollisionUtility.getMeshObjects(obj, newTransform, transformFromMeshObj, includeChildren) + + return transformFromMeshObj + + @staticmethod + def getCollisionData(dataHolder: Optional[Object], transform: Matrix, useMacros: bool, includeChildren: bool): + """Returns collision data, surface types and vertex positions from mesh objects""" + + object.select_all(action="DESELECT") + dataHolder.select_set(True) + + colPolyFromSurfaceType: dict[SurfaceType, list[CollisionPoly]] = {} + surfaceList: list[SurfaceType] = [] + polyList: list[CollisionPoly] = [] + vertexList: list[CollisionVertex] = [] + colBounds: list[tuple[int, int, int]] = [] + + transformFromMeshObj: dict[Object, Matrix] = {} + if dataHolder.type == "MESH" and not dataHolder.ignore_collision: + transformFromMeshObj[dataHolder] = transform + transformFromMeshObj = CollisionUtility.getMeshObjects( + dataHolder, transform, transformFromMeshObj, includeChildren + ) + for meshObj, transform in transformFromMeshObj.items(): + # Note: ``isinstance``only used to get the proper type hints + if not meshObj.ignore_collision and isinstance(meshObj.data, Mesh): + if len(meshObj.data.materials) == 0: + 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 + + # get bounds and vertices data + planePoint = transform @ meshObj.data.vertices[face.vertices[0]].co + (x1, y1, z1) = Utility.roundPosition(planePoint) + (x2, y2, z2) = Utility.roundPosition(transform @ meshObj.data.vertices[face.vertices[1]].co) + (x3, y3, z3) = Utility.roundPosition(transform @ meshObj.data.vertices[face.vertices[2]].co) + CollisionUtility.updateBounds((x1, y1, z1), colBounds) + CollisionUtility.updateBounds((x2, y2, z2), colBounds) + CollisionUtility.updateBounds((x3, y3, z3), colBounds) + + normal = (transform.inverted().transposed() @ face.normal).normalized() + distance = round( + -1 * (normal[0] * planePoint[0] + normal[1] * planePoint[1] + normal[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("INFO: Ignore denormalized triangle.") + continue + + indices: list[int] = [] + for pos in [(x1, y1, z1), (x2, y2, z2), (x3, y3, z3)]: + vertexIndex = CollisionUtility.getVertexIndex(pos, vertexList) + if vertexIndex is None: + vertexList.append(CollisionVertex(pos)) + indices.append(len(vertexList) - 1) + else: + indices.append(vertexIndex) + 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/873c55faad48a67f7544be713cc115e2b858a4e8/src/code/z_bgcheck.c#L202 + # + # 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/873c55faad48a67f7544be713cc115e2b858a4e8/src/code/z_bgcheck.c#L2976 + + # Address 1): sort by ascending y coordinate + indices.sort(key=lambda index: vertexList[index].pos[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 = Vector(vertexList[indices[0]].pos) + v1 = Vector(vertexList[indices[1]].pos) + v2 = Vector(vertexList[indices[2]].pos) + if (v1 - v0).cross(v2 - v0).dot(Vector(normal)) < 0: + 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, + int(Utility.getPropValue(colProp, "floorProperty"), base=16), + 0, # unused? + int(Utility.getPropValue(colProp, "wallSetting"), base=16), + int(Utility.getPropValue(colProp, "floorSetting"), base=16), + colProp.decreaseHeight, + colProp.eponaBlock, + int(Utility.getPropValue(colProp, "sound"), base=16), + int(Utility.getPropValue(colProp, "terrain"), base=16), + 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, + ) + + if surfaceType not in colPolyFromSurfaceType: + colPolyFromSurfaceType[surfaceType] = [] + + colPolyFromSurfaceType[surfaceType].append( + CollisionPoly( + indices, + colProp.ignoreCameraCollision, + colProp.ignoreActorCollision, + colProp.ignoreProjectileCollision, + useConveyor, + normal, + distance, + useMacros, + ) + ) + + count = 0 + for surface, colPolyList in colPolyFromSurfaceType.items(): + for colPoly in colPolyList: + colPoly.type = count + polyList.append(colPoly) + surfaceList.append(surface) + count += 1 + + return colBounds, vertexList, polyList, surfaceList + + +@dataclass +class CollisionHeader: + """This class defines the collision header used by the scene""" + + name: str + minBounds: tuple[int, int, int] + maxBounds: tuple[int, int, int] + vertices: CollisionVertices + collisionPoly: CollisionPolygons + surfaceType: SurfaceTypes + bgCamInfo: BgCamInformations + waterbox: WaterBoxes + + @staticmethod + def new( + name: str, + sceneName: str, + dataHolder: Object, + transform: Matrix, + useMacros: bool, + includeChildren: bool, + ): + # Ideally everything would be separated but this is complicated since it's all tied together + colBounds, vertexList, polyList, surfaceTypeList = CollisionUtility.getCollisionData( + dataHolder, transform, useMacros, includeChildren + ) + + return CollisionHeader( + name, + colBounds[0], + colBounds[1], + CollisionVertices(f"{sceneName}_vertices", vertexList), + CollisionPolygons(f"{sceneName}_polygons", polyList), + SurfaceTypes(f"{sceneName}_polygonTypes", surfaceTypeList), + BgCamInformations.new(f"{sceneName}_bgCamInfo", f"{sceneName}_camPosData", dataHolder, transform), + WaterBoxes.new(f"{sceneName}_waterBoxes", dataHolder, transform, useMacros), + ) + + def getCmd(self): + """Returns the collision header scene command""" + + return indent + f"SCENE_CMD_COL_HEADER(&{self.name}),\n" + + def getC(self): + """Returns the collision header for the selected scene""" + + headerData = CData() + colData = CData() + varName = f"CollisionHeader {self.name}" + + wBoxPtrLine = colPolyPtrLine = vtxPtrLine = "0, NULL" + camPtrLine = surfacePtrLine = "NULL" + + # Add waterbox data if necessary + if len(self.waterbox.waterboxList) > 0: + colData.append(self.waterbox.getC()) + wBoxPtrLine = f"ARRAY_COUNT({self.waterbox.name}), {self.waterbox.name}" + + # Add camera data if necessary + if len(self.bgCamInfo.bgCamInfoList) > 0 or len(self.bgCamInfo.crawlspacePosList) > 0: + infoData = self.bgCamInfo.getInfoArrayC() + if "&" in infoData.source: + colData.append(self.bgCamInfo.getDataArrayC()) + colData.append(infoData) + camPtrLine = f"{self.bgCamInfo.name}" + + # Add surface types + if len(self.surfaceType.surfaceTypeList) > 0: + colData.append(self.surfaceType.getC()) + surfacePtrLine = f"{self.surfaceType.name}" + + # Add vertex data + if len(self.vertices.vertexList) > 0: + colData.append(self.vertices.getC()) + vtxPtrLine = f"ARRAY_COUNT({self.vertices.name}), {self.vertices.name}" + + # Add collision poly data + if len(self.collisionPoly.polyList) > 0: + colData.append(self.collisionPoly.getC()) + colPolyPtrLine = f"ARRAY_COUNT({self.collisionPoly.name}), {self.collisionPoly.name}" + + # build the C data of the collision header + + # .h + headerData.header = f"extern {varName};\n" + + # .c + headerData.source += ( + (varName + " = {\n") + + ",\n".join( + indent + val + for val in [ + ("{ " + ", ".join(f"{val}" for val in self.minBounds) + " }"), + ("{ " + ", ".join(f"{val}" for val in self.maxBounds) + " }"), + vtxPtrLine, + colPolyPtrLine, + surfacePtrLine, + camPtrLine, + wBoxPtrLine, + ] + ) + + "\n};\n\n" + ) + + headerData.append(colData) + return headerData diff --git a/fast64_internal/oot/exporter/collision/camera.py b/fast64_internal/oot/exporter/collision/camera.py new file mode 100644 index 000000000..012defcee --- /dev/null +++ b/fast64_internal/oot/exporter/collision/camera.py @@ -0,0 +1,222 @@ +import math + +from dataclasses import dataclass, field +from mathutils import Quaternion, Matrix +from bpy.types import Object +from ....utility import PluginError, CData, indent +from ...oot_utility import getObjectList +from ...collision.constants import decomp_compat_map_CameraSType +from ...collision.properties import OOTCameraPositionProperty +from ..utility import Utility + + +@dataclass +class CrawlspaceCamera: + """This class defines camera data for crawlspaces, if used""" + + points: list[tuple[int, int, int]] + camIndex: int + + arrayIndex: int = field(init=False, default=0) + + def getDataEntryC(self): + """Returns an entry for the camera data array""" + + return "".join(indent + "{ " + f"{point[0]:6}, {point[1]:6}, {point[2]:6}" + " },\n" for point in self.points) + + def getInfoEntryC(self, posDataName: str): + """Returns a crawlspace entry for the camera informations array""" + + return indent + "{ " + f"CAM_SET_CRAWLSPACE, 6, &{posDataName}[{self.arrayIndex}]" + " },\n" + + +@dataclass +class CameraData: + """This class defines camera data, if used""" + + pos: tuple[int, int, int] + rot: tuple[int, int, int] + fov: int + roomImageOverrideBgCamIndex: int + + def getEntryC(self): + """Returns an entry for the camera data array""" + + return ( + (indent + "{ " + ", ".join(f"{p:6}" for p in self.pos) + " },\n") + + (indent + "{ " + ", ".join(f"0x{r:04X}" for r in self.rot) + " },\n") + + (indent + "{ " + f"{self.fov:6}, {self.roomImageOverrideBgCamIndex:6}, {-1:6}" + " },\n") + ) + + +@dataclass +class CameraInfo: + """This class defines camera information data""" + + setting: str + count: int + data: CameraData + camIndex: int + + arrayIndex: int = field(init=False, default=0) + + def __post_init__(self): + self.hasPosData = self.data is not None + + def getInfoEntryC(self, posDataName: str): + """Returns an entry for the camera information array""" + + ptr = f"&{posDataName}[{self.arrayIndex}]" if self.hasPosData else "NULL" + return indent + "{ " + f"{self.setting}, {self.count}, {ptr}" + " },\n" + + +@dataclass +class BgCamInformations: + """This class defines the array of camera informations and the array of the associated data""" + + name: str + posDataName: str + bgCamInfoList: list[CameraInfo] + crawlspacePosList: list[CrawlspaceCamera] + arrayIdx: int + crawlspaceCount: int + camFromIndex: dict[int, CameraInfo | CrawlspaceCamera] + + @staticmethod + def getCrawlspacePosList(dataHolder: Object, transform: Matrix): + """Returns a list of crawlspace data from every splines objects with the type 'Crawlspace'""" + + crawlspacePosList: list[CrawlspaceCamera] = [] + crawlspaceObjList = getObjectList(dataHolder.children_recursive, "CURVE", splineType="Crawlspace") + for obj in crawlspaceObjList: + if Utility.validateCurveData(obj): + points = [ + [round(value) for value in transform @ obj.matrix_world @ point.co] + for point in obj.data.splines[0].points + ] + crawlspacePosList.append( + CrawlspaceCamera( + [points[0], points[0], points[0], points[1], points[1], points[1]], + obj.ootSplineProperty.index, + ) + ) + return crawlspacePosList + + @staticmethod + def getBgCamInfoList(dataHolder: Object, transform: Matrix): + """Returns a list of camera informations from camera objects""" + + camObjList = getObjectList(dataHolder.children_recursive, "CAMERA") + camPosData: dict[int, CameraData] = {} + camInfoData: dict[int, CameraInfo] = {} + + for camObj in camObjList: + camProp: OOTCameraPositionProperty = camObj.ootCameraPositionProperty + + if camProp.camSType == "Custom": + setting = camProp.camSTypeCustom + else: + setting = decomp_compat_map_CameraSType.get(camProp.camSType, camProp.camSType) + + if camProp.hasPositionData: + if camProp.index in camPosData: + raise PluginError(f"ERROR: Repeated camera position index: {camProp.index} for {camObj.name}") + + # Camera faces opposite direction + pos, rot, _, _ = Utility.getConvertedTransformWithOrientation( + transform, dataHolder, camObj, Quaternion((0, 1, 0), math.radians(180.0)) + ) + + fov = math.degrees(camObj.data.angle) + camPosData[camProp.index] = CameraData( + pos, + rot, + round(fov * 100 if fov > 3.6 else fov), # see CAM_DATA_SCALED() macro + camObj.ootCameraPositionProperty.bgImageOverrideIndex, + ) + + if camProp.index in camInfoData: + raise PluginError(f"ERROR: Repeated camera entry: {camProp.index} for {camObj.name}") + + camInfoData[camProp.index] = CameraInfo( + setting, + 3 if camProp.hasPositionData else 0, # cameras are using 3 entries in the data array + camPosData[camProp.index] if camProp.hasPositionData else None, + camProp.index, + ) + return list(camInfoData.values()) + + @staticmethod + def getCamTable(dataHolder: Object, crawlspacePosList: list[CrawlspaceCamera], bgCamInfoList: list[CameraInfo]): + camFromIndex: dict[int, CameraInfo | CrawlspaceCamera] = {} + for bgCam in bgCamInfoList: + if bgCam.camIndex not in camFromIndex: + camFromIndex[bgCam.camIndex] = bgCam + else: + raise PluginError(f"ERROR (CameraInfo): Camera index already used: {bgCam.camIndex}") + + for crawlCam in crawlspacePosList: + if crawlCam.camIndex not in camFromIndex: + camFromIndex[crawlCam.camIndex] = crawlCam + else: + raise PluginError(f"ERROR (Crawlspace): Camera index already used: {crawlCam.camIndex}") + + camFromIndex = dict(sorted(camFromIndex.items())) + if list(camFromIndex.keys()) != list(range(len(camFromIndex))): + raise PluginError("ERROR: The camera indices are not consecutive!") + + i = 0 + for val in camFromIndex.values(): + if isinstance(val, CrawlspaceCamera): + val.arrayIndex = i + i += 6 # crawlspaces are using 6 entries in the data array + elif val.hasPosData: + val.arrayIndex = i + i += 3 + return camFromIndex + + @staticmethod + def new(name: str, posDataName: str, dataHolder: Object, transform: Matrix): + crawlspacePosList = BgCamInformations.getCrawlspacePosList(dataHolder, transform) + bgCamInfoList = BgCamInformations.getBgCamInfoList(dataHolder, transform) + camFromIndex = BgCamInformations.getCamTable(dataHolder, crawlspacePosList, bgCamInfoList) + return BgCamInformations(name, posDataName, bgCamInfoList, crawlspacePosList, 0, 6, camFromIndex) + + def getDataArrayC(self): + """Returns the camera data/crawlspace positions array""" + + posData = CData() + listName = f"Vec3s {self.posDataName}[]" + + # .h + posData.header = f"extern {listName};\n" + + # .c + posData.source = listName + " = {\n" + for val in self.camFromIndex.values(): + if isinstance(val, CrawlspaceCamera): + posData.source += val.getDataEntryC() + "\n" + elif val.hasPosData: + posData.source += val.data.getEntryC() + "\n" + posData.source = posData.source[:-1] # remove extra newline + posData.source += "};\n\n" + + return posData + + def getInfoArrayC(self): + """Returns the array containing the informations of each cameras""" + + bgCamInfoData = CData() + listName = f"BgCamInfo {self.name}[]" + + # .h + bgCamInfoData.header = f"extern {listName};\n" + + # .c + bgCamInfoData.source = ( + (listName + " = {\n") + + "".join(val.getInfoEntryC(self.posDataName) for val in self.camFromIndex.values()) + + "};\n\n" + ) + + return bgCamInfoData diff --git a/fast64_internal/oot/exporter/collision/polygons.py b/fast64_internal/oot/exporter/collision/polygons.py new file mode 100644 index 000000000..3339d42cc --- /dev/null +++ b/fast64_internal/oot/exporter/collision/polygons.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass, field +from typing import Optional +from mathutils import Vector +from ....utility import PluginError, CData, indent + + +@dataclass +class CollisionPoly: + """This class defines a single collision poly""" + + indices: list[int] + ignoreCamera: bool + ignoreEntity: bool + ignoreProjectile: bool + enableConveyor: bool + normal: Vector + dist: int + useMacros: bool + + type: Optional[int] = field(init=False, default=None) + + def __post_init__(self): + for i, val in enumerate(self.normal): + if val < -1.0 or val > 1.0: + raise PluginError(f"ERROR: Invalid value for normal {['X', 'Y', 'Z'][i]}! (``{val}``)") + + def getFlags_vIA(self): + """Returns the value of ``flags_vIA``""" + + vtxId = self.indices[0] & 0x1FFF + if self.ignoreProjectile or self.ignoreEntity or self.ignoreCamera: + flag1 = ("COLPOLY_IGNORE_PROJECTILES" if self.useMacros else "(1 << 2)") if self.ignoreProjectile else "" + flag2 = ("COLPOLY_IGNORE_ENTITY" if self.useMacros else "(1 << 1)") if self.ignoreEntity else "" + flag3 = ("COLPOLY_IGNORE_CAMERA" if self.useMacros else "(1 << 0)") if self.ignoreCamera else "" + flags = "(" + " | ".join(flag for flag in [flag1, flag2, flag3] if len(flag) > 0) + ")" + else: + flags = "COLPOLY_IGNORE_NONE" if self.useMacros else "0" + + return f"COLPOLY_VTX({vtxId}, {flags})" if self.useMacros else f"((({flags} & 7) << 13) | ({vtxId} & 0x1FFF))" + + def getFlags_vIB(self): + """Returns the value of ``flags_vIB``""" + + vtxId = self.indices[1] & 0x1FFF + if self.enableConveyor: + flags = "COLPOLY_IS_FLOOR_CONVEYOR" if self.useMacros else "(1 << 0)" + else: + flags = "COLPOLY_IGNORE_NONE" if self.useMacros else "0" + return f"COLPOLY_VTX({vtxId}, {flags})" if self.useMacros else f"((({flags} & 7) << 13) | ({vtxId} & 0x1FFF))" + + def getEntryC(self): + """Returns an entry for the collision poly array""" + + vtxId = self.indices[2] & 0x1FFF + if self.type is None: + raise PluginError("ERROR: Surface Type missing!") + return ( + (indent + "{ ") + + ", ".join( + ( + f"{self.type}", + self.getFlags_vIA(), + self.getFlags_vIB(), + f"COLPOLY_VTX_INDEX({vtxId})" if self.useMacros else f"{vtxId} & 0x1FFF", + ("{ " + ", ".join(f"COLPOLY_SNORMAL({val})" for val in self.normal) + " }"), + f"{self.dist}", + ) + ) + + " }," + ) + + +@dataclass +class CollisionPolygons: + """This class defines the array of collision polys""" + + name: str + polyList: list[CollisionPoly] + + def getC(self): + colPolyData = CData() + listName = f"CollisionPoly {self.name}[{len(self.polyList)}]" + + # .h + colPolyData.header = f"extern {listName};\n" + + # .c + colPolyData.source = (listName + " = {\n") + "\n".join(poly.getEntryC() for poly in self.polyList) + "\n};\n\n" + + return colPolyData diff --git a/fast64_internal/oot/exporter/collision/surface.py b/fast64_internal/oot/exporter/collision/surface.py new file mode 100644 index 000000000..67fc810bf --- /dev/null +++ b/fast64_internal/oot/exporter/collision/surface.py @@ -0,0 +1,129 @@ +from dataclasses import dataclass +from ....utility import CData, indent + + +@dataclass(unsafe_hash=True) +class SurfaceType: + """This class defines a single surface type""" + + # surface type 0 + bgCamIndex: int + exitIndex: int + floorType: int + unk18: int # unused? + wallType: int + floorProperty: int + isSoft: bool + isHorseBlocked: bool + + # surface type 1 + material: int + floorEffect: int + lightSetting: int + echo: int + canHookshot: bool + conveyorSpeed: int + conveyorDirection: int + isWallDamage: bool # unk27 + + useMacros: bool + + def getIsSoftC(self): + return "1" if self.isSoft else "0" + + def getIsHorseBlockedC(self): + return "1" if self.isHorseBlocked else "0" + + def getCanHookshotC(self): + return "1" if self.canHookshot else "0" + + def getIsWallDamageC(self): + return "1" if self.isWallDamage else "0" + + def getSurfaceType0(self): + """Returns surface type properties for the first element of the data array""" + + if self.useMacros: + return ( + ("SURFACETYPE0(") + + f"{self.bgCamIndex}, {self.exitIndex}, {self.floorType}, {self.unk18}, " + + f"{self.wallType}, {self.floorProperty}, {self.getIsSoftC()}, {self.getIsHorseBlockedC()}" + + ")" + ) + else: + return ( + (indent * 2 + "(") + + " | ".join( + prop + for prop in [ + f"(({self.getIsHorseBlockedC()} & 1) << 31)", + f"(({self.getIsSoftC()} & 1) << 30)", + f"(({self.floorProperty} & 0x0F) << 26)", + f"(({self.wallType} & 0x1F) << 21)", + f"(({self.unk18} & 0x07) << 18)", + f"(({self.floorType} & 0x1F) << 13)", + f"(({self.exitIndex} & 0x1F) << 8)", + f"({self.bgCamIndex} & 0xFF)", + ] + ) + + ")" + ) + + def getSurfaceType1(self): + """Returns surface type properties for the second element of the data array""" + + if self.useMacros: + return ( + ("SURFACETYPE1(") + + f"{self.material}, {self.floorEffect}, {self.lightSetting}, {self.echo}, " + + f"{self.getCanHookshotC()}, {self.conveyorSpeed}, {self.conveyorDirection}, {self.getIsWallDamageC()}" + + ")" + ) + else: + return ( + (indent * 2 + "(") + + " | ".join( + prop + for prop in [ + f"(({self.getIsWallDamageC()} & 1) << 27)", + f"(({self.conveyorDirection} & 0x3F) << 21)", + f"(({self.conveyorSpeed} & 0x07) << 18)", + f"(({self.getCanHookshotC()} & 1) << 17)", + f"(({self.echo} & 0x3F) << 11)", + f"(({self.lightSetting} & 0x1F) << 6)", + f"(({self.floorEffect} & 0x03) << 4)", + f"({self.material} & 0x0F)", + ] + ) + + ")" + ) + + def getEntryC(self): + """Returns an entry for the surface type array""" + + if self.useMacros: + return indent + "{ " + self.getSurfaceType0() + ", " + self.getSurfaceType1() + " }," + else: + return (indent + "{\n") + self.getSurfaceType0() + ",\n" + self.getSurfaceType1() + ("\n" + indent + "},") + + +@dataclass +class SurfaceTypes: + """This class defines the array of surface types""" + + name: str + surfaceTypeList: list[SurfaceType] + + def getC(self): + surfaceData = CData() + listName = f"SurfaceType {self.name}[{len(self.surfaceTypeList)}]" + + # .h + surfaceData.header = f"extern {listName};\n" + + # .c + surfaceData.source = ( + (listName + " = {\n") + "\n".join(poly.getEntryC() for poly in self.surfaceTypeList) + "\n};\n\n" + ) + + return surfaceData diff --git a/fast64_internal/oot/exporter/collision/vertex.py b/fast64_internal/oot/exporter/collision/vertex.py new file mode 100644 index 000000000..39d081bab --- /dev/null +++ b/fast64_internal/oot/exporter/collision/vertex.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from ....utility import CData, indent + + +@dataclass +class CollisionVertex: + """This class defines a vertex data""" + + pos: tuple[int, int, int] + + def getEntryC(self): + """Returns a vertex entry""" + + return indent + "{ " + ", ".join(f"{p:6}" for p in self.pos) + " }," + + +@dataclass +class CollisionVertices: + """This class defines the array of vertices""" + + name: str + vertexList: list[CollisionVertex] + + def getC(self): + vertData = CData() + listName = f"Vec3s {self.name}[{len(self.vertexList)}]" + + # .h + vertData.header = f"extern {listName};\n" + + # .c + vertData.source = ( + (listName + " = {\n") + "\n".join(vertex.getEntryC() for vertex in self.vertexList) + "\n};\n\n" + ) + + return vertData diff --git a/fast64_internal/oot/exporter/collision/waterbox.py b/fast64_internal/oot/exporter/collision/waterbox.py new file mode 100644 index 000000000..15d158239 --- /dev/null +++ b/fast64_internal/oot/exporter/collision/waterbox.py @@ -0,0 +1,141 @@ +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 Utility + + +@dataclass +class WaterBox: + """This class defines waterbox data""" + + # Properties + bgCamIndex: int + lightIndex: int + roomIndexC: str + setFlag19C: str + + xMin: int + ySurface: int + zMin: int + xLength: int + zLength: int + + useMacros: bool + + @staticmethod + def new( + position: tuple[int, int, int], + scale: float, + emptyDisplaySize: float, + bgCamIndex: int, + lightIndex: int, + roomIndex: int, + setFlag19: bool, + useMacros: bool, + ): + # The scale ordering is due to the fact that scaling happens AFTER rotation. + # Thus the translation uses Y-up, while the scale uses Z-up. + xMax = round(position[0] + scale[0] * emptyDisplaySize) + zMax = round(position[2] + scale[1] * emptyDisplaySize) + xMin = round(position[0] - scale[0] * emptyDisplaySize) + zMin = round(position[2] - scale[1] * emptyDisplaySize) + + return WaterBox( + bgCamIndex, + lightIndex, + f"0x{roomIndex:02X}" if roomIndex == 0x3F else f"{roomIndex}", + "1" if setFlag19 else "0", + xMin, + round(position[1] + scale[2] * emptyDisplaySize), + zMin, + xMax - xMin, + zMax - zMin, + useMacros, + ) + + def getProperties(self): + """Returns the waterbox properties""" + + if self.useMacros: + return f"WATERBOX_PROPERTIES({self.bgCamIndex}, {self.lightIndex}, {self.roomIndexC}, {self.setFlag19C})" + else: + return ( + "(" + + " | ".join( + prop + for prop in [ + f"(({self.setFlag19C} & 1) << 19)", + f"(({self.roomIndexC} & 0x3F) << 13)", + f"(({self.lightIndex} & 0x1F) << 8)", + f"(({self.bgCamIndex}) & 0xFF)", + ] + ) + + ")" + ) + + def getEntryC(self): + """Returns a waterbox entry""" + + return ( + (indent + "{ ") + + f"{self.xMin}, {self.ySurface}, {self.zMin}, {self.xLength}, {self.zLength}, " + + self.getProperties() + + " }," + ) + + +@dataclass +class WaterBoxes: + """This class defines the array of waterboxes""" + + name: str + waterboxList: list[WaterBox] + + @staticmethod + def new(name: str, dataHolder: Object, transform: Matrix, useMacros: bool): + waterboxList: list[WaterBox] = [] + waterboxObjList = getObjectList(dataHolder.children_recursive, "EMPTY", "Water Box") + for waterboxObj in waterboxObjList: + emptyScale = waterboxObj.empty_display_size + pos, _, scale, orientedRot = Utility.getConvertedTransform(transform, dataHolder, waterboxObj, True) + checkIdentityRotation(waterboxObj, orientedRot, False) + + wboxProp = waterboxObj.ootWaterBoxProperty + + # temp solution + roomObj = None + if dataHolder.type == "EMPTY" and dataHolder.ootEmptyType == "Scene": + for obj in dataHolder.children_recursive: + if obj.type == "EMPTY" and obj.ootEmptyType == "Room": + for o in obj.children_recursive: + if o == waterboxObj: + roomObj = obj + break + + waterboxList.append( + WaterBox.new( + pos, + scale, + emptyScale, + wboxProp.camera, + wboxProp.lighting, + roomObj.ootRoomHeader.roomIndex if roomObj is not None else 0x3F, + wboxProp.flag19, + useMacros, + ) + ) + return WaterBoxes(name, waterboxList) + + def getC(self): + wboxData = CData() + listName = f"WaterBox {self.name}[{len(self.waterboxList)}]" + + # .h + wboxData.header = f"extern {listName};\n" + + # .c + wboxData.source = (listName + " = {\n") + "\n".join(wBox.getEntryC() for wBox in self.waterboxList) + "\n};\n\n" + + return wboxData diff --git a/fast64_internal/oot/exporter/cutscene/__init__.py b/fast64_internal/oot/exporter/cutscene/__init__.py new file mode 100644 index 000000000..f1cca53b6 --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/__init__.py @@ -0,0 +1,141 @@ +import bpy + +from dataclasses import dataclass, field +from typing import Optional +from bpy.types import Object +from ....utility import PluginError, CData, indent +from ...oot_utility import getCustomProperty +from ...scene.properties import OOTSceneHeaderProperty +from .data import CutsceneData + + +# NOTE: ``paramNumber`` is the expected number of parameters inside the parsed commands, +# this account for the unused parameters. Every classes are based on the commands arguments from ``z64cutscene_commands.h`` + +# NOTE: ``params`` is the list of parsed parameters, it can't be ``None`` if we're importing a scene, +# when it's ``None`` it will get the data from the cutscene objects + + +@dataclass +class Cutscene: + """This class defines a cutscene, including its data and its informations""" + + name: str + data: CutsceneData + totalEntries: int + frameCount: int + useMacros: bool + motionOnly: bool + + paramNumber: int = field(init=False, default=2) + + @staticmethod + def new(name: Optional[str], csObj: Optional[Object], useMacros: bool, motionOnly: bool): + # when csObj is None it means we're in import context + if csObj is not None: + if name is None: + name = csObj.name.removeprefix("Cutscene.").replace(".", "_") + data = CutsceneData.new(csObj, useMacros, motionOnly) + return Cutscene(name, data, data.totalEntries, data.frameCount, useMacros, motionOnly) + + def getC(self): + """Returns the cutscene data""" + + if self.data is not None: + csData = CData() + declarationBase = f"CutsceneData {self.name}[]" + + # this list's order defines the order of the commands in the cutscene array + dataListNames = [] + + if not self.motionOnly: + dataListNames = [ + "textList", + "miscList", + "rumbleList", + "transitionList", + "lightSettingsList", + "timeList", + "seqList", + "fadeSeqList", + ] + + dataListNames.extend( + [ + "playerCueList", + "actorCueList", + "camEyeSplineList", + "camATSplineList", + "camEyeSplineRelPlayerList", + "camATSplineRelPlayerList", + "camEyeList", + "camATList", + ] + ) + + if self.data.motionFrameCount > self.frameCount: + self.frameCount += self.data.motionFrameCount - self.frameCount + + # .h + csData.header = f"extern {declarationBase};\n" + + # .c + csData.source = ( + declarationBase + + " = {\n" + + (indent + f"CS_BEGIN_CUTSCENE({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") + + "};\n\n" + ) + + return csData + else: + raise PluginError("ERROR: CutsceneData not initialised!") + + +@dataclass +class SceneCutscene: + """This class hosts cutscene data""" + + entries: list[Cutscene] + + @staticmethod + def new(props: OOTSceneHeaderProperty, headerIndex: int, useMacros: bool): + csObj: Object = props.csWriteObject + cutsceneObjects: list[Object] = [csObj for csObj in props.extraCutscenes] + entries: list[Cutscene] = [] + + if headerIndex > 0 and len(cutsceneObjects) > 0: + raise PluginError("ERROR: Extra cutscenes can only belong to the main header!") + + cutsceneObjects.insert(0, csObj) + for csObj in cutsceneObjects: + if csObj is not None: + if csObj.ootEmptyType != "Cutscene": + raise PluginError( + "ERROR: Object selected as cutscene is wrong type, must be empty with Cutscene type" + ) + elif csObj.parent is not None: + raise PluginError("ERROR: Cutscene empty object should not be parented to anything") + + writeType = props.csWriteType + csWriteCustom = None + if writeType == "Custom": + csWriteCustom = getCustomProperty(props, "csWriteCustom") + + if props.writeCutscene: + # if csWriteCustom is None then the name will auto-set from the csObj passed in the class + entries.append( + Cutscene.new(csWriteCustom, csObj, useMacros, bpy.context.scene.fast64.oot.exportMotionOnly) + ) + return SceneCutscene(entries) + + def getCmd(self): + """Returns the cutscene data scene command""" + if len(self.entries) == 0: + raise PluginError("ERROR: Cutscene entry list is empty!") + + # entry No. 0 is always self.csObj + return indent + f"SCENE_CMD_CUTSCENE_DATA({self.entries[0].name}),\n" diff --git a/fast64_internal/oot/exporter/cutscene/actor_cue.py b/fast64_internal/oot/exporter/cutscene/actor_cue.py new file mode 100644 index 000000000..7a38c3f0d --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/actor_cue.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass, field +from ....utility import PluginError, indent +from ...oot_constants import ootData +from ...cutscene.motion.utility import getRotation, getInteger +from .common import CutsceneCmdBase + + +@dataclass +class CutsceneCmdActorCue(CutsceneCmdBase): + """This class contains a single Actor Cue command data""" + + actionID: int + rot: list[str] + startPos: list[int] + endPos: list[int] + isPlayer: bool + + paramNumber: int = field(init=False, default=15) + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdActorCue( + getInteger(params[1]), + getInteger(params[2]), + getInteger(params[0]), + [getRotation(params[3]), getRotation(params[4]), getRotation(params[5])], + [getInteger(params[6]), getInteger(params[7]), getInteger(params[8])], + [getInteger(params[9]), getInteger(params[10]), getInteger(params[11])], + ) + + def getCmd(self): + self.validateFrames() + + if len(self.rot) == 0: + raise PluginError("ERROR: Rotation list is empty!") + + if len(self.startPos) == 0: + raise PluginError("ERROR: Start Position list is empty!") + + if len(self.endPos) == 0: + raise PluginError("ERROR: End Position list is empty!") + + return indent * 3 + ( + f"CS_{'PLAYER' if self.isPlayer else 'ACTOR'}_CUE(" + + f"{self.actionID}, {self.startFrame}, {self.endFrame}, " + + "".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" + ) + + +@dataclass +class CutsceneCmdActorCueList(CutsceneCmdBase): + """This class contains the Actor Cue List command data""" + + isPlayer: bool + commandType: str + entryTotal: int + + entries: list[CutsceneCmdActorCue] = field(init=False, default_factory=list) + paramNumber: int = field(init=False, default=2) + listName: str = field(init=False, default="actorCueList") + + @staticmethod + def from_params(params: list[str], isPlayer: bool): + if isPlayer: + commandType = "Player" + entryTotal = getInteger(params[0]) + else: + commandType = params[0] + if commandType.startswith("0x"): + # make it a 4 digit hex + commandType = commandType.removeprefix("0x") + commandType = "0x" + "0" * (4 - len(commandType)) + commandType + else: + commandType = ootData.enumData.enumByKey["csCmd"].itemById[commandType].key + entryTotal = getInteger(params[1].strip()) + + return CutsceneCmdActorCueList(None, None, isPlayer, commandType, entryTotal) + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: No Actor Cue entry found!") + + return ( + indent * 2 + + ( + f"CS_{'PLAYER' if self.isPlayer else 'ACTOR'}_CUE_LIST(" + + f"{self.commandType + ', ' if not self.isPlayer else ''}" + + f"{self.entryTotal}),\n" + ) + + "".join(entry.getCmd() for entry in self.entries) + ) diff --git a/fast64_internal/oot/exporter/cutscene/camera.py b/fast64_internal/oot/exporter/cutscene/camera.py new file mode 100644 index 000000000..fa9ddc469 --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/camera.py @@ -0,0 +1,157 @@ +from dataclasses import dataclass, field +from ....utility import PluginError, indent +from ...cutscene.motion.utility import getInteger +from .common import CutsceneCmdBase + + +@dataclass +class CutsceneCmdCamPoint(CutsceneCmdBase): + """This class contains a single Camera Point command data""" + + continueFlag: str + camRoll: int + frame: int + viewAngle: float + pos: list[int] + + paramNumber: int = field(init=False, default=8) + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdCamPoint( + None, + None, + params[0], + getInteger(params[1]), + getInteger(params[2]), + float(params[3][:-1]), + [getInteger(params[4]), getInteger(params[5]), getInteger(params[6])], + ) + + def getCmd(self): + if len(self.pos) == 0: + raise PluginError("ERROR: Pos list is empty!") + + return indent * 3 + ( + f"CS_CAM_POINT({self.continueFlag}, {self.camRoll}, {self.frame}, {self.viewAngle}f, " + + "".join(f"{pos}, " for pos in self.pos) + + "0),\n" + ) + + +@dataclass +class CutsceneCmdCamEyeSpline(CutsceneCmdBase): + """This class contains the Camera Eye Spline data""" + + entries: list[CutsceneCmdCamPoint] + paramNumber: int = field(init=False, default=2) + listName: str = field(init=False, default="camEyeSplineList") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdCamEyeSpline(getInteger(params[0]), getInteger(params[1])) + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + + return self.getCamListCmd("CS_CAM_EYE_SPLINE", self.startFrame, self.endFrame) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdCamATSpline(CutsceneCmdBase): + """This class contains the Camera AT (look-at) Spline data""" + + entries: list[CutsceneCmdCamPoint] + paramNumber: int = field(init=False, default=2) + listName: str = field(init=False, default="camATSplineList") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdCamATSpline(getInteger(params[0]), getInteger(params[1])) + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + + return self.getCamListCmd("CS_CAM_AT_SPLINE", self.startFrame, self.endFrame) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdCamEyeSplineRelToPlayer(CutsceneCmdBase): + """This class contains the Camera Eye Spline Relative to the Player data""" + + entries: list[CutsceneCmdCamPoint] + paramNumber: int = field(init=False, default=2) + listName: str = field(init=False, default="camEyeSplineRelPlayerList") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdCamEyeSplineRelToPlayer(getInteger(params[0]), getInteger(params[1])) + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + + return self.getCamListCmd("CS_CAM_EYE_SPLINE_REL_TO_PLAYER", self.startFrame, self.endFrame) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdCamATSplineRelToPlayer(CutsceneCmdBase): + """This class contains the Camera AT Spline Relative to the Player data""" + + entries: list[CutsceneCmdCamPoint] + paramNumber: int = field(init=False, default=2) + listName: str = field(init=False, default="camATSplineRelPlayerList") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdCamATSplineRelToPlayer(getInteger(params[0]), getInteger(params[1])) + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + + return self.getCamListCmd("CS_CAM_AT_SPLINE_REL_TO_PLAYER", self.startFrame, self.endFrame) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdCamEye(CutsceneCmdBase): + """This class contains a single Camera Eye point""" + + # This feature is not used in the final game and lacks polish, it is recommended to use splines in all cases. + entries: list + paramNumber: int = field(init=False, default=2) + listName: str = field(init=False, default="camEyeList") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdCamEye(getInteger(params[0]), getInteger(params[1])) + + def getCmd(self): + return self.getCamListCmd("CS_CAM_EYE", self.startFrame, self.endFrame) + + +@dataclass +class CutsceneCmdCamAT(CutsceneCmdBase): + """This class contains a single Camera AT point""" + + # This feature is not used in the final game and lacks polish, it is recommended to use splines in all cases. + entries: list + paramNumber: int = field(init=False, default=2) + listName: str = field(init=False, default="camATList") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdCamAT(getInteger(params[0]), getInteger(params[1])) + + def getCmd(self): + return self.getCamListCmd("CS_CAM_AT", self.startFrame, self.endFrame) diff --git a/fast64_internal/oot/exporter/cutscene/common.py b/fast64_internal/oot/exporter/cutscene/common.py new file mode 100644 index 000000000..ab91fb283 --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/common.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import Optional +from ....utility import PluginError, indent +from ...oot_constants import ootData +from ...cutscene.motion.utility import getInteger + + +@dataclass +class CutsceneCmdBase: + """This class contains common Cutscene data""" + + startFrame: Optional[int] + endFrame: Optional[int] + + def validateFrames(self, checkEndFrame: bool = True): + if self.startFrame is None: + raise PluginError("ERROR: Start Frame is None!") + if checkEndFrame and self.endFrame is None: + raise PluginError("ERROR: End Frame is None!") + + @staticmethod + def getEnumValue(enumKey: str, value: str, isSeqLegacy: bool = False): + enum = ootData.enumData.enumByKey[enumKey] + item = enum.itemById.get(value) + if item is None: + setting = getInteger(value) + if isSeqLegacy: + setting -= 1 + item = enum.itemByIndex.get(setting) + return item.key if item is not None else value + + def getGenericListCmd(self, cmdName: str, entryTotal: int): + if entryTotal is None: + raise PluginError(f"ERROR: ``{cmdName}``'s entry total is None!") + return indent * 2 + f"{cmdName}({entryTotal}),\n" + + def getCamListCmd(self, cmdName: str, startFrame: int, endFrame: int): + self.validateFrames() + return indent * 2 + f"{cmdName}({startFrame}, {endFrame}),\n" + + def getGenericSeqCmd(self, cmdName: str, type: str, startFrame: int, endFrame: int): + self.validateFrames() + if type is None: + raise PluginError("ERROR: Seq type is None!") + return indent * 3 + f"{cmdName}({type}, {startFrame}, {endFrame}" + ", 0" * 8 + "),\n" diff --git a/fast64_internal/oot/exporter/cutscene/data.py b/fast64_internal/oot/exporter/cutscene/data.py new file mode 100644 index 000000000..25a92db7d --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/data.py @@ -0,0 +1,413 @@ +import bpy +import math + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING +from bpy.types import Object, Bone +from ....utility import PluginError +from ...oot_constants import ootData +from .actor_cue import CutsceneCmdActorCueList, CutsceneCmdActorCue +from .seq import CutsceneCmdStartStopSeqList, CutsceneCmdFadeSeqList, CutsceneCmdStartStopSeq, CutsceneCmdFadeSeq +from .text import CutsceneCmdTextList, CutsceneCmdText, CutsceneCmdTextNone, CutsceneCmdTextOcarinaAction + +from .misc import ( + CutsceneCmdLightSetting, + CutsceneCmdTime, + CutsceneCmdMisc, + CutsceneCmdRumbleController, + CutsceneCmdDestination, + CutsceneCmdMiscList, + CutsceneCmdRumbleControllerList, + CutsceneCmdTransition, + CutsceneCmdLightSettingList, + CutsceneCmdTimeList, +) + +from .camera import ( + CutsceneCmdCamPoint, + CutsceneCmdCamEyeSpline, + CutsceneCmdCamATSpline, + CutsceneCmdCamEyeSplineRelToPlayer, + CutsceneCmdCamATSplineRelToPlayer, + CutsceneCmdCamEye, + CutsceneCmdCamAT, +) + +if TYPE_CHECKING: + from ...cutscene.properties import OOTCutsceneProperty, OOTCSTextProperty + + +cmdToClass = { + "TextList": CutsceneCmdTextList, + "LightSettingsList": CutsceneCmdLightSettingList, + "TimeList": CutsceneCmdTimeList, + "MiscList": CutsceneCmdMiscList, + "RumbleList": CutsceneCmdRumbleControllerList, + "StartSeqList": CutsceneCmdStartStopSeqList, + "StopSeqList": CutsceneCmdStartStopSeqList, + "FadeOutSeqList": CutsceneCmdFadeSeqList, + "StartSeq": CutsceneCmdStartStopSeq, + "StopSeq": CutsceneCmdStartStopSeq, + "FadeOutSeq": CutsceneCmdFadeSeq, +} + +cmdToList = { + "TextList": "textList", + "LightSettingsList": "lightSettingsList", + "TimeList": "timeList", + "MiscList": "miscList", + "RumbleList": "rumbleList", +} + + +@dataclass +class CutsceneData: + """This class defines the command data inside a cutscene""" + + useMacros: bool + motionOnly: bool + + totalEntries: int = field(init=False, default=0) + frameCount: int = field(init=False, default=0) + motionFrameCount: int = field(init=False, default=0) + camEndFrame: int = field(init=False, default=0) + destination: CutsceneCmdDestination = field(init=False, default=None) + actorCueList: list[CutsceneCmdActorCueList] = field(init=False, default_factory=list) + playerCueList: list[CutsceneCmdActorCueList] = field(init=False, default_factory=list) + camEyeSplineList: list[CutsceneCmdCamEyeSpline] = field(init=False, default_factory=list) + camATSplineList: list[CutsceneCmdCamATSpline] = field(init=False, default_factory=list) + camEyeSplineRelPlayerList: list[CutsceneCmdCamEyeSplineRelToPlayer] = field(init=False, default_factory=list) + camATSplineRelPlayerList: list[CutsceneCmdCamATSplineRelToPlayer] = field(init=False, default_factory=list) + camEyeList: list[CutsceneCmdCamEye] = field(init=False, default_factory=list) + camATList: list[CutsceneCmdCamAT] = field(init=False, default_factory=list) + textList: list[CutsceneCmdTextList] = field(init=False, default_factory=list) + miscList: list[CutsceneCmdMiscList] = field(init=False, default_factory=list) + rumbleList: list[CutsceneCmdRumbleControllerList] = field(init=False, default_factory=list) + transitionList: list[CutsceneCmdTransition] = field(init=False, default_factory=list) + lightSettingsList: list[CutsceneCmdLightSettingList] = field(init=False, default_factory=list) + timeList: list[CutsceneCmdTimeList] = field(init=False, default_factory=list) + seqList: list[CutsceneCmdStartStopSeqList] = field(init=False, default_factory=list) + fadeSeqList: list[CutsceneCmdFadeSeqList] = field(init=False, default_factory=list) + + @staticmethod + def new(csObj: Object, useMacros: bool, motionOnly: bool): + csProp: "OOTCutsceneProperty" = csObj.ootCutsceneProperty + csObjects = { + "CS Actor Cue List": [], + "CS Player Cue List": [], + "camShot": [], + } + + for obj in csObj.children_recursive: + if obj.type == "EMPTY" and obj.ootEmptyType in csObjects.keys(): + csObjects[obj.ootEmptyType].append(obj) + elif obj.type == "ARMATURE": + csObjects["camShot"].append(obj) + csObjects["camShot"].sort(key=lambda obj: obj.name) + + newCutsceneData = CutsceneData(useMacros, motionOnly) + newCutsceneData.setCutsceneData(csObjects, csProp) + return newCutsceneData + + 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 getEnumValueFromProp(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 setActorCueListData(self, csObjects: dict[str, list[Object]], isPlayer: bool): + """Returns the Actor Cue List commands from the corresponding objects""" + + playerOrActor = f"{'Player' if isPlayer else 'Actor'}" + actorCueListObjects = csObjects[f"CS {playerOrActor} Cue List"] + actorCueListObjects.sort(key=lambda o: o.ootCSMotionProperty.actorCueProp.cueStartFrame) + + self.totalEntries += 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.useMacros: + commandType = ootData.enumData.enumByKey["csCmd"].itemByKey[commandType].id + + # ignoring dummy cue + newActorCueList = CutsceneCmdActorCueList(None, None, isPlayer, commandType, entryTotal - 1) + + 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 + + newActorCueList.entries.append( + CutsceneCmdActorCue( + startFrame, + endFrame, + actionID, + self.getOoTRotation(childObj), + self.getOoTPosition(childObj.location), + self.getOoTPosition(obj.children[i].location), + isPlayer, + ) + ) + + self.actorCueList.append(newActorCueList) + + def getCameraShotPointData(self, bones: list[Bone], 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, + ("CS_CAM_CONTINUE" if self.useMacros 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, "CS_CAM_STOP" if self.useMacros 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 getCamClassOrList(self, isClass: bool, camMode: str, useAT: bool): + """Returns the camera dataclass depending on the camera mode""" + + # TODO: improve this + if isClass: + camCmdClassMap = { + "splineEyeOrAT": CutsceneCmdCamATSpline if useAT else CutsceneCmdCamEyeSpline, + "splineEyeOrATRelPlayer": CutsceneCmdCamATSplineRelToPlayer + if useAT + else CutsceneCmdCamEyeSplineRelToPlayer, + "eyeOrAT": CutsceneCmdCamAT if useAT else CutsceneCmdCamEye, + } + else: + camCmdClassMap = { + "splineEyeOrAT": "camATSplineList" if useAT else "camEyeSplineList", + "splineEyeOrATRelPlayer": "camATSplineRelPlayerList" if useAT else "camEyeSplineRelPlayerList", + "eyeOrAT": "camATList" if useAT else "camEyeList", + } + + return camCmdClassMap[camMode] + + def getNewCamData(self, shotObj: Object, useAT: bool): + """Returns the Camera Shot data from the corresponding Armatures""" + + entries = self.getCameraShotPointData(shotObj.data.bones, useAT) + startFrame = shotObj.data.ootCamShotProp.shotStartFrame + + # "fake" end frame + endFrame = startFrame + max(2, sum(point.frame for point in entries)) + (entries[-2].frame if useAT else 1) + + if not useAT: + for pointData in entries: + pointData.frame = 0 + self.camEndFrame = endFrame + + return self.getCamClassOrList(True, shotObj.data.ootCamShotProp.shotCamMode, useAT)( + startFrame, endFrame, entries + ) + + def setCameraShotData(self, csObjects: dict[str, list[Object]]): + shotObjects = csObjects["camShot"] + + if len(shotObjects) > 0: + motionFrameCount = -1 + for shotObj in shotObjects: + camMode = shotObj.data.ootCamShotProp.shotCamMode + + eyeCamList = getattr(self, self.getCamClassOrList(False, camMode, False)) + eyeCamList.append(self.getNewCamData(shotObj, False)) + + atCamList = getattr(self, self.getCamClassOrList(False, camMode, True)) + atCamList.append(self.getNewCamData(shotObj, True)) + + motionFrameCount = max(motionFrameCount, self.camEndFrame + 1) + self.motionFrameCount += motionFrameCount + self.totalEntries += len(shotObjects) * 2 + + def getNewTextCmd(self, textEntry: "OOTCSTextProperty"): + match textEntry.textboxType: + case "Text": + return CutsceneCmdText( + textEntry.startFrame, + textEntry.endFrame, + textEntry.textID, + self.getEnumValueFromProp("csTextType", textEntry, "csTextType"), + textEntry.topOptionTextID, + textEntry.bottomOptionTextID, + ) + case "None": + return CutsceneCmdTextNone(textEntry.startFrame, textEntry.endFrame) + case "OcarinaAction": + return CutsceneCmdTextOcarinaAction( + textEntry.startFrame, + textEntry.endFrame, + self.getEnumValueFromProp("ocarinaSongActionId", textEntry, "ocarinaAction"), + textEntry.ocarinaMessageId, + ) + raise PluginError("ERROR: Unknown text type!") + + def setCutsceneData(self, csObjects: dict[str, list[Object]], csProp: "OOTCutsceneProperty"): + self.setActorCueListData(csObjects, True) + self.setActorCueListData(csObjects, False) + self.setCameraShotData(csObjects) + + # don't process the cutscene empty if we don't want its data + if self.motionOnly: + return + + if csProp.csUseDestination: + self.destination = CutsceneCmdDestination( + csProp.csDestinationStartFrame, + None, + self.getEnumValueFromProp("csDestination", csProp, "csDestination"), + ) + self.totalEntries += 1 + + self.frameCount = csProp.csEndFrame + self.totalEntries += len(csProp.csLists) + + for entry in csProp.csLists: + match entry.listType: + case "StartSeqList" | "StopSeqList" | "FadeOutSeqList": + isFadeOutSeq = entry.listType == "FadeOutSeqList" + cmdList = cmdToClass[entry.listType](None, None) + cmdList.entryTotal = len(entry.seqList) + if not isFadeOutSeq: + cmdList.type = "start" if entry.listType == "StartSeqList" else "stop" + for elem in entry.seqList: + data = cmdToClass[entry.listType.removesuffix("List")](elem.startFrame, elem.endFrame) + if isFadeOutSeq: + data.seqPlayer = self.getEnumValueFromProp("csFadeOutSeqPlayer", elem, "csSeqPlayer") + else: + data.type = cmdList.type + data.seqId = self.getEnumValueFromProp("seqId", elem, "csSeqID") + cmdList.entries.append(data) + if isFadeOutSeq: + self.fadeSeqList.append(cmdList) + else: + self.seqList.append(cmdList) + case "Transition": + self.transitionList.append( + CutsceneCmdTransition( + entry.transitionStartFrame, + entry.transitionEndFrame, + self.getEnumValueFromProp("csTransitionType", entry, "transitionType"), + ) + ) + case _: + curList = getattr(entry, (entry.listType[0].lower() + entry.listType[1:])) + cmdList = cmdToClass[entry.listType](None, None) + cmdList.entryTotal = len(curList) + for elem in curList: + match entry.listType: + case "TextList": + cmdList.entries.append(self.getNewTextCmd(elem)) + case "LightSettingsList": + cmdList.entries.append( + CutsceneCmdLightSetting( + elem.startFrame, elem.endFrame, False, elem.lightSettingsIndex + ) + ) + case "TimeList": + cmdList.entries.append( + CutsceneCmdTime(elem.startFrame, elem.endFrame, elem.hour, elem.minute) + ) + case "MiscList": + cmdList.entries.append( + CutsceneCmdMisc( + elem.startFrame, + elem.endFrame, + self.getEnumValueFromProp("csMiscType", elem, "csMiscType"), + ) + ) + case "RumbleList": + cmdList.entries.append( + CutsceneCmdRumbleController( + elem.startFrame, + elem.endFrame, + elem.rumbleSourceStrength, + elem.rumbleDuration, + elem.rumbleDecreaseRate, + ) + ) + case _: + raise PluginError("ERROR: Unknown Cutscene List Type!") + getattr(self, cmdToList[entry.listType]).append(cmdList) diff --git a/fast64_internal/oot/exporter/cutscene/misc.py b/fast64_internal/oot/exporter/cutscene/misc.py new file mode 100644 index 000000000..a332dbf6c --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/misc.py @@ -0,0 +1,228 @@ +from dataclasses import dataclass, field +from typing import Optional +from ....utility import PluginError, indent +from ...cutscene.motion.utility import getInteger +from .common import CutsceneCmdBase + + +@dataclass +class CutsceneCmdMisc(CutsceneCmdBase): + """This class contains a single misc command entry""" + + type: str # see ``CutsceneMiscType`` in decomp + + paramNumber: int = field(init=False, default=14) + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdMisc( + getInteger(params[1]), getInteger(params[2]), CutsceneCmdBase.getEnumValue("csMiscType", params[0]) + ) + + def getCmd(self): + self.validateFrames() + return indent * 3 + (f"CS_MISC({self.type}, {self.startFrame}, {self.endFrame}" + ", 0" * 11 + "),\n") + + +@dataclass +class CutsceneCmdLightSetting(CutsceneCmdBase): + """This class contains Light Setting command data""" + + isLegacy: bool + lightSetting: int + + paramNumber: int = field(init=False, default=11) + + @staticmethod + def from_params(params: list[str], isLegacy: bool): + lightSetting = getInteger(params[0]) + return CutsceneCmdLightSetting( + getInteger(params[1]), getInteger(params[2]), isLegacy, lightSetting - 1 if isLegacy else lightSetting + ) + + def getCmd(self): + self.validateFrames(False) + return indent * 3 + (f"CS_LIGHT_SETTING({self.lightSetting}, {self.startFrame}" + ", 0" * 12 + "),\n") + + +@dataclass +class CutsceneCmdTime(CutsceneCmdBase): + """This class contains Time Ocarina Action command data""" + + hour: int + minute: int + + paramNumber: int = field(init=False, default=5) + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdTime( + getInteger(params[1]), + getInteger(params[2]), + getInteger(params[3]), + getInteger(params[4]), + ) + + def getCmd(self): + self.validateFrames(False) + return indent * 3 + f"CS_TIME(0, {self.startFrame}, 0, {self.hour}, {self.minute}),\n" + + +@dataclass +class CutsceneCmdRumbleController(CutsceneCmdBase): + """This class contains Rumble Controller command data""" + + sourceStrength: int + duration: int + decreaseRate: int + + paramNumber: int = field(init=False, default=8) + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdRumbleController( + getInteger(params[1]), + getInteger(params[2]), + getInteger(params[3]), + getInteger(params[4]), + getInteger(params[5]), + ) + + def getCmd(self): + self.validateFrames(False) + return indent * 3 + ( + f"CS_RUMBLE_CONTROLLER(" + + f"0, {self.startFrame}, 0, {self.sourceStrength}, {self.duration}, {self.decreaseRate}, 0, 0),\n" + ) + + +@dataclass +class CutsceneCmdMiscList(CutsceneCmdBase): + """This class contains Misc command data""" + + entryTotal: Optional[int] = field(init=False, default=None) + entries: list[CutsceneCmdMisc] = field(init=False, default_factory=list) + paramNumber: int = field(init=False, default=1) + listName: str = field(init=False, default="miscList") + + @staticmethod + def from_params(params: list[str]): + new = CutsceneCmdMiscList() + new.entryTotal = getInteger(params[0]) + return new + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + return self.getGenericListCmd("CS_MISC_LIST", self.entryTotal) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdLightSettingList(CutsceneCmdBase): + """This class contains Light Setting List command data""" + + entryTotal: Optional[int] = field(init=False, default=None) + entries: list[CutsceneCmdLightSetting] = field(init=False, default_factory=list) + paramNumber: int = field(init=False, default=1) + listName: str = field(init=False, default="lightSettingsList") + + @staticmethod + def from_params(params: list[str]): + new = CutsceneCmdLightSettingList() + new.entryTotal = getInteger(params[0]) + return new + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + return self.getGenericListCmd("CS_LIGHT_SETTING_LIST", self.entryTotal) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdTimeList(CutsceneCmdBase): + """This class contains Time List command data""" + + entryTotal: Optional[int] = field(init=False, default=None) + entries: list[CutsceneCmdTime] = field(init=False, default_factory=list) + paramNumber: int = field(init=False, default=1) + listName: str = field(init=False, default="timeList") + + @staticmethod + def from_params(params: list[str]): + new = CutsceneCmdTimeList() + new.entryTotal = getInteger(params[0]) + return new + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + return self.getGenericListCmd("CS_TIME_LIST", self.entryTotal) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdRumbleControllerList(CutsceneCmdBase): + """This class contains Rumble Controller List command data""" + + entryTotal: Optional[int] = field(init=False, default=None) + entries: list[CutsceneCmdRumbleController] = field(init=False, default_factory=list) + paramNumber: int = field(init=False, default=1) + listName: str = field(init=False, default="rumbleList") + + @staticmethod + def from_params(params: list[str]): + new = CutsceneCmdRumbleControllerList() + new.entryTotal = getInteger(params[0]) + return new + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + return self.getGenericListCmd("CS_RUMBLE_CONTROLLER_LIST", self.entryTotal) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdDestination(CutsceneCmdBase): + """This class contains Destination command data""" + + id: str + + paramNumber: int = field(init=False, default=3) + listName: str = field(init=False, default="destination") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdDestination( + getInteger(params[1]), None, CutsceneCmdBase.getEnumValue("csDestination", params[0]) + ) + + def getCmd(self): + self.validateFrames(False) + return indent * 2 + f"CS_DESTINATION({self.id}, {self.startFrame}, 0),\n" + + +@dataclass +class CutsceneCmdTransition(CutsceneCmdBase): + """This class contains Transition command data""" + + type: str + + paramNumber: int = field(init=False, default=3) + listName: str = field(init=False, default="transitionList") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdTransition( + getInteger(params[1]), getInteger(params[2]), CutsceneCmdBase.getEnumValue("csTransitionType", params[0]) + ) + + def getCmd(self): + self.validateFrames() + return indent * 2 + f"CS_TRANSITION({self.type}, {self.startFrame}, {self.endFrame}),\n" diff --git a/fast64_internal/oot/exporter/cutscene/seq.py b/fast64_internal/oot/exporter/cutscene/seq.py new file mode 100644 index 000000000..f8afeb78b --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/seq.py @@ -0,0 +1,93 @@ +from dataclasses import dataclass, field +from typing import Optional +from ....utility import PluginError +from ...cutscene.motion.utility import getInteger +from .common import CutsceneCmdBase + + +@dataclass +class CutsceneCmdStartStopSeq(CutsceneCmdBase): + """This class contains Start/Stop Seq command data""" + + isLegacy: bool = field(init=False, default=False) + seqId: Optional[str] = field(init=False, default=None) + paramNumber: int = field(init=False, default=11) + type: Optional[str] = field(init=False, default=None) # "start" or "stop" + + @staticmethod + def from_params(params: list[str], isLegacy: bool): + return CutsceneCmdFadeSeq( + getInteger(params[1]), getInteger(params[2]), CutsceneCmdBase.getEnumValue("seqId", params[0], isLegacy) + ) + + def getCmd(self): + self.validateFrames() + if self.type is None: + raise PluginError("ERROR: Type is None!") + return self.getGenericSeqCmd(f"CS_{self.type.upper()}_SEQ", self.seqId, self.startFrame, self.endFrame) + + +@dataclass +class CutsceneCmdFadeSeq(CutsceneCmdBase): + """This class contains Fade Seq command data""" + + seqPlayer: str = field(init=False, default=str()) + paramNumber: int = field(init=False, default=11) + enumKey: str = field(init=False, default="csFadeOutSeqPlayer") + + @staticmethod + def from_params(params: list[str], enumKey: str): + return CutsceneCmdFadeSeq( + getInteger(params[1]), getInteger(params[2]), CutsceneCmdBase.getEnumValue(enumKey, params[0]) + ) + + def getCmd(self): + self.validateFrames() + return self.getGenericSeqCmd("CS_FADE_OUT_SEQ", self.seqPlayer, self.startFrame, self.endFrame) + + +@dataclass +class CutsceneCmdStartStopSeqList(CutsceneCmdBase): + """This class contains Start/Stop Seq List command data""" + + entryTotal: int = field(init=False, default=0) + type: str = field(init=False, default=str()) # "start" or "stop" + entries: list[CutsceneCmdStartStopSeq] = field(init=False, default_factory=list) + paramNumber: int = field(init=False, default=1) + listName: str = field(init=False, default="seqList") + + @staticmethod + def from_params(params: list[str]): + new = CutsceneCmdStartStopSeqList() + new.entryTotal = getInteger(params[0]) + return new + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + return self.getGenericListCmd(f"CS_{self.type.upper()}_SEQ_LIST", self.entryTotal) + "".join( + entry.getCmd() for entry in self.entries + ) + + +@dataclass +class CutsceneCmdFadeSeqList(CutsceneCmdBase): + """This class contains Fade Seq List command data""" + + entryTotal: int = field(init=False, default=0) + entries: list[CutsceneCmdFadeSeq] = field(init=False, default_factory=list) + paramNumber: int = field(init=False, default=1) + listName: str = field(init=False, default="fadeSeqList") + + @staticmethod + def from_params(params: list[str]): + new = CutsceneCmdFadeSeqList() + new.entryTotal = getInteger(params[0]) + return new + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + return self.getGenericListCmd("CS_FADE_OUT_SEQ_LIST", self.entryTotal) + "".join( + entry.getCmd() for entry in self.entries + ) diff --git a/fast64_internal/oot/exporter/cutscene/text.py b/fast64_internal/oot/exporter/cutscene/text.py new file mode 100644 index 000000000..a19efb96e --- /dev/null +++ b/fast64_internal/oot/exporter/cutscene/text.py @@ -0,0 +1,106 @@ +from dataclasses import dataclass, field +from ....utility import PluginError, indent +from ...cutscene.motion.utility import getInteger +from .common import CutsceneCmdBase + + +@dataclass +class CutsceneCmdText(CutsceneCmdBase): + """This class contains Text command data""" + + textId: int + type: str + altTextId1: int + altTextId2: int + + paramNumber: int = field(init=False, default=6) + id: str = field(init=False, default="Text") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdText( + getInteger(params[1]), + getInteger(params[2]), + getInteger(params[0]), + CutsceneCmdBase.getEnumValue("csTextType", params[3]), + getInteger(params[4]), + getInteger(params[5]), + ) + + def getCmd(self): + self.validateFrames() + return indent * 3 + ( + f"CS_TEXT(" + + f"{self.textId}, {self.startFrame}, {self.endFrame}, {self.type}, {self.altTextId1}, {self.altTextId2}" + + "),\n" + ) + + +@dataclass +class CutsceneCmdTextNone(CutsceneCmdBase): + """This class contains Text None command data""" + + paramNumber: int = field(init=False, default=2) + id: str = field(init=False, default="None") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdTextNone(getInteger(params[0]), getInteger(params[1])) + + def getCmd(self): + self.validateFrames() + return indent * 3 + f"CS_TEXT_NONE({self.startFrame}, {self.endFrame}),\n" + + +@dataclass +class CutsceneCmdTextOcarinaAction(CutsceneCmdBase): + """This class contains Text Ocarina Action command data""" + + ocarinaActionId: str + messageId: int + + paramNumber: int = field(init=False, default=4) + id: str = field(init=False, default="OcarinaAction") + + @staticmethod + def from_params(params: list[str]): + return CutsceneCmdTextOcarinaAction( + getInteger(params[1]), + getInteger(params[2]), + CutsceneCmdBase.getEnumValue("ocarinaSongActionId", params[0]), + getInteger(params[3]), + ) + + def getCmd(self): + self.validateFrames() + return indent * 3 + ( + f"CS_TEXT_OCARINA_ACTION(" + + f"{self.ocarinaActionId}, {self.startFrame}, {self.endFrame}, {self.messageId}" + + "),\n" + ) + + +@dataclass +class CutsceneCmdTextList(CutsceneCmdBase): + """This class contains Text List command data""" + + entryTotal: int = field(init=False, default=0) + entries: list[CutsceneCmdText | CutsceneCmdTextNone | CutsceneCmdTextOcarinaAction] = field( + init=False, default_factory=list + ) + paramNumber: int = field(init=False, default=1) + listName: str = field(init=False, default="textList") + + @staticmethod + def from_params(params: list[str]): + new = CutsceneCmdTextList() + new.entryTotal = getInteger(params[0]) + return new + + def getCmd(self): + if len(self.entries) == 0: + raise PluginError("ERROR: Entry list is empty!") + + return self.getGenericListCmd("CS_TEXT_LIST", self.entryTotal) + "".join( + entry.getCmd() for entry in self.entries + ) diff --git a/fast64_internal/oot/exporter/decomp_edit/__init__.py b/fast64_internal/oot/exporter/decomp_edit/__init__.py new file mode 100644 index 000000000..4f0693f15 --- /dev/null +++ b/fast64_internal/oot/exporter/decomp_edit/__init__.py @@ -0,0 +1,59 @@ +import os +import re +import shutil + +from ...oot_utility import ExportInfo, RemoveInfo, getSceneDirFromLevelName +from ..scene import Scene +from ..file import SceneFile +from .scene_table import SceneTableUtility +from .spec import SpecUtility + + +class Files: # TODO: find a better name + """This class handles editing decomp files""" + + @staticmethod + def remove_old_room_files(exportInfo: "ExportInfo", scene: "Scene"): + if exportInfo.customSubPath is not None: + sceneDir = exportInfo.customSubPath + exportInfo.name + else: + sceneDir = getSceneDirFromLevelName(exportInfo.name) + + scenePath = os.path.join(exportInfo.exportPath, sceneDir) + for filename in os.listdir(scenePath): + filepath = os.path.join(scenePath, filename) + if os.path.isfile(filepath): + match = re.match(scene.name + "\_room\_(\d+)\.[ch]", filename) + if match is not None and int(match.group(1)) >= len(scene.rooms.entries): + os.remove(filepath) + + @staticmethod + def remove_scene_dir(remove_info: "RemoveInfo"): + if remove_info.customSubPath is not None: + sceneDir = remove_info.customSubPath + remove_info.name + else: + sceneDir = getSceneDirFromLevelName(remove_info.name) + + scenePath = os.path.join(remove_info.exportPath, sceneDir) + if os.path.exists(scenePath): + shutil.rmtree(scenePath) + + @staticmethod + def add_scene_edits(exportInfo: "ExportInfo", scene: "Scene", sceneFile: "SceneFile"): + """Edits decomp files""" + + Files.remove_old_room_files(exportInfo, scene) + SpecUtility.add_segments(exportInfo, scene, sceneFile) + SceneTableUtility.edit_scene_table( + exportInfo.exportPath, + exportInfo.name, + scene.mainHeader.infos.drawConfig, + ) + + @staticmethod + def remove_scene(remove_info: "RemoveInfo"): + """Removes data from decomp files""" + + Files.remove_scene_dir(remove_info) + SpecUtility.remove_segments(remove_info.exportPath, remove_info.name) + SceneTableUtility.delete_scene_table_entry(remove_info.exportPath, remove_info.name) diff --git a/fast64_internal/oot/exporter/decomp_edit/config.py b/fast64_internal/oot/exporter/decomp_edit/config.py new file mode 100644 index 000000000..a11eec82e --- /dev/null +++ b/fast64_internal/oot/exporter/decomp_edit/config.py @@ -0,0 +1,155 @@ +import os +import re + +from ....utility import PluginError, readFile, writeFile +from ...scene.properties import OOTBootupSceneOptions + + +class Config: + @staticmethod + def writeBootupSettings( + configPath: str, + bootMode: str, + newGameOnly: bool, + entranceIndex: str, + linkAge: str, + timeOfDay: str, + cutsceneIndex: str, + saveFileNameData: str, + ): + if os.path.exists(configPath): + originalData = readFile(configPath) + data = originalData + else: + originalData = "" + data = ( + f"// #define BOOT_TO_SCENE\n" + + f"// #define BOOT_TO_SCENE_NEW_GAME_ONLY\n" + + f"// #define BOOT_TO_FILE_SELECT\n" + + f"// #define BOOT_TO_MAP_SELECT\n" + + f"#define BOOT_ENTRANCE 0\n" + + f"#define BOOT_AGE LINK_AGE_CHILD\n" + + f"#define BOOT_TIME NEXT_TIME_NONE\n" + + f"#define BOOT_CUTSCENE 0xFFEF\n" + + f"#define BOOT_PLAYER_NAME 0x15, 0x12, 0x17, 0x14, 0x3E, 0x3E, 0x3E, 0x3E\n\n" + ) + + data = re.sub( + r"(//\s*)?#define\s*BOOT_TO_SCENE", + ("" if bootMode == "Play" else "// ") + "#define BOOT_TO_SCENE", + data, + ) + data = re.sub( + r"(//\s*)?#define\s*BOOT_TO_SCENE_NEW_GAME_ONLY", + ("" if newGameOnly else "// ") + "#define BOOT_TO_SCENE_NEW_GAME_ONLY", + data, + ) + data = re.sub( + r"(//\s*)?#define\s*BOOT_TO_FILE_SELECT", + ("" if bootMode == "File Select" else "// ") + "#define BOOT_TO_FILE_SELECT", + data, + ) + data = re.sub( + r"(//\s*)?#define\s*BOOT_TO_MAP_SELECT", + ("" if bootMode == "Map Select" else "// ") + "#define BOOT_TO_MAP_SELECT", + data, + ) + data = re.sub(r"#define\s*BOOT_ENTRANCE\s*[^\s]*", f"#define BOOT_ENTRANCE {entranceIndex}", data) + data = re.sub(r"#define\s*BOOT_AGE\s*[^\s]*", f"#define BOOT_AGE {linkAge}", data) + data = re.sub(r"#define\s*BOOT_TIME\s*[^\s]*", f"#define BOOT_TIME {timeOfDay}", data) + data = re.sub(r"#define\s*BOOT_CUTSCENE\s*[^\s]*", f"#define BOOT_CUTSCENE {cutsceneIndex}", data) + data = re.sub(r"#define\s*BOOT_PLAYER_NAME\s*[^\n]*", f"#define BOOT_PLAYER_NAME {saveFileNameData}", data) + + if data != originalData: + writeFile(configPath, data) + + @staticmethod + def setBootupScene(configPath: str, entranceIndex: str, options: "OOTBootupSceneOptions"): + # ``options`` argument type: OOTBootupSceneOptions + linkAge = "LINK_AGE_CHILD" + timeOfDay = "NEXT_TIME_NONE" + cutsceneIndex = "0xFFEF" + newEntranceIndex = "0" + saveName = "LINK" + + if options.bootMode != "Map Select": + newEntranceIndex = entranceIndex + saveName = options.newGameName + + if options.overrideHeader: + timeOfDay, linkAge = Config.getParamsFromOptions(options) + if options.headerOption == "Cutscene": + cutsceneIndex = "0xFFF" + format(options.cutsceneIndex - 4, "X") + + saveFileNameData = ", ".join(["0x" + format(i, "02X") for i in Config.stringToSaveNameBytes(saveName)]) + + Config.writeBootupSettings( + configPath, + options.bootMode, + options.newGameOnly, + newEntranceIndex, + linkAge, + timeOfDay, + cutsceneIndex, + saveFileNameData, + ) + + @staticmethod + def clearBootupScene(configPath: str): + Config.writeBootupSettings( + configPath, + "", + False, + "0", + "LINK_AGE_CHILD", + "NEXT_TIME_NONE", + "0xFFEF", + "0x15, 0x12, 0x17, 0x14, 0x3E, 0x3E, 0x3E, 0x3E", + ) + + @staticmethod + def getParamsFromOptions(options: "OOTBootupSceneOptions") -> tuple[str, str]: + timeOfDay = ( + "NEXT_TIME_DAY" + if options.headerOption == "Child Day" or options.headerOption == "Adult Day" + else "NEXT_TIME_NIGHT" + ) + + linkAge = ( + "LINK_AGE_ADULT" + if options.headerOption == "Adult Day" or options.headerOption == "Adult Night" + else "LINK_AGE_CHILD" + ) + + return timeOfDay, linkAge + + # converts ascii text to format for save file name. + # see src/code/z_message_PAL.c:Message_Decode() + @staticmethod + def stringToSaveNameBytes(name: str) -> bytearray: + specialChar = { + " ": 0x3E, + ".": 0x40, + "-": 0x3F, + } + + result = bytearray([0x3E] * 8) + + if len(name) > 8: + raise PluginError("Save file name for scene bootup must be 8 characters or less.") + for i in range(len(name)): + value = ord(name[i]) + if name[i] in specialChar: + result[i] = specialChar[name[i]] + elif value >= ord("0") and value <= ord("9"): # numbers + result[i] = value - ord("0") + elif value >= ord("A") and value <= ord("Z"): # uppercase + result[i] = value - ord("7") + elif value >= ord("a") and value <= ord("z"): # lowercase + result[i] = value - ord("=") + else: + raise PluginError( + name + " has some invalid characters and cannot be used as a save file name for scene bootup." + ) + + return result diff --git a/fast64_internal/oot/exporter/decomp_edit/scene_table.py b/fast64_internal/oot/exporter/decomp_edit/scene_table.py new file mode 100644 index 000000000..8ae966864 --- /dev/null +++ b/fast64_internal/oot/exporter/decomp_edit/scene_table.py @@ -0,0 +1,288 @@ +import os +import bpy + +from dataclasses import dataclass, field +from typing import Optional +from ....utility import PluginError, writeFile +from ...oot_constants import ootEnumSceneID, ootSceneNameToID + +ADDED_SCENES_COMMENT = "// Added scenes" + + +def get_original_index(enum_value: str) -> Optional[int]: + """ + Returns the original index of a specific scene + """ + for index, scene_enum in enumerate( + [elem[0] for elem in ootEnumSceneID[1:]] + ): # ignore first value in array ('Custom') + if scene_enum == enum_value: + return index + return None + + +def get_scene_enum_from_name(scene_name: str): + return ootSceneNameToID.get(scene_name, f"SCENE_{scene_name.upper()}") + + +@dataclass +class SceneTableEntry: + """Defines an entry of ``scene_table.h``""" + + # macro parameters + spec_name: str # name of the scene segment in spec + title_card_name: str # name of the title card segment in spec, or `none` for no title card + enum_value: str # enum value for this scene + draw_config: str # scene draw config index + unk1: str + unk2: str + + @staticmethod + def from_line(original_line: str): + macro_start = "DEFINE_SCENE(" + + if macro_start in original_line: + # remove the index and the macro's name with the parenthesis + index = original_line.index(macro_start) + len(macro_start) + parsed = original_line[index:].removesuffix(")") + + params = parsed.split(", ") + assert len(params) == 6 + + return SceneTableEntry(*params) + else: + raise PluginError("ERROR: This line is not a scene table entry!") + + @staticmethod + def from_scene(scene_name: str, draw_config: str): + # TODO: Implement title cards + return SceneTableEntry( + scene_name if scene_name.endswith("_scene") else f"{scene_name}_scene", + "none", + get_scene_enum_from_name(scene_name), + draw_config, + "0", + "0", + ) + + def to_c(self, index: int): + """Returns the entry as C code""" + return ( + f"/* 0x{index:02X} */ " + f"DEFINE_SCENE({self.spec_name}, {self.title_card_name}, {self.enum_value}, " + f"{self.draw_config}, {self.unk1}, {self.unk2})" + ) + + +@dataclass +class SceneTableSection: + """Defines a section of the scene table, with is a list of entires with an optional preprocessor directive / comment""" + + directive: Optional[str] # can also be a comment starting with // + entries: list[SceneTableEntry] = field(default_factory=list) + + def to_c(self, index: int): + directive = f"{self.directive}\n" if self.directive else "" + terminator = "\n#endif" if self.directive and self.directive.startswith("#if") else "" + entry_string = "\n".join(entry.to_c(index + i) for i, entry in enumerate(self.entries)) + return f"{directive}{entry_string}{terminator}\n\n" + + +@dataclass +class SceneTable: + """Defines a ``scene_table.h`` file data""" + + header: str + sections: list[SceneTableSection] = field(default_factory=list) + + @staticmethod + def new(export_path: str): + # read the file's data + try: + with open(export_path) as file_data: + data = file_data.read() + file_data.seek(0) + lines = file_data.readlines() + except FileNotFoundError: + raise PluginError("ERROR: Can't find scene_table.h!") + + # Find first instance of "DEFINE_SCENE(", indicating a scene define macro + first_macro_index = data.index("DEFINE_SCENE(") + if first_macro_index == -1: + return SceneTable(data, []) # No scene defines found - add to end + + # Go backwards up to previous newline + try: + header_end_index = data[:first_macro_index].rfind("\n") + except ValueError: + header_end_index = 0 + + header = data[: header_end_index + 1] + + lines = data[header_end_index + 1 :].split("\n") + lines = list(filter(None, lines)) # removes empty lines + lines = [line.strip() for line in lines] + + sections: list[SceneTableSection] = [] + current_section: Optional[SceneTableSection] = None + + for line in lines: + if line.startswith("#if"): + if current_section: # handles non-directive section preceding directive section + sections.append(current_section) + current_section = SceneTableSection(line) + elif line.startswith("#endif"): + sections.append(current_section) + current_section = None # handles back-to-back directive sections + elif line.startswith("//"): + if current_section: # handles non-directive section preceding directive section + sections.append(current_section) + current_section = SceneTableSection(line) + else: + if not current_section: + current_section = SceneTableSection(None) + current_section.entries.append(SceneTableEntry.from_line(line)) + + if current_section: + sections.append(current_section) # add last section if non-directive + + return SceneTable(header, sections) + + def get_entries_flattened(self) -> list[SceneTableEntry]: + """ + Returns all entries as a single array, without sections. + This is a shallow copy of the data and adding/removing from this list not change the scene table internally. + """ + + return [entry for section in self.sections for entry in section.entries] + + def get_index_from_enum(self, enum_value: str) -> Optional[int]: + """Returns the index (int) of the chosen scene if found, else return ``None``""" + + for i, entry in enumerate(self.get_entries_flattened()): + if entry.enum_value == enum_value: + return i + + return None + + def set_entry_at_enum(self, entry: SceneTableEntry, enum_value: str): + """Replaces entry in the scene table with the given enum_value""" + for section in self.sections: + for entry_index in range(len(section.entries)): + if section.entries[entry_index].enum_value == enum_value: + section.entries[entry_index] = entry + + def append(self, entry: SceneTableEntry): + """Appends an entry to the scene table, only used by custom scenes""" + + # Find current added scenes comment, or add one if not found + current_section = None + for section in self.sections: + if section.directive == ADDED_SCENES_COMMENT: + current_section = section + break + if current_section is None: + current_section = SceneTableSection(ADDED_SCENES_COMMENT) + self.sections.append(current_section) + + if entry not in current_section.entries: + current_section.entries.append(entry) + else: + raise PluginError("ERROR: (Append) Entry already in the table!") + + def insert(self, entry: SceneTableEntry, index: int): + """Inserts an entry in the scene table, only used by non-custom scenes""" + + if entry in self.get_entries_flattened(): + raise PluginError("ERROR: (Insert) Entry already in the table!") + if index < 0 or index > len(self.get_entries_flattened()) - 1: + raise PluginError(f"ERROR: (Insert) The index is not valid! ({index})") + + i = 0 + for section in self.sections: + for entry_index in range(len(section.entries)): + if i == index: + section.entries.insert(entry_index, entry) + return + else: + i += 1 + + def update(self, entry: SceneTableEntry, enum_value: str): + """Updates an entry if the enum_value exists in the scene table, otherwise appends/inserts entry depending on if custom or not""" + + original_index = get_original_index(enum_value) # index in unmodified scene table + current_index = self.get_index_from_enum(enum_value) # index in current scene table + + if current_index is None: # Not in scene table currently + if original_index is not None: + # insert mode - we want to place vanilla scenes into their original locations if previously deleted + self.insert(entry, original_index) + else: + # this is a custom level, append to end + self.append(entry) + else: + # update mode (for both vanilla and custom scenes since they already exist in the table) + self.set_entry_at_enum(entry, enum_value) + + def remove(self, enum_value: str): + """Removes an entry from the scene table""" + + for section in self.sections: + for entry in section.entries: + if entry.enum_value == enum_value: + section.entries.remove(entry) + return + + def to_c(self): + """Returns the scene table as C code""" + data = f"{self.header}" + index = 0 + for section in self.sections: + data += section.to_c(index) + index += len(section.entries) + + if data[-2:] == "\n\n": # For consistency with vanilla + data = data[:-1] + return data + + +class SceneTableUtility: + """This class hosts different function to edit the scene table""" + + @staticmethod + def get_draw_config(scene_name: str): + """Read draw config from scene table""" + scene_table = SceneTable.new( + os.path.join(bpy.path.abspath(bpy.context.scene.ootDecompPath), "include/tables/scene_table.h") + ) + + spec_dict = {entry.spec_name: entry for entry in scene_table.get_entries_flattened()} + entry = spec_dict.get(f"{scene_name}_scene") + if entry is not None: + return entry.draw_config + + raise PluginError(f"ERROR: Scene name {scene_name} not found in scene table.") + + @staticmethod + def edit_scene_table(export_path: str, export_name: str, draw_config: str): + """Update the scene table entry of the selected scene""" + path = os.path.join(export_path, "include/tables/scene_table.h") + scene_table = SceneTable.new(path) + export_enum = get_scene_enum_from_name(export_name) + + scene_table.update(SceneTableEntry.from_scene(export_name, draw_config), export_enum) + + # write the file with the final data + writeFile(path, scene_table.to_c()) + + @staticmethod + def delete_scene_table_entry(export_path: str, export_name: str): + """Remove the scene table entry of the selected scene""" + path = os.path.join(export_path, "include/tables/scene_table.h") + scene_table = SceneTable.new(path) + export_enum = get_scene_enum_from_name(export_name) + + scene_table.remove(export_enum) + + # write the file with the final data + writeFile(path, scene_table.to_c()) diff --git a/fast64_internal/oot/exporter/decomp_edit/spec.py b/fast64_internal/oot/exporter/decomp_edit/spec.py new file mode 100644 index 000000000..2bc053178 --- /dev/null +++ b/fast64_internal/oot/exporter/decomp_edit/spec.py @@ -0,0 +1,300 @@ +import os +import bpy +import re + +from dataclasses import dataclass, field +from typing import Optional +from ....utility import PluginError, writeFile, indent +from ...oot_utility import ExportInfo, getSceneDirFromLevelName +from ..scene import Scene +from ..file import SceneFile + + +@dataclass +class SpecCommand: + """This class defines a single spec command""" + + type: str + content: str = "" + comment: str = "" + + def to_c(self): + comment = f" //{self.comment}" if self.comment != "" else "" + + # Note: This is a hacky way of handling internal preprocessor directives, which would be parsed as if they were commands. + # This is fine as long you are not trying to modify this spec entry, and currently there are no internal preprocessor directives + # in scene segments anyway. + + indent_string = indent if not self.type.startswith("#") else "" + content = f" {self.content.strip()}" if self.content != "" else "" + return f"{indent_string}{self.type}{content}{comment}\n" + + +@dataclass +class SpecEntry: + """Defines an entry of ``spec``""" + + commands: list[SpecCommand] + + @staticmethod + def new(original: list[str]): + commands: list[SpecCommand] = [] + for line in original: + comment = "" + if "//" in line: + comment = line[line.index("//") + len("//") :] + line = line[: line.index("//")].strip() + split = line.split(" ") + commands.append(SpecCommand(split[0], " ".join(split[1:]) if len(split) > 1 else "", comment)) + + return SpecEntry(commands) + + def get_name(self) -> Optional[str]: + """Returns segment name, with quotes removed""" + for command in self.commands: + if command.type == "name": + return command.content.replace('"', "") + return "" + + def to_c(self): + return "beginseg\n" + "".join(command.to_c() for command in self.commands) + "endseg" + + +@dataclass +class SpecSection: + """Defines an 'section' of ``spec``, which is a list of segment definitions that are optionally surrounded by a preprocessor directive""" + + directive: Optional[str] + entries: list[SpecEntry] = field(default_factory=list) + + def to_c(self): + directive = f"{self.directive}\n" if self.directive else "" + terminator = "\n#endif" if self.directive and self.directive.startswith("#if") else "" + entry_string = "\n\n".join(entry.to_c() for entry in self.entries) + return f"{directive}{entry_string}{terminator}\n\n" + + +@dataclass +class SpecFile: + """This class defines the spec's file data""" + + header: str # contents of file before scene segment definitions + build_directory: Optional[str] + sections: list[SpecSection] = field(default_factory=list) # list of the different spec entries + + @staticmethod + def new(export_path: str): + # read the file's data + data = "" + try: + with open(export_path, "r") as file_data: + data = file_data.read() + except FileNotFoundError: + raise PluginError("ERROR: Can't find spec!") + + # Find first instance of "/assets/scenes/", indicating a scene file + first_scene_include_index = data.index("/assets/scenes/") + if first_scene_include_index == -1: + return SpecFile(data, None, []) # No scene files found - add to end + + # Get build directory, which is text right before /assets/scenes/... + build_directory = None + for dir in ["$(BUILD_DIR)", "build"]: + if data[:first_scene_include_index].endswith(dir): + build_directory = dir + + # Go backwards up to previous "endseg" definition + try: + header_endseg_index = data[:first_scene_include_index].rfind("endseg") + except ValueError: + raise PluginError("endseg not found, scene segements cannot be the first segments in spec file") + + header = data[: header_endseg_index + len("endseg")] + + # This technically includes data after scene segments + # However, as long as we don't have to modify them, they should be fine + lines = data[header_endseg_index + len("endseg") :].split("\n") + lines = list(filter(None, lines)) # removes empty lines + lines = [line.strip() for line in lines] + + sections: list[SpecSection] = [] + current_section: Optional[SpecSection] = None + while len(lines) > 0: + line = lines.pop(0) + if line.startswith("#if"): + if current_section: # handles non-directive section preceding directive section + sections.append(current_section) + current_section = SpecSection(line) + elif line.startswith("#endif"): + sections.append(current_section) + current_section = None # handles back-to-back directive sections + elif line.startswith("beginseg"): + if not current_section: + current_section = SpecSection(None) + segment_lines = [] + while len(lines) > 0 and not lines[0].startswith("endseg"): + next_line = lines.pop(0) + segment_lines.append(next_line) + if len(lines) == 0: + raise PluginError("In spec file, a beginseg was found unterminated.") + lines.pop(0) # remove endseg line + current_section.entries.append(SpecEntry.new(segment_lines)) + else: + # This code should ignore any other line, including comments. + pass + + if current_section: + sections.append(current_section) # add last section if non-directive + + return SpecFile(header, build_directory, sections) + + def get_entries_flattened(self) -> list[SpecEntry]: + """ + Returns all entries as a single array, without sections. + This is a shallow copy of the data and adding/removing from this list not change the spec file internally. + """ + return [entry for section in self.sections for entry in section.entries] + + def find(self, segment_name: str) -> SpecEntry: + """Returns an entry from a segment name, returns ``None`` if nothing was found""" + + for entry in self.get_entries_flattened(): + if entry.get_name() == segment_name: + return entry + return None + + def append(self, entry: SpecEntry): + """Appends an entry to the list""" + + if len(self.sections) > 0 and self.sections[-1].directive is None: + self.sections[-1].entries.append(entry) + else: + section = SpecSection(None, [entry]) + self.sections.append(section) + + def remove(self, segment_name: str): + """Removes an entry from a segment name""" + for i in range(len(self.sections)): + section = self.sections[i] + for j in range(len(section.entries)): + entry = section.entries[j] + if entry.get_name() == segment_name: + section.entries.remove(entry) + if len(section.entries) == 0: + self.sections.remove(section) + return + + def to_c(self): + return f"{self.header}\n\n" + "".join(section.to_c() for section in self.sections) + + +class SpecUtility: + """This class hosts different functions to edit the spec file""" + + @staticmethod + def remove_segments(export_path: str, scene_name: str): + path = os.path.join(export_path, "spec") + spec_file = SpecFile.new(path) + SpecUtility.remove_segments_from_spec(spec_file, scene_name) + writeFile(path, spec_file.to_c()) + + @staticmethod + def remove_segments_from_spec(spec_file: SpecFile, scene_name: str): + # get the scene and current segment name and remove the scene + scene_segment_name = f"{scene_name}_scene" + spec_file.remove(scene_segment_name) + + # mark the other scene elements to remove (like rooms) + segments_to_remove: list[str] = [] + for entry in spec_file.get_entries_flattened(): + # Note: you cannot do startswith(scene_name), ex. entra vs entra_n + if entry.get_name() == f"{scene_name}_scene" or re.match(f"^{scene_name}\_room\_[0-9]+$", entry.get_name()): + segments_to_remove.append(entry.get_name()) + + # remove the segments + for segment_name in segments_to_remove: + spec_file.remove(segment_name) + + @staticmethod + def add_segments(exportInfo: "ExportInfo", scene: "Scene", sceneFile: "SceneFile"): + hasSceneTex = sceneFile.hasSceneTextures() + hasSceneCS = sceneFile.hasCutscenes() + roomTotal = len(scene.rooms.entries) + csTotal = 0 + + csTotal += len(scene.mainHeader.cutscene.entries) + if scene.altHeader is not None: + for cs in scene.altHeader.cutscenes: + csTotal += len(cs.cutscene.entries) + + # get the spec's data + exportPath = os.path.join(exportInfo.exportPath, "spec") + specFile = SpecFile.new(exportPath) + build_directory = specFile.build_directory + + # get the scene and current segment name and remove the scene + sceneName = exportInfo.name + sceneSegmentName = f"{sceneName}_scene" + SpecUtility.remove_segments_from_spec(specFile, exportInfo.name) + + assert build_directory is not None + isSingleFile = bpy.context.scene.ootSceneExportSettings.singleFile + includeDir = f"{build_directory}/" + if exportInfo.customSubPath is not None: + includeDir += f"{exportInfo.customSubPath + sceneName}" + else: + includeDir += f"{getSceneDirFromLevelName(sceneName)}" + + sceneCmds = [ + SpecCommand("name", f'"{sceneSegmentName}"'), + SpecCommand("compress", ""), + SpecCommand("romalign", "0x1000"), + ] + + # scene + if isSingleFile: + sceneCmds.append(SpecCommand("include", f'"{includeDir}/{sceneSegmentName}.o"')) + else: + sceneCmds.extend( + [ + SpecCommand("include", f'"{includeDir}/{sceneSegmentName}_main.o"'), + SpecCommand("include", f'"{includeDir}/{sceneSegmentName}_col.o"'), + ] + ) + + if hasSceneTex: + sceneCmds.append(SpecCommand("include", f'"{includeDir}/{sceneSegmentName}_tex.o"')) + + if hasSceneCS: + for i in range(csTotal): + sceneCmds.append(SpecCommand("include", f'"{includeDir}/{sceneSegmentName}_cs_{i}.o"')) + + sceneCmds.append(SpecCommand("number", "2")) + specFile.append(SpecEntry(sceneCmds)) + + # rooms + for i in range(roomTotal): + roomSegmentName = f"{sceneName}_room_{i}" + + roomCmds = [ + SpecCommand("name", f'"{roomSegmentName}"'), + SpecCommand("compress"), + SpecCommand("romalign", "0x1000"), + ] + + if isSingleFile: + roomCmds.append(SpecCommand("include", f'"{includeDir}/{roomSegmentName}.o"')) + else: + roomCmds.extend( + [ + SpecCommand("include", f'"{includeDir}/{roomSegmentName}_main.o"'), + SpecCommand("include", f'"{includeDir}/{roomSegmentName}_model_info.o"'), + SpecCommand("include", f'"{includeDir}/{roomSegmentName}_model.o"'), + ] + ) + + roomCmds.append(SpecCommand("number", "3")) + specFile.append(SpecEntry(roomCmds)) + + # finally, write the spec file + writeFile(exportPath, specFile.to_c()) diff --git a/fast64_internal/oot/exporter/file.py b/fast64_internal/oot/exporter/file.py new file mode 100644 index 000000000..892702cd3 --- /dev/null +++ b/fast64_internal/oot/exporter/file.py @@ -0,0 +1,117 @@ +import os + +from dataclasses import dataclass +from ...utility import writeFile + + +@dataclass +class RoomFile: + """This class hosts the C data for every room files""" + + name: str + roomMain: str + roomModel: str + roomModelInfo: str + singleFileExport: bool + path: str + header: str + + def write(self): + """Writes the room files""" + + if self.singleFileExport: + roomMainPath = f"{self.name}.c" + self.roomMain += self.roomModelInfo + self.roomModel + else: + roomMainPath = f"{self.name}_main.c" + writeFile(os.path.join(self.path, f"{self.name}_model_info.c"), self.roomModelInfo) + writeFile(os.path.join(self.path, f"{self.name}_model.c"), self.roomModel) + + writeFile(os.path.join(self.path, roomMainPath), self.roomMain) + + +@dataclass +class SceneFile: + """This class hosts the C data for every scene files""" + + name: str + sceneMain: str + sceneCollision: str + sceneCutscenes: list[str] + sceneTextures: str + roomList: dict[int, RoomFile] + singleFileExport: bool + path: str + header: str + + def hasCutscenes(self): + return len(self.sceneCutscenes) > 0 + + def hasSceneTextures(self): + return len(self.sceneTextures) > 0 + + def getSourceWithSceneInclude(self, sceneInclude: str, source: str): + """Returns the source with the includes if missing""" + ret = "" + if sceneInclude not in source: + ret = sceneInclude + return ret + source + + 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' + + for roomData in self.roomList.values(): + roomData.roomMain = self.getSourceWithSceneInclude(sceneInclude, roomData.roomMain) + + if not self.singleFileExport: + 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 + ) + + if not self.singleFileExport: + self.sceneCollision = self.getSourceWithSceneInclude(sceneInclude, self.sceneCollision) + + if self.hasSceneTextures(): + self.sceneTextures = self.getSourceWithSceneInclude(sceneInclude, self.sceneTextures) + + if self.hasCutscenes(): + for i in range(len(self.sceneCutscenes)): + self.sceneCutscenes[i] = self.getSourceWithSceneInclude(csInclude, self.sceneCutscenes[i]) + + def write(self): + """Writes the scene files""" + self.setIncludeData() + + for room in self.roomList.values(): + self.header += room.header + room.write() + + if self.singleFileExport: + sceneMainPath = f"{self.name}.c" + + if self.hasCutscenes(): + self.sceneMain += "".join(cs for cs in self.sceneCutscenes) + + self.sceneMain += self.sceneCollision + + if self.hasSceneTextures(): + self.sceneMain += self.sceneTextures + else: + sceneMainPath = f"{self.name}_main.c" + writeFile(os.path.join(self.path, f"{self.name}_col.c"), self.sceneCollision) + if self.hasCutscenes(): + for i, cs in enumerate(self.sceneCutscenes): + writeFile(os.path.join(self.path, f"{self.name}_cs_{i}.c"), cs) + if self.hasSceneTextures(): + writeFile(os.path.join(self.path, f"{self.name}_tex.c"), self.sceneTextures) + + writeFile(os.path.join(self.path, sceneMainPath), self.sceneMain) + + self.header += "\n#endif\n" + writeFile(os.path.join(self.path, f"{self.name}.h"), self.header) diff --git a/fast64_internal/oot/exporter/room/__init__.py b/fast64_internal/oot/exporter/room/__init__.py new file mode 100644 index 000000000..3fe3d6c2e --- /dev/null +++ b/fast64_internal/oot/exporter/room/__init__.py @@ -0,0 +1,228 @@ +from dataclasses import dataclass +from typing import Optional +from mathutils import Matrix +from bpy.types import Object +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 ..file import RoomFile +from ..utility import Utility, altHeaderList +from .header import RoomAlternateHeader, RoomHeader +from .shape import RoomShapeUtility, RoomShape, RoomShapeImageMulti, RoomShapeImageBase + + +@dataclass +class Room: + """This class defines a room""" + + name: str + roomIndex: int + mainHeader: Optional[RoomHeader] + altHeader: Optional[RoomAlternateHeader] + roomShape: Optional[RoomShape] + hasAlternateHeaders: bool + + @staticmethod + def new( + name: str, + transform: Matrix, + sceneObj: Object, + roomObj: Object, + roomShapeType: str, + model: OOTModel, + roomIndex: int, + sceneName: str, + saveTexturesAsPNG: bool, + ): + i = 0 + mainHeaderProps = roomObj.ootRoomHeader + altHeader = RoomAlternateHeader(f"{name}_alternateHeaders") + altProp = roomObj.ootAlternateRoomHeaders + + if mainHeaderProps.roomShape == "ROOM_SHAPE_TYPE_IMAGE" and len(mainHeaderProps.bgImageList) == 0: + raise PluginError(f'Room {roomObj.name} uses room shape "Image" but doesn\'t have any BG images.') + + if mainHeaderProps.roomShape == "ROOM_SHAPE_TYPE_IMAGE" and roomIndex >= 1: + raise PluginError(f'Room shape "Image" can only have one room in the scene.') + + mainHeader = RoomHeader.new( + f"{name}_header{i:02}", + mainHeaderProps, + sceneObj, + roomObj, + transform, + i, + ) + hasAlternateHeaders = False + + for i, header in enumerate(altHeaderList, 1): + altP: OOTRoomHeaderProperty = getattr(altProp, f"{header}Header") + if not altP.usePreviousHeader: + hasAlternateHeaders = True + newRoomHeader = RoomHeader.new( + f"{name}_header{i:02}", + altP, + sceneObj, + roomObj, + transform, + i, + ) + setattr(altHeader, header, newRoomHeader) + + altHeader.cutscenes = [ + RoomHeader.new( + f"{name}_header{i:02}", + csHeader, + sceneObj, + roomObj, + transform, + i, + ) + for i, csHeader in enumerate(altProp.cutsceneHeaders, 4) + ] + + hasAlternateHeaders = True if len(altHeader.cutscenes) > 0 else hasAlternateHeaders + altHeader = altHeader if hasAlternateHeaders else None + headers: list[RoomHeader] = [mainHeader] + if altHeader is not None: + headers.extend([altHeader.childNight, altHeader.adultDay, altHeader.adultNight]) + if len(altHeader.cutscenes) > 0: + headers.extend(altHeader.cutscenes) + addMissingObjectsToAllRoomHeaders(roomObj, headers) + + roomShape = RoomShapeUtility.create_shape( + sceneName, name, roomShapeType, model, transform, sceneObj, roomObj, saveTexturesAsPNG, mainHeaderProps + ) + return Room(name, roomIndex, mainHeader, altHeader, roomShape, hasAlternateHeaders) + + def getRoomHeaderFromIndex(self, headerIndex: int) -> RoomHeader | None: + """Returns the current room 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: RoomHeader, hasAltHeaders: bool): + """Returns the room 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.roomShape.get_cmds() + + curHeader.infos.getCmds() + + (curHeader.objects.getCmd() if len(curHeader.objects.objectList) > 0 else "") + + (curHeader.actors.getCmd() if len(curHeader.actors.actorList) > 0 else "") + + Utility.getEndCmd() + + "};\n\n" + ) + + return cmdListData + + def getRoomMainC(self): + """Returns the C data of the main informations of a room""" + + roomC = CData() + roomHeaders: list[tuple[RoomHeader, str]] = [] + altHeaderPtrList = None + + if self.hasAlternateHeaders: + roomHeaders: list[tuple[RoomHeader, str]] = [ + (self.altHeader.childNight, "Child Night"), + (self.altHeader.adultDay, "Adult Day"), + (self.altHeader.adultNight, "Adult Night"), + ] + + for i, csHeader in enumerate(self.altHeader.cutscenes): + roomHeaders.append((csHeader, f"Cutscene No. {i + 1}")) + + altHeaderPtrListName = f"SceneCmd* {self.altHeader.name}" + + # .h + roomC.header = f"extern {altHeaderPtrListName}[];\n" + + # .c + altHeaderPtrList = ( + f"{altHeaderPtrListName}[]" + + " = {\n" + + "\n".join( + indent + f"{curHeader.name}," if curHeader is not None else indent + "NULL," + for (curHeader, _) in roomHeaders + ) + + "\n};\n\n" + ) + + roomHeaders.insert(0, (self.mainHeader, "Child Day (Default)")) + for i, (curHeader, headerDesc) in enumerate(roomHeaders): + if curHeader is not None: + roomC.source += "/**\n * " + f"Header {headerDesc}\n" + "*/\n" + roomC.source += curHeader.getHeaderDefines() + roomC.append(self.getCmdList(curHeader, i == 0 and self.hasAlternateHeaders)) + + if i == 0 and self.hasAlternateHeaders and altHeaderPtrList is not None: + roomC.source += altHeaderPtrList + + if len(curHeader.objects.objectList) > 0: + roomC.append(curHeader.objects.getC()) + + if len(curHeader.actors.actorList) > 0: + roomC.append(curHeader.actors.getC()) + + return roomC + + def getRoomShapeModelC(self, textureSettings: TextureExportSettings): + """Returns the C data of the room model""" + roomModel = CData() + + for i, entry in enumerate(self.roomShape.dl_entries): + if entry.opaque is not None: + roomModel.append(entry.opaque.to_c(self.roomShape.model.f3d)) + + if entry.transparent is not None: + roomModel.append(entry.transparent.to_c(self.roomShape.model.f3d)) + + # type ``ROOM_SHAPE_TYPE_IMAGE`` only allows 1 room + if i == 0 and isinstance(self.roomShape, RoomShapeImageBase): + break + + roomModel.append(self.roomShape.model.to_c(textureSettings, OOTGfxFormatter(ScrollMethod.Vertex)).all()) + + if isinstance(self.roomShape, RoomShapeImageMulti): + # roomModel.append(self.roomShape.multiImg.getC()) # Error? double call in getRoomShapeC()? + roomModel.append(self.roomShape.to_c_img(textureSettings.includeDir)) + + return roomModel + + def getNewRoomFile(self, path: str, isSingleFile: bool, textureExportSettings: TextureExportSettings): + """Returns a new ``RoomFile`` element""" + + roomMainData = self.getRoomMainC() + roomModelData = self.getRoomShapeModelC(textureExportSettings) + roomModelInfoData = self.roomShape.to_c() + + return RoomFile( + self.name, + roomMainData.source, + roomModelData.source, + roomModelInfoData.source, + isSingleFile, + path, + roomMainData.header + roomModelData.header + roomModelInfoData.header, + ) diff --git a/fast64_internal/oot/exporter/room/header.py b/fast64_internal/oot/exporter/room/header.py new file mode 100644 index 000000000..433659b3b --- /dev/null +++ b/fast64_internal/oot/exporter/room/header.py @@ -0,0 +1,259 @@ +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 ...room.properties import OOTRoomHeaderProperty +from ..utility import Utility +from ..actor import Actor + + +@dataclass +class RoomInfos: + """This class stores various room header informations""" + + ### General ### + + index: int + roomShape: str + + ### Behavior ### + + roomBehavior: str + playerIdleType: str + disableWarpSongs: bool + showInvisActors: bool + + ### Skybox And Time ### + + disableSky: bool + disableSunMoon: bool + hour: int + minute: int + timeSpeed: float + echo: str + + ### Wind ### + + setWind: bool + direction: tuple[int, int, int] + strength: int + + @staticmethod + def new(props: Optional[OOTRoomHeaderProperty]): + return RoomInfos( + props.roomIndex, + props.roomShape, + Utility.getPropValue(props, "roomBehaviour"), + Utility.getPropValue(props, "linkIdleMode"), + props.disableWarpSongs, + props.showInvisibleActors, + props.disableSkybox, + props.disableSunMoon, + 0xFF if props.leaveTimeUnchanged else props.timeHours, + 0xFF if props.leaveTimeUnchanged else props.timeMinutes, + max(-128, min(127, round(props.timeSpeed * 0xA))), + props.echo, + props.setWind, + [d for d in props.windVector] if props.setWind else None, + props.windStrength if props.setWind else None, + ) + + def getCmds(self): + """Returns the echo settings, room behavior, skybox disables and time settings room commands""" + showInvisActors = "true" if self.showInvisActors else "false" + disableWarpSongs = "true" if self.disableWarpSongs else "false" + disableSkybox = "true" if self.disableSky else "false" + disableSunMoon = "true" if self.disableSunMoon else "false" + + roomBehaviorArgs = f"{self.roomBehavior}, {self.playerIdleType}, {showInvisActors}, {disableWarpSongs}" + cmdList = [ + f"SCENE_CMD_ECHO_SETTINGS({self.echo})", + f"SCENE_CMD_ROOM_BEHAVIOR({roomBehaviorArgs})", + f"SCENE_CMD_SKYBOX_DISABLES({disableSkybox}, {disableSunMoon})", + f"SCENE_CMD_TIME_SETTINGS({self.hour}, {self.minute}, {self.timeSpeed})", + ] + + if self.setWind: + cmdList.append(f"SCENE_CMD_WIND_SETTINGS({', '.join(f'{dir}' for dir in self.direction)}, {self.strength})") + + return indent + f",\n{indent}".join(cmdList) + ",\n" + + +@dataclass +class RoomObjects: + """This class defines an OoT object array""" + + name: str + objectList: list[str] + + @staticmethod + def new(name: str, props: Optional[OOTRoomHeaderProperty]): + objectList: list[str] = [] + for objProp in props.objectList: + if objProp.objectKey == "Custom": + objectList.append(objProp.objectIDCustom) + else: + objectList.append(ootData.objectData.objectsByKey[objProp.objectKey].id) + return RoomObjects(name, objectList) + + def getDefineName(self): + """Returns the name of the define for the total of entries in the object list""" + + return f"LENGTH_{self.name.upper()}" + + def getCmd(self): + """Returns the object list room command""" + + return indent + f"SCENE_CMD_OBJECT_LIST({self.getDefineName()}, {self.name}),\n" + + def getC(self): + """Returns the array with the objects the room uses""" + + objectList = CData() + + listName = f"s16 {self.name}" + + # .h + objectList.header = f"extern {listName}[];\n" + + # .c + objectList.source = ( + (f"{listName}[{self.getDefineName()}]" + " = {\n") + + ",\n".join(indent + objectID for objectID in self.objectList) + + ",\n};\n\n" + ) + + return objectList + + +@dataclass +class RoomActors: + """This class defines an OoT actor array""" + + name: str + actorList: list[Actor] + + @staticmethod + def new(name: str, sceneObj: Optional[Object], roomObj: Optional[Object], transform: Matrix, headerIndex: int): + actorList: list[Actor] = [] + actorObjList = getObjectList(sceneObj.children_recursive, "EMPTY", "Actor", parentObj=roomObj) + for obj in actorObjList: + actorProp = obj.ootActorProperty + if not Utility.isCurrentHeaderValid(actorProp.headerSettings, headerIndex): + continue + + # The Actor list is filled with ``("None", f"{i} (Deleted from the XML)", "None")`` for + # the total number of actors defined in the XML. If the user deletes one, this will prevent + # 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": + pos, rot, _, _ = Utility.getConvertedTransform(transform, sceneObj, obj, True) + actor = Actor() + + if actorProp.actorID == "Custom": + actor.id = actorProp.actorIDCustom + else: + actor.id = actorProp.actorID + + 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.name = ( + ootData.actorData.actorsByID[actorProp.actorID].name.replace( + f" - {actorProp.actorID.removeprefix('ACTOR_')}", "" + ) + if actorProp.actorID != "Custom" + else "Custom Actor" + ) + + actor.pos = pos + actor.params = actorProp.actorParam + actorList.append(actor) + return RoomActors(name, actorList) + + def getDefineName(self): + """Returns the name of the define for the total of entries in the actor list""" + + return f"LENGTH_{self.name.upper()}" + + def getCmd(self): + """Returns the actor list room command""" + + return indent + f"SCENE_CMD_ACTOR_LIST({self.getDefineName()}, {self.name}),\n" + + def getC(self): + """Returns the array with the actors the room uses""" + + actorList = CData() + listName = f"ActorEntry {self.name}" + + # .h + actorList.header = f"extern {listName}[];\n" + + # .c + actorList.source = ( + (f"{listName}[{self.getDefineName()}]" + " = {\n") + + "\n".join(actor.getActorEntry() for actor in self.actorList) + + "};\n\n" + ) + + return actorList + + +@dataclass +class RoomHeader: + """This class defines a room header""" + + name: str + infos: Optional[RoomInfos] + objects: Optional[RoomObjects] + actors: Optional[RoomActors] + + @staticmethod + def new( + name: str, + props: Optional[OOTRoomHeaderProperty], + sceneObj: Optional[Object], + roomObj: Optional[Object], + transform: Matrix, + headerIndex: int, + ): + return RoomHeader( + name, + RoomInfos.new(props), + RoomObjects.new(f"{name}_objectList", props), + RoomActors.new(f"{name}_actorList", sceneObj, roomObj, transform, headerIndex), + ) + + def getHeaderDefines(self): + """Returns a string containing defines for actor and object lists lengths""" + + headerDefines = "" + + if len(self.objects.objectList) > 0: + defineName = self.objects.getDefineName() + headerDefines += f"#define {defineName} {len(self.objects.objectList)}\n" + + if len(self.actors.actorList) > 0: + defineName = self.actors.getDefineName() + headerDefines += f"#define {defineName} {len(self.actors.actorList)}\n" + + return headerDefines + + +@dataclass +class RoomAlternateHeader: + """This class stores alternate header data""" + + name: str + + childNight: Optional[RoomHeader] = field(init=False, default=None) + adultDay: Optional[RoomHeader] = field(init=False, default=None) + adultNight: Optional[RoomHeader] = field(init=False, default=None) + cutscenes: list[RoomHeader] = field(init=False, default_factory=list) diff --git a/fast64_internal/oot/exporter/room/shape.py b/fast64_internal/oot/exporter/room/shape.py new file mode 100644 index 000000000..96278f917 --- /dev/null +++ b/fast64_internal/oot/exporter/room/shape.py @@ -0,0 +1,752 @@ +import bpy +import shutil +import os + +from dataclasses import dataclass, field +from typing import Optional +from ....utility import PluginError, CData, toAlnum, indent +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 ..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 ( + CullGroup, + checkUniformScale, + ootConvertTranslation, +) + + +@dataclass +class RoomShapeDListsEntry: # previously OOTDLGroup + OOTRoomMeshGroup + name: str + opaque: Optional[GfxList] = field(init=False, default=None) + transparent: Optional[GfxList] = field(init=False, default=None) + + def __post_init__(self): + self.name = toAlnum(self.name) + + def to_c(self): + opaque = self.opaque.name if self.opaque else "NULL" + transparent = self.transparent.name if self.transparent else "NULL" + return f"{opaque}, {transparent}" + + def add_dl_call(self, display_list: GfxList, draw_layer: str): + if draw_layer == "Opaque": + if self.opaque is None: + self.opaque = GfxList(self.name + "_opaque", GfxListTag.Draw, DLFormat.Static) + self.opaque.commands.append(SPDisplayList(display_list)) + elif draw_layer == "Transparent": + if self.transparent is None: + self.transparent = GfxList(self.name + "_transparent", GfxListTag.Draw, DLFormat.Static) + self.transparent.commands.append(SPDisplayList(display_list)) + else: + raise PluginError("Unhandled draw layer: " + str(draw_layer)) + + def terminate_dls(self): + if self.opaque is not None: + self.opaque.commands.append(SPEndDisplayList()) + + if self.transparent is not None: + self.transparent.commands.append(SPEndDisplayList()) + + def create_dls(self): + if self.opaque is None: + self.opaque = GfxList(self.name + "_opaque", GfxListTag.Draw, DLFormat.Static) + if self.transparent is None: + self.transparent = GfxList(self.name + "_transparent", GfxListTag.Draw, DLFormat.Static) + + def is_empty(self): + return self.opaque is None and self.transparent is None + + +@dataclass +class RoomShape: # previously OOTRoomMesh + """This class is the base class for all room shapes.""" + + name: str + """Name of struct itself""" + + model: OOTModel + """Stores all graphical data""" + + dl_entry_array_name: str + """Name of RoomShapeDListsEntry list""" + + dl_entries: list[RoomShapeDListsEntry] = field(init=False, default_factory=list) + """List of DL entries""" + + occlusion_planes: OcclusionPlaneCandidatesList = field(init=False) + """F3DEX3 occlusion planes""" + + def __post_init__(self): + self.occlusion_planes = OcclusionPlaneCandidatesList(self.name) + + def to_c_dl_entries(self): + """Converts list of dl entries to c. This is usually appended to end of CData in to_c().""" + info_data = CData() + list_name = f"RoomShapeDListsEntry {self.dl_entry_array_name}" + f"[{len(self.dl_entries)}]" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + info_data.source = ( + (list_name + " = {\n") + + (indent + f",\n{indent}".join("{ " + elem.to_c() + " }" for elem in self.dl_entries)) + + "\n};\n\n" + ) + + return info_data + + def to_c(self) -> CData: + raise PluginError("to_c() not implemented.") + + def to_c_img(self, include_dir: str): + """Returns C representation of image data in room shape""" + return CData() + + def get_type(self) -> str: + """Returns value in oot_constants.ootEnumRoomShapeType""" + raise PluginError("get_type() not implemented.") + + def add_dl_entry(self, cull_group: Optional[CullGroup] = None) -> RoomShapeDListsEntry: + entry = RoomShapeDListsEntry(f"{self.name}_entry_{len(self.dl_entries)}") + self.dl_entries.append(entry) + return entry + + def remove_unused_entries(self): + new_list = [] + for entry in self.dl_entries: + if not entry.is_empty(): + new_list.append(entry) + self.dl_entries = new_list + + def terminate_dls(self): + for entry in self.dl_entries: + entry.terminate_dls() + + def get_occlusion_planes_cmd(self): + return ( + indent + + f"SCENE_CMD_OCCLUSION_PLANE_CANDIDATES_LIST({len(self.occlusion_planes.planes)}, {self.occlusion_planes.name}),\n" + ) + + def get_cmds(self) -> str: + """Returns the room shape room commands""" + cmds = indent + f"SCENE_CMD_ROOM_SHAPE(&{self.name}),\n" + if len(self.occlusion_planes.planes) > 0: + cmds += self.get_occlusion_planes_cmd() + return cmds + + def copy_bg_images(self, export_path: str): + return # by default, do nothing + + +@dataclass +class RoomShapeNormal(RoomShape): + """This class defines the basic informations shared by other image classes""" + + def to_c(self): + """Returns the C data for the room shape""" + + info_data = CData() + list_name = f"RoomShapeNormal {self.name}" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + num_entries = f"ARRAY_COUNT({self.dl_entry_array_name})" + info_data.source = ( + (list_name + " = {\n" + indent) + + f",\n{indent}".join( + [ + f"{self.get_type()}", + num_entries, + f"{self.dl_entry_array_name}", + f"{self.dl_entry_array_name} + {num_entries}", + ] + ) + + "\n};\n\n" + ) + + info_data.append(self.occlusion_planes.to_c()) + info_data.append(self.to_c_dl_entries()) + + return info_data + + def get_type(self): + return "ROOM_SHAPE_TYPE_NORMAL" + + +@dataclass +class RoomShapeImageEntry: # OOTBGImage + name: str # source + image: bpy.types.Image + + # width: str + # height: str + format: str # fmt + size: str # siz + other_mode_flags: str # tlutMode + # bg_cam_index: int = 0 # bgCamIndex: for which bg cam index is this entry for + + unk_00: int = field(init=False, default=130) # for multi images only + unk_0C: int = field(init=False, default=0) + tlut: str = field(init=False, default="NULL") + format: str = field(init=False, default="G_IM_FMT_RGBA") + size: str = field(init=False, default="G_IM_SIZ_16b") + tlut_count: int = field(init=False, default=0) # tlutCount + + def get_width(self) -> int: + return self.image.size[0] if self.image else 0 + + def get_height(self) -> int: + return self.image.size[1] if self.image else 0 + + @staticmethod + def new(name: str, prop: OOTBGProperty): + if prop.image is None: + raise PluginError( + 'A room is has room shape "Image" but does not have an image set in one of its BG images.' + ) + return RoomShapeImageEntry( + toAlnum(f"{name}_bg_{prop.image.name}"), + prop.image, + prop.otherModeFlags, + ) + + def get_filename(self) -> str: + return f"{self.name}.jpg" + + def to_c_multi(self, bg_cam_index: int): + return ( + indent + + "{\n" + + indent * 2 + + f",\n{indent * 2}".join( + [ + f"0x{self.unk_00:04X}, {bg_cam_index}", + f"{self.name}", + f"0x{self.unk_0C:08X}", + f"{self.tlut}", + f"{self.get_width()}, {self.get_height()}", + f"{self.format}, {self.size}", + f"{self.other_mode_flags}, 0x{self.tlut_count:04X},", + ] + ) + + "\n" + + indent + + "},\n" + ) + + def to_c_single(self) -> str: + return indent + f",\n{indent}".join( + [ + f"{self.name}", + f"0x{self.unk_0C:08X}", + f"{self.tlut}", + f"{self.get_width()}, {self.get_height()}", + f"{self.format}, {self.size}", + f"{self.other_mode_flags}, 0x{self.tlut_count:04X},", + ] + ) + + +@dataclass +class RoomShapeImageBase(RoomShape): + """This class defines the basic informations shared by other image classes""" + + def get_amount_type(self): + raise PluginError("get_amount_type() not implemented.") + + def get_type(self): + return "ROOM_SHAPE_TYPE_IMAGE" + + def to_c_dl_entries(self): + if len(self.dl_entries) > 1: + raise PluginError("RoomShapeImage only allows one one dl entry, but multiple found") + + info_data = CData() + list_name = f"RoomShapeDListsEntry {self.dl_entry_array_name}" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + info_data.source = (list_name + " = {\n") + (indent + self.dl_entries[0].to_c()) + "\n};\n\n" + + return info_data + + def to_c_img_single(self, bg_entry: RoomShapeImageEntry, include_dir: str): + """Gets C representation of image data""" + bits_per_value = 64 + + data = CData() + + # .h + data.header = f"extern u{bits_per_value} {bg_entry.name}[];\n" + + # .c + data.source = ( + # This is to force 8 byte alignment + (f"Gfx {bg_entry.name}_aligner[] = " + "{ gsSPEndDisplayList() };\n" if bits_per_value != 64 else "") + + (f"u{bits_per_value} {bg_entry.name}[SCREEN_WIDTH * SCREEN_HEIGHT / 4]" + " = {\n") + + f'#include "{include_dir + bg_entry.get_filename()}.inc.c"' + + "\n};\n\n" + ) + return data + + def copy_bg_image(self, entry: RoomShapeImageEntry, export_path: str): + jpeg_compatibility = False + image = entry.image + image_filename = entry.get_filename() + if jpeg_compatibility: + is_packed = image.packed_file is not None + if not is_packed: + image.pack() + oldpath = image.filepath + old_format = image.file_format + try: + image.filepath = bpy.path.abspath(os.path.join(export_path, image_filename)) + image.file_format = "JPEG" + image.save() + if not is_packed: + image.unpack() + image.filepath = oldpath + image.file_format = old_format + except Exception as e: + image.filepath = oldpath + image.file_format = old_format + raise Exception(str(e)) + else: + filepath = bpy.path.abspath(os.path.join(export_path, image_filename)) + shutil.copy(bpy.path.abspath(image.filepath), filepath) + + def copy_bg_images(self, export_path: str): + raise PluginError("BG image copying not handled!") + + +@dataclass +class RoomShapeImageSingle(RoomShapeImageBase): + bg_entry: RoomShapeImageEntry + + def get_amount_type(self): + return "ROOM_SHAPE_IMAGE_AMOUNT_SINGLE" + + def to_c(self): + """Returns the single background image mode variable""" + + info_data = CData() + list_name = f"RoomShapeImageSingle {self.name}" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + info_data.source = ( + f"{list_name} = {{\n" + + f"{indent}{{ {self.get_type()}, {self.get_amount_type()}, &{self.dl_entry_array_name}, }},\n" + + self.bg_entry.to_c_single() + + f"\n}};\n\n" + ) + + info_data.append(self.occlusion_planes.to_c()) + info_data.append(self.to_c_dl_entries()) + + return info_data + + def to_c_img(self, include_dir: str): + """Returns the image data for image room shapes""" + + return self.to_c_img_single(self.bg_entry, include_dir) + + def copy_bg_images(self, export_path: str): + self.copy_bg_image(self.bg_entry, export_path) + + +@dataclass +class RoomShapeImageMulti(RoomShapeImageBase): + bg_entry_array_name: str + bg_entries: list[RoomShapeImageEntry] = field(init=False, default_factory=list) + + def get_amount_type(self): + return "ROOM_SHAPE_IMAGE_AMOUNT_MULTI" + + def to_c_bg_entries(self) -> CData: + info_data = CData() + list_name = f"RoomShapeImageMultiBgEntry {self.bg_entry_array_name}[{len(self.bg_entries)}]" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + info_data.source = ( + list_name + " = {\n" + f"".join(elem.to_c_multi(i) for i, elem in enumerate(self.bg_entries)) + "};\n\n" + ) + + return info_data + + def to_c(self) -> CData: + """Returns the multiple background image mode variable""" + + info_data = CData() + list_name = f"RoomShapeImageMulti {self.name}" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + info_data.source = ( + (list_name + " = {\n" + indent) + + f",\n{indent}".join( + [ + "{ " + f"{self.get_type()}, {self.get_amount_type()}, &{self.dl_entry_array_name}" + " }", + f"ARRAY_COUNT({self.bg_entry_array_name})", + f"{self.bg_entry_array_name}", + ] + ) + + ",\n};\n\n" + ) + + info_data.append(self.occlusion_planes.to_c()) + info_data.append(self.to_c_bg_entries()) + info_data.append(self.to_c_dl_entries()) + + return info_data + + def to_c_img(self, include_dir: str): + """Returns the image data for image room shapes""" + + data = CData() + + for entry in self.bg_entries: + data.append(self.to_c_img_single(entry, include_dir)) + + return data + + def copy_bg_images(self, export_path: str): + for bg_entry in self.bg_entries: + self.copy_bg_image(bg_entry, export_path) + + +@dataclass +class RoomShapeCullableEntry( + RoomShapeDListsEntry +): # inheritance is due to functional relation, previously OOTRoomMeshGroup + bounds_sphere_center: tuple[float, float, float] + bounds_sphere_radius: float + + def to_c(self): + center = ", ".join([f"{n}" for n in self.bounds_sphere_center]) + opaque = self.opaque.name if self.opaque else "NULL" + transparent = self.transparent.name if self.transparent else "NULL" + return f" {{ {center} }}, {self.bounds_sphere_radius}, {opaque}, {transparent}" + + +@dataclass +class RoomShapeCullable(RoomShape): + def get_type(self): + return "ROOM_SHAPE_TYPE_CULLABLE" + + def add_dl_entry(self, cull_group: Optional[CullGroup] = None) -> RoomShapeDListsEntry: + if cull_group is None: + raise PluginError("RoomShapeCullable should always be provided a cull group.") + entry = RoomShapeCullableEntry( + f"{self.name}_entry_{len(self.dl_entries)}", cull_group.position, cull_group.cullDepth + ) + self.dl_entries.append(entry) + return entry + + def to_c_dl_entries(self): + info_data = CData() + list_name = f"RoomShapeCullableEntry {self.dl_entry_array_name}[{len(self.dl_entries)}]" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + info_data.source = ( + (list_name + " = {\n") + + (indent + f",\n{indent}".join("{ " + elem.to_c() + " }" for elem in self.dl_entries)) + + "\n};\n\n" + ) + + return info_data + + def to_c(self): + """Returns the C data for the room shape""" + + info_data = CData() + list_name = f"RoomShapeCullable {self.name}" + + # .h + info_data.header = f"extern {list_name};\n" + + # .c + num_entries = f"ARRAY_COUNTU({self.dl_entry_array_name})" # U? see ddan_room_0 + info_data.source = ( + (list_name + " = {\n" + indent) + + f",\n{indent}".join( + [ + f"{self.get_type()}", + num_entries, + f"{self.dl_entry_array_name}", + f"{self.dl_entry_array_name} + {num_entries}", + ] + ) + + "\n};\n\n" + ) + + info_data.append(self.occlusion_planes.to_c()) + info_data.append(self.to_c_dl_entries()) + + return info_data + + +class RoomShapeUtility: + @staticmethod + def create_shape( + scene_name: str, + room_name: str, + room_shape_type: str, + model: OOTModel, + transform: Matrix, + sceneObj: Object, + roomObj: Object, + saveTexturesAsPNG: bool, + props: OOTRoomHeaderProperty, + ): + name = f"{room_name}_shapeHeader" + dl_name = f"{room_name}_shapeDListsEntry" + room_shape = None + + if room_shape_type == "ROOM_SHAPE_TYPE_CULLABLE": + room_shape = RoomShapeCullable(name, model, dl_name) + elif room_shape_type == "ROOM_SHAPE_TYPE_NORMAL": + room_shape = RoomShapeNormal(name, model, dl_name) + elif room_shape_type == "ROOM_SHAPE_TYPE_IMAGE": + if len(props.bgImageList) == 0: + raise PluginError("Cannot create room shape of type image without any images.") + elif len(props.bgImageList) == 1: + room_shape = RoomShapeImageSingle( + name, model, dl_name, RoomShapeImageEntry.new(scene_name, props.bgImageList[0]) + ) + else: + bg_name = f"{room_name}_shapeMultiBg" + room_shape = RoomShapeImageMulti(name, model, dl_name, bg_name) + for bg_image in props.bgImageList: + room_shape.bg_entries.append(RoomShapeImageEntry.new(scene_name, bg_image)) + + pos, _, scale, _ = Utility.getConvertedTransform(transform, sceneObj, roomObj, True) + cull_group = CullGroup(pos, scale, roomObj.ootRoomHeader.defaultCullDistance) + dl_entry = room_shape.add_dl_entry(cull_group) + boundingBox = BoundingBox() + ootProcessMesh( + room_shape, + dl_entry, + sceneObj, + roomObj, + transform, + not saveTexturesAsPNG, + None, + boundingBox, + ) + if isinstance(dl_entry, RoomShapeCullableEntry): + dl_entry.bounds_sphere_center, dl_entry.bounds_sphere_radius = boundingBox.getEnclosingSphere() + + if bpy.context.scene.f3d_type == "F3DEX3": + addOcclusionQuads(roomObj, room_shape.occlusion_planes, True, transform @ sceneObj.matrix_world.inverted()) + + room_shape.terminate_dls() + room_shape.remove_unused_entries() + return room_shape + + +class BoundingBox: + def __init__(self): + self.minPoint = None + self.maxPoint = None + self.points = [] + + def addPoint(self, point: tuple[float, float, float]): + if self.minPoint is None: + self.minPoint = list(point[:]) + else: + for i in range(3): + if point[i] < self.minPoint[i]: + self.minPoint[i] = point[i] + if self.maxPoint is None: + self.maxPoint = list(point[:]) + else: + for i in range(3): + if point[i] > self.maxPoint[i]: + self.maxPoint[i] = point[i] + self.points.append(point) + + def addMeshObj(self, obj: bpy.types.Object, transform: Matrix): + mesh = obj.data + for vertex in mesh.vertices: + self.addPoint(transform @ vertex.co) + + def getEnclosingSphere(self) -> tuple[float, float]: + centroid = (Vector(self.minPoint) + Vector(self.maxPoint)) / 2 + radius = 0 + for point in self.points: + distance = (Vector(point) - centroid).length + if distance > radius: + radius = distance + + transformedCentroid = [round(value) for value in centroid] + transformedRadius = round(radius) + return transformedCentroid, transformedRadius + + +# This function should be called on a copy of an object +# The copy will have modifiers / scale applied and will be made single user +# When we duplicated obj hierarchy we stripped all ignore_renders from hierarchy. +def ootProcessMesh( + roomShape: RoomShape, + dlEntry: RoomShapeDListsEntry, + sceneObj, + obj, + transformMatrix, + convertTextureData, + LODHierarchyObject, + boundingBox: BoundingBox, +): + relativeTransform = transformMatrix @ sceneObj.matrix_world.inverted() @ obj.matrix_world + translation, rotation, scale = relativeTransform.decompose() + + if obj.type == "EMPTY" and obj.ootEmptyType == "Cull Group": + if LODHierarchyObject is not None: + raise PluginError( + obj.name + + " cannot be used as a cull group because it is " + + "in the sub-hierarchy of the LOD group empty " + + LODHierarchyObject.name + ) + + cullProp = obj.ootCullGroupProperty + checkUniformScale(scale, obj) + dlEntry = roomShape.add_dl_entry( + CullGroup( + ootConvertTranslation(translation), + scale if cullProp.sizeControlsCull else [cullProp.manualRadius], + obj.empty_display_size if cullProp.sizeControlsCull else 1, + ) + ) + + elif obj.type == "MESH" and not obj.ignore_render: + triConverterInfo = TriangleConverterInfo(obj, None, roomShape.model.f3d, relativeTransform, getInfoDict(obj)) + fMeshes = saveStaticModel( + triConverterInfo, + roomShape.model, + obj, + relativeTransform, + roomShape.model.name, + convertTextureData, + False, + "oot", + ) + if fMeshes is not None: + for drawLayer, fMesh in fMeshes.items(): + dlEntry.add_dl_call(fMesh.draw, drawLayer) + + boundingBox.addMeshObj(obj, relativeTransform) + + alphabeticalChildren = sorted(obj.children, key=lambda childObj: childObj.original_name.lower()) + for childObj in alphabeticalChildren: + if childObj.type == "EMPTY" and childObj.ootEmptyType == "LOD": + ootProcessLOD( + roomShape, + dlEntry, + sceneObj, + childObj, + transformMatrix, + convertTextureData, + LODHierarchyObject, + boundingBox, + ) + else: + ootProcessMesh( + roomShape, + dlEntry, + sceneObj, + childObj, + transformMatrix, + convertTextureData, + LODHierarchyObject, + boundingBox, + ) + + +def ootProcessLOD( + roomShape: RoomShape, + dlEntry: RoomShapeDListsEntry, + sceneObj, + obj, + transformMatrix, + convertTextureData, + LODHierarchyObject, + boundingBox: BoundingBox, +): + relativeTransform = transformMatrix @ sceneObj.matrix_world.inverted() @ obj.matrix_world + translation, rotation, scale = relativeTransform.decompose() + ootTranslation = ootConvertTranslation(translation) + + LODHierarchyObject = obj + name = toAlnum(roomShape.model.name + "_" + obj.name + "_lod") + opaqueLOD = roomShape.model.addLODGroup(name + "_opaque", ootTranslation, obj.f3d_lod_always_render_farthest) + transparentLOD = roomShape.model.addLODGroup( + name + "_transparent", ootTranslation, obj.f3d_lod_always_render_farthest + ) + + index = 0 + for childObj in obj.children: + # This group will not be converted to C directly, but its display lists will be converted through the FLODGroup. + childDLEntry = RoomShapeDListsEntry(f"{name}{str(index)}") + index += 1 + + if childObj.type == "EMPTY" and childObj.ootEmptyType == "LOD": + ootProcessLOD( + roomShape, + childDLEntry, + sceneObj, + childObj, + transformMatrix, + convertTextureData, + LODHierarchyObject, + boundingBox, + ) + else: + ootProcessMesh( + roomShape, + childDLEntry, + sceneObj, + childObj, + transformMatrix, + convertTextureData, + LODHierarchyObject, + boundingBox, + ) + + # We handle case with no geometry, for the cases where we have "gaps" in the LOD hierarchy. + # This can happen if a LOD does not use transparency while the levels above and below it does. + childDLEntry.create_dls() + childDLEntry.terminate_dls() + + # Add lod AFTER processing hierarchy, so that DLs will be built by then + opaqueLOD.add_lod(childDLEntry.opaque, childObj.f3d_lod_z * bpy.context.scene.ootBlenderScale) + transparentLOD.add_lod(childDLEntry.transparent, childObj.f3d_lod_z * bpy.context.scene.ootBlenderScale) + + opaqueLOD.create_data() + transparentLOD.create_data() + + dlEntry.add_dl_call(opaqueLOD.draw, "Opaque") + dlEntry.add_dl_call(transparentLOD.draw, "Transparent") diff --git a/fast64_internal/oot/exporter/scene/__init__.py b/fast64_internal/oot/exporter/scene/__init__.py new file mode 100644 index 000000000..f8b83cdd2 --- /dev/null +++ b/fast64_internal/oot/exporter/scene/__init__.py @@ -0,0 +1,242 @@ +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/exporter/scene/actors.py b/fast64_internal/oot/exporter/scene/actors.py new file mode 100644 index 000000000..8f7735b6e --- /dev/null +++ b/fast64_internal/oot/exporter/scene/actors.py @@ -0,0 +1,236 @@ +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 Utility +from ..actor import Actor + + +@dataclass +class TransitionActor(Actor): + """Defines a Transition Actor""" + + isRoomTransition: Optional[bool] = field(init=False, default=None) + roomFrom: Optional[int] = field(init=False, default=None) + roomTo: Optional[int] = field(init=False, default=None) + cameraFront: Optional[str] = field(init=False, default=None) + cameraBack: Optional[str] = field(init=False, default=None) + + def getEntryC(self): + """Returns a single transition actor entry""" + + sides = [(self.roomFrom, self.cameraFront), (self.roomTo, self.cameraBack)] + roomData = "{ " + ", ".join(f"{room}, {cam}" for room, cam in sides) + " }" + posData = "{ " + ", ".join(f"{round(pos)}" for pos in self.pos) + " }" + + actorInfos = [roomData, self.id, posData, self.rot, self.params] + infoDescs = ["Room & Cam Index (Front, Back)", "Actor ID", "Position", "Rotation Y", "Parameters"] + + return ( + (indent + f"// {self.name}\n" + indent if self.name != "" else "") + + "{\n" + + ",\n".join((indent * 2) + f"/* {desc:30} */ {info}" for desc, info in zip(infoDescs, actorInfos)) + + ("\n" + indent + "},\n") + ) + + +@dataclass +class SceneTransitionActors: + name: str + entries: list[TransitionActor] + + @staticmethod + def new(name: str, sceneObj: Object, transform: Matrix, headerIndex: int): + # we need to get the corresponding room index if a transition actor + # do not change rooms + roomObjList = getObjectList(sceneObj.children_recursive, "EMPTY", "Room") + actorToRoom: dict[Object, Object] = {} + for obj in roomObjList: + for childObj in obj.children_recursive: + if childObj.type == "EMPTY" and childObj.ootEmptyType == "Transition Actor": + actorToRoom[childObj] = obj + + actorObjList = getObjectList(sceneObj.children_recursive, "EMPTY", "Transition Actor") + actorObjList.sort(key=lambda obj: actorToRoom[obj].ootRoomHeader.roomIndex) + + entries: list[TransitionActor] = [] + for obj in actorObjList: + transActorProp = obj.ootTransitionActorProperty + if ( + Utility.isCurrentHeaderValid(transActorProp.actor.headerSettings, headerIndex) + and transActorProp.actor.actorID != "None" + ): + pos, rot, _, _ = Utility.getConvertedTransform(transform, sceneObj, obj, True) + transActor = TransitionActor() + + if transActorProp.isRoomTransition: + if transActorProp.fromRoom is None or transActorProp.toRoom is None: + raise PluginError("ERROR: Missing room empty object assigned to transition.") + fromIndex = transActorProp.fromRoom.ootRoomHeader.roomIndex + toIndex = transActorProp.toRoom.ootRoomHeader.roomIndex + else: + fromIndex = toIndex = actorToRoom[obj].ootRoomHeader.roomIndex + front = (fromIndex, Utility.getPropValue(transActorProp, "cameraTransitionFront")) + back = (toIndex, Utility.getPropValue(transActorProp, "cameraTransitionBack")) + + if transActorProp.actor.actorID == "Custom": + transActor.id = transActorProp.actor.actorIDCustom + else: + transActor.id = transActorProp.actor.actorID + + transActor.name = ( + ootData.actorData.actorsByID[transActorProp.actor.actorID].name.replace( + f" - {transActorProp.actor.actorID.removeprefix('ACTOR_')}", "" + ) + if transActorProp.actor.actorID != "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.roomFrom, transActor.cameraFront = front + transActor.roomTo, transActor.cameraBack = back + entries.append(transActor) + return SceneTransitionActors(name, entries) + + def getCmd(self): + """Returns the transition actor list scene command""" + + return indent + f"SCENE_CMD_TRANSITION_ACTOR_LIST({len(self.entries)}, {self.name}),\n" + + def getC(self): + """Returns the transition actor array""" + + transActorList = CData() + listName = f"TransitionActorEntry {self.name}" + + # .h + transActorList.header = f"extern {listName}[];\n" + + # .c + transActorList.source = ( + (f"{listName}[]" + " = {\n") + "\n".join(transActor.getEntryC() for transActor in self.entries) + "};\n\n" + ) + + return transActorList + + +@dataclass +class EntranceActor(Actor): + """Defines an Entrance Actor""" + + roomIndex: Optional[int] = field(init=False, default=None) + spawnIndex: Optional[int] = field(init=False, default=None) + + def getEntryC(self): + """Returns a single spawn entry""" + + return indent + "{ " + f"{self.spawnIndex}, {self.roomIndex}" + " },\n" + + +@dataclass +class SceneEntranceActors: + name: str + entries: list[EntranceActor] + + @staticmethod + def new(name: str, sceneObj: Object, transform: Matrix, headerIndex: int): + """Returns the entrance actor list based on empty objects with the type 'Entrance'""" + + entranceActorFromIndex: dict[int, EntranceActor] = {} + 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" + ): + 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_')}", "" + ) + if entranceProp.actor.actorID != "Custom" + else "Custom Actor" + ) + + entranceActor.id = "ACTOR_PLAYER" if not entranceProp.customActor else entranceProp.actor.actorIDCustom + entranceActor.pos = pos + entranceActor.rot = ", ".join(f"DEG_TO_BINANG({(r * (180 / 0x8000)):.3f})" for r in rot) + entranceActor.params = entranceProp.actor.actorParam + if entranceProp.tiedRoom is not None: + entranceActor.roomIndex = entranceProp.tiedRoom.ootRoomHeader.roomIndex + else: + raise PluginError("ERROR: Missing room empty object assigned to the entrance.") + entranceActor.spawnIndex = entranceProp.spawnIndex + + if entranceProp.spawnIndex not in entranceActorFromIndex: + entranceActorFromIndex[entranceProp.spawnIndex] = entranceActor + else: + raise PluginError(f"ERROR: Repeated Spawn Index: {entranceProp.spawnIndex}") + + entranceActorFromIndex = dict(sorted(entranceActorFromIndex.items())) + if list(entranceActorFromIndex.keys()) != list(range(len(entranceActorFromIndex))): + raise PluginError("ERROR: The spawn indices are not consecutive!") + + return SceneEntranceActors(name, list(entranceActorFromIndex.values())) + + 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" + + def getC(self): + """Returns the spawn actor array""" + + spawnActorList = CData() + listName = f"ActorEntry {self.name}" + + # .h + spawnActorList.header = f"extern {listName}[];\n" + + # .c + spawnActorList.source = ( + (f"{listName}[]" + " = {\n") + "".join(entrance.getActorEntry() for entrance in self.entries) + "};\n\n" + ) + + return spawnActorList + + +@dataclass +class SceneSpawns(Utility): + """This class handles scene actors (transition actors and entrance actors)""" + + name: str + entries: list[EntranceActor] + + 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" + + def getC(self): + """Returns the spawn array""" + + spawnList = CData() + listName = f"Spawn {self.name}" + + # .h + spawnList.header = f"extern {listName}[];\n" + + # .c + spawnList.source = ( + (f"{listName}[]" + " = {\n") + + (indent + "// { Spawn Actor List Index, Room Index }\n") + + "".join(entrance.getEntryC() for entrance in self.entries) + + "};\n\n" + ) + + return spawnList diff --git a/fast64_internal/oot/exporter/scene/general.py b/fast64_internal/oot/exporter/scene/general.py new file mode 100644 index 000000000..2a1638dea --- /dev/null +++ b/fast64_internal/oot/exporter/scene/general.py @@ -0,0 +1,251 @@ +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 Utility + + +@dataclass +class EnvLightSettings: + """This class defines the information of one environment light setting""" + + envLightMode: str + ambientColor: tuple[int, int, int] + light1Color: tuple[int, int, int] + light1Dir: tuple[int, int, int] + light2Color: tuple[int, int, int] + light2Dir: tuple[int, int, int] + fogColor: tuple[int, int, int] + fogNear: int + zFar: int + blendRate: int + + def getBlendFogNear(self): + """Returns the packed blend rate and fog near values""" + + return f"(({self.blendRate} << 10) | {self.fogNear})" + + def getColorValues(self, vector: tuple[int, int, int]): + """Returns and formats color values""" + + return ", ".join(f"{v:5}" for v in vector) + + def getDirectionValues(self, vector: tuple[int, int, int]): + """Returns and formats direction values""" + + return ", ".join(f"{v - 0x100 if v > 0x7F else v:5}" for v in vector) + + def getEntryC(self, index: int): + """Returns an environment light entry""" + + isLightingCustom = self.envLightMode == "Custom" + + vectors = [ + (self.ambientColor, "Ambient Color", self.getColorValues), + (self.light1Dir, "Diffuse0 Direction", self.getDirectionValues), + (self.light1Color, "Diffuse0 Color", self.getColorValues), + (self.light2Dir, "Diffuse1 Direction", self.getDirectionValues), + (self.light2Color, "Diffuse1 Color", self.getColorValues), + (self.fogColor, "Fog Color", self.getColorValues), + ] + + fogData = [ + (self.getBlendFogNear(), "Blend Rate & Fog Near"), + (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 + "{\n") + + "".join( + indent * 2 + f"{'{ ' + vecToC(vector) + ' },':26} // {desc}\n" for (vector, desc, vecToC) in vectors + ) + + "".join(indent * 2 + f"{fogValue + ',':26} // {fogDesc}\n" for fogValue, fogDesc in fogData) + + (indent + "},\n") + ) + + return lightData + + +@dataclass +class SceneLighting: + """This class hosts lighting data""" + + name: str + envLightMode: str + settings: list[EnvLightSettings] + + @staticmethod + def new(name: str, props: OOTSceneHeaderProperty): + envLightMode = Utility.getPropValue(props, "skyboxLighting") + lightList: list[OOTLightProperty] = [] + settings: list[EnvLightSettings] = [] + + if envLightMode == "LIGHT_MODE_TIME": + todLights = props.timeOfDayLights + lightList = [todLights.dawn, todLights.day, todLights.dusk, todLights.night] + else: + lightList = props.lightList + + 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, + ) + ) + return SceneLighting(name, envLightMode, settings) + + def getCmd(self): + """Returns the env light settings scene command""" + + return ( + indent + "SCENE_CMD_ENV_LIGHT_SETTINGS(" + ) + f"{len(self.settings)}, {self.name if len(self.settings) > 0 else 'NULL'}),\n" + + def getC(self): + """Returns a ``CData`` containing the C data of env. light settings""" + + lightSettingsC = CData() + lightName = f"EnvLightSettings {self.name}[{len(self.settings)}]" + + # .h + lightSettingsC.header = f"extern {lightName};\n" + + # .c + lightSettingsC.source = ( + (lightName + " = {\n") + "".join(light.getEntryC(i) for i, light in enumerate(self.settings)) + "};\n\n" + ) + + return lightSettingsC + + +@dataclass +class SceneInfos: + """This class stores various scene header informations""" + + ### General ### + + keepObjectID: str + naviHintType: str + drawConfig: str + appendNullEntrance: bool + useDummyRoomList: bool + + ### Skybox And Sound ### + + # Skybox + skyboxID: str + skyboxConfig: str + + # Sound + sequenceID: str + ambienceID: str + specID: str + + ### Camera And World Map ### + + # World Map + worldMapLocation: str + + # Camera + sceneCamType: str + + @staticmethod + def new(props: OOTSceneHeaderProperty, sceneObj: Object): + return SceneInfos( + Utility.getPropValue(props, "globalObject"), + Utility.getPropValue(props, "naviCup"), + Utility.getPropValue(props.sceneTableEntry, "drawConfig"), + props.appendNullEntrance, + sceneObj.fast64.oot.scene.write_dummy_room_list, + Utility.getPropValue(props, "skyboxID"), + Utility.getPropValue(props, "skyboxCloudiness"), + Utility.getPropValue(props, "musicSeq"), + Utility.getPropValue(props, "nightSeq"), + Utility.getPropValue(props, "audioSessionPreset"), + Utility.getPropValue(props, "mapLocation"), + Utility.getPropValue(props, "cameraMode"), + ) + + def getCmds(self, lights: SceneLighting): + """Returns the sound settings, misc settings, special files and skybox settings scene commands""" + + return ( + indent + + f",\n{indent}".join( + [ + f"SCENE_CMD_SOUND_SETTINGS({self.specID}, {self.ambienceID}, {self.sequenceID})", + f"SCENE_CMD_MISC_SETTINGS({self.sceneCamType}, {self.worldMapLocation})", + f"SCENE_CMD_SPECIAL_FILES({self.naviHintType}, {self.keepObjectID})", + f"SCENE_CMD_SKYBOX_SETTINGS({self.skyboxID}, {self.skyboxConfig}, {lights.envLightMode})", + ] + ) + + ",\n" + ) + + +@dataclass +class SceneExits(Utility): + """This class hosts exit data""" + + name: str + exitList: list[tuple[int, str]] + + @staticmethod + def new(name: str, props: OOTSceneHeaderProperty): + # TODO: proper implementation of exits + + exitList: list[tuple[int, str]] = [] + for i, exitProp in enumerate(props.exitList): + if exitProp.exitIndex != "Custom": + raise PluginError("ERROR: Exits are unfinished, please use 'Custom'.") + exitList.append((i, exitProp.exitIndexCustom)) + return SceneExits(name, exitList) + + def getCmd(self): + """Returns the exit list scene command""" + + return indent + f"SCENE_CMD_EXIT_LIST({self.name}),\n" + + def getC(self): + """Returns a ``CData`` containing the C data of the exit array""" + + exitListC = CData() + listName = f"u16 {self.name}[{len(self.exitList)}]" + + # .h + exitListC.header = f"extern {listName};\n" + + # .c + exitListC.source = ( + (listName + " = {\n") + # @TODO: use the enum name instead of the raw index + + "\n".join(indent + f"{value}," for (_, value) in self.exitList) + + "\n};\n\n" + ) + + return exitListC diff --git a/fast64_internal/oot/exporter/scene/header.py b/fast64_internal/oot/exporter/scene/header.py new file mode 100644 index 000000000..72bc8d32f --- /dev/null +++ b/fast64_internal/oot/exporter/scene/header.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass, field +from typing import Optional +from mathutils import Matrix +from bpy.types import Object +from ....utility import CData +from ...scene.properties import OOTSceneHeaderProperty +from ..cutscene import SceneCutscene +from .general import SceneLighting, SceneInfos, SceneExits +from .actors import SceneTransitionActors, SceneEntranceActors, SceneSpawns +from .pathways import ScenePathways + + +@dataclass +class SceneHeader: + """This class defines a scene header""" + + name: str + infos: Optional[SceneInfos] + lighting: Optional[SceneLighting] + cutscene: Optional[SceneCutscene] + exits: Optional[SceneExits] + transitionActors: Optional[SceneTransitionActors] + entranceActors: Optional[SceneEntranceActors] + spawns: Optional[SceneSpawns] + path: Optional[ScenePathways] + + @staticmethod + def new( + name: str, props: OOTSceneHeaderProperty, sceneObj: Object, transform: Matrix, headerIndex: int, useMacros: bool + ): + entranceActors = SceneEntranceActors.new(f"{name}_playerEntryList", sceneObj, transform, headerIndex) + return SceneHeader( + name, + SceneInfos.new(props, sceneObj), + SceneLighting.new(f"{name}_lightSettings", props), + SceneCutscene.new(props, headerIndex, useMacros), + SceneExits.new(f"{name}_exitList", props), + SceneTransitionActors.new(f"{name}_transitionActors", sceneObj, transform, headerIndex), + entranceActors, + SceneSpawns(f"{name}_entranceList", entranceActors.entries), + ScenePathways.new(f"{name}_pathway", sceneObj, transform, headerIndex), + ) + + def getC(self): + """Returns the ``CData`` containing the header's data""" + + headerData = CData() + + # Write the spawn position list data and the entrance list + if len(self.entranceActors.entries) > 0: + headerData.append(self.entranceActors.getC()) + headerData.append(self.spawns.getC()) + + # Write the transition actor list data + if len(self.transitionActors.entries) > 0: + headerData.append(self.transitionActors.getC()) + + # Write the exit list + if len(self.exits.exitList) > 0: + headerData.append(self.exits.getC()) + + # Write the light data + if len(self.lighting.settings) > 0: + headerData.append(self.lighting.getC()) + + # Write the path data, if used + if len(self.path.pathList) > 0: + headerData.append(self.path.getC()) + + return headerData + + +@dataclass +class SceneAlternateHeader: + """This class stores alternate header data for the scene""" + + name: str + + childNight: Optional[SceneHeader] = field(init=False, default=None) + adultDay: Optional[SceneHeader] = field(init=False, default=None) + adultNight: Optional[SceneHeader] = field(init=False, default=None) + cutscenes: list[SceneHeader] = field(init=False, default_factory=list) diff --git a/fast64_internal/oot/exporter/scene/pathways.py b/fast64_internal/oot/exporter/scene/pathways.py new file mode 100644 index 000000000..331af8dbe --- /dev/null +++ b/fast64_internal/oot/exporter/scene/pathways.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass, field +from mathutils import Matrix +from bpy.types import Object +from ....utility import PluginError, CData, indent +from ...oot_utility import getObjectList +from ..utility import Utility + + +@dataclass +class Path: + """This class defines a pathway""" + + name: str + points: list[tuple[int, int, int]] = field(default_factory=list) + + def getC(self): + """Returns the pathway position array""" + + pathData = CData() + pathName = f"Vec3s {self.name}" + + # .h + pathData.header = f"extern {pathName}[];\n" + + # .c + pathData.source = ( + f"{pathName}[]" + + " = {\n" + + "\n".join( + indent + "{ " + ", ".join(f"{round(curPoint):5}" for curPoint in point) + " }," for point in self.points + ) + + "\n};\n\n" + ) + + return pathData + + +@dataclass +class ScenePathways: + """This class hosts pathways array data""" + + name: str + pathList: list[Path] + + @staticmethod + def new(name: str, sceneObj: Object, transform: Matrix, headerIndex: int): + pathFromIndex: dict[int, Path] = {} + pathObjList = getObjectList(sceneObj.children_recursive, "CURVE", splineType="Path") + + for obj in pathObjList: + relativeTransform = transform @ sceneObj.matrix_world.inverted() @ obj.matrix_world + pathProps = obj.ootSplineProperty + isHeaderValid = Utility.isCurrentHeaderValid(pathProps.headerSettings, headerIndex) + if isHeaderValid and Utility.validateCurveData(obj): + if pathProps.index not in pathFromIndex: + pathFromIndex[pathProps.index] = Path( + f"{name}List{pathProps.index:02}", + [relativeTransform @ point.co.xyz for point in obj.data.splines[0].points], + ) + else: + raise PluginError(f"ERROR: Path index already used ({obj.name})") + + pathFromIndex = dict(sorted(pathFromIndex.items())) + if list(pathFromIndex.keys()) != list(range(len(pathFromIndex))): + raise PluginError("ERROR: Path indices are not consecutive!") + + return ScenePathways(name, list(pathFromIndex.values())) + + def getCmd(self): + """Returns the path list scene command""" + + return indent + f"SCENE_CMD_PATH_LIST({self.name}),\n" if len(self.pathList) > 0 else "" + + def getC(self): + """Returns a ``CData`` containing the C data of the pathway array""" + + pathData = CData() + pathListData = CData() + listName = f"Path {self.name}[{len(self.pathList)}]" + + # .h + pathListData.header = f"extern {listName};\n" + + # .c + pathListData.source = listName + " = {\n" + + for path in self.pathList: + pathListData.source += indent + "{ " + f"ARRAY_COUNTU({path.name}), {path.name}" + " },\n" + pathData.append(path.getC()) + + pathListData.source += "};\n\n" + pathData.append(pathListData) + + return pathData diff --git a/fast64_internal/oot/exporter/scene/rooms.py b/fast64_internal/oot/exporter/scene/rooms.py new file mode 100644 index 000000000..6948ef6bb --- /dev/null +++ b/fast64_internal/oot/exporter/scene/rooms.py @@ -0,0 +1,99 @@ +from dataclasses import dataclass +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 ..room import Room + + +@dataclass +class RoomEntries: + name: str + entries: list[Room] + + @staticmethod + def new(name: str, sceneName: str, model: OOTModel, sceneObj: Object, transform: Matrix, saveTexturesAsPNG: bool): + """Returns the room list from empty objects with the type 'Room'""" + + roomDict: dict[int, Room] = {} + roomObjs = getObjectList(sceneObj.children_recursive, "EMPTY", "Room") + + if len(roomObjs) == 0: + raise PluginError("ERROR: The scene has no child empties with the 'Room' empty type.") + + for roomObj in roomObjs: + roomHeader = roomObj.ootRoomHeader + roomIndex = roomHeader.roomIndex + + if roomIndex in roomDict: + raise PluginError(f"ERROR: Room index {roomIndex} used more than once!") + + roomName = f"{sceneName}_room_{roomIndex}" + roomDict[roomIndex] = Room.new( + roomName, + transform, + sceneObj, + roomObj, + roomHeader.roomShape, + model.addSubModel( + OOTModel( + f"{roomName}_dl", + model.DLFormat, + None, + ) + ), + roomIndex, + sceneName, + saveTexturesAsPNG, + ) + + for i in range(min(roomDict.keys()), len(roomDict)): + if i not in roomDict: + raise PluginError(f"Room indices are not consecutive. Missing room index: {i}") + + return RoomEntries(name, [roomDict[i] for i in range(min(roomDict.keys()), len(roomDict))]) + + def getCmd(self): + """Returns the room list scene command""" + + return indent + f"SCENE_CMD_ROOM_LIST({len(self.entries)}, {self.name}),\n" + + def getC(self, useDummyRoomList: bool): + """Returns the ``CData`` containing the room list array""" + + roomList = CData() + listName = f"RomFile {self.name}[]" + + # generating segment rom names for every room + segNames = [] + for i in range(len(self.entries)): + roomName = self.entries[i].name + segNames.append((f"_{roomName}SegmentRomStart", f"_{roomName}SegmentRomEnd")) + + # .h + roomList.header += f"extern {listName};\n" + + if not useDummyRoomList: + # Write externs for rom segments + roomList.header += "".join( + f"extern u8 {startName}[];\n" + f"extern u8 {stopName}[];\n" for startName, stopName in segNames + ) + + # .c + roomList.source = listName + " = {\n" + + if useDummyRoomList: + roomList.source = ( + "// Dummy room list\n" + roomList.source + ((indent + "{ NULL, NULL },\n") * len(self.entries)) + ) + else: + roomList.source += ( + " },\n".join( + indent + "{ " + f"(uintptr_t){startName}, (uintptr_t){stopName}" for startName, stopName in segNames + ) + + " },\n" + ) + + roomList.source += "};\n\n" + return roomList diff --git a/fast64_internal/oot/exporter/utility.py b/fast64_internal/oot/exporter/utility.py new file mode 100644 index 000000000..16a76590f --- /dev/null +++ b/fast64_internal/oot/exporter/utility.py @@ -0,0 +1,95 @@ +from math import radians +from mathutils import Quaternion, Matrix +from bpy.types import Object +from ...utility import PluginError, indent +from ..oot_utility import ootConvertTranslation, ootConvertRotation +from ..actor.properties import OOTActorHeaderProperty + + +altHeaderList = ["childNight", "adultDay", "adultNight"] + + +class Utility: + """This class hosts different functions used across different sub-systems of this exporter""" + + @staticmethod + def validateCurveData(curveObj: Object): + """Performs safety checks related to curve objects""" + + curveData = curveObj.data + if curveObj.type != "CURVE" or curveData.splines[0].type != "NURBS": + # Curve was likely not intended to be exported + return False + + if len(curveData.splines) != 1: + # Curve was intended to be exported but has multiple disconnected segments + raise PluginError(f"Exported curves should have only one single segment, found {len(curveData.splines)}") + + return True + + @staticmethod + def roundPosition(position) -> tuple[int, int, int]: + """Returns the rounded position values""" + + return (round(position[0]), round(position[1]), round(position[2])) + + @staticmethod + def isCurrentHeaderValid(headerSettings: OOTActorHeaderProperty, headerIndex: int): + """Checks if the an alternate header can be used""" + + preset = headerSettings.sceneSetupPreset + + if preset == "All Scene Setups" or (preset == "All Non-Cutscene Scene Setups" and headerIndex < 4): + return True + + if preset == "Custom": + for i, header in enumerate(["childDay"] + altHeaderList): + if getattr(headerSettings, f"{header}Header") and i == headerIndex: + return True + + for csHeader in headerSettings.cutsceneHeaders: + if csHeader.headerIndex == headerIndex: + return True + + return False + + @staticmethod + def getPropValue(data, propName: str): + """Returns a property's value based on if the value is 'Custom'""" + + value = getattr(data, propName) + return value if value != "Custom" else getattr(data, f"{propName}Custom") + + @staticmethod + def getConvertedTransformWithOrientation( + transform: Matrix, dataHolder: Object, obj: Object, orientation: Quaternion | Matrix + ): + relativeTransform = transform @ dataHolder.matrix_world.inverted() @ obj.matrix_world + blenderTranslation, blenderRotation, scale = relativeTransform.decompose() + rotation = blenderRotation @ orientation + convertedTranslation = ootConvertTranslation(blenderTranslation) + convertedRotation = ootConvertRotation(rotation) + + return convertedTranslation, convertedRotation, scale, rotation + + @staticmethod + def getConvertedTransform(transform: Matrix, dataHolder: Object, obj: Object, handleOrientation: bool): + # Hacky solution to handle Z-up to Y-up conversion + # We cannot apply rotation to empty, as that modifies scale + if handleOrientation: + orientation = Quaternion((1, 0, 0), radians(90.0)) + else: + orientation = Matrix.Identity(4) + return Utility.getConvertedTransformWithOrientation(transform, dataHolder, obj, orientation) + + @staticmethod + def getAltHeaderListCmd(altName: str): + """Returns the scene alternate header list command""" + + return indent + f"SCENE_CMD_ALTERNATE_HEADER_LIST({altName}),\n" + + @staticmethod + def getEndCmd(): + """Returns the scene end command""" + + return indent + "SCENE_CMD_END(),\n" diff --git a/fast64_internal/oot/file_settings.py b/fast64_internal/oot/file_settings.py index 3cf6c8701..1f451becd 100644 --- a/fast64_internal/oot/file_settings.py +++ b/fast64_internal/oot/file_settings.py @@ -1,5 +1,5 @@ from bpy.utils import register_class, unregister_class -from bpy.props import StringProperty, FloatProperty +from bpy.props import StringProperty, FloatProperty, BoolProperty from bpy.types import Scene from ..utility import prop_split from ..render_settings import on_update_render_settings @@ -21,6 +21,10 @@ def draw(self, context): col.prop(context.scene.fast64.oot, "headerTabAffectsVisibility") col.prop(context.scene.fast64.oot, "hackerFeaturesEnabled") + if not context.scene.fast64.oot.hackerFeaturesEnabled: + col.prop(context.scene.fast64.oot, "useDecompFeatures") + col.prop(context.scene.fast64.oot, "exportMotionOnly") + oot_classes = (OOT_FileSettingsPanel,) diff --git a/fast64_internal/oot/oot_level_classes.py b/fast64_internal/oot/oot_level_classes.py deleted file mode 100644 index 4db0256d5..000000000 --- a/fast64_internal/oot/oot_level_classes.py +++ /dev/null @@ -1,521 +0,0 @@ -import bpy -import os -import shutil - -from typing import Optional -from bpy.types import Object -from ..utility import PluginError, toAlnum, indent -from .collision.exporter import OOTCollision -from .oot_model_classes import OOTModel -from ..f3d.f3d_gbi import ( - SPDisplayList, - SPEndDisplayList, - GfxListTag, - GfxList, -) -from ..f3d.occlusion_planes.exporter import OcclusionPlaneCandidatesList - - -class OOTCommonCommands: - def getAltHeaderListCmd(self, altName): - return indent + f"SCENE_CMD_ALTERNATE_HEADER_LIST({altName}),\n" - - def getEndCmd(self): - return indent + "SCENE_CMD_END(),\n" - - -class OOTActor: - def __init__(self, actorName, actorID, position, rotation, actorParam): - self.actorName = actorName - self.actorID = actorID - self.actorParam = actorParam - self.position = position - self.rotation = rotation - - -class OOTTransitionActor: - def __init__(self, actorName, actorID, frontRoom, backRoom, frontCam, backCam, position, rotationY, actorParam): - self.actorName = actorName - self.actorID = actorID - self.actorParam = actorParam - self.frontRoom = frontRoom - self.backRoom = backRoom - self.frontCam = frontCam - self.backCam = backCam - self.position = position - self.rotationY = rotationY - - -class OOTExit: - def __init__(self, index): - self.index = index - - -class OOTEntrance: - def __init__(self, roomIndex, startPositionIndex): - self.roomIndex = roomIndex - self.startPositionIndex = startPositionIndex - - -class OOTLight: - def __init__(self): - self.ambient = (0, 0, 0) - self.diffuse0 = (0, 0, 0) - self.diffuseDir0 = (0, 0, 0) - self.diffuse1 = (0, 0, 0) - self.diffuseDir1 = (0, 0, 0) - self.fogColor = (0, 0, 0) - self.fogNear = 0 - self.z_far = 0 - self.transitionSpeed = 0 - - def getBlendFogNear(self): - return f"(({self.transitionSpeed} << 10) | {self.fogNear})" - - -class OOTSceneTableEntry: - def __init__(self): - self.drawConfig = 0 - - -class OOTScene(OOTCommonCommands): - def __init__(self, name, model): - self.name: str = toAlnum(name) - self.write_dummy_room_list = False - self.rooms = {} - self.transitionActorList = set() - self.entranceList = set() - self.startPositions = {} - self.lights = [] - self.model = model - self.collision = OOTCollision(self.name) - - self.globalObject = None - self.naviCup = None - - # Skybox - self.skyboxID = None - self.skyboxCloudiness = None - self.skyboxLighting = None - self.isSkyboxLightingCustom = False - - # Camera - self.mapLocation = None - self.cameraMode = None - - self.musicSeq = None - self.nightSeq = None - - self.childNightHeader: Optional[OOTScene] = None - self.adultDayHeader: Optional[OOTScene] = None - self.adultNightHeader: Optional[OOTScene] = None - self.cutsceneHeaders: list["OOTScene"] = [] - - self.exitList = [] - self.pathList = set() - self.cameraList = [] - - self.writeCutscene = False - self.csWriteType = "Object" - self.csName = "" - self.csWriteCustom = "" - self.extraCutscenes: list[Object] = [] - - self.sceneTableEntry = OOTSceneTableEntry() - - def getAlternateHeaderScene(self, name): - scene = OOTScene(name, self.model) - scene.write_dummy_room_list = self.write_dummy_room_list - scene.rooms = self.rooms - scene.collision = self.collision - scene.exitList = [] - scene.pathList = set() - scene.cameraList = self.cameraList - return scene - - def sceneName(self): - return self.name + "_scene" - - def roomListName(self): - return self.sceneName() + "_roomList" - - def entranceListName(self, headerIndex): - return self.sceneName() + "_header" + format(headerIndex, "02") + "_entranceList" - - def startPositionsName(self, headerIndex): - return f"{self.sceneName()}_header{headerIndex:02}_playerEntryList" - - def exitListName(self, headerIndex): - return self.sceneName() + "_header" + format(headerIndex, "02") + "_exitList" - - def lightListName(self, headerIndex): - return self.sceneName() + "_header" + format(headerIndex, "02") + "_lightSettings" - - def transitionActorListName(self, headerIndex): - return self.sceneName() + "_header" + format(headerIndex, "02") + "_transitionActors" - - def pathListName(self, headerIndex: int): - return self.sceneName() + "_pathway" + str(headerIndex) - - def cameraListName(self): - return self.sceneName() + "_cameraList" - - def alternateHeadersName(self): - return self.sceneName() + "_alternateHeaders" - - def hasAlternateHeaders(self): - return not ( - self.childNightHeader == None - and self.adultDayHeader == None - and self.adultNightHeader == None - and len(self.cutsceneHeaders) == 0 - ) - - def validateIndices(self): - self.collision.cameraData.validateCamPositions() - self.validateStartPositions() - self.validateRoomIndices() - - def validateStartPositions(self): - count = 0 - while count < len(self.startPositions): - if count not in self.startPositions: - raise PluginError( - "Error: Entrances (start positions) do not have a consecutive list of indices. " - + "Missing index: " - + str(count) - ) - count = count + 1 - - def validateRoomIndices(self): - count = 0 - while count < len(self.rooms): - if count not in self.rooms: - raise PluginError( - "Error: Room indices do not have a consecutive list of indices. " + "Missing index: " + str(count) - ) - count = count + 1 - - def validatePathIndices(self): - count = 0 - while count < len(self.pathList): - if count not in self.pathList: - raise PluginError( - "Error: Path list does not have a consecutive list of indices.\n" + "Missing index: " + str(count) - ) - count = count + 1 - - def addRoom(self, roomIndex, roomName, roomShape): - roomModel = self.model.addSubModel(OOTModel(roomName + "_dl", self.model.DLFormat, None)) - room = OOTRoom(roomIndex, roomName, roomModel, roomShape) - if roomIndex in self.rooms: - raise PluginError("Repeat room index " + str(roomIndex) + " for " + str(roomName)) - self.rooms[roomIndex] = room - return room - - def sortEntrances(self): - self.entranceList = sorted(self.entranceList, key=lambda x: x.startPositionIndex) - if self.appendNullEntrance: - self.entranceList.append(OOTEntrance(0, 0)) - - if self.childNightHeader is not None: - self.childNightHeader.sortEntrances() - if self.adultDayHeader is not None: - self.adultDayHeader.sortEntrances() - if self.adultNightHeader is not None: - self.adultNightHeader.sortEntrances() - for header in self.cutsceneHeaders: - header.sortEntrances() - - def copyBgImages(self, exportPath: str): - for i in range(len(self.rooms)): - self.rooms[i].mesh.copyBgImages(exportPath) - - -class OOTBGImage: - def __init__(self, name: str, image: bpy.types.Image, otherModeFlags: str): - self.name = name - self.image = image - self.otherModeFlags = otherModeFlags - - def getFilename(self) -> str: - return f"{self.name}.jpg" - - def singlePropertiesC(self, tabDepth: int) -> str: - return (indent * tabDepth) + (indent * tabDepth).join( - ( - f"{self.name},\n", - f"0x00000000,\n", # (``unk_0C`` in decomp) - "NULL,\n", - f"{self.image.size[0]}, {self.image.size[1]},\n", - f"G_IM_FMT_RGBA, G_IM_SIZ_16b,\n", # RGBA16 - f"{self.otherModeFlags}, 0x0000", - ) - ) - - def multiPropertiesC(self, tabDepth: int, cameraIndex: int) -> str: - return (indent * tabDepth) + f"0x0082, {cameraIndex},\n" + self.singlePropertiesC(tabDepth) + "\n" - - -class OOTRoomMesh: - def __init__(self, roomName, roomShape, model): - self.roomName = roomName - self.roomShape = roomShape - self.meshEntries = [] - self.model = model - self.bgImages = [] - - def terminateDLs(self): - for entry in self.meshEntries: - entry.DLGroup.terminateDLs() - - def headerName(self): - return str(self.roomName) + "_shapeHeader" - - def entriesName(self): - return str(self.roomName) + ( - "_shapeDListEntry" if self.roomShape != "ROOM_SHAPE_TYPE_CULLABLE" else "_shapeCullableEntry" - ) - - def addMeshGroup(self, cullGroup): - meshGroup = OOTRoomMeshGroup(cullGroup, self.model.DLFormat, self.roomName, len(self.meshEntries)) - self.meshEntries.append(meshGroup) - return meshGroup - - def currentMeshGroup(self): - return self.meshEntries[-1] - - def removeUnusedEntries(self): - newList = [] - for meshEntry in self.meshEntries: - if not meshEntry.DLGroup.isEmpty(): - newList.append(meshEntry) - self.meshEntries = newList - - def copyBgImages(self, exportPath: str): - jpegCompatibility = False # maybe delete some code later if jpeg compatibility improves - for bgImage in self.bgImages: - image = bgImage.image - imageFileName = bgImage.getFilename() - if jpegCompatibility: - isPacked = image.packed_file is not None - if not isPacked: - image.pack() - oldpath = image.filepath - oldFormat = image.file_format - try: - image.filepath = bpy.path.abspath(os.path.join(exportPath, imageFileName)) - image.file_format = "JPEG" - image.save() - if not isPacked: - image.unpack() - image.filepath = oldpath - image.file_format = oldFormat - except Exception as e: - image.filepath = oldpath - image.file_format = oldFormat - raise Exception(str(e)) - else: - filepath = bpy.path.abspath(os.path.join(exportPath, imageFileName)) - shutil.copy(bpy.path.abspath(image.filepath), filepath) - - def getMultiBgStructName(self): - return self.roomName + "BgImage" - - -class OOTDLGroup: - def __init__(self, name, DLFormat): - self.opaque = None - self.transparent = None - self.DLFormat = DLFormat - self.name = toAlnum(name) - - def addDLCall(self, displayList, drawLayer): - if drawLayer == "Opaque": - if self.opaque is None: - self.opaque = GfxList(self.name + "_opaque", GfxListTag.Draw, self.DLFormat) - self.opaque.commands.append(SPDisplayList(displayList)) - elif drawLayer == "Transparent": - if self.transparent is None: - self.transparent = GfxList(self.name + "_transparent", GfxListTag.Draw, self.DLFormat) - self.transparent.commands.append(SPDisplayList(displayList)) - else: - raise PluginError("Unhandled draw layer: " + str(drawLayer)) - - def terminateDLs(self): - if self.opaque is not None: - self.opaque.commands.append(SPEndDisplayList()) - - if self.transparent is not None: - self.transparent.commands.append(SPEndDisplayList()) - - def createDLs(self): - if self.opaque is None: - self.opaque = GfxList(self.name + "_opaque", GfxListTag.Draw, self.DLFormat) - if self.transparent is None: - self.transparent = GfxList(self.name + "_transparent", GfxListTag.Draw, self.DLFormat) - - def isEmpty(self): - return self.opaque is None and self.transparent is None - - -class OOTRoomMeshGroup: - def __init__(self, cullGroup, DLFormat, roomName, entryIndex): - self.cullGroup = cullGroup - self.roomName = roomName - self.entryIndex = entryIndex - - self.DLGroup = OOTDLGroup(self.entryName(), DLFormat) - - def entryName(self): - return self.roomName + "_entry_" + str(self.entryIndex) - - -class OOTRoom(OOTCommonCommands): - def __init__(self, index, name, model, roomShape): - self.ownerName = toAlnum(name) - self.index = index - self.actorList = set() - self.occlusion_planes = OcclusionPlaneCandidatesList(self.roomName()) - self.mesh = OOTRoomMesh(self.roomName(), roomShape, model) - - # Room behaviour - self.roomBehaviour = None - self.disableWarpSongs = False - self.showInvisibleActors = False - self.linkIdleMode = None - - # Wind - self.setWind = False - self.windVector = [0, 0, 0] - self.windStrength = 0 - - # Time - self.timeHours = 0x00 - self.timeMinutes = 0x00 - self.timeSpeed = 0xA - - # Skybox - self.disableSkybox = False - self.disableSunMoon = False - - # Echo - self.echo = 0x00 - - self.objectIDList = [] - - self.childNightHeader = None - self.adultDayHeader = None - self.adultNightHeader = None - self.cutsceneHeaders = [] - - self.appendNullEntrance = False - - def getAlternateHeaderRoom(self, name): - room = OOTRoom(self.index, name, self.mesh.model, self.mesh.roomShape) - room.mesh = self.mesh - return room - - def roomName(self): - return self.ownerName + "_room_" + str(self.index) - - def objectListName(self, headerIndex): - return self.roomName() + "_header" + format(headerIndex, "02") + "_objectList" - - def actorListName(self, headerIndex): - return self.roomName() + "_header" + format(headerIndex, "02") + "_actorList" - - def alternateHeadersName(self): - return self.roomName() + "_alternateHeaders" - - def hasAlternateHeaders(self): - return not ( - self.childNightHeader == None - and self.adultDayHeader == None - and self.adultNightHeader == None - and len(self.cutsceneHeaders) == 0 - ) - - def getObjectLengthDefineName(self, headerIndex: int): - return f"LENGTH_{self.objectListName(headerIndex).upper()}" - - def getActorLengthDefineName(self, headerIndex: int): - return f"LENGTH_{self.actorListName(headerIndex).upper()}" - - -def addActor(owner, actor, actorProp, propName, actorObjName): - sceneSetup = actorProp.headerSettings - if ( - sceneSetup.sceneSetupPreset == "All Scene Setups" - or sceneSetup.sceneSetupPreset == "All Non-Cutscene Scene Setups" - ): - getattr(owner, propName).add(actor) - if owner.childNightHeader is not None: - getattr(owner.childNightHeader, propName).add(actor) - if owner.adultDayHeader is not None: - getattr(owner.adultDayHeader, propName).add(actor) - if owner.adultNightHeader is not None: - getattr(owner.adultNightHeader, propName).add(actor) - if sceneSetup.sceneSetupPreset == "All Scene Setups": - for cutsceneHeader in owner.cutsceneHeaders: - getattr(cutsceneHeader, propName).add(actor) - elif sceneSetup.sceneSetupPreset == "Custom": - if sceneSetup.childDayHeader and owner is not None: - getattr(owner, propName).add(actor) - if sceneSetup.childNightHeader and owner.childNightHeader is not None: - getattr(owner.childNightHeader, propName).add(actor) - if sceneSetup.adultDayHeader and owner.adultDayHeader is not None: - getattr(owner.adultDayHeader, propName).add(actor) - if sceneSetup.adultNightHeader and owner.adultNightHeader is not None: - getattr(owner.adultNightHeader, propName).add(actor) - for cutsceneHeader in sceneSetup.cutsceneHeaders: - if cutsceneHeader.headerIndex >= len(owner.cutsceneHeaders) + 4: - raise PluginError( - actorObjName - + " uses a cutscene header index that is outside the range of the current number of cutscene headers." - ) - getattr(owner.cutsceneHeaders[cutsceneHeader.headerIndex - 4], propName).add(actor) - else: - raise PluginError("Unhandled scene setup preset: " + str(sceneSetup.sceneSetupPreset)) - - -def addStartPosition(scene, index, actor, actorProp, actorObjName): - sceneSetup = actorProp.headerSettings - if ( - sceneSetup.sceneSetupPreset == "All Scene Setups" - or sceneSetup.sceneSetupPreset == "All Non-Cutscene Scene Setups" - ): - addStartPosAtIndex(scene.startPositions, index, actor) - if scene.childNightHeader is not None: - addStartPosAtIndex(scene.childNightHeader.startPositions, index, actor) - if scene.adultDayHeader is not None: - addStartPosAtIndex(scene.adultDayHeader.startPositions, index, actor) - if scene.adultNightHeader is not None: - addStartPosAtIndex(scene.adultNightHeader.startPositions, index, actor) - if sceneSetup.sceneSetupPreset == "All Scene Setups": - for cutsceneHeader in scene.cutsceneHeaders: - addStartPosAtIndex(cutsceneHeader.startPositions, index, actor) - elif sceneSetup.sceneSetupPreset == "Custom": - if sceneSetup.childDayHeader and scene is not None: - addStartPosAtIndex(scene.startPositions, index, actor) - if sceneSetup.childNightHeader and scene.childNightHeader is not None: - addStartPosAtIndex(scene.childNightHeader.startPositions, index, actor) - if sceneSetup.adultDayHeader and scene.adultDayHeader is not None: - addStartPosAtIndex(scene.adultDayHeader.startPositions, index, actor) - if sceneSetup.adultNightHeader and scene.adultNightHeader is not None: - addStartPosAtIndex(scene.adultNightHeader.startPositions, index, actor) - for cutsceneHeader in sceneSetup.cutsceneHeaders: - if cutsceneHeader.headerIndex >= len(scene.cutsceneHeaders) + 4: - raise PluginError( - actorObjName - + " uses a cutscene header index that is outside the range of the current number of cutscene headers." - ) - addStartPosAtIndex(scene.cutsceneHeaders[cutsceneHeader.headerIndex - 4].startPositions, index, actor) - else: - raise PluginError("Unhandled scene setup preset: " + str(sceneSetup.sceneSetupPreset)) - - -def addStartPosAtIndex(startPosDict, index, value): - if index in startPosDict: - raise PluginError("Error: Repeated start position spawn index: " + str(index)) - startPosDict[index] = value diff --git a/fast64_internal/oot/oot_level_parser.py b/fast64_internal/oot/oot_level_parser.py index 055ab5a64..c5a9939cc 100644 --- a/fast64_internal/oot/oot_level_parser.py +++ b/fast64_internal/oot/oot_level_parser.py @@ -8,7 +8,7 @@ from .collision.properties import OOTMaterialCollisionProperty from .oot_model_classes import OOTF3DContext from .oot_f3d_writer import getColliderMat -from .scene.exporter.to_c import getDrawConfig +from .exporter.decomp_edit.scene_table import SceneTableUtility from .scene.properties import OOTSceneHeaderProperty, OOTLightProperty, OOTImportSceneSettingsProperty from .room.properties import OOTRoomHeaderProperty from .actor.properties import OOTActorProperty, OOTActorHeaderProperty @@ -241,7 +241,7 @@ def parseScene( f3dContext.addMatrix("&gMtxClear", mathutils.Matrix.Scale(1 / bpy.context.scene.ootBlenderScale, 4)) if not settings.isCustomDest: - drawConfigName = getDrawConfig(sceneName) + drawConfigName = SceneTableUtility.get_draw_config(sceneName) drawConfigData = readFile(os.path.join(importPath, "src/code/z_scene_table.c")) parseDrawConfig(drawConfigName, sceneData, drawConfigData, f3dContext) @@ -273,7 +273,10 @@ def parseScene( if not settings.isCustomDest: setCustomProperty( - sceneObj.ootSceneHeader.sceneTableEntry, "drawConfig", getDrawConfig(sceneName), ootEnumDrawConfig + sceneObj.ootSceneHeader.sceneTableEntry, + "drawConfig", + SceneTableUtility.get_draw_config(sceneName), + ootEnumDrawConfig, ) if bpy.context.scene.fast64.oot.headerTabAffectsVisibility: @@ -946,9 +949,9 @@ def parsePathList( ): pathData = getDataMatch(sceneData, pathListName, "Path", "path list") pathList = [value.replace("{", "").strip() for value in pathData.split("},") if value.strip() != ""] - for pathEntry in pathList: + for i, pathEntry in enumerate(pathList): numPoints, pathName = [value.strip() for value in pathEntry.split(",")] - parsePath(sceneObj, sceneData, pathName, headerIndex, sharedSceneData) + parsePath(sceneObj, sceneData, pathName, headerIndex, sharedSceneData, i) def createCurveFromPoints(points: list[tuple[float, float, float]], name: str): @@ -979,7 +982,12 @@ def createCurveFromPoints(points: list[tuple[float, float, float]], name: str): def parsePath( - sceneObj: bpy.types.Object, sceneData: str, pathName: str, headerIndex: int, sharedSceneData: SharedSceneData + sceneObj: bpy.types.Object, + sceneData: str, + pathName: str, + headerIndex: int, + sharedSceneData: SharedSceneData, + orderIndex: int, ): pathData = getDataMatch(sceneData, pathName, "Vec3s", "path") pathPointsEntries = [value.replace("{", "").strip() for value in pathData.split("},") if value.strip() != ""] @@ -993,6 +1001,7 @@ def parsePath( curveObj = createCurveFromPoints(pathPoints, pathName) splineProp = curveObj.ootSplineProperty + splineProp.index = orderIndex unsetAllHeadersExceptSpecified(splineProp.headerSettings, headerIndex) sharedSceneData.pathDict[pathPoints] = curveObj diff --git a/fast64_internal/oot/oot_level_writer.py b/fast64_internal/oot/oot_level_writer.py deleted file mode 100644 index 9ffce0687..000000000 --- a/fast64_internal/oot/oot_level_writer.py +++ /dev/null @@ -1,903 +0,0 @@ -import bpy, os, math, mathutils -from ..f3d.f3d_gbi import TextureExportSettings -from ..f3d.f3d_writer import TriangleConverterInfo, saveStaticModel, getInfoDict -from .scene.properties import OOTSceneProperties, OOTSceneHeaderProperty, OOTAlternateSceneHeaderProperty -from .room.properties import OOTRoomHeaderProperty, OOTAlternateRoomHeaderProperty -from .oot_constants import ootData -from .oot_spline import assertCurveValid, ootConvertPath -from .oot_model_classes import OOTModel -from .oot_object import addMissingObjectsToAllRoomHeaders -from .oot_f3d_writer import writeTextureArraysNew, writeTextureArraysExisting1D -from .collision.constants import decomp_compat_map_CameraSType -from ..f3d.occlusion_planes.exporter import addOcclusionQuads - -from .collision.exporter import ( - OOTCameraData, - OOTCameraPosData, - OOTWaterBox, - OOTCrawlspaceData, - exportCollisionCommon, -) - -from ..utility import ( - PluginError, - CData, - checkIdentityRotation, - restoreHiddenState, - unhideAllAndGetHiddenState, - ootGetBaseOrCustomLight, - exportColor, - toAlnum, - checkObjectReference, - writeCDataSourceOnly, - writeCDataHeaderOnly, - readFile, - writeFile, -) - -from .scene.exporter.to_c import ( - setBootupScene, - getIncludes, - getSceneC, - modifySceneTable, - editSpecFile, - modifySceneFiles, -) - -from .oot_utility import ( - OOTObjectCategorizer, - CullGroup, - checkUniformScale, - ootDuplicateHierarchy, - ootCleanupScene, - ootGetPath, - getCustomProperty, - ootConvertTranslation, - ootConvertRotation, - getSceneDirFromLevelName, - isPathObject, -) - -from .oot_level_classes import ( - OOTLight, - OOTExit, - OOTScene, - OOTRoom, - OOTActor, - OOTTransitionActor, - OOTEntrance, - OOTDLGroup, - OOTBGImage, - addActor, - addStartPosition, -) - - -def ootPreprendSceneIncludes(scene, file): - exportFile = getIncludes(scene) - exportFile.append(file) - return exportFile - - -def ootCreateSceneHeader(levelC): - sceneHeader = CData() - - sceneHeader.append(levelC.sceneMainC) - if levelC.sceneTexturesIsUsed(): - sceneHeader.append(levelC.sceneTexturesC) - sceneHeader.append(levelC.sceneCollisionC) - if levelC.sceneCutscenesIsUsed(): - for i in range(len(levelC.sceneCutscenesC)): - sceneHeader.append(levelC.sceneCutscenesC[i]) - for roomName, roomMainC in levelC.roomMainC.items(): - sceneHeader.append(roomMainC) - for roomName, roomOcclusionPlanesC in levelC.roomOcclusionPlanesC.items(): - sceneHeader.append(roomOcclusionPlanesC) - for roomName, roomShapeInfoC in levelC.roomShapeInfoC.items(): - sceneHeader.append(roomShapeInfoC) - for roomName, roomModelC in levelC.roomModelC.items(): - sceneHeader.append(roomModelC) - - return sceneHeader - - -def ootCombineSceneFiles(levelC): - sceneC = CData() - - sceneC.append(levelC.sceneMainC) - if levelC.sceneTexturesIsUsed(): - sceneC.append(levelC.sceneTexturesC) - sceneC.append(levelC.sceneCollisionC) - if levelC.sceneCutscenesIsUsed(): - for i in range(len(levelC.sceneCutscenesC)): - sceneC.append(levelC.sceneCutscenesC[i]) - return sceneC - - -def ootExportSceneToC(originalSceneObj, transformMatrix, sceneName, DLFormat, savePNG, exportInfo, bootToSceneOptions): - checkObjectReference(originalSceneObj, "Scene object") - isCustomExport = exportInfo.isCustomExportPath - exportPath = exportInfo.exportPath - - scene = ootConvertScene(originalSceneObj, transformMatrix, sceneName, DLFormat, not savePNG) - - exportSubdir = "" - if exportInfo.customSubPath is not None: - exportSubdir = exportInfo.customSubPath - if not isCustomExport and exportInfo.customSubPath is None: - exportSubdir = os.path.dirname(getSceneDirFromLevelName(sceneName)) - - roomObjList = [ - obj for obj in originalSceneObj.children_recursive if obj.type == "EMPTY" and obj.ootEmptyType == "Room" - ] - for roomObj in roomObjList: - room = scene.rooms[roomObj.ootRoomHeader.roomIndex] - addMissingObjectsToAllRoomHeaders(roomObj, room, ootData) - - sceneInclude = exportSubdir + "/" + sceneName + "/" - levelPath = ootGetPath(exportPath, isCustomExport, exportSubdir, sceneName, True, True) - levelC = getSceneC(scene, TextureExportSettings(False, savePNG, sceneInclude, levelPath)) - - if not isCustomExport: - writeTextureArraysExistingScene(scene.model, exportPath, sceneInclude + sceneName + "_scene.h") - else: - textureArrayData = writeTextureArraysNew(scene.model, None) - levelC.sceneTexturesC.append(textureArrayData) - - if bpy.context.scene.ootSceneExportSettings.singleFile: - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, ootCombineSceneFiles(levelC)), - os.path.join(levelPath, scene.sceneName() + ".c"), - ) - for i in range(len(scene.rooms)): - roomC = CData() - roomC.append(levelC.roomMainC[scene.rooms[i].roomName()]) - roomC.append(levelC.roomOcclusionPlanesC[scene.rooms[i].roomName()]) - roomC.append(levelC.roomShapeInfoC[scene.rooms[i].roomName()]) - roomC.append(levelC.roomModelC[scene.rooms[i].roomName()]) - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, roomC), os.path.join(levelPath, scene.rooms[i].roomName() + ".c") - ) - else: - # Export the scene segment .c files - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, levelC.sceneMainC), os.path.join(levelPath, scene.sceneName() + "_main.c") - ) - if levelC.sceneTexturesIsUsed(): - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, levelC.sceneTexturesC), - os.path.join(levelPath, scene.sceneName() + "_tex.c"), - ) - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, levelC.sceneCollisionC), - os.path.join(levelPath, scene.sceneName() + "_col.c"), - ) - if levelC.sceneCutscenesIsUsed(): - for i in range(len(levelC.sceneCutscenesC)): - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, levelC.sceneCutscenesC[i]), - os.path.join(levelPath, scene.sceneName() + "_cs_" + str(i) + ".c"), - ) - - # Export the room segment .c files - for roomName, roomMainC in levelC.roomMainC.items(): - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, roomMainC), os.path.join(levelPath, roomName + "_main.c") - ) - for roomName, roomOcclusionPlanesC in levelC.roomOcclusionPlanesC.items(): - if len(roomOcclusionPlanesC.source) > 0: - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, roomOcclusionPlanesC), os.path.join(levelPath, roomName + "_occ.c") - ) - for roomName, roomShapeInfoC in levelC.roomShapeInfoC.items(): - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, roomShapeInfoC), os.path.join(levelPath, roomName + "_model_info.c") - ) - for roomName, roomModelC in levelC.roomModelC.items(): - writeCDataSourceOnly( - ootPreprendSceneIncludes(scene, roomModelC), os.path.join(levelPath, roomName + "_model.c") - ) - - # Export the scene .h file - writeCDataHeaderOnly(ootCreateSceneHeader(levelC), os.path.join(levelPath, scene.sceneName() + ".h")) - - # Copy bg images - scene.copyBgImages(levelPath) - - if not isCustomExport: - writeOtherSceneProperties(scene, exportInfo, levelC) - - if bootToSceneOptions is not None and bootToSceneOptions.bootToScene: - setBootupScene( - os.path.join(exportPath, "include/config/config_debug.h") - if not isCustomExport - else os.path.join(levelPath, "config_bootup.h"), - "ENTR_" + sceneName.upper() + "_" + str(bootToSceneOptions.spawnIndex), - bootToSceneOptions, - ) - - -def writeTextureArraysExistingScene(fModel: OOTModel, exportPath: str, sceneInclude: str): - drawConfigPath = os.path.join(exportPath, "src/code/z_scene_table.c") - drawConfigData = readFile(drawConfigPath) - newData = drawConfigData - - if f'#include "{sceneInclude}"' not in newData: - additionalIncludes = f'#include "{sceneInclude}"\n' - else: - additionalIncludes = "" - - for flipbook in fModel.flipbooks: - if flipbook.exportMode == "Array": - newData = writeTextureArraysExisting1D(newData, flipbook, additionalIncludes) - else: - raise PluginError("Scenes can only use array flipbooks.") - - if newData != drawConfigData: - writeFile(drawConfigPath, newData) - - -def writeOtherSceneProperties(scene, exportInfo, levelC): - modifySceneTable(scene, exportInfo) - editSpecFile( - True, - exportInfo, - levelC.sceneTexturesIsUsed(), - levelC.sceneCutscenesIsUsed(), - len(scene.rooms), - len(levelC.sceneCutscenesC), - [len(occ.source) > 0 for occ in levelC.roomOcclusionPlanesC.values()], - ) - modifySceneFiles(scene, exportInfo) - - -def readSceneData( - scene: OOTScene, - scene_properties: OOTSceneProperties, - sceneHeader: OOTSceneHeaderProperty, - alternateSceneHeaders: OOTAlternateSceneHeaderProperty, -): - scene.write_dummy_room_list = scene_properties.write_dummy_room_list - scene.sceneTableEntry.drawConfig = getCustomProperty(sceneHeader.sceneTableEntry, "drawConfig") - scene.globalObject = getCustomProperty(sceneHeader, "globalObject") - scene.naviCup = getCustomProperty(sceneHeader, "naviCup") - scene.skyboxID = getCustomProperty(sceneHeader, "skyboxID") - scene.skyboxCloudiness = getCustomProperty(sceneHeader, "skyboxCloudiness") - scene.skyboxLighting = getCustomProperty(sceneHeader, "skyboxLighting") - scene.isSkyboxLightingCustom = sceneHeader.skyboxLighting == "Custom" - scene.mapLocation = getCustomProperty(sceneHeader, "mapLocation") - scene.cameraMode = getCustomProperty(sceneHeader, "cameraMode") - scene.musicSeq = getCustomProperty(sceneHeader, "musicSeq") - scene.nightSeq = getCustomProperty(sceneHeader, "nightSeq") - scene.audioSessionPreset = getCustomProperty(sceneHeader, "audioSessionPreset") - scene.appendNullEntrance = sceneHeader.appendNullEntrance - - if ( - sceneHeader.skyboxLighting == "0x00" - or sceneHeader.skyboxLighting == "0" - or sceneHeader.skyboxLighting == "LIGHT_MODE_TIME" - ): # Time of Day - scene.lights.append(getLightData(sceneHeader.timeOfDayLights.dawn)) - scene.lights.append(getLightData(sceneHeader.timeOfDayLights.day)) - scene.lights.append(getLightData(sceneHeader.timeOfDayLights.dusk)) - scene.lights.append(getLightData(sceneHeader.timeOfDayLights.night)) - else: - for lightProp in sceneHeader.lightList: - scene.lights.append(getLightData(lightProp)) - - for exitProp in sceneHeader.exitList: - scene.exitList.append(getExitData(exitProp)) - - scene.writeCutscene = getCustomProperty(sceneHeader, "writeCutscene") - if scene.writeCutscene: - scene.csWriteType = getattr(sceneHeader, "csWriteType") - - if scene.csWriteType == "Custom": - scene.csWriteCustom = getCustomProperty(sceneHeader, "csWriteCustom") - else: - if sceneHeader.csWriteObject is None: - raise PluginError("No object selected for cutscene reference") - elif sceneHeader.csWriteObject.ootEmptyType != "Cutscene": - raise PluginError("Object selected as cutscene is wrong type, must be empty with Cutscene type") - elif sceneHeader.csWriteObject.parent is not None: - raise PluginError("Cutscene empty object should not be parented to anything") - else: - scene.csName = sceneHeader.csWriteObject.name.removeprefix("Cutscene.") - - if alternateSceneHeaders is not None: - scene.collision.cameraData = OOTCameraData(scene.name) - - if not alternateSceneHeaders.childNightHeader.usePreviousHeader: - scene.childNightHeader = scene.getAlternateHeaderScene(scene.name) - readSceneData(scene.childNightHeader, scene_properties, alternateSceneHeaders.childNightHeader, None) - - if not alternateSceneHeaders.adultDayHeader.usePreviousHeader: - scene.adultDayHeader = scene.getAlternateHeaderScene(scene.name) - readSceneData(scene.adultDayHeader, scene_properties, alternateSceneHeaders.adultDayHeader, None) - - if not alternateSceneHeaders.adultNightHeader.usePreviousHeader: - scene.adultNightHeader = scene.getAlternateHeaderScene(scene.name) - readSceneData(scene.adultNightHeader, scene_properties, alternateSceneHeaders.adultNightHeader, None) - - for i in range(len(alternateSceneHeaders.cutsceneHeaders)): - cutsceneHeaderProp = alternateSceneHeaders.cutsceneHeaders[i] - cutsceneHeader = scene.getAlternateHeaderScene(scene.name) - readSceneData(cutsceneHeader, scene_properties, cutsceneHeaderProp, None) - scene.cutsceneHeaders.append(cutsceneHeader) - - for extraCS in sceneHeader.extraCutscenes: - scene.extraCutscenes.append(extraCS.csObject) - else: - if len(sceneHeader.extraCutscenes) > 0: - raise PluginError( - "Extra cutscenes (not in any header) only belong in the main scene, not alternate headers" - ) - - -def getConvertedTransform(transformMatrix, sceneObj, obj, handleOrientation): - # Hacky solution to handle Z-up to Y-up conversion - # We cannot apply rotation to empty, as that modifies scale - if handleOrientation: - orientation = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) - else: - orientation = mathutils.Matrix.Identity(4) - return getConvertedTransformWithOrientation(transformMatrix, sceneObj, obj, orientation) - - -def getConvertedTransformWithOrientation(transformMatrix, sceneObj, obj, orientation): - relativeTransform = transformMatrix @ sceneObj.matrix_world.inverted() @ obj.matrix_world - blenderTranslation, blenderRotation, scale = relativeTransform.decompose() - rotation = blenderRotation @ orientation - convertedTranslation = ootConvertTranslation(blenderTranslation) - convertedRotation = ootConvertRotation(rotation) - - return convertedTranslation, convertedRotation, scale, rotation - - -def getExitData(exitProp): - if exitProp.exitIndex != "Custom": - raise PluginError("Exit index enums not implemented yet.") - return OOTExit(exitProp.exitIndexCustom) - - -def getLightData(lightProp): - light = OOTLight() - light.ambient = exportColor(lightProp.ambient) - light.diffuse0, light.diffuseDir0 = ootGetBaseOrCustomLight(lightProp, 0, True, True) - light.diffuse1, light.diffuseDir1 = ootGetBaseOrCustomLight(lightProp, 1, True, True) - light.fogColor = exportColor(lightProp.fogColor) - light.fogNear = lightProp.fogNear - light.transitionSpeed = lightProp.transitionSpeed - light.z_far = lightProp.z_far - return light - - -def readRoomData( - sceneName: str, - room: OOTRoom, - roomHeader: OOTRoomHeaderProperty, - alternateRoomHeaders: OOTAlternateRoomHeaderProperty, -): - room.roomIndex = roomHeader.roomIndex - room.roomBehaviour = getCustomProperty(roomHeader, "roomBehaviour") - room.disableWarpSongs = roomHeader.disableWarpSongs - room.showInvisibleActors = roomHeader.showInvisibleActors - - # room heat behavior is active if the idle mode is 0x03 - room.linkIdleMode = getCustomProperty(roomHeader, "linkIdleMode") if not roomHeader.roomIsHot else "0x03" - - room.linkIdleModeCustom = roomHeader.linkIdleModeCustom - room.setWind = roomHeader.setWind - room.windVector = roomHeader.windVector[:] - room.windStrength = roomHeader.windStrength - if roomHeader.leaveTimeUnchanged: - room.timeHours = "0xFF" - room.timeMinutes = "0xFF" - else: - room.timeHours = roomHeader.timeHours - room.timeMinutes = roomHeader.timeMinutes - room.timeSpeed = max(-128, min(127, int(round(roomHeader.timeSpeed * 0xA)))) - room.disableSkybox = roomHeader.disableSkybox - room.disableSunMoon = roomHeader.disableSunMoon - room.echo = roomHeader.echo - - for obj in roomHeader.objectList: - # export using the key if the legacy prop isn't present - if "objectID" not in obj: - if obj.objectKey != "Custom": - objectID = ootData.objectData.objectsByKey[obj.objectKey].id - else: - objectID = obj.objectIDCustom - else: - objectID = ootData.objectData.ootEnumObjectIDLegacy[obj["objectID"]][0] - if objectID == "Custom": - objectID = obj.objectIDCustom - - room.objectIDList.append(objectID) - - if len(room.objectIDList) > 16: - print( - "Warning: A room can only have a maximum of 16 objects in its object list, unless more memory is allocated in code.", - ) - - if alternateRoomHeaders is not None: - if not alternateRoomHeaders.childNightHeader.usePreviousHeader: - room.childNightHeader = room.getAlternateHeaderRoom(room.ownerName) - readRoomData(sceneName, room.childNightHeader, alternateRoomHeaders.childNightHeader, None) - - if not alternateRoomHeaders.adultDayHeader.usePreviousHeader: - room.adultDayHeader = room.getAlternateHeaderRoom(room.ownerName) - readRoomData(sceneName, room.adultDayHeader, alternateRoomHeaders.adultDayHeader, None) - - if not alternateRoomHeaders.adultNightHeader.usePreviousHeader: - room.adultNightHeader = room.getAlternateHeaderRoom(room.ownerName) - readRoomData(sceneName, room.adultNightHeader, alternateRoomHeaders.adultNightHeader, None) - - for i in range(len(alternateRoomHeaders.cutsceneHeaders)): - cutsceneHeaderProp = alternateRoomHeaders.cutsceneHeaders[i] - cutsceneHeader = room.getAlternateHeaderRoom(room.ownerName) - readRoomData(sceneName, cutsceneHeader, cutsceneHeaderProp, None) - room.cutsceneHeaders.append(cutsceneHeader) - - if roomHeader.roomShape == "ROOM_SHAPE_TYPE_IMAGE": - for bgImage in roomHeader.bgImageList: - if bgImage.image is None: - raise PluginError( - 'A room is has room shape "Image" but does not have an image set in one of its BG images.' - ) - room.mesh.bgImages.append( - OOTBGImage( - toAlnum(sceneName + "_bg_" + bgImage.image.name), - bgImage.image, - bgImage.otherModeFlags, - ) - ) - - -def readCamPos(camPosProp, obj, scene, sceneObj, transformMatrix): - # Camera faces opposite direction - orientation = mathutils.Quaternion((0, 1, 0), math.radians(180.0)) - translation, rotation, scale, orientedRotation = getConvertedTransformWithOrientation( - transformMatrix, sceneObj, obj, orientation - ) - camPosProp = obj.ootCameraPositionProperty - index = camPosProp.index - # TODO: FOV conversion? - if index in scene.collision.cameraData.camPosDict: - raise PluginError(f"Error: Repeated camera position index: {index} for {obj.name}") - if camPosProp.camSType == "Custom": - camSType = camPosProp.camSTypeCustom - else: - camSType = decomp_compat_map_CameraSType.get(camPosProp.camSType, camPosProp.camSType) - - fov = math.degrees(obj.data.angle) - if fov > 3.6: - fov *= 100 # see CAM_DATA_SCALED() macro - - scene.collision.cameraData.camPosDict[index] = OOTCameraPosData( - camSType, - camPosProp.hasPositionData, - translation, - rotation, - round(fov), - camPosProp.bgImageOverrideIndex, - ) - - -def readCrawlspace(obj, scene, transformMatrix): - splineProp = obj.ootSplineProperty - index = splineProp.index - - if index in scene.collision.cameraData.camPosDict: - raise PluginError(f"Error: Repeated camera position index: {index} for {obj.name}") - - if splineProp.camSType == "Custom": - camSType = splineProp.camSTypeCustom - else: - camSType = decomp_compat_map_CameraSType.get(splineProp.camSType, splineProp.camSType) - - crawlspace = OOTCrawlspaceData(camSType) - spline = obj.data.splines[0] - for point in spline.points: - position = [round(value) for value in transformMatrix @ obj.matrix_world @ point.co] - crawlspace.points.append(position) - - scene.collision.cameraData.camPosDict[index] = crawlspace - - -def readPathProp(pathProp, obj, scene, sceneObj, sceneName, transformMatrix): - relativeTransform = transformMatrix @ sceneObj.matrix_world.inverted() @ obj.matrix_world - # scene.pathList[obj.name] = ootConvertPath(sceneName, obj, relativeTransform) - - # actorProp should be an actor, but its purpose is to access headerSettings so this also works. - addActor(scene, ootConvertPath(sceneName, obj, relativeTransform), obj.ootSplineProperty, "pathList", obj.name) - - -def ootConvertScene(originalSceneObj, transformMatrix, sceneName, DLFormat, convertTextureData): - if originalSceneObj.type != "EMPTY" or originalSceneObj.ootEmptyType != "Scene": - raise PluginError(originalSceneObj.name + ' is not an empty with the "Scene" empty type.') - - if bpy.context.scene.exportHiddenGeometry: - hiddenState = unhideAllAndGetHiddenState(bpy.context.scene) - - # Don't remove ignore_render, as we want to reuse this for collision - sceneObj, allObjs = ootDuplicateHierarchy(originalSceneObj, None, True, OOTObjectCategorizer()) - - if bpy.context.scene.exportHiddenGeometry: - restoreHiddenState(hiddenState) - - roomObjs = [ - child for child in sceneObj.children_recursive if child.type == "EMPTY" and child.ootEmptyType == "Room" - ] - if len(roomObjs) == 0: - raise PluginError("The scene has no child empties with the 'Room' empty type.") - - try: - scene = OOTScene(sceneName, OOTModel(sceneName + "_dl", DLFormat, None)) - readSceneData(scene, sceneObj.fast64.oot.scene, sceneObj.ootSceneHeader, sceneObj.ootAlternateSceneHeaders) - processedRooms = set() - - for obj in sceneObj.children_recursive: - translation, rotation, scale, orientedRotation = getConvertedTransform(transformMatrix, sceneObj, obj, True) - - if obj.type == "EMPTY" and obj.ootEmptyType == "Room": - roomObj = obj - roomHeader = roomObj.ootRoomHeader - roomIndex = roomHeader.roomIndex - if roomIndex in processedRooms: - raise PluginError("Error: room index " + str(roomIndex) + " is used more than once.") - processedRooms.add(roomIndex) - room = scene.addRoom(roomIndex, sceneName, roomHeader.roomShape) - readRoomData(sceneName, room, roomHeader, roomObj.ootAlternateRoomHeaders) - - if roomHeader.roomShape == "ROOM_SHAPE_TYPE_IMAGE" and len(roomHeader.bgImageList) < 1: - raise PluginError(f'Room {roomObj.name} uses room shape "Image" but doesn\'t have any BG images.') - if roomHeader.roomShape == "ROOM_SHAPE_TYPE_IMAGE" and len(processedRooms) > 1: - raise PluginError(f'Room shape "Image" can only have one room in the scene.') - - cullGroup = CullGroup(translation, scale, obj.ootRoomHeader.defaultCullDistance) - DLGroup = room.mesh.addMeshGroup(cullGroup).DLGroup - boundingBox = BoundingBox() - ootProcessMesh( - room.mesh, DLGroup, sceneObj, roomObj, transformMatrix, convertTextureData, None, boundingBox - ) - centroid, radius = boundingBox.getEnclosingSphere() - cullGroup.position = centroid - cullGroup.cullDepth = radius - - if bpy.context.scene.f3d_type == "F3DEX3": - addOcclusionQuads( - obj, room.occlusion_planes, True, transformMatrix @ sceneObj.matrix_world.inverted() - ) - - room.mesh.terminateDLs() - room.mesh.removeUnusedEntries() - ootProcessEmpties(scene, room, sceneObj, roomObj, transformMatrix) - elif obj.type == "EMPTY" and obj.ootEmptyType == "Water Box": - # 0x3F = -1 in 6bit value - ootProcessWaterBox(sceneObj, obj, transformMatrix, scene, 0x3F) - elif obj.type == "CAMERA": - camPosProp = obj.ootCameraPositionProperty - readCamPos(camPosProp, obj, scene, sceneObj, transformMatrix) - elif obj.type == "CURVE" and assertCurveValid(obj): - if isPathObject(obj): - readPathProp(obj.ootSplineProperty, obj, scene, sceneObj, sceneName, transformMatrix) - else: - readCrawlspace(obj, scene, transformMatrix) - - scene.validateIndices() - scene.sortEntrances() - exportCollisionCommon(scene.collision, sceneObj, transformMatrix, True, sceneName) - - ootCleanupScene(originalSceneObj, allObjs) - - except Exception as e: - ootCleanupScene(originalSceneObj, allObjs) - raise Exception(str(e)) - - return scene - - -class BoundingBox: - def __init__(self): - self.minPoint = None - self.maxPoint = None - self.points = [] - - def addPoint(self, point: tuple[float, float, float]): - if self.minPoint is None: - self.minPoint = list(point[:]) - else: - for i in range(3): - if point[i] < self.minPoint[i]: - self.minPoint[i] = point[i] - if self.maxPoint is None: - self.maxPoint = list(point[:]) - else: - for i in range(3): - if point[i] > self.maxPoint[i]: - self.maxPoint[i] = point[i] - self.points.append(point) - - def addMeshObj(self, obj: bpy.types.Object, transform: mathutils.Matrix): - mesh = obj.data - for vertex in mesh.vertices: - self.addPoint(transform @ vertex.co) - - def getEnclosingSphere(self) -> tuple[float, float]: - centroid = (mathutils.Vector(self.minPoint) + mathutils.Vector(self.maxPoint)) / 2 - radius = 0 - for point in self.points: - distance = (mathutils.Vector(point) - centroid).length - if distance > radius: - radius = distance - - # print(f"Radius: {radius}, Centroid: {centroid}") - - transformedCentroid = [round(value) for value in centroid] - transformedRadius = round(radius) - return transformedCentroid, transformedRadius - - -# This function should be called on a copy of an object -# The copy will have modifiers / scale applied and will be made single user -# When we duplicated obj hierarchy we stripped all ignore_renders from hierarchy. -def ootProcessMesh( - roomMesh, DLGroup, sceneObj, obj, transformMatrix, convertTextureData, LODHierarchyObject, boundingBox: BoundingBox -): - relativeTransform = transformMatrix @ sceneObj.matrix_world.inverted() @ obj.matrix_world - translation, rotation, scale = relativeTransform.decompose() - - if obj.type == "EMPTY" and obj.ootEmptyType == "Cull Group": - if LODHierarchyObject is not None: - raise PluginError( - obj.name - + " cannot be used as a cull group because it is " - + "in the sub-hierarchy of the LOD group empty " - + LODHierarchyObject.name - ) - - cullProp = obj.ootCullGroupProperty - checkUniformScale(scale, obj) - DLGroup = roomMesh.addMeshGroup( - CullGroup( - ootConvertTranslation(translation), - scale if cullProp.sizeControlsCull else [cullProp.manualRadius], - obj.empty_display_size if cullProp.sizeControlsCull else 1, - ) - ).DLGroup - - elif obj.type == "MESH" and not obj.ignore_render: - triConverterInfo = TriangleConverterInfo(obj, None, roomMesh.model.f3d, relativeTransform, getInfoDict(obj)) - fMeshes = saveStaticModel( - triConverterInfo, - roomMesh.model, - obj, - relativeTransform, - roomMesh.model.name, - convertTextureData, - False, - "oot", - ) - if fMeshes is not None: - for drawLayer, fMesh in fMeshes.items(): - DLGroup.addDLCall(fMesh.draw, drawLayer) - - boundingBox.addMeshObj(obj, relativeTransform) - - alphabeticalChildren = sorted(obj.children, key=lambda childObj: childObj.original_name.lower()) - for childObj in alphabeticalChildren: - if childObj.type == "EMPTY" and childObj.ootEmptyType == "LOD": - ootProcessLOD( - roomMesh, - DLGroup, - sceneObj, - childObj, - transformMatrix, - convertTextureData, - LODHierarchyObject, - boundingBox, - ) - else: - ootProcessMesh( - roomMesh, - DLGroup, - sceneObj, - childObj, - transformMatrix, - convertTextureData, - LODHierarchyObject, - boundingBox, - ) - - -def ootProcessLOD( - roomMesh, DLGroup, sceneObj, obj, transformMatrix, convertTextureData, LODHierarchyObject, boundingBox: BoundingBox -): - relativeTransform = transformMatrix @ sceneObj.matrix_world.inverted() @ obj.matrix_world - translation, rotation, scale = relativeTransform.decompose() - ootTranslation = ootConvertTranslation(translation) - - LODHierarchyObject = obj - name = toAlnum(roomMesh.model.name + "_" + obj.name + "_lod") - opaqueLOD = roomMesh.model.addLODGroup(name + "_opaque", ootTranslation, obj.f3d_lod_always_render_farthest) - transparentLOD = roomMesh.model.addLODGroup( - name + "_transparent", ootTranslation, obj.f3d_lod_always_render_farthest - ) - - index = 0 - for childObj in obj.children: - # This group will not be converted to C directly, but its display lists will be converted through the FLODGroup. - childDLGroup = OOTDLGroup(name + str(index), roomMesh.model.DLFormat) - index += 1 - - if childObj.type == "EMPTY" and childObj.ootEmptyType == "LOD": - ootProcessLOD( - roomMesh, - childDLGroup, - sceneObj, - childObj, - transformMatrix, - convertTextureData, - LODHierarchyObject, - boundingBox, - ) - else: - ootProcessMesh( - roomMesh, - childDLGroup, - sceneObj, - childObj, - transformMatrix, - convertTextureData, - LODHierarchyObject, - boundingBox, - ) - - # We handle case with no geometry, for the cases where we have "gaps" in the LOD hierarchy. - # This can happen if a LOD does not use transparency while the levels above and below it does. - childDLGroup.createDLs() - childDLGroup.terminateDLs() - - # Add lod AFTER processing hierarchy, so that DLs will be built by then - opaqueLOD.add_lod(childDLGroup.opaque, childObj.f3d_lod_z * bpy.context.scene.ootBlenderScale) - transparentLOD.add_lod(childDLGroup.transparent, childObj.f3d_lod_z * bpy.context.scene.ootBlenderScale) - - opaqueLOD.create_data() - transparentLOD.create_data() - - DLGroup.addDLCall(opaqueLOD.draw, "Opaque") - DLGroup.addDLCall(transparentLOD.draw, "Transparent") - - -def ootProcessEmpties(scene, room, sceneObj, obj, transformMatrix): - translation, rotation, scale, orientedRotation = getConvertedTransform(transformMatrix, sceneObj, obj, True) - - if obj.type == "EMPTY": - if obj.ootEmptyType == "Actor": - actorProp = obj.ootActorProperty - - # The Actor list is filled with ``("None", f"{i} (Deleted from the XML)", "None")`` for - # the total number of actors defined in the XML. If the user deletes one, this will prevent - # 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.rotOverride: - actorRot = ", ".join([actorProp.rotOverrideX, actorProp.rotOverrideY, actorProp.rotOverrideZ]) - else: - actorRot = ", ".join(f"DEG_TO_BINANG({(rot * (180 / 0x8000)):.3f})" for rot in rotation) - - actorName = ( - ootData.actorData.actorsByID[actorProp.actorID].name.replace( - f" - {actorProp.actorID.removeprefix('ACTOR_')}", "" - ) - if actorProp.actorID != "Custom" - else "Custom Actor" - ) - - addActor( - room, - OOTActor( - actorName, - getCustomProperty(actorProp, "actorID"), - translation, - actorRot, - actorProp.actorParam, - ), - actorProp, - "actorList", - obj.name, - ) - elif obj.ootEmptyType == "Transition Actor": - transActorProp = obj.ootTransitionActorProperty - if transActorProp.actor.actorID != "None": - if transActorProp.isRoomTransition: - if transActorProp.fromRoom is None or transActorProp.toRoom is None: - raise PluginError("ERROR: Missing room empty object assigned to transition.") - fromIndex = transActorProp.fromRoom.ootRoomHeader.roomIndex - toIndex = transActorProp.toRoom.ootRoomHeader.roomIndex - else: - fromIndex = toIndex = room.roomIndex - front = (fromIndex, getCustomProperty(transActorProp, "cameraTransitionFront")) - back = (toIndex, getCustomProperty(transActorProp, "cameraTransitionBack")) - - transActorName = ( - ootData.actorData.actorsByID[transActorProp.actor.actorID].name.replace( - f" - {transActorProp.actor.actorID.removeprefix('ACTOR_')}", "" - ) - if transActorProp.actor.actorID != "Custom" - else "Custom Actor" - ) - - addActor( - scene, - OOTTransitionActor( - transActorName, - getCustomProperty(transActorProp.actor, "actorID"), - front[0], - back[0], - front[1], - back[1], - translation, - rotation[1], # TODO: Correct axis? - transActorProp.actor.actorParam, - ), - transActorProp.actor, - "transitionActorList", - obj.name, - ) - elif obj.ootEmptyType == "Entrance": - entranceProp = obj.ootEntranceProperty - spawnIndex = entranceProp.spawnIndex - - if entranceProp.tiedRoom is not None: - roomIndex = entranceProp.tiedRoom.ootRoomHeader.roomIndex - else: - raise PluginError("ERROR: Missing room empty object assigned to the entrance.") - - addActor(scene, OOTEntrance(roomIndex, spawnIndex), entranceProp.actor, "entranceList", obj.name) - addStartPosition( - scene, - spawnIndex, - OOTActor( - "", - "ACTOR_PLAYER" if not entranceProp.customActor else entranceProp.actor.actorIDCustom, - translation, - ", ".join(f"DEG_TO_BINANG({(rot * (180 / 0x8000)):.3f})" for rot in rotation), - entranceProp.actor.actorParam, - ), - entranceProp.actor, - obj.name, - ) - elif obj.ootEmptyType == "Water Box": - ootProcessWaterBox(sceneObj, obj, transformMatrix, scene, room.roomIndex) - elif obj.type == "CAMERA": - camPosProp = obj.ootCameraPositionProperty - readCamPos(camPosProp, obj, scene, sceneObj, transformMatrix) - elif obj.type == "CURVE" and assertCurveValid(obj): - if isPathObject(obj): - readPathProp(obj.ootSplineProperty, obj, scene, sceneObj, scene.name, transformMatrix) - else: - readCrawlspace(obj, scene, transformMatrix) - - for childObj in obj.children: - ootProcessEmpties(scene, room, sceneObj, childObj, transformMatrix) - - -def ootProcessWaterBox(sceneObj, obj, transformMatrix, scene, roomIndex): - translation, rotation, scale, orientedRotation = getConvertedTransform(transformMatrix, sceneObj, obj, True) - - checkIdentityRotation(obj, orientedRotation, False) - waterBoxProp = obj.ootWaterBoxProperty - scene.collision.waterBoxes.append( - OOTWaterBox( - roomIndex, - getCustomProperty(waterBoxProp, "lighting"), - getCustomProperty(waterBoxProp, "camera"), - waterBoxProp.flag19, - translation, - scale, - obj.empty_display_size, - ) - ) diff --git a/fast64_internal/oot/oot_object.py b/fast64_internal/oot/oot_object.py index 4417d26a4..a4eb89f89 100644 --- a/fast64_internal/oot/oot_object.py +++ b/fast64_internal/oot/oot_object.py @@ -1,7 +1,7 @@ from bpy.types import Object from ..utility import ootGetSceneOrRoomHeader -from .data import OoT_Data -from .oot_level_classes import OOTRoom +from .oot_constants import ootData +from .exporter.room.header import RoomHeader def addMissingObjectToProp(roomObj: Object, headerIndex: int, objectKey: str): @@ -14,28 +14,25 @@ def addMissingObjectToProp(roomObj: Object, headerIndex: int, objectKey: str): objectList[-1].objectKey = objectKey -def addMissingObjectsToRoomHeader(roomObj: Object, room: OOTRoom, ootData: OoT_Data, headerIndex: int): +def addMissingObjectsToRoomHeader(roomObj: Object, curHeader: RoomHeader, headerIndex: int): """Adds missing objects to the object list""" - if len(room.actorList) > 0: - for roomActor in room.actorList: - actor = ootData.actorData.actorsByID.get(roomActor.actorID) + if len(curHeader.actors.actorList) > 0: + for roomActor in curHeader.actors.actorList: + actor = ootData.actorData.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 room.objectIDList): - room.objectIDList.append(objID) + if not (objID in curHeader.objects.objectList): + curHeader.objects.objectList.append(objID) addMissingObjectToProp(roomObj, headerIndex, objKey) -def addMissingObjectsToAllRoomHeaders(roomObj: Object, room: OOTRoom, ootData: OoT_Data): +def addMissingObjectsToAllRoomHeaders(roomObj: Object, headers: list[RoomHeader]): """ Adds missing objects (required by actors) to all headers of a room, both to the roomObj empty and the exported room """ - sceneLayers = [room, room.childNightHeader, room.adultDayHeader, room.adultNightHeader] - for i, layer in enumerate(sceneLayers): - if layer is not None: - addMissingObjectsToRoomHeader(roomObj, layer, ootData, i) - for i in range(len(room.cutsceneHeaders)): - addMissingObjectsToRoomHeader(roomObj, room.cutsceneHeaders[i], ootData, i + 4) + for i, curHeader in enumerate(headers): + if curHeader is not None: + addMissingObjectsToRoomHeader(roomObj, curHeader, i) diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/oot/oot_utility.py index 3306da81a..f8febd130 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/oot/oot_utility.py @@ -7,8 +7,10 @@ from mathutils import Vector from bpy.types import Object from bpy.utils import register_class, unregister_class -from typing import Callable, Optional +from bpy.types import Object +from typing import Callable, Optional, TYPE_CHECKING from .oot_constants import ootSceneIDToName +from dataclasses import dataclass from ..utility import ( PluginError, @@ -24,6 +26,9 @@ binOps, ) +if TYPE_CHECKING: + from .scene.properties import OOTBootupSceneOptions + def isPathObject(obj: bpy.types.Object) -> bool: return obj.type == "CURVE" and obj.ootSplineProperty.splineType == "Path" @@ -208,13 +213,52 @@ def getSceneDirFromLevelName(name): return None +@dataclass class ExportInfo: - def __init__(self, isCustomExport, exportPath, customSubPath, name): - self.isCustomExportPath = isCustomExport - self.exportPath = exportPath - self.customSubPath = customSubPath - self.name = name - self.option: Optional[str] = None + """Contains all parameters used for a scene export. Any new parameters for scene export should be added here.""" + + isCustomExportPath: bool + """Whether or not we are exporting to a known decomp repo""" + + exportPath: str + """Either the decomp repo root, or a specified custom folder (if ``isCustomExportPath`` is true)""" + + customSubPath: Optional[str] + """If ``isCustomExportPath``, then this is the relative path used for writing filepaths in files like spec. + For decomp repos, the relative path is automatically determined and thus this will be ``None``.""" + + name: str + """ The name of the scene, similar to the folder names of scenes in decomp. + If ``option`` is not "Custom", then this is usually overriden by the name derived from ``option`` before being passed in.""" + + option: str + """ The scene enum value that we are exporting to (can be Custom)""" + + saveTexturesAsPNG: bool + """ Whether to write textures as C data or as .png files.""" + + isSingleFile: bool + """ Whether to export scene files as a single file or as multiple.""" + + useMacros: bool + """ Whether to use macros or numeric/binary representations of certain values.""" + + hackerootBootOption: "OOTBootupSceneOptions" + """ Options for setting the bootup scene in HackerOoT.""" + + +@dataclass +class RemoveInfo: + """Contains all parameters used for a scene removal.""" + + exportPath: str + """The path to the decomp repo root""" + + customSubPath: Optional[str] + """The relative path to the scene directory, if a custom scene is being removed""" + + name: str + """The name of the level to remove""" class OOTObjectCategorizer: @@ -247,7 +291,7 @@ def sortObjects(self, allObjs): # This also sets all origins relative to the scene object. -def ootDuplicateHierarchy(obj, ignoreAttr, includeEmpties, objectCategorizer): +def ootDuplicateHierarchy(obj, ignoreAttr, includeEmpties, objectCategorizer) -> tuple[Object, list[Object]]: # Duplicate objects to apply scale / modifiers / linked data bpy.ops.object.select_all(action="DESELECT") ootSelectMeshChildrenOnly(obj, includeEmpties) @@ -940,3 +984,46 @@ def getNewPath(type: str, isClosedShape: bool): bpy.context.view_layer.active_layer_collection.collection.objects.link(newPath) return newPath + + +def getObjectList( + objList: list[Object], + objType: str, + emptyType: Optional[str] = None, + splineType: Optional[str] = None, + parentObj: Object = None, +): + """ + Returns a list containing objects matching ``objType``. Sorts by object name. + + Parameters: + - ``objList``: the list of objects to iterate through, usually ``obj.children_recursive`` + - ``objType``: the object's type (``EMPTY``, ``CURVE``, etc.) + - ``emptyType``: optional, filters the object by the given empty type + - ``splineType``: optional, filters the object by the given spline type + - ``parentObj``: optional, checks if the found object is parented to ``parentObj`` + """ + + ret: list[Object] = [] + for obj in objList: + if obj.type == objType: + cond = True + + if emptyType is not None: + cond = obj.ootEmptyType == emptyType + elif splineType is not None: + cond = obj.ootSplineProperty.splineType == splineType + + if parentObj is not None: + if emptyType == "Actor" and obj.ootEmptyType == "Room": + for o in obj.children_recursive: + if o.type == objType and o.ootEmptyType == emptyType: + ret.append(o) + continue + else: + cond = cond and obj.parent is not None and obj.parent.name == parentObj.name + + if cond: + ret.append(obj) + ret.sort(key=lambda o: o.name) + return ret diff --git a/fast64_internal/oot/scene/exporter/to_c/__init__.py b/fast64_internal/oot/scene/exporter/to_c/__init__.py deleted file mode 100644 index 61a8fed1f..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .scene import getIncludes, getSceneC -from .scene_table_c import modifySceneTable, getDrawConfig -from .spec import editSpecFile -from .scene_folder import modifySceneFiles, deleteSceneFiles -from .scene_bootup import setBootupScene, clearBootupScene -from .scene_cutscene import getCutsceneC diff --git a/fast64_internal/oot/scene/exporter/to_c/actor.py b/fast64_internal/oot/scene/exporter/to_c/actor.py deleted file mode 100644 index 446efc331..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/actor.py +++ /dev/null @@ -1,132 +0,0 @@ -from .....utility import CData, indent -from ....oot_level_classes import OOTScene, OOTRoom, OOTActor, OOTTransitionActor, OOTEntrance - - -################### -# Written to Room # -################### - -# Actor List - - -def getActorEntry(actor: OOTActor): - """Returns a single actor entry""" - posData = "{ " + ", ".join(f"{round(pos)}" for pos in actor.position) + " }" - rotData = "{ " + "".join(actor.rotation) + " }" - - actorInfos = [actor.actorID, posData, rotData, actor.actorParam] - infoDescs = ["Actor ID", "Position", "Rotation", "Parameters"] - - return ( - indent - + (f"// {actor.actorName}\n" + indent if actor.actorName != "" else "") - + "{\n" - + ",\n".join((indent * 2) + f"/* {desc:10} */ {info}" for desc, info in zip(infoDescs, actorInfos)) - + ("\n" + indent + "},\n") - ) - - -def getActorList(outRoom: OOTRoom, headerIndex: int): - """Returns the actor list for the current header""" - actorList = CData() - declarationBase = f"ActorEntry {outRoom.actorListName(headerIndex)}" - - # .h - actorList.header = f"extern {declarationBase}[];\n" - - # .c - actorList.source = ( - (f"{declarationBase}[{outRoom.getActorLengthDefineName(headerIndex)}]" + " = {\n") - + "\n".join(getActorEntry(actor) for actor in outRoom.actorList) - + "};\n\n" - ) - - return actorList - - -#################### -# Written to Scene # -#################### - -# Transition Actor List - - -def getTransitionActorEntry(transActor: OOTTransitionActor): - """Returns a single transition actor entry""" - sides = [(transActor.frontRoom, transActor.frontCam), (transActor.backRoom, transActor.backCam)] - roomData = "{ " + ", ".join(f"{room}, {cam}" for room, cam in sides) + " }" - posData = "{ " + ", ".join(f"{round(pos)}" for pos in transActor.position) + " }" - rotData = f"DEG_TO_BINANG({(transActor.rotationY * (180 / 0x8000)):.3f})" - - actorInfos = [roomData, transActor.actorID, posData, rotData, transActor.actorParam] - infoDescs = ["Room & Cam Index (Front, Back)", "Actor ID", "Position", "Rotation Y", "Parameters"] - - return ( - (indent + f"// {transActor.actorName}\n" + indent if transActor.actorName != "" else "") - + "{\n" - + ",\n".join((indent * 2) + f"/* {desc:30} */ {info}" for desc, info in zip(infoDescs, actorInfos)) - + ("\n" + indent + "},\n") - ) - - -def getTransitionActorList(outScene: OOTScene, headerIndex: int): - """Returns the transition actor list for the current header""" - transActorList = CData() - declarationBase = f"TransitionActorEntry {outScene.transitionActorListName(headerIndex)}" - - # .h - transActorList.header = f"extern {declarationBase}[];\n" - - # .c - transActorList.source = ( - (f"{declarationBase}[]" + " = {\n") - + "\n".join(getTransitionActorEntry(transActor) for transActor in outScene.transitionActorList) - + "};\n\n" - ) - - return transActorList - - -# Entrance List - - -def getSpawnActorList(outScene: OOTScene, headerIndex: int): - """Returns the spawn actor list for the current header""" - spawnActorList = CData() - declarationBase = f"ActorEntry {outScene.startPositionsName(headerIndex)}" - - # .h - spawnActorList.header = f"extern {declarationBase}[];\n" - - # .c - spawnActorList.source = ( - (f"{declarationBase}[]" + " = {\n") - + "".join(getActorEntry(spawnActor) for spawnActor in outScene.startPositions.values()) - + "};\n\n" - ) - - return spawnActorList - - -def getSpawnEntry(entrance: OOTEntrance): - """Returns a single spawn entry""" - return indent + "{ " + f"{entrance.startPositionIndex}, {entrance.roomIndex}" + " },\n" - - -def getSpawnList(outScene: OOTScene, headerIndex: int): - """Returns the spawn list for the current header""" - spawnList = CData() - declarationBase = f"Spawn {outScene.entranceListName(headerIndex)}" - - # .h - spawnList.header = f"extern {declarationBase}[];\n" - - # .c - spawnList.source = ( - (f"{declarationBase}[]" + " = {\n") - + (indent + "// { Spawn Actor List Index, Room Index }\n") - + "".join(getSpawnEntry(entrance) for entrance in outScene.entranceList) - + "};\n\n" - ) - - return spawnList diff --git a/fast64_internal/oot/scene/exporter/to_c/room_commands.py b/fast64_internal/oot/scene/exporter/to_c/room_commands.py deleted file mode 100644 index b75efe252..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/room_commands.py +++ /dev/null @@ -1,100 +0,0 @@ -from .....utility import CData, indent -from ....oot_level_classes import OOTRoom - - -def getEchoSettingsCmd(outRoom: OOTRoom): - return indent + f"SCENE_CMD_ECHO_SETTINGS({outRoom.echo})" - - -def getRoomBehaviourCmd(outRoom: OOTRoom): - showInvisibleActors = "true" if outRoom.showInvisibleActors else "false" - disableWarpSongs = "true" if outRoom.disableWarpSongs else "false" - - return ( - (indent + "SCENE_CMD_ROOM_BEHAVIOR(") - + ", ".join([outRoom.roomBehaviour, outRoom.linkIdleMode, showInvisibleActors, disableWarpSongs]) - + ")" - ) - - -def getSkyboxDisablesCmd(outRoom: OOTRoom): - disableSkybox = "true" if outRoom.disableSkybox else "false" - disableSunMoon = "true" if outRoom.disableSunMoon else "false" - - return indent + f"SCENE_CMD_SKYBOX_DISABLES({disableSkybox}, {disableSunMoon})" - - -def getTimeSettingsCmd(outRoom: OOTRoom): - return indent + f"SCENE_CMD_TIME_SETTINGS({outRoom.timeHours}, {outRoom.timeMinutes}, {outRoom.timeSpeed})" - - -def getWindSettingsCmd(outRoom: OOTRoom): - return ( - indent - + f"SCENE_CMD_WIND_SETTINGS({', '.join(f'{dir}' for dir in outRoom.windVector)}, {outRoom.windStrength}),\n" - ) - - -def getOcclusionPlaneCandidatesListCmd(outRoom: OOTRoom): - return ( - indent - + f"SCENE_CMD_OCCLUSION_PLANE_CANDIDATES_LIST({len(outRoom.occlusion_planes.planes)}, {outRoom.occlusion_planes.name})" - ) - - -def getRoomShapeCmd(outRoom: OOTRoom): - return indent + f"SCENE_CMD_ROOM_SHAPE(&{outRoom.mesh.headerName()})" - - -def getObjectListCmd(outRoom: OOTRoom, headerIndex: int): - return ( - indent + "SCENE_CMD_OBJECT_LIST(" - ) + f"{outRoom.getObjectLengthDefineName(headerIndex)}, {outRoom.objectListName(headerIndex)})" - - -def getActorListCmd(outRoom: OOTRoom, headerIndex: int): - return ( - indent + "SCENE_CMD_ACTOR_LIST(" - ) + f"{outRoom.getActorLengthDefineName(headerIndex)}, {outRoom.actorListName(headerIndex)})" - - -def getRoomCommandList(outRoom: OOTRoom, headerIndex: int): - cmdListData = CData() - declarationBase = f"SceneCmd {outRoom.roomName()}_header{headerIndex:02}" - - getCmdFunc1ArgList = [ - getEchoSettingsCmd, - getRoomBehaviourCmd, - getSkyboxDisablesCmd, - getTimeSettingsCmd, - getRoomShapeCmd, - ] - - getCmdFunc2ArgList = [] - - if outRoom.setWind: - getCmdFunc1ArgList.append(getWindSettingsCmd) - - if len(outRoom.occlusion_planes.planes) > 0: - getCmdFunc1ArgList.append(getOcclusionPlaneCandidatesListCmd) - - if len(outRoom.objectIDList) > 0: - getCmdFunc2ArgList.append(getObjectListCmd) - - if len(outRoom.actorList) > 0: - getCmdFunc2ArgList.append(getActorListCmd) - - roomCmdData = ( - (outRoom.getAltHeaderListCmd(outRoom.alternateHeadersName()) if outRoom.hasAlternateHeaders() else "") - + "".join(getCmd(outRoom) + ",\n" for getCmd in getCmdFunc1ArgList) - + "".join(getCmd(outRoom, headerIndex) + ",\n" for getCmd in getCmdFunc2ArgList) - + outRoom.getEndCmd() - ) - - # .h - cmdListData.header = f"extern {declarationBase}[];\n" - - # .c - cmdListData.source = f"{declarationBase}[]" + " = {\n" + roomCmdData + "};\n\n" - - return cmdListData diff --git a/fast64_internal/oot/scene/exporter/to_c/room_header.py b/fast64_internal/oot/scene/exporter/to_c/room_header.py deleted file mode 100644 index ac93c14ce..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/room_header.py +++ /dev/null @@ -1,83 +0,0 @@ -from .....utility import CData, indent -from ....oot_level_classes import OOTRoom -from .actor import getActorList -from .room_commands import getRoomCommandList - - -def getHeaderDefines(outRoom: OOTRoom, headerIndex: int): - """Returns a string containing defines for actor and object lists lengths""" - headerDefines = "" - - if len(outRoom.objectIDList) > 0: - headerDefines += f"#define {outRoom.getObjectLengthDefineName(headerIndex)} {len(outRoom.objectIDList)}\n" - - if len(outRoom.actorList) > 0: - headerDefines += f"#define {outRoom.getActorLengthDefineName(headerIndex)} {len(outRoom.actorList)}\n" - - return headerDefines - - -# Object List -def getObjectList(outRoom: OOTRoom, headerIndex: int): - objectList = CData() - declarationBase = f"s16 {outRoom.objectListName(headerIndex)}" - - # .h - objectList.header = f"extern {declarationBase}[];\n" - - # .c - objectList.source = ( - (f"{declarationBase}[{outRoom.getObjectLengthDefineName(headerIndex)}]" + " = {\n") - + ",\n".join(indent + objectID for objectID in outRoom.objectIDList) - + ",\n};\n\n" - ) - - return objectList - - -# Room Header -def getRoomData(outRoom: OOTRoom): - roomC = CData() - - roomHeaders = [ - (outRoom.childNightHeader, "Child Night"), - (outRoom.adultDayHeader, "Adult Day"), - (outRoom.adultNightHeader, "Adult Night"), - ] - - for i, csHeader in enumerate(outRoom.cutsceneHeaders): - roomHeaders.append((csHeader, f"Cutscene No. {i + 1}")) - - declarationBase = f"SceneCmd* {outRoom.alternateHeadersName()}" - - # .h - roomC.header = f"extern {declarationBase}[];\n" - - # .c - altHeaderPtrList = ( - f"{declarationBase}[]" - + " = {\n" - + "\n".join( - indent + f"{curHeader.roomName()}_header{i:02}," if curHeader is not None else indent + "NULL," - for i, (curHeader, headerDesc) in enumerate(roomHeaders, 1) - ) - + "\n};\n\n" - ) - - roomHeaders.insert(0, (outRoom, "Child Day (Default)")) - for i, (curHeader, headerDesc) in enumerate(roomHeaders): - if curHeader is not None: - roomC.source += "/**\n * " + f"Header {headerDesc}\n" + "*/\n" - roomC.source += getHeaderDefines(curHeader, i) - roomC.append(getRoomCommandList(curHeader, i)) - - if i == 0 and outRoom.hasAlternateHeaders(): - roomC.source += altHeaderPtrList - - if len(curHeader.objectIDList) > 0: - roomC.append(getObjectList(curHeader, i)) - - if len(curHeader.actorList) > 0: - roomC.append(getActorList(curHeader, i)) - - return roomC diff --git a/fast64_internal/oot/scene/exporter/to_c/room_shape.py b/fast64_internal/oot/scene/exporter/to_c/room_shape.py deleted file mode 100644 index c4029d05f..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/room_shape.py +++ /dev/null @@ -1,154 +0,0 @@ -from .....utility import CData, indent -from .....f3d.f3d_gbi import ScrollMethod, TextureExportSettings -from ....oot_model_classes import OOTGfxFormatter -from ....oot_constants import ootEnumRoomShapeType -from ....oot_level_classes import OOTRoom, OOTRoomMeshGroup, OOTRoomMesh - -ootRoomShapeStructs = [ - "RoomShapeNormal", - "RoomShapeImage", - "RoomShapeCullable", -] - -ootRoomShapeEntryStructs = [ - "RoomShapeDListsEntry", - "RoomShapeDListsEntry", - "RoomShapeCullableEntry", -] - - -def getRoomShapeDLEntry(meshEntry: OOTRoomMeshGroup, roomShape: str): - opaqueName = meshEntry.DLGroup.opaque.name if meshEntry.DLGroup.opaque is not None else "NULL" - transparentName = meshEntry.DLGroup.transparent.name if meshEntry.DLGroup.transparent is not None else "NULL" - - roomShapeDListsEntries = "{ " - if roomShape == "ROOM_SHAPE_TYPE_CULLABLE": - roomShapeDListsEntries += ( - "{ " + ", ".join(f"{pos}" for pos in meshEntry.cullGroup.position) + " }, " - ) + f"{meshEntry.cullGroup.cullDepth}, " - roomShapeDListsEntries += f"{opaqueName}, {transparentName}" + " },\n" - - return roomShapeDListsEntries - - -# Texture files must be saved separately. -def getRoomShapeImageData(roomMesh: OOTRoomMesh, textureSettings: TextureExportSettings): - code = CData() - - if len(roomMesh.bgImages) > 1: - declarationBase = f"RoomShapeImageMultiBgEntry {roomMesh.getMultiBgStructName()}" - - # .h - code.header += f"extern {declarationBase}[{len(roomMesh.bgImages)}];\n" - - # .c - code.source += f"{declarationBase}[{len(roomMesh.bgImages)}] = {{\n" - for i in range(len(roomMesh.bgImages)): - bgImage = roomMesh.bgImages[i] - code.source += indent + "{\n" + bgImage.multiPropertiesC(2, i) + indent + "},\n" - code.source += f"}};\n\n" - - bitsPerValue = 64 - for bgImage in roomMesh.bgImages: - # .h - code.header += f"extern u{bitsPerValue} {bgImage.name}[];\n" - - # .c - code.source += ( - # This is to force 8 byte alignment - (f"Gfx {bgImage.name}_aligner[] = " + "{ gsSPEndDisplayList() };\n" if bitsPerValue != 64 else "") - + (f"u{bitsPerValue} {bgImage.name}[SCREEN_WIDTH * SCREEN_HEIGHT / 4]" + " = {\n") - + f'#include "{textureSettings.includeDir + bgImage.getFilename()}.inc.c"' - + "\n};\n\n" - ) - - return code - - -def getRoomShape(outRoom: OOTRoom): - roomShapeInfo = CData() - roomShapeDLArray = CData() - mesh = outRoom.mesh - - shapeTypeIdx = [value[0] for value in ootEnumRoomShapeType].index(mesh.roomShape) - dlEntryType = ootRoomShapeEntryStructs[shapeTypeIdx] - structName = ootRoomShapeStructs[shapeTypeIdx] - roomShapeImageFormat = "Multi" if len(mesh.bgImages) > 1 else "Single" - - if mesh.roomShape == "ROOM_SHAPE_TYPE_IMAGE": - structName += roomShapeImageFormat - - roomShapeInfo.header = f"extern {structName} {mesh.headerName()};\n" - - if mesh.roomShape != "ROOM_SHAPE_TYPE_IMAGE": - entryName = mesh.entriesName() - dlEntryDeclarationBase = f"{dlEntryType} {mesh.entriesName()}[{len(mesh.meshEntries)}]" - - roomShapeInfo.source = ( - "\n".join( - ( - f"{structName} {mesh.headerName()} = {{", - indent + f"{mesh.roomShape},", - indent + f"ARRAY_COUNT({entryName}),", - indent + f"{entryName},", - indent + f"{entryName} + ARRAY_COUNT({entryName})", - f"}};", - ) - ) - + "\n\n" - ) - - roomShapeDLArray.header = f"extern {dlEntryDeclarationBase};\n" - roomShapeDLArray.source = dlEntryDeclarationBase + " = {\n" - - for entry in mesh.meshEntries: - roomShapeDLArray.source += indent + getRoomShapeDLEntry(entry, mesh.roomShape) - - roomShapeDLArray.source += "};\n\n" - else: - # type 1 only allows 1 room - entry = mesh.meshEntries[0] - - roomShapeImageFormatValue = ( - "ROOM_SHAPE_IMAGE_AMOUNT_SINGLE" if roomShapeImageFormat == "Single" else "ROOM_SHAPE_IMAGE_AMOUNT_MULTI" - ) - - roomShapeInfo.source += ( - (f"{structName} {mesh.headerName()}" + " = {\n") - + (indent + f"{{ ROOM_SHAPE_TYPE_IMAGE, {roomShapeImageFormatValue}, &{mesh.entriesName()} }},\n") - + ( - mesh.bgImages[0].singlePropertiesC(1) - if roomShapeImageFormat == "Single" - else indent + f"ARRAY_COUNTU({mesh.getMultiBgStructName()}), {mesh.getMultiBgStructName()}," - ) - + "\n};\n\n" - ) - - roomShapeDLArray.header = f"extern {dlEntryType} {mesh.entriesName()};\n" - roomShapeDLArray.source = ( - f"{dlEntryType} {mesh.entriesName()} = {getRoomShapeDLEntry(entry, mesh.roomShape)[:-2]};\n\n" - ) - - roomShapeInfo.append(roomShapeDLArray) - return roomShapeInfo - - -def getRoomModel(outRoom: OOTRoom, textureExportSettings: TextureExportSettings): - roomModel = CData() - mesh = outRoom.mesh - - for i, entry in enumerate(mesh.meshEntries): - if entry.DLGroup.opaque is not None: - roomModel.append(entry.DLGroup.opaque.to_c(mesh.model.f3d)) - - if entry.DLGroup.transparent is not None: - roomModel.append(entry.DLGroup.transparent.to_c(mesh.model.f3d)) - - # type ``ROOM_SHAPE_TYPE_IMAGE`` only allows 1 room - if i == 0 and mesh.roomShape == "ROOM_SHAPE_TYPE_IMAGE": - break - - roomModel.append(mesh.model.to_c(textureExportSettings, OOTGfxFormatter(ScrollMethod.Vertex)).all()) - roomModel.append(getRoomShapeImageData(outRoom.mesh, textureExportSettings)) - - return roomModel diff --git a/fast64_internal/oot/scene/exporter/to_c/scene.py b/fast64_internal/oot/scene/exporter/to_c/scene.py deleted file mode 100644 index 9045ae30b..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene.py +++ /dev/null @@ -1,79 +0,0 @@ -from .....utility import CData, PluginError -from .....f3d.f3d_gbi import TextureExportSettings -from ....oot_level_classes import OOTScene -from .scene_header import getSceneData, getSceneModel -from .scene_collision import getSceneCollision -from .scene_cutscene import getSceneCutscenes -from .room_header import getRoomData -from .room_shape import getRoomModel, getRoomShape - - -class OOTSceneC: - def sceneTexturesIsUsed(self): - return len(self.sceneTexturesC.source) > 0 - - def sceneCutscenesIsUsed(self): - return len(self.sceneCutscenesC) > 0 - - def __init__(self): - # Main header file for both the scene and room(s) - self.header = CData() - - # Files for the scene segment - self.sceneMainC = CData() - self.sceneTexturesC = CData() - self.sceneCollisionC = CData() - self.sceneCutscenesC = [] - - # Files for room segments - self.roomMainC = {} - self.roomOcclusionPlanesC = {} - self.roomShapeInfoC = {} - self.roomModelC = {} - - -def getSceneC(outScene: OOTScene, textureExportSettings: TextureExportSettings): - """Generates C code for each scene element and returns the data""" - sceneC = OOTSceneC() - - sceneC.sceneMainC = getSceneData(outScene) - sceneC.sceneTexturesC = getSceneModel(outScene, textureExportSettings) - sceneC.sceneCollisionC = getSceneCollision(outScene) - sceneC.sceneCutscenesC = getSceneCutscenes(outScene) - - for outRoom in outScene.rooms.values(): - outRoomName = outRoom.roomName() - - if len(outRoom.mesh.meshEntries) > 0: - roomShapeInfo = getRoomShape(outRoom) - roomModel = getRoomModel(outRoom, textureExportSettings) - else: - raise PluginError(f"Error: Room {outRoom.index} has no mesh children.") - - sceneC.roomMainC[outRoomName] = getRoomData(outRoom) - sceneC.roomOcclusionPlanesC[outRoomName] = outRoom.occlusion_planes.to_c() - sceneC.roomShapeInfoC[outRoomName] = roomShapeInfo - sceneC.roomModelC[outRoomName] = roomModel - - return sceneC - - -def getIncludes(outScene: OOTScene): - """Returns the files to include""" - # @TODO: avoid including files where it's not needed - includeData = CData() - - fileNames = [ - "ultra64", - "z64", - "macros", - outScene.sceneName(), - "segment_symbols", - "command_macros_base", - "z64cutscene_commands", - "variables", - ] - - includeData.source = "\n".join(f'#include "{fileName}.h"' for fileName in fileNames) + "\n\n" - - return includeData diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_bootup.py b/fast64_internal/oot/scene/exporter/to_c/scene_bootup.py deleted file mode 100644 index fd688a12d..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_bootup.py +++ /dev/null @@ -1,151 +0,0 @@ -import os, re -from typing import Any -from .....utility import PluginError, writeFile, readFile - - -def writeBootupSettings( - configPath: str, - bootMode: str, - newGameOnly: bool, - entranceIndex: str, - linkAge: str, - timeOfDay: str, - cutsceneIndex: str, - saveFileNameData: str, -): - if os.path.exists(configPath): - originalData = readFile(configPath) - data = originalData - else: - originalData = "" - data = ( - f"// #define BOOT_TO_SCENE\n" - + f"// #define BOOT_TO_SCENE_NEW_GAME_ONLY\n" - + f"// #define BOOT_TO_FILE_SELECT\n" - + f"// #define BOOT_TO_MAP_SELECT\n" - + f"#define BOOT_ENTRANCE 0\n" - + f"#define BOOT_AGE LINK_AGE_CHILD\n" - + f"#define BOOT_TIME NEXT_TIME_NONE\n" - + f"#define BOOT_CUTSCENE 0xFFEF\n" - + f"#define BOOT_PLAYER_NAME 0x15, 0x12, 0x17, 0x14, 0x3E, 0x3E, 0x3E, 0x3E\n\n" - ) - - data = re.sub( - r"(//\s*)?#define\s*BOOT_TO_SCENE", - ("" if bootMode == "Play" else "// ") + "#define BOOT_TO_SCENE", - data, - ) - data = re.sub( - r"(//\s*)?#define\s*BOOT_TO_SCENE_NEW_GAME_ONLY", - ("" if newGameOnly else "// ") + "#define BOOT_TO_SCENE_NEW_GAME_ONLY", - data, - ) - data = re.sub( - r"(//\s*)?#define\s*BOOT_TO_FILE_SELECT", - ("" if bootMode == "File Select" else "// ") + "#define BOOT_TO_FILE_SELECT", - data, - ) - data = re.sub( - r"(//\s*)?#define\s*BOOT_TO_MAP_SELECT", - ("" if bootMode == "Map Select" else "// ") + "#define BOOT_TO_MAP_SELECT", - data, - ) - data = re.sub(r"#define\s*BOOT_ENTRANCE\s*[^\s]*", f"#define BOOT_ENTRANCE {entranceIndex}", data) - data = re.sub(r"#define\s*BOOT_AGE\s*[^\s]*", f"#define BOOT_AGE {linkAge}", data) - data = re.sub(r"#define\s*BOOT_TIME\s*[^\s]*", f"#define BOOT_TIME {timeOfDay}", data) - data = re.sub(r"#define\s*BOOT_CUTSCENE\s*[^\s]*", f"#define BOOT_CUTSCENE {cutsceneIndex}", data) - data = re.sub(r"#define\s*BOOT_PLAYER_NAME\s*[^\n]*", f"#define BOOT_PLAYER_NAME {saveFileNameData}", data) - - if data != originalData: - writeFile(configPath, data) - - -def setBootupScene(configPath: str, entranceIndex: str, options): - # ``options`` argument type: OOTBootupSceneOptions - linkAge = "LINK_AGE_CHILD" - timeOfDay = "NEXT_TIME_NONE" - cutsceneIndex = "0xFFEF" - newEntranceIndex = "0" - saveName = "LINK" - - if options.bootMode != "Map Select": - newEntranceIndex = entranceIndex - saveName = options.newGameName - - if options.overrideHeader: - timeOfDay, linkAge = getParamsFromOptions(options) - if options.headerOption == "Cutscene": - cutsceneIndex = "0xFFF" + format(options.cutsceneIndex - 4, "X") - - saveFileNameData = ", ".join(["0x" + format(i, "02X") for i in stringToSaveNameBytes(saveName)]) - - writeBootupSettings( - configPath, - options.bootMode, - options.newGameOnly, - newEntranceIndex, - linkAge, - timeOfDay, - cutsceneIndex, - saveFileNameData, - ) - - -def clearBootupScene(configPath: str): - writeBootupSettings( - configPath, - "", - False, - "0", - "LINK_AGE_CHILD", - "NEXT_TIME_NONE", - "0xFFEF", - "0x15, 0x12, 0x17, 0x14, 0x3E, 0x3E, 0x3E, 0x3E", - ) - - -def getParamsFromOptions(options: Any) -> tuple[str, str]: - timeOfDay = ( - "NEXT_TIME_DAY" - if options.headerOption == "Child Day" or options.headerOption == "Adult Day" - else "NEXT_TIME_NIGHT" - ) - - linkAge = ( - "LINK_AGE_ADULT" - if options.headerOption == "Adult Day" or options.headerOption == "Adult Night" - else "LINK_AGE_CHILD" - ) - - return timeOfDay, linkAge - - -# converts ascii text to format for save file name. -# see src/code/z_message_PAL.c:Message_Decode() -def stringToSaveNameBytes(name: str) -> bytearray: - specialChar = { - " ": 0x3E, - ".": 0x40, - "-": 0x3F, - } - - result = bytearray([0x3E] * 8) - - if len(name) > 8: - raise PluginError("Save file name for scene bootup must be 8 characters or less.") - for i in range(len(name)): - value = ord(name[i]) - if name[i] in specialChar: - result[i] = specialChar[name[i]] - elif value >= ord("0") and value <= ord("9"): # numbers - result[i] = value - ord("0") - elif value >= ord("A") and value <= ord("Z"): # uppercase - result[i] = value - ord("7") - elif value >= ord("a") and value <= ord("z"): # lowercase - result[i] = value - ord("=") - else: - raise PluginError( - name + " has some invalid characters and cannot be used as a save file name for scene bootup." - ) - - return result diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_collision.py b/fast64_internal/oot/scene/exporter/to_c/scene_collision.py deleted file mode 100644 index 241b53dad..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_collision.py +++ /dev/null @@ -1,9 +0,0 @@ -from ....collision.exporter.to_c import ootCollisionToC -from ....oot_level_classes import OOTScene - - -# Writes the collision data for a scene -def getSceneCollision(outScene: OOTScene): - # @TODO: delete this function and rename ``ootCollisionToC`` into ``getSceneCollision`` - # when the ``oot_collision.py`` code is cleaned up - return ootCollisionToC(outScene.collision) diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_commands.py b/fast64_internal/oot/scene/exporter/to_c/scene_commands.py deleted file mode 100644 index db7d85440..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_commands.py +++ /dev/null @@ -1,116 +0,0 @@ -from .....utility import CData, indent -from ....oot_level_classes import OOTScene - - -def getSoundSettingsCmd(outScene: OOTScene): - return indent + f"SCENE_CMD_SOUND_SETTINGS({outScene.audioSessionPreset}, {outScene.nightSeq}, {outScene.musicSeq})" - - -def getRoomListCmd(outScene: OOTScene): - return indent + f"SCENE_CMD_ROOM_LIST({len(outScene.rooms)}, {outScene.roomListName()})" - - -def getTransActorListCmd(outScene: OOTScene, headerIndex: int): - return ( - indent + "SCENE_CMD_TRANSITION_ACTOR_LIST(" - ) + f"{len(outScene.transitionActorList)}, {outScene.transitionActorListName(headerIndex)})" - - -def getMiscSettingsCmd(outScene: OOTScene): - return indent + f"SCENE_CMD_MISC_SETTINGS({outScene.cameraMode}, {outScene.mapLocation})" - - -def getColHeaderCmd(outScene: OOTScene): - return indent + f"SCENE_CMD_COL_HEADER(&{outScene.collision.headerName()})" - - -def getSpawnListCmd(outScene: OOTScene, headerIndex: int): - return ( - indent + "SCENE_CMD_ENTRANCE_LIST(" - ) + f"{outScene.entranceListName(headerIndex) if len(outScene.entranceList) > 0 else 'NULL'})" - - -def getSpecialFilesCmd(outScene: OOTScene): - return indent + f"SCENE_CMD_SPECIAL_FILES({outScene.naviCup}, {outScene.globalObject})" - - -def getPathListCmd(outScene: OOTScene, headerIndex: int): - return indent + f"SCENE_CMD_PATH_LIST({outScene.pathListName(headerIndex)})" - - -def getSpawnActorListCmd(outScene: OOTScene, headerIndex: int): - return ( - (indent + "SCENE_CMD_SPAWN_LIST(") - + f"{len(outScene.startPositions)}, " - + f"{outScene.startPositionsName(headerIndex) if len(outScene.startPositions) > 0 else 'NULL'})" - ) - - -def getSkyboxSettingsCmd(outScene: OOTScene): - return ( - indent - + f"SCENE_CMD_SKYBOX_SETTINGS({outScene.skyboxID}, {outScene.skyboxCloudiness}, {outScene.skyboxLighting})" - ) - - -def getExitListCmd(outScene: OOTScene, headerIndex: int): - return indent + f"SCENE_CMD_EXIT_LIST({outScene.exitListName(headerIndex)})" - - -def getLightSettingsCmd(outScene: OOTScene, headerIndex: int): - return ( - indent + "SCENE_CMD_ENV_LIGHT_SETTINGS(" - ) + f"{len(outScene.lights)}, {outScene.lightListName(headerIndex) if len(outScene.lights) > 0 else 'NULL'})" - - -def getCutsceneDataCmd(outScene: OOTScene, headerIndex: int): - match outScene.csWriteType: - case "Object": - csDataName = outScene.csName - case _: - csDataName = outScene.csWriteCustom - - return indent + f"SCENE_CMD_CUTSCENE_DATA({csDataName})" - - -def getSceneCommandList(outScene: OOTScene, headerIndex: int): - cmdListData = CData() - declarationBase = f"SceneCmd {outScene.sceneName()}_header{headerIndex:02}" - - getCmdFunc1ArgList = [ - getSoundSettingsCmd, - getRoomListCmd, - getMiscSettingsCmd, - getColHeaderCmd, - getSpecialFilesCmd, - getSkyboxSettingsCmd, - ] - - getCmdFunc2ArgList = [getSpawnListCmd, getSpawnActorListCmd, getLightSettingsCmd] - - if len(outScene.transitionActorList) > 0: - getCmdFunc2ArgList.append(getTransActorListCmd) - - if len(outScene.pathList) > 0: - getCmdFunc2ArgList.append(getPathListCmd) - - if len(outScene.exitList) > 0: - getCmdFunc2ArgList.append(getExitListCmd) - - if outScene.writeCutscene: - getCmdFunc2ArgList.append(getCutsceneDataCmd) - - sceneCmdData = ( - (outScene.getAltHeaderListCmd(outScene.alternateHeadersName()) if outScene.hasAlternateHeaders() else "") - + "".join(getCmd(outScene) + ",\n" for getCmd in getCmdFunc1ArgList) - + "".join(getCmd(outScene, headerIndex) + ",\n" for getCmd in getCmdFunc2ArgList) - + outScene.getEndCmd() - ) - - # .h - cmdListData.header = f"extern {declarationBase}[]" + ";\n" - - # .c - cmdListData.source = f"{declarationBase}[]" + " = {\n" + sceneCmdData + "};\n\n" - - return cmdListData diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_cutscene.py b/fast64_internal/oot/scene/exporter/to_c/scene_cutscene.py deleted file mode 100644 index 8f0ab5c09..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_cutscene.py +++ /dev/null @@ -1,51 +0,0 @@ -import bpy - -from .....utility import CData -from ....oot_level_classes import OOTScene -from ....cutscene.exporter import getNewCutsceneExport - - -def getCutsceneC(csName: str): - csData = CData() - declarationBase = f"CutsceneData {csName}[]" - - # .h - csData.header = f"extern {declarationBase};\n" - - # .c - csData.source = ( - declarationBase - + " = {\n" - + getNewCutsceneExport(csName, bpy.context.scene.exportMotionOnly).getExportData() - + "};\n\n" - ) - - return csData - - -def getSceneCutscenes(outScene: OOTScene): - cutscenes: list[CData] = [] - altHeaders: list[OOTScene] = [ - outScene, - outScene.childNightHeader, - outScene.adultDayHeader, - outScene.adultNightHeader, - ] - altHeaders.extend(outScene.cutsceneHeaders) - csObjects = [] - - for curHeader in altHeaders: - # curHeader is either None or an OOTScene. This can either be the main scene itself, - # or one of the alternate / cutscene headers. - if curHeader is not None and curHeader.writeCutscene: - if curHeader.csWriteType == "Object" and curHeader.csName not in csObjects: - cutscenes.append(getCutsceneC(curHeader.csName)) - csObjects.append(curHeader.csName) - - for csObj in outScene.extraCutscenes: - name = csObj.name.removeprefix("Cutscene.") - if not name in csObjects: - cutscenes.append(getCutsceneC(name)) - csObjects.append(name) - - return cutscenes diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_folder.py b/fast64_internal/oot/scene/exporter/to_c/scene_folder.py deleted file mode 100644 index 57e7873ff..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_folder.py +++ /dev/null @@ -1,29 +0,0 @@ -import os, re, shutil -from ....oot_utility import ExportInfo, getSceneDirFromLevelName -from ....oot_level_classes import OOTScene - - -def modifySceneFiles(outScene: OOTScene, exportInfo: ExportInfo): - if exportInfo.customSubPath is not None: - sceneDir = exportInfo.customSubPath + exportInfo.name - else: - sceneDir = getSceneDirFromLevelName(outScene.name) - - scenePath = os.path.join(exportInfo.exportPath, sceneDir) - for filename in os.listdir(scenePath): - filepath = os.path.join(scenePath, filename) - if os.path.isfile(filepath): - match = re.match(outScene.name + "\_room\_(\d+)\.[ch]", filename) - if match is not None and int(match.group(1)) >= len(outScene.rooms): - os.remove(filepath) - - -def deleteSceneFiles(exportInfo: ExportInfo): - if exportInfo.customSubPath is not None: - sceneDir = exportInfo.customSubPath + exportInfo.name - else: - sceneDir = getSceneDirFromLevelName(exportInfo.name) - - scenePath = os.path.join(exportInfo.exportPath, sceneDir) - if os.path.exists(scenePath): - shutil.rmtree(scenePath) diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_header.py b/fast64_internal/oot/scene/exporter/to_c/scene_header.py deleted file mode 100644 index 9598a562e..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_header.py +++ /dev/null @@ -1,219 +0,0 @@ -from .....utility import CData, indent -from .....f3d.f3d_gbi import ScrollMethod, TextureExportSettings -from ....oot_model_classes import OOTGfxFormatter -from ....oot_level_classes import OOTScene, OOTLight -from .scene_pathways import getPathData -from .actor import getTransitionActorList, getSpawnActorList, getSpawnList -from .scene_commands import getSceneCommandList - - -################## -# Light Settings # -################## -def getColorValues(vector: tuple[int, int, int]): - return ", ".join(f"{v:5}" for v in vector) - - -def getDirectionValues(vector: tuple[int, int, int]): - return ", ".join(f"{v - 0x100 if v > 0x7F else v:5}" for v in vector) - - -def getLightSettingsEntry(light: OOTLight, lightMode: str, isLightingCustom: bool, index: int): - vectors = [ - (light.ambient, "Ambient Color", getColorValues), - (light.diffuseDir0, "Diffuse0 Direction", getDirectionValues), - (light.diffuse0, "Diffuse0 Color", getColorValues), - (light.diffuseDir1, "Diffuse1 Direction", getDirectionValues), - (light.diffuse1, "Diffuse1 Color", getColorValues), - (light.fogColor, "Fog Color", getColorValues), - ] - - fogData = [ - (light.getBlendFogNear(), "Blend Rate & Fog Near"), - (f"{light.z_far}", "Z Far"), - ] - - lightDescs = ["Dawn", "Day", "Dusk", "Night"] - - if not isLightingCustom and lightMode == "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 lightMode == "LIGHT_MODE_SETTINGS" - lightDesc = f"// {'Indoor' if isIndoor else 'Custom'} No. {index + 1} Lighting\n" - - lightData = ( - (indent + lightDesc) - + (indent + "{\n") - + "".join(indent * 2 + f"{'{ ' + vecToC(vector) + ' },':26} // {desc}\n" for vector, desc, vecToC in vectors) - + "".join(indent * 2 + f"{fogValue + ',':26} // {fogDesc}\n" for fogValue, fogDesc in fogData) - + (indent + "},\n") - ) - - return lightData - - -def getLightSettings(outScene: OOTScene, headerIndex: int): - lightSettingsData = CData() - declarationBase = f"EnvLightSettings {outScene.lightListName(headerIndex)}[{len(outScene.lights)}]" - - # .h - lightSettingsData.header = f"extern {declarationBase};\n" - - # .c - lightSettingsData.source = ( - (declarationBase + " = {\n") - + "".join( - getLightSettingsEntry(light, outScene.skyboxLighting, outScene.isSkyboxLightingCustom, i) - for i, light in enumerate(outScene.lights) - ) - + "};\n\n" - ) - - return lightSettingsData - - -######## -# Mesh # -######## -# Writes the textures and material setup displaylists that are shared between multiple rooms (is written to the scene) -def getSceneModel(outScene: OOTScene, textureExportSettings: TextureExportSettings) -> CData: - return outScene.model.to_c(textureExportSettings, OOTGfxFormatter(ScrollMethod.Vertex)).all() - - -############# -# Exit List # -############# -def getExitList(outScene: OOTScene, headerIndex: int): - exitList = CData() - declarationBase = f"u16 {outScene.exitListName(headerIndex)}[{len(outScene.exitList)}]" - - # .h - exitList.header = f"extern {declarationBase};\n" - - # .c - exitList.source = ( - (declarationBase + " = {\n") - # @TODO: use the enum name instead of the raw index - + "\n".join(indent + f"{exitEntry.index}," for exitEntry in outScene.exitList) - + "\n};\n\n" - ) - - return exitList - - -############# -# Room List # -############# -def getRoomList(outScene: OOTScene): - roomList = CData() - declarationBase = f"RomFile {outScene.roomListName()}[]" - - # generating segment rom names for every room - segNames = [] - for i in range(len(outScene.rooms)): - roomName = outScene.rooms[i].roomName() - segNames.append((f"_{roomName}SegmentRomStart", f"_{roomName}SegmentRomEnd")) - - # .h - roomList.header += f"extern {declarationBase};\n" - - if not outScene.write_dummy_room_list: - # Write externs for rom segments - roomList.header += "".join( - f"extern u8 {startName}[];\n" + f"extern u8 {stopName}[];\n" for startName, stopName in segNames - ) - - # .c - roomList.source = declarationBase + " = {\n" - - if outScene.write_dummy_room_list: - roomList.source = ( - "// Dummy room list\n" + roomList.source + ((indent + "{ NULL, NULL },\n") * len(outScene.rooms)) - ) - else: - roomList.source += ( - " },\n".join(indent + "{ " + f"(u32){startName}, (u32){stopName}" for startName, stopName in segNames) - + " },\n" - ) - - roomList.source += "};\n\n" - return roomList - - -################ -# Scene Header # -################ -def getHeaderData(header: OOTScene, headerIndex: int): - headerData = CData() - - # Write the spawn position list data - if len(header.startPositions) > 0: - headerData.append(getSpawnActorList(header, headerIndex)) - - # Write the transition actor list data - if len(header.transitionActorList) > 0: - headerData.append(getTransitionActorList(header, headerIndex)) - - # Write the entrance list - if len(header.entranceList) > 0: - headerData.append(getSpawnList(header, headerIndex)) - - # Write the exit list - if len(header.exitList) > 0: - headerData.append(getExitList(header, headerIndex)) - - # Write the light data - if len(header.lights) > 0: - headerData.append(getLightSettings(header, headerIndex)) - - # Write the path data, if used - if len(header.pathList) > 0: - headerData.append(getPathData(header, headerIndex)) - - return headerData - - -def getSceneData(outScene: OOTScene): - sceneC = CData() - - headers = [ - (outScene.childNightHeader, "Child Night"), - (outScene.adultDayHeader, "Adult Day"), - (outScene.adultNightHeader, "Adult Night"), - ] - - for i, csHeader in enumerate(outScene.cutsceneHeaders): - headers.append((csHeader, f"Cutscene No. {i + 1}")) - - altHeaderPtrs = "\n".join( - indent + f"{curHeader.sceneName()}_header{i:02}," - if curHeader is not None - else indent + "NULL," - if i < 4 - else "" - for i, (curHeader, headerDesc) in enumerate(headers, 1) - ) - - headers.insert(0, (outScene, "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(getSceneCommandList(curHeader, i)) - - if i == 0: - if outScene.hasAlternateHeaders(): - declarationBase = f"SceneCmd* {outScene.alternateHeadersName()}[]" - sceneC.header += f"extern {declarationBase};\n" - sceneC.source += declarationBase + " = {\n" + altHeaderPtrs + "\n};\n\n" - - # Write the room segment list - sceneC.append(getRoomList(outScene)) - - sceneC.append(getHeaderData(curHeader, i)) - - return sceneC diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_pathways.py b/fast64_internal/oot/scene/exporter/to_c/scene_pathways.py deleted file mode 100644 index 48aad26bc..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_pathways.py +++ /dev/null @@ -1,47 +0,0 @@ -from .....utility import CData, indent -from ....oot_spline import OOTPath -from ....oot_level_classes import OOTScene - - -def getPathPointData(path: OOTPath, headerIndex: int, pathIndex: int): - pathData = CData() - declarationBase = f"Vec3s {path.pathName(headerIndex, pathIndex)}" - - # .h - pathData.header = f"extern {declarationBase}[];\n" - - # .c - pathData.source = ( - f"{declarationBase}[]" - + " = {\n" - + "\n".join( - indent + "{ " + ", ".join(f"{round(curPoint):5}" for curPoint in point) + " }," for point in path.points - ) - + "\n};\n\n" - ) - - return pathData - - -def getPathData(outScene: OOTScene, headerIndex: int): - pathData = CData() - pathListData = CData() - declarationBase = f"Path {outScene.pathListName(headerIndex)}[{len(outScene.pathList)}]" - - # .h - pathListData.header = f"extern {declarationBase};\n" - - # .c - pathListData.source = declarationBase + " = {\n" - - # Parse in alphabetical order of names - sortedPathList = sorted(outScene.pathList, key=lambda x: x.objName.lower()) - for i, curPath in enumerate(sortedPathList): - pathName = curPath.pathName(headerIndex, i) - pathListData.source += indent + "{ " + f"ARRAY_COUNTU({pathName}), {pathName}" + " },\n" - pathData.append(getPathPointData(curPath, headerIndex, i)) - - pathListData.source += "};\n\n" - pathData.append(pathListData) - - return pathData diff --git a/fast64_internal/oot/scene/exporter/to_c/scene_table_c.py b/fast64_internal/oot/scene/exporter/to_c/scene_table_c.py deleted file mode 100644 index c0cee9909..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/scene_table_c.py +++ /dev/null @@ -1,322 +0,0 @@ -import os -import enum -import bpy - -from dataclasses import dataclass, field -from typing import Optional -from .....utility import PluginError, writeFile -from ....oot_constants import ootEnumSceneID, ootSceneNameToID -from ....oot_utility import getCustomProperty, ExportInfo -from ....oot_level_classes import OOTScene - - -ADDED_SCENES_COMMENT = "// Added scenes" - - -class SceneIndexType(enum.IntEnum): - """Used to figure out the value of ``selectedSceneIndex``""" - - # this is using negative numbers since this is used as a return type if the scene index wasn't found - CUSTOM = -1 # custom scene - VANILLA_REMOVED = -2 # vanilla scene that was removed, this is to know if it should insert an entry - - -@dataclass -class SceneTableEntry: - """Defines an entry of ``scene_table.h``""" - - index: int - original: Optional[str] # the original line from the parsed file - scene: Optional[OOTScene] = None - exportName: Optional[str] = None - isCustomScene: bool = False - prefix: Optional[str] = None # ifdefs, endifs, comments etc, everything before the current entry - suffix: Optional[str] = None # remaining data after the last entry - parsed: Optional[str] = None - - # macro parameters - specName: Optional[str] = None # name of the scene segment in spec - titleCardName: Optional[str] = None # name of the title card segment in spec, or `none` for no title card - enumValue: Optional[str] = None # enum value for this scene - drawConfigIdx: Optional[str] = None # scene draw config index - unk1: Optional[str] = None - unk2: Optional[str] = None - - def __post_init__(self): - # parse the entry parameters from file data or an ``OOTScene`` - macroStart = "DEFINE_SCENE(" - if self.original is not None and macroStart in self.original: - # remove the index and the macro's name with the parenthesis - index = self.original.index(macroStart) + len(macroStart) - self.parsed = self.original[index:].removesuffix(")\n") - - parameters = self.parsed.split(", ") - assert len(parameters) == 6 - self.setParameters(*parameters) - elif self.scene is not None: - self.setParametersFromScene() - - def setParameters( - self, specName: str, titleCardName: str, enumValue: str, drawConfigIdx: str, unk1: str = "0", unk2: str = "0" - ): - """Sets the entry's parameters""" - self.specName = specName - self.titleCardName = titleCardName - self.enumValue = enumValue - self.drawConfigIdx = drawConfigIdx - self.unk1 = unk1 - self.unk2 = unk2 - - def setParametersFromScene(self, scene: Optional[OOTScene] = None): - """Use the ``OOTScene`` data to set the entry's parameters""" - scene = self.scene if scene is None else scene - # TODO: Implement title cards - name = scene.name if scene is not None else self.exportName - self.setParameters( - f"{scene.name.lower() if self.isCustomScene else scene.name}_scene", - "none", - ootSceneNameToID.get(name, f"SCENE_{name.upper()}"), - getCustomProperty(scene.sceneTableEntry, "drawConfig"), - ) - - def to_c(self): - """Returns the entry as C code""" - return ( - (self.prefix if self.prefix is not None else "") - + f"/* 0x{self.index:02X} */ " - + f"DEFINE_SCENE({self.specName}, {self.titleCardName}, {self.enumValue}, " - + f"{self.drawConfigIdx}, {self.unk1}, {self.unk2})\n" - + (self.suffix if self.suffix is not None else "") - ) - - -@dataclass -class SceneTable: - """Defines a ``scene_table.h`` file data""" - - exportPath: str - exportName: Optional[str] - selectedSceneEnumValue: Optional[str] - entries: list[SceneTableEntry] = field(default_factory=list) - sceneEnumValues: list[str] = field(default_factory=list) # existing values in ``scene_table.h`` - isFirstCustom: bool = False # if true, adds the "Added Scenes" comment to the C data - selectedSceneIndex: int = 0 - customSceneIndex: Optional[int] = None # None if the selected custom scene isn't in the table yet - - def __post_init__(self): - # read the file's data - try: - with open(self.exportPath) as fileData: - data = fileData.read() - fileData.seek(0) - lines = fileData.readlines() - except FileNotFoundError: - raise PluginError("ERROR: Can't find scene_table.h!") - - # parse the entries and populate the list of entries (``self.entries``) - prefix = "" - self.isFirstCustom = ADDED_SCENES_COMMENT not in data - entryIndex = 0 # we don't use ``enumerate`` since not every line is an actual entry - assert len(lines) > 0 - for line in lines: - # skip the lines before an entry, create one from the file's data - # and add the skipped lines as a prefix of the current entry - if ( - not line.startswith("#") # ifdefs or endifs - and not line.startswith(" *") # multi-line comments - and "//" not in line # single line comments - and "/**" not in line # multi-line comments - and line != "\n" - and line != "" - ): - entry = SceneTableEntry(entryIndex, line, prefix=prefix) - self.entries.append(entry) - self.sceneEnumValues.append(entry.enumValue) - prefix = "" - entryIndex += 1 - else: - if prefix.startswith("#") and line.startswith("#"): - # add newline if there's two consecutive preprocessor directives - prefix += "\n" - prefix += line - - # add whatever's after the last entry - if len(prefix) > 0 and prefix != "\n": - self.entries[-1].suffix = prefix - - # get the scene index for the scene chosen by the user - if self.selectedSceneEnumValue is not None: - self.selectedSceneIndex = self.getIndexFromEnumValue() - - # dictionary of entries from spec names - self.entryBySpecName = {entry.specName: entry for entry in self.entries} - - # set the custom scene index - if self.selectedSceneIndex == SceneIndexType.CUSTOM: - entry = self.entryBySpecName.get(f"{self.exportName}_scene") - if entry is not None: - self.customSceneIndex = entry.index - - def getIndexFromEnumValue(self): - """Returns the index (int) of the chosen scene if vanilla and found, else return an enum value from ``SceneIndexType``""" - if self.selectedSceneEnumValue == "Custom": - return SceneIndexType.CUSTOM - for i in range(len(self.sceneEnumValues)): - if self.sceneEnumValues[i] == self.selectedSceneEnumValue: - return i - # if the index is not found and it's not a custom export it means it's a vanilla scene that was removed - return SceneIndexType.VANILLA_REMOVED - - def getOriginalIndex(self): - """ - Returns the index of a specific scene defined by which one the user chose - or by the ``sceneName`` parameter if it's not set to ``None`` - """ - i = 0 - if self.selectedSceneEnumValue != "Custom": - for elem in ootEnumSceneID: - if elem[0] == self.selectedSceneEnumValue: - # returns i - 1 because the first entry is the ``Custom`` option - return i - 1 - i += 1 - raise PluginError("ERROR: Scene Index not found!") - - def getInsertionIndex(self, index: Optional[int] = None) -> int: - """Returns the index to know where to insert data""" - # special case where the scene is "Inside the Great Deku Tree" - # since it's the first scene simply return 0 - if self.selectedSceneEnumValue == "SCENE_DEKU_TREE": - return 0 - - # if index is None this means this is looking for ``original_scene_index - 1`` - # else, this means the table is shifted - if index is None: - currentIndex = self.getOriginalIndex() - else: - currentIndex = index - - for i in range(len(self.sceneEnumValues)): - if self.sceneEnumValues[i] == ootEnumSceneID[currentIndex][0]: - return i + 1 - - # if the index hasn't been found yet, do it again but decrement the index - return self.getInsertionIndex(currentIndex - 1) - - def updateEntryIndex(self): - """Updates every entry index so they follow each other""" - for i, entry in enumerate(self.entries): - if entry.index != i: - entry.index = i - - def getIndex(self): - """Returns the selected scene index if it's a vanilla one, else returns the custom scene index""" - assert self.selectedSceneIndex != SceneIndexType.VANILLA_REMOVED - - # this function's usage makes ``customSceneIndex is None`` impossible - if self.selectedSceneIndex < 0 and self.customSceneIndex is None: - raise PluginError("ERROR: Custom Scene Index is None!") - - return self.selectedSceneIndex if self.selectedSceneIndex >= 0 else self.customSceneIndex - - def append(self, entry: SceneTableEntry): - """Appends an entry to the scene table, only used by custom scenes""" - # add the "added scenes" comment if it's not already there - if self.isFirstCustom: - entry.prefix = f"\n{ADDED_SCENES_COMMENT}\n" - self.isFirstCustom = False - - if entry not in self.entries: - if entry.index >= 0: - self.customSceneIndex = entry.index - self.entries.append(entry) - else: - raise PluginError(f"ERROR: (Append) The index is not valid! ({entry.index})") - else: - raise PluginError("ERROR: (Append) Entry already in the table!") - - def insert(self, entry: SceneTableEntry): - """Inserts an entry in the scene table, only used by non-custom scenes""" - if not entry in self.entries: - if entry.index >= 0: - if entry.index < len(self.entries): - nextEntry = self.entries[entry.index] # the next entry is at the insertion index - - # move the next entry's prefix to the one we're going to insert - if len(nextEntry.prefix) > 0 and not "INCLUDE_TEST_SCENES" in nextEntry.prefix: - entry.prefix = nextEntry.prefix - nextEntry.prefix = "" - - self.entries.insert(entry.index, entry) - else: - raise PluginError(f"ERROR: (Insert) The index is not valid! ({entry.index})") - else: - raise PluginError("ERROR: (Insert) Entry already in the table!") - - def remove(self, index: int): - """Removes an entry from the scene table""" - isCustom = index == SceneIndexType.CUSTOM - if index >= 0 or isCustom: - entry = self.entries[self.getIndex()] - - # move the prefix of the entry to remove to the next entry - # if there's no next entry this prefix becomes the suffix of the last entry - if len(entry.prefix) > 0: - nextIndex = index + 1 - if not isCustom and nextIndex < len(self.entries): - self.entries[nextIndex].prefix = entry.prefix - else: - previousIndex = entry.index - 1 - if entry.index == len(self.entries) - 1 and ADDED_SCENES_COMMENT in entry.prefix: - entry.prefix = entry.prefix.removesuffix(f"\n{ADDED_SCENES_COMMENT}\n") - self.entries[previousIndex].suffix = entry.prefix - - self.entries.remove(entry) - elif index == SceneIndexType.VANILLA_REMOVED: - raise PluginError("INFO: This scene was already removed.") - else: - raise PluginError("ERROR: Unexpected scene index value.") - - def to_c(self): - """Returns the scene table as C code""" - return "".join(entry.to_c() for entry in self.entries) - - -def getDrawConfig(sceneName: str): - """Read draw config from scene table""" - sceneTable = SceneTable( - os.path.join(bpy.path.abspath(bpy.context.scene.ootDecompPath), "include/tables/scene_table.h"), None, None - ) - - entry = sceneTable.entryBySpecName.get(f"{sceneName}_scene") - if entry is not None: - return entry.drawConfigIdx - - raise PluginError(f"ERROR: Scene name {sceneName} not found in scene table.") - - -def modifySceneTable(scene: Optional[OOTScene], exportInfo: ExportInfo): - """Remove, append, insert or update the scene table entry of the selected scene""" - sceneTable = SceneTable( - os.path.join(exportInfo.exportPath, "include/tables/scene_table.h"), - exportInfo.name if exportInfo.option == "Custom" else None, - exportInfo.option, - ) - - if scene is None: - # remove mode - sceneTable.remove(sceneTable.selectedSceneIndex) - elif sceneTable.selectedSceneIndex == SceneIndexType.CUSTOM and sceneTable.customSceneIndex is None: - # custom mode: new custom scene - sceneTable.append(SceneTableEntry(len(sceneTable.entries) - 1, None, scene, exportInfo.name, True)) - elif sceneTable.selectedSceneIndex == SceneIndexType.VANILLA_REMOVED: - # insert mode - sceneTable.insert(SceneTableEntry(sceneTable.getInsertionIndex(), None, scene, exportInfo.name, False)) - else: - # update mode (for both vanilla and custom scenes since they already exist in the table) - sceneTable.entries[sceneTable.getIndex()].setParametersFromScene(scene) - - # update the indices - sceneTable.updateEntryIndex() - - # write the file with the final data - writeFile(sceneTable.exportPath, sceneTable.to_c()) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py deleted file mode 100644 index 9c1600582..000000000 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ /dev/null @@ -1,303 +0,0 @@ -import os -import bpy -import enum - -from dataclasses import dataclass, field -from typing import List, Optional -from .....utility import PluginError, writeFile, indent -from ....oot_utility import ExportInfo, getSceneDirFromLevelName - - -# either "$(BUILD_DIR)", "$(BUILD)" or "build" -buildDirectory = None - - -class CommandType(enum.Enum): - """This class defines the different spec command types""" - - NAME = 0 - COMPRESS = 1 - AFTER = 2 - FLAGS = 3 - ALIGN = 4 - ADDRESS = 5 - ROMALIGN = 6 - INCLUDE = 7 - INCLUDE_DATA_WITH_RODATA = 8 - NUMBER = 9 - PAD_TEXT = 10 - - @staticmethod - def from_string(value: str): - """Returns one of the enum values from a string""" - - cmdType = CommandType._member_map_.get(value.upper()) - if cmdType is None: - raise PluginError(f"ERROR: Can't find value: ``{value}`` in the enum!") - return cmdType - - -@dataclass -class SpecEntryCommand: - """This class defines a single spec command""" - - type: CommandType - content: str = "" - prefix: str = "" - suffix: str = "" - - def to_c(self): - return self.prefix + indent + f"{self.type.name.lower()} {self.content}".strip() + self.suffix + "\n" - - -@dataclass -class SpecEntry: - """Defines an entry of ``spec``""" - - original: Optional[list[str]] = field(default_factory=list) # the original lines from the parsed file - commands: list[SpecEntryCommand] = field(default_factory=list) # list of the different spec commands - segmentName: str = "" # the name of the current segment - prefix: str = "" # data between two commands - suffix: str = "" # remaining data after the entry (used for the last entry) - contentSuffix: str = "" # remaining data after the last command in the current entry - - def __post_init__(self): - if self.original is not None: - global buildDirectory - # parse the commands from the existing data - prefix = "" - for line in self.original: - line = line.strip() - dontHaveComments = ( - not line.startswith("// ") and not line.startswith("/* ") and not line.startswith(" */") - ) - - if line != "\n": - if not line.startswith("#") and dontHaveComments: - split = line.split(" ") - command = split[0] - if len(split) > 2: - content = " ".join(elem for i, elem in enumerate(split) if i > 0) - elif len(split) > 1: - content = split[1] - elif command == "name": - content = self.segmentName - else: - content = "" - - if buildDirectory is None and (content.startswith('"build') or content.startswith('"$(BUILD')): - buildDirectory = content.split("/")[0].removeprefix('"') - - self.commands.append( - SpecEntryCommand( - CommandType.from_string(command), - content, - (prefix + ("\n" if len(prefix) > 0 else "")) if prefix != "\n" else "", - ) - ) - prefix = "" - else: - if prefix.startswith("#") and line.startswith("#"): - # add newline if there's two consecutive preprocessor directives - prefix += "\n" - prefix += (f"\n{indent}" if not dontHaveComments else "") + line - # if there's a prefix it's the remaining data after the last entry - if len(prefix) > 0: - self.contentSuffix = prefix - - if len(self.segmentName) == 0 and len(self.commands[0].content) > 0: - self.segmentName = self.commands[0].content - else: - raise PluginError("ERROR: The segment name can't be set!") - - def to_c(self): - return ( - (self.prefix if len(self.prefix) > 0 else "\n") - + "beginseg\n" - + "".join(cmd.to_c() for cmd in self.commands) - + (f"{self.contentSuffix}\n" if len(self.contentSuffix) > 0 else "") - + "endseg" - + (self.suffix if self.suffix == "\n" else f"\n{self.suffix}\n" if len(self.suffix) > 0 else "") - ) - - -@dataclass -class SpecFile: - """This class defines the spec's file data""" - - exportPath: str # path to the spec file - entries: list[SpecEntry] = field(default_factory=list) # list of the different spec entries - - def __post_init__(self): - # read the file's data - try: - with open(self.exportPath, "r") as fileData: - lines = fileData.readlines() - except FileNotFoundError: - raise PluginError("ERROR: Can't find spec!") - - prefix = "" - parsedLines = [] - assert len(lines) > 0 - for line in lines: - # if we're inside a spec entry or if the lines between two entries do not contains these characters - # fill the ``parsedLine`` list if it's inside a segment - # when we reach the end of the current segment add a new ``SpecEntry`` to ``self.entries`` - isNotEmptyOrNewline = len(line) > 0 and line != "\n" - if ( - len(parsedLines) > 0 - or not line.startswith(" *") - and "/*\n" not in line - and not line.startswith("#") - and isNotEmptyOrNewline - ): - if "beginseg" not in line and "endseg" not in line: - # if inside a segment, between beginseg and endseg - parsedLines.append(line) - elif "endseg" in line: - # else, if the line has endseg in it (> if we reached the end of the current segment) - entry = SpecEntry(parsedLines, prefix=prefix) - self.entries.append(entry) - prefix = "" - parsedLines = [] - else: - # else, if between 2 segments and the line is something we don't need - if prefix.startswith("#") and line.startswith("#"): - # add newline if there's two consecutive preprocessor directives - prefix += "\n" - prefix += line - # set the last's entry's suffix to the remaining prefix - self.entries[-1].suffix = prefix.removesuffix("\n") - - def find(self, segmentName: str): - """Returns an entry from a segment name, returns ``None`` if nothing was found""" - - for i, entry in enumerate(self.entries): - if entry.segmentName == segmentName: - return self.entries[i] - return None - - def append(self, entry: SpecEntry): - """Appends an entry to the list""" - - # prefix/suffix shenanigans - lastEntry = self.entries[-1] - if len(lastEntry.suffix) > 0: - entry.prefix = f"{lastEntry.suffix}\n\n" - lastEntry.suffix = "" - self.entries.append(entry) - - def remove(self, segmentName: str): - """Removes an entry from a segment name""" - - # prefix/suffix shenanigans - entry = self.find(segmentName) - if entry is not None: - if len(entry.prefix) > 0 and entry.prefix != "\n": - lastEntry = self.entries[self.entries.index(entry) - 1] - lastEntry.suffix = (lastEntry.suffix if lastEntry.suffix is not None else "") + entry.prefix[:-2] - self.entries.remove(entry) - - def to_c(self): - return "\n".join(entry.to_c() for entry in self.entries) - - -def editSpecFile( - isScene: bool, - exportInfo: ExportInfo, - hasSceneTex: bool, - hasSceneCS: bool, - roomTotal: int, - csTotal: int, - roomIndexHasOcclusion: List[bool], -): - global buildDirectory - - # get the spec's data - specFile = SpecFile(os.path.join(exportInfo.exportPath, "spec")) - - # get the scene and current segment name and remove the scene - sceneName = exportInfo.name - sceneSegmentName = f"{sceneName}_scene" - specFile.remove(f'"{sceneSegmentName}"') - - # mark the other scene elements to remove (like rooms) - segmentsToRemove: list[str] = [] - for entry in specFile.entries: - if entry.segmentName.startswith(f'"{sceneName}_'): - segmentsToRemove.append(entry.segmentName) - - # remove the segments - for segmentName in segmentsToRemove: - specFile.remove(segmentName) - - if isScene: - assert buildDirectory is not None - isSingleFile = bpy.context.scene.ootSceneExportSettings.singleFile - includeDir = f"{buildDirectory}/" - if exportInfo.customSubPath is not None: - includeDir += f"{exportInfo.customSubPath + sceneName}" - else: - includeDir += f"{getSceneDirFromLevelName(sceneName)}" - - sceneCmds = [ - SpecEntryCommand(CommandType.NAME, f'"{sceneSegmentName}"'), - SpecEntryCommand(CommandType.COMPRESS), - SpecEntryCommand(CommandType.ROMALIGN, "0x1000"), - ] - - # scene - if isSingleFile: - sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}.o"')) - else: - sceneCmds.extend( - [ - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_main.o"'), - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_col.o"'), - ] - ) - - if hasSceneTex: - sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_tex.o"')) - - if hasSceneCS: - for i in range(csTotal): - sceneCmds.append( - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_cs_{i}.o"') - ) - - sceneCmds.append(SpecEntryCommand(CommandType.NUMBER, "2")) - specFile.append(SpecEntry(None, sceneCmds)) - - # rooms - for i in range(roomTotal): - roomSegmentName = f"{sceneName}_room_{i}" - - roomCmds = [ - SpecEntryCommand(CommandType.NAME, f'"{roomSegmentName}"'), - SpecEntryCommand(CommandType.COMPRESS), - SpecEntryCommand(CommandType.ROMALIGN, "0x1000"), - ] - - if isSingleFile: - roomCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}.o"')) - else: - roomCmds.extend( - [ - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}_main.o"'), - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}_model_info.o"'), - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}_model.o"'), - ] - ) - if roomIndexHasOcclusion[i]: - roomCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}_occ.o"')) - - roomCmds.append(SpecEntryCommand(CommandType.NUMBER, "3")) - specFile.append(SpecEntry(None, roomCmds)) - specFile.entries[-1].suffix = "\n" - - # finally, write the spec file - writeFile(specFile.exportPath, specFile.to_c()) - - # reset build directory name so it can update properly on the next run - buildDirectory = None diff --git a/fast64_internal/oot/scene/operators.py b/fast64_internal/oot/scene/operators.py index f23f11f16..845283235 100644 --- a/fast64_internal/oot/scene/operators.py +++ b/fast64_internal/oot/scene/operators.py @@ -7,19 +7,13 @@ from bpy.utils import register_class, unregister_class from bpy.ops import object from mathutils import Matrix, Vector -from ...f3d.f3d_gbi import DLFormat +from ...f3d.f3d_gbi import TextureExportSettings, DLFormat from ...utility import PluginError, raisePluginError, ootGetSceneOrRoomHeader -from ..oot_utility import ExportInfo, sceneNameFromID -from ..oot_level_writer import ootExportSceneToC +from ..oot_utility import ExportInfo, RemoveInfo, sceneNameFromID from ..oot_constants import ootEnumMusicSeq, ootEnumSceneID from ..oot_level_parser import parseScene -from .exporter.to_c import clearBootupScene, modifySceneTable, editSpecFile, deleteSceneFiles - - -def ootRemoveSceneC(exportInfo): - modifySceneTable(None, exportInfo) - editSpecFile(False, exportInfo, False, False, 0, 0, []) - deleteSceneFiles(exportInfo) +from ..exporter.decomp_edit.config import Config +from ..exporter import SceneExport, Files def run_ops_without_view_layer_update(func): @@ -99,7 +93,7 @@ class OOT_ClearBootupScene(Operator): bl_options = {"REGISTER", "UNDO", "PRESET"} def execute(self, context): - clearBootupScene(os.path.join(abspath(context.scene.ootDecompPath), "include/config/config_debug.h")) + Config.clearBootupScene(os.path.join(abspath(context.scene.ootDecompPath), "include/config/config_debug.h")) self.report({"INFO"}, "Success!") return {"FINISHED"} @@ -159,27 +153,39 @@ def execute(self, context): settings = context.scene.ootSceneExportSettings levelName = settings.name option = settings.option + + bootOptions = context.scene.fast64.oot.bootupSceneOptions + hackerFeaturesEnabled = context.scene.fast64.oot.hackerFeaturesEnabled + if settings.customExport: - exportInfo = ExportInfo(True, bpy.path.abspath(settings.exportPath), None, levelName) + isCustomExport = True + exportPath = bpy.path.abspath(settings.exportPath) + customSubPath = None else: if option == "Custom": - subfolder = "assets/scenes/" + settings.subFolder + "/" + customSubPath = "assets/scenes/" + settings.subFolder + "/" else: levelName = sceneNameFromID(option) - subfolder = None - exportInfo = ExportInfo(False, bpy.path.abspath(context.scene.ootDecompPath), subfolder, levelName) + 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, + ) - exportInfo.option = option - bootOptions = context.scene.fast64.oot.bootupSceneOptions - hackerFeaturesEnabled = context.scene.fast64.oot.hackerFeaturesEnabled - ootExportSceneToC( + SceneExport.export( obj, finalTransform, - levelName, - DLFormat.Static, - context.scene.saveTextures, exportInfo, - bootOptions if hackerFeaturesEnabled else None, ) self.report({"INFO"}, "Success!") @@ -228,10 +234,9 @@ def execute(self, context): else: levelName = sceneNameFromID(option) subfolder = None - exportInfo = ExportInfo(False, abspath(context.scene.ootDecompPath), subfolder, levelName) - exportInfo.option = option + removeInfo = RemoveInfo(abspath(context.scene.ootDecompPath), subfolder, levelName) - ootRemoveSceneC(exportInfo) + Files.remove_scene(removeInfo) self.report({"INFO"}, "Success!") return {"FINISHED"} diff --git a/fast64_internal/oot/scene/properties.py b/fast64_internal/oot/scene/properties.py index 4477354c2..b48948e06 100644 --- a/fast64_internal/oot/scene/properties.py +++ b/fast64_internal/oot/scene/properties.py @@ -489,6 +489,9 @@ class OOTExportSceneSettingsProperty(PropertyGroup): ) option: EnumProperty(items=ootEnumSceneID, default="SCENE_DEKU_TREE") + # keeping this on purpose, will be removed once old code is cleaned-up + useNewExporter: BoolProperty(name="Use New Exporter", default=True) + def draw_props(self, layout: UILayout): if self.customExport: prop_split(layout, self, "exportPath", "Directory") @@ -503,6 +506,7 @@ def draw_props(self, layout: UILayout): layout.prop(self, "singleFile") layout.prop(self, "customExport") + # layout.prop(self, "useNewExporter") class OOTImportSceneSettingsProperty(PropertyGroup): diff --git a/fast64_internal/oot/spline/properties.py b/fast64_internal/oot/spline/properties.py index a0c9e91e3..b0c32f27c 100644 --- a/fast64_internal/oot/spline/properties.py +++ b/fast64_internal/oot/spline/properties.py @@ -19,14 +19,17 @@ class OOTSplineProperty(PropertyGroup): camSTypeCustom: StringProperty(default="CAM_SET_CRAWLSPACE") def draw_props(self, layout: UILayout, altSceneProp: OOTAlternateSceneHeaderProperty, objName: str): + camIndexName = "" prop_split(layout, self, "splineType", "Type") if self.splineType == "Path": headerProp: OOTActorHeaderProperty = self.headerSettings headerProp.draw_props(layout, "Curve", altSceneProp, objName) + camIndexName = "Path Index" elif self.splineType == "Crawlspace": layout.label(text="This counts as a camera for index purposes.", icon="INFO") - prop_split(layout, self, "index", "Index") drawEnumWithCustom(layout, self, "camSType", "Camera S Type", "") + camIndexName = "Camera Index" + prop_split(layout, self, "index", camIndexName) oot_spline_classes = (OOTSplineProperty,) diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 1871fac22..fb5beeff7 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -1,12 +1,12 @@ import os import bpy -from bpy.types import PropertyGroup, UILayout, Scene, Context +from bpy.types import PropertyGroup, UILayout, Context from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty, PointerProperty 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 +from ...utility import directory_path_checks, directory_ui_warnings, prop_split, upgrade_old_prop from ..sm64_constants import defaultExtendSegment4 from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings from ..tools import SM64_AddrConvProperties @@ -83,17 +83,8 @@ class SM64_Properties(PropertyGroup): def binary_export(self): return self.export_type in ["Binary", "Insertable Binary"] - def get_legacy_export_type(self, scene: Scene): - legacy_export_types = ("C", "Binary", "Insertable Binary") - - for export_key in ["animExportType", "colExportType", "DLExportType", "geoExportType"]: - export_type = legacy_export_types[scene.get(export_key, 0)] - if export_type != "C": - return export_type - - return "C" - - def upgrade_version_1(self, scene: Scene): + @staticmethod + def upgrade_changed_props(): old_scene_props_to_new = { "importRom": "import_rom", "exportRom": "export_rom", @@ -102,34 +93,29 @@ def upgrade_version_1(self, scene: Scene): "blenderToSM64Scale": "blender_to_sm64_scale", "decompPath": "decomp_path", "extendBank4": "extend_bank_4", + "refreshVer": "refresh_version", + "exportType": "export_type", } - for old, new in old_scene_props_to_new.items(): - setattr(self, new, scene.get(old, getattr(self, new))) - - refresh_version = scene.get("refreshVer", None) - if refresh_version is not None: - self.refresh_version = enum_refresh_versions[refresh_version][0] - - self.show_importing_menus = self.get("showImportingMenus", self.show_importing_menus) - - export_type = self.get("exportType", None) - if export_type is not None: - self.export_type = enum_export_type[export_type][0] - - self.version = 2 - - @staticmethod - def upgrade_changed_props(): for scene in bpy.data.scenes: sm64_props: SM64_Properties = scene.fast64.sm64 - if sm64_props.version == 0: - sm64_props.export_type = sm64_props.get_legacy_export_type(scene) - sm64_props.version = 1 - print("Upgraded global SM64 settings to version 1") - if sm64_props.version == 1: - sm64_props.upgrade_version_1(scene) - print("Upgraded global SM64 settings to version 2") sm64_props.address_converter.upgrade_changed_props(scene) + if sm64_props.version == SM64_Properties.cur_version: + continue + upgrade_old_prop( + sm64_props, + "export_type", + scene, + { + "animExportType", + "colExportType", + "DLExportType", + "geoExportType", + }, + ) + for old, new in old_scene_props_to_new.items(): + upgrade_old_prop(sm64_props, new, scene, old) + upgrade_old_prop(sm64_props, "show_importing_menus", sm64_props, "showImportingMenus") + sm64_props.version = SM64_Properties.cur_version def draw_props(self, layout: UILayout, show_repo_settings: bool = True): col = layout.column() diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index e257b81e4..d0f173103 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -14,6 +14,7 @@ all_values_equal_x, checkIsSM64PreInlineGeoLayout, prop_split, + upgrade_old_prop, ) from .sm64_constants import ( @@ -1715,12 +1716,8 @@ class SM64_GeoASMProperties(bpy.types.PropertyGroup): @staticmethod def upgrade_object(obj: bpy.types.Object): geo_asm = obj.fast64.sm64.geo_asm - - func = obj.get("geoASMFunc") or obj.get("geo_func") or geo_asm.func - geo_asm.func = func - - param = obj.get("geoASMParam") or obj.get("func_param") or geo_asm.param - geo_asm.param = str(param) + upgrade_old_prop(geo_asm, "func", obj, {"geoASMFunc", "geo_func"}) + upgrade_old_prop(geo_asm, "param", obj, {"geoASMParam", "func_param"}) class SM64_AreaProperties(bpy.types.PropertyGroup): @@ -1771,11 +1768,7 @@ class SM64_GameObjectProperties(bpy.types.PropertyGroup): def upgrade_object(obj): game_object: SM64_GameObjectProperties = obj.fast64.sm64.game_object - game_object.bparams = obj.get("sm64_obj_bparam", game_object.bparams) - - # delete legacy property - if "sm64_obj_bparam" in obj: - del obj["sm64_obj_bparam"] + upgrade_old_prop(game_object, "bparams", obj, "sm64_obj_bparam") # get combined bparams, if they arent the default value then return because they have been set combined_bparams = game_object.get_combined_bparams() diff --git a/fast64_internal/sm64/tools/properties.py b/fast64_internal/sm64/tools/properties.py index 9384a2239..342df25d1 100644 --- a/fast64_internal/sm64/tools/properties.py +++ b/fast64_internal/sm64/tools/properties.py @@ -2,10 +2,10 @@ from bpy.path import abspath from bpy.types import PropertyGroup, UILayout, Scene -from bpy.props import StringProperty, EnumProperty, BoolProperty +from bpy.props import StringProperty, EnumProperty, BoolProperty, IntProperty from bpy.utils import register_class, unregister_class -from ...utility import prop_split, intToHex +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 @@ -13,18 +13,18 @@ class SM64_AddrConvProperties(PropertyGroup): + version: IntProperty(name="SM64_AddrConvProperties Version", default=0) + cur_version = 1 + rom: StringProperty(name="Import ROM", subtype="FILE_PATH") address: StringProperty(name="Address") level: EnumProperty(items=level_enums, name="Level", default="IC") clipboard: BoolProperty(name="Copy to Clipboard", default=True) def upgrade_changed_props(self, scene: Scene): - old_address = scene.pop("convertibleAddr", None) - if old_address is not None: - self.address = intToHex(int(old_address, 16)) - old_level = scene.pop("level", None) - if old_level is not None: - self["level"] = old_level + upgrade_old_prop(self, "address", scene, "convertibleAddr", fix_forced_base_16=True) + upgrade_old_prop(self, "level", scene, "level") + self.version = SM64_AddrConvProperties.cur_version def draw_props(self, layout: UILayout, import_rom: PathLike = None): col = layout.column() diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index ae0eadcd7..5f4751f07 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -2,7 +2,7 @@ from math import pi, ceil, degrees, radians, copysign from mathutils import * from .utility_anim import * -from typing import Callable, Iterable, Any, Optional, Tuple, Union +from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union from bpy.types import UILayout CollectionProperty = Any # collection prop as defined by using bpy.props.CollectionProperty @@ -1768,3 +1768,84 @@ def json_to_prop_group(prop_group, data: dict, blacklist: list[str] = None, whit default.from_dict(data.get(prop, None)) else: setattr(prop_group, prop, data.get(prop, default)) + + +T = TypeVar("T") +SetOrVal = T | list[T] + + +def get_first_set_prop(old_loc, old_props: SetOrVal[str]): + """Pops all old props and returns the first one that is set""" + + def as_set(val: SetOrVal[T]) -> set[T]: + if isinstance(val, Iterable) and not isinstance(val, str): + return set(val) + else: + return {val} + + result = None + for old_prop in as_set(old_props): + old_value = old_loc.pop(old_prop, None) + if old_value is not None: + result = old_value + return result + + +def upgrade_old_prop( + new_loc, + new_prop: str, + old_loc, + old_props: SetOrVal[str], + old_enum: list[str] = None, + fix_forced_base_16=False, +): + try: + new_prop_def = new_loc.bl_rna.properties[new_prop] + new_prop_value = getattr(new_loc, new_prop) + assert not old_enum or new_prop_def.type == "ENUM" + assert not (old_enum and fix_forced_base_16) + + old_value = get_first_set_prop(old_loc, old_props) + if old_value is None: + return False + + if new_prop_def.type == "ENUM": + if not isinstance(old_value, int): + raise ValueError(f"({old_value}) not an int, but {new_prop} is an enum") + if old_enum: + if old_value >= len(old_enum): + raise ValueError(f"({old_value}) not in {old_enum}") + old_value = old_enum[old_value] + else: + if old_value >= len(new_prop_def.enum_items): + raise ValueError(f"({old_value}) not in {new_prop}´s enum items") + old_value = new_prop_def.enum_items[old_value].identifier + elif isinstance(new_prop_value, bpy.types.PropertyGroup): + recursiveCopyOldPropertyGroup(old_value, new_prop_value) + print(f"Upgraded {new_prop} from old location {old_loc} with props {old_props} via recursive group copy") + return True + elif isinstance(new_prop_value, bpy.types.Collection): + copyPropertyCollection(old_value, new_prop_value) + print(f"Upgraded {new_prop} from old location {old_loc} with props {old_props} via collection copy") + return True + elif fix_forced_base_16: + try: + if not isinstance(old_value, str): + raise ValueError(f"({old_value}) not a string") + old_value = int(old_value, 16) + if new_prop_def.type == "STRING": + old_value = intToHex(old_value) + except ValueError as exc: + raise ValueError(f"({old_value}) not a valid base 16 integer") from exc + + if new_prop_def.type == "STRING": + old_value = str(old_value) + if getattr(new_loc, new_prop, None) == old_value: + return False + setattr(new_loc, new_prop, old_value) + print(f'{new_prop} set to "{getattr(new_loc, new_prop)}"') + return True + except Exception as exc: + print(f"Failed to upgrade {new_prop} from old location {old_loc} with props {old_props}") + traceback.print_exc() + return False