diff --git a/fast64_internal/f3d/f3d_parser.py b/fast64_internal/f3d/f3d_parser.py index d20ad2ef4..e8e8a34c4 100644 --- a/fast64_internal/f3d/f3d_parser.py +++ b/fast64_internal/f3d/f3d_parser.py @@ -2156,15 +2156,6 @@ def parseMacroArgs(data): return params -def getImportData(filepaths): - data = "" - for path in filepaths: - if os.path.exists(path): - data += readFile(path) - - return data - - def parseMatrices(sceneData: str, f3dContext: F3DContext, importScale: float = 1): for match in re.finditer(rf"Mtx\s*([a-zA-Z0-9\_]+)\s*=\s*\{{(.*?)\}}\s*;", sceneData, flags=re.DOTALL): name = "&" + match.group(1) diff --git a/fast64_internal/oot/README.md b/fast64_internal/oot/README.md index 69ba2e02d..c5edf8301 100644 --- a/fast64_internal/oot/README.md +++ b/fast64_internal/oot/README.md @@ -124,6 +124,22 @@ Heavy modifications of Links model can cause his matrices array to shift from wh 6. In the actor header file, (in src/overlays/actors/\/), set the joint/morph table sizes to be (number of bones + 1) 7. In the actor source file, this value should also be used for the limbCount argument in SkelAnime_InitFlex(). +### Actor Colliders +You can add actor colliders to an armature or display list using the operators in the OOT Tools menu. The collider properties will be shown under the object properties tab, except for mesh collider which will store part of the collider properties in the material properties tab. The struct name is also stored in the object properties, which is what determines what the export collider name will be. You can also toggle collider visibility by type in the OOT Tools menu. + +- For quad colliders, the actual shape is ignored and only the properties are relevant. +- For joint sphere colliders, the armature specific section is shared among all joint spheres on the armature. This means that an armature can only have one joint sphere collider collection. + +The scale of the collider objects determines their sizes, so don't worry if they have unapplied scales. + +### Actor Collider Importing +Actor collider importing is a bit finicky. +1. Some actors will modify their collider data, so that the actual data is not representative of in game shape. For example, most shield/sword collider vertices are modified by actors. Quad colliders seem to exclusively fall under this case, so the actual shape is ignored when importing/exporting. +2. While skeletons are transformed by Actor_SetScale(), colliders are not. Therefore, having the correct actor scale is important to matching collider sizes. Currently fast64 tries to find the first instance of CHAIN_VEC3F_DIV1000(scale, ....) or Actor_SetScale(), but sometimes this is not correct. Make sure to check the actor file and manually set an actor scale for importing colliders if things are scaled weirdly. +3. Some actor files contain multiple "parts", which means multiple skeletons and multiple colliders. You may want to manually specify which colliders to import in the import settings. Only Bongo Bongo and Barinade imports handle these cases automatically currently. +4. Some actors like Ganon2 have multiple joint sphere collider "groups", which is not supported by fast64. Only the first group will be imported, so if a different one is needed then the name will have to be manually set in the import settings. +5. Some actors with joint sphere colliders do not use the joint index field as an actual joint index, so when importing the parenting hierarchy won't make sense. Uncheck "parent joint spheres to bones" to do object parenting instead. + ### Creating a Cutscene **Creating the cutscene itself:** diff --git a/fast64_internal/oot/__init__.py b/fast64_internal/oot/__init__.py index d9f19d7f0..5e9b5c003 100644 --- a/fast64_internal/oot/__init__.py +++ b/fast64_internal/oot/__init__.py @@ -52,6 +52,15 @@ oot_operator_unregister, ) +from .actor_collider import ( + actor_collider_props_register, + actor_collider_props_unregister, + actor_collider_ops_register, + actor_collider_ops_unregister, + actor_collider_panel_register, + actor_collider_panel_unregister, +) + class OOT_Properties(bpy.types.PropertyGroup): """Global OOT Scene Properties found under scene.fast64.oot""" @@ -84,6 +93,7 @@ def oot_panel_register(): anim_panels_register() skeleton_panels_register() cutscene_panels_register() + actor_collider_panel_register(), def oot_panel_unregister(): @@ -96,6 +106,7 @@ def oot_panel_unregister(): anim_panels_unregister() skeleton_panels_unregister() cutscene_panels_unregister() + actor_collider_panel_unregister(), def oot_register(registerPanels): @@ -112,6 +123,8 @@ def oot_register(registerPanels): actor_props_register() oot_obj_register() spline_props_register() + actor_collider_props_register(), # register before f3d + actor_collider_ops_register(), f3d_props_register() anim_ops_register() skeleton_ops_register() @@ -146,6 +159,8 @@ def oot_unregister(unregisterPanels): actor_props_unregister() spline_props_unregister() f3d_props_unregister() + actor_collider_props_unregister(), + actor_collider_ops_unregister(), anim_ops_unregister() skeleton_ops_unregister() skeleton_props_unregister() diff --git a/fast64_internal/oot/actor_collider/__init__.py b/fast64_internal/oot/actor_collider/__init__.py new file mode 100644 index 000000000..0330455ab --- /dev/null +++ b/fast64_internal/oot/actor_collider/__init__.py @@ -0,0 +1,20 @@ +from .properties import ( + OOTActorColliderImportExportSettings, + drawColliderVisibilityOperators, + actor_collider_props_register, + actor_collider_props_unregister, +) +from .operators import ( + OOT_AddActorCollider, + OOT_CopyColliderProperties, + actor_collider_ops_register, + actor_collider_ops_unregister, +) +from .panels import ( + actor_collider_panel_register, + actor_collider_panel_unregister, + isActorCollider, +) + +from .importer import parseColliderData +from .exporter import getColliderData, removeExistingColliderData, writeColliderData diff --git a/fast64_internal/oot/actor_collider/exporter.py b/fast64_internal/oot/actor_collider/exporter.py new file mode 100644 index 000000000..fb2b49fee --- /dev/null +++ b/fast64_internal/oot/actor_collider/exporter.py @@ -0,0 +1,242 @@ +import bpy, mathutils, os, re, math +from ...utility import CData, PluginError, readFile, writeFile +from ..oot_utility import getOrderedBoneList, getOOTScale +from ..file_reading import getNonLinkActorFilepath, getLinkColliderFilepath + + +def removeExistingColliderData(exportPath: str, overlayName: str, isLink: bool, newColliderData: str) -> str: + if not isLink: + actorPath = getNonLinkActorFilepath(exportPath, overlayName) + else: + actorPath = getLinkColliderFilepath(exportPath) + + data = readFile(actorPath) + newActorData = data + + for colliderMatch in re.finditer( + r"static\s*Collider[a-zA-Z0-9]*\s*([a-zA-Z0-9\_]*)", newColliderData, flags=re.DOTALL + ): + name = colliderMatch.group(1) + match = re.search( + r"static\s*Collider[a-zA-Z0-9]*\s*" + re.escape(name) + r".*?\}\s*;", newActorData, flags=re.DOTALL + ) + if match: + newActorData = newActorData[: match.start(0)] + newActorData[match.end(0) :] + + if newActorData != data: + writeFile(actorPath, newActorData) + + +def writeColliderData(obj: bpy.types.Object, exportPath: str, overlayName: str, isLink: bool) -> str: + if not isLink: + actorFilePath = getNonLinkActorFilepath(exportPath, overlayName) + else: + actorFilePath = getLinkColliderFilepath(exportPath) + + actor = os.path.basename(actorFilePath)[:-2] + colliderData = getColliderData(obj) + + colliderFilename = actor + "_colliders.c" + colliderInclude = f'#include "{colliderFilename}"' + + actorData = readFile(actorFilePath) + if colliderInclude not in actorData: + actorData = colliderInclude + "\n" + actorData + writeFile(actorFilePath, actorData) + + colliderFilePath = os.path.join(os.path.dirname(actorFilePath), colliderFilename) + writeFile(colliderFilePath, f'#include "global.h"\n\n' + colliderData.source) + + +def getColliderData(parentObj: bpy.types.Object) -> CData: + + # TODO: Handle hidden? + colliderObjs = [obj for obj in parentObj.children if obj.ootGeometryType == "Actor Collider"] + + data = CData() + data.source += getColliderDataSingle(colliderObjs, "COLSHAPE_CYLINDER", "ColliderCylinderInit") + data.source += getColliderDataJointSphere(colliderObjs) + data.source += getColliderDataMesh(colliderObjs) + data.source += getColliderDataSingle(colliderObjs, "COLSHAPE_QUAD", "ColliderQuadInit") + + return data + + +def getShapeData(obj: bpy.types.Object, bone: bpy.types.Bone | None = None) -> str: + shape = obj.ootActorCollider.colliderShape + translate, rotate, scale = obj.matrix_local.decompose() + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + noXYRotation = rotate.to_euler()[0] < 0.01 and rotate.to_euler()[1] < 0.01 + + if shape == "COLSHAPE_JNTSPH": + if obj.parent is None: + raise PluginError(f"Joint sphere collider {obj.name} must be parented to a mesh or armature.") + + isUniform = abs(scale[0] - scale[1]) < 0.001 and abs(scale[1] - scale[2]) < 0.001 + if not isUniform: + raise PluginError(f"Sphere collider {obj.name} must have uniform scale (radius).") + + if isinstance(obj.parent.data, bpy.types.Armature) and bone is not None: + boneList = getOrderedBoneList(obj.parent) + limb = boneList.index(bone) + 1 + else: + limb = obj.ootActorColliderItem.limbOverride + + # When object is parented to bone, its matrix_local is relative to the tail(?) of that bone. + # No need to apply yUpToZUp here? + translateData = ", ".join( + [ + str(round(value)) + for value in getOOTScale(obj.parent.ootActorScale) + * (translate + (mathutils.Vector((0, bone.length, 0))) if bone is not None else translate) + ] + ) + scale = bpy.context.scene.ootBlenderScale * scale + radius = round(abs(scale[0])) + + return f"{{ {limb}, {{ {{ {translateData} }} , {radius} }}, 100 }},\n" + + elif shape == "COLSHAPE_CYLINDER": + if not noXYRotation: + raise PluginError(f"Cylinder collider {obj.name} must have zero rotation around XY axis.") + + isUniformXY = abs(scale[0] - scale[1]) < 0.001 + + # Convert to OOT space transforms + translate = bpy.context.scene.ootBlenderScale * (yUpToZUp.inverted() @ translate) + scale = bpy.context.scene.ootBlenderScale * (yUpToZUp.inverted() @ scale) + + if not isUniformXY: + raise PluginError(f"Cylinder collider {obj.name} must have uniform XY scale (radius).") + radius = round(abs(scale[0])) + height = round(scale[1] * 2) + + yShift = round(translate[1]) + position = [round(translate[0]), 0, round(translate[2])] + + return f"{{ {radius}, {height}, {yShift}, {{ {position[0]}, {position[1]}, {position[2]} }} }},\n" + + elif shape == "COLSHAPE_TRIS": + pass # handled in its own function + elif shape == "COLSHAPE_QUAD": + # geometry data ignored + return "{ { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } } },\n" + else: + raise PluginError(f"Invalid shape: {shape} for {obj.name}") + + +def getColliderDataSingle(colliderObjs: list[bpy.types.Object], shape: str, structName: str) -> str: + filteredObjs = [obj for obj in colliderObjs if obj.ootActorCollider.colliderShape == shape] + colliderData = "" + for obj in filteredObjs: + collider = obj.ootActorCollider + colliderItem = obj.ootActorColliderItem + data = f"static {structName}{'Type1' if collider.physics.isType1 else ''} {collider.name} = {{\n" + data += collider.to_c(1) + data += colliderItem.to_c(1) + data += "\t" + getShapeData(obj) + data += "};\n\n" + + colliderData += data + + return colliderData + + +def getColliderDataJointSphere(colliderObjs: list[bpy.types.Object]) -> str: + sphereObjs = [ + obj + for obj in colliderObjs + if obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH" and obj.parent is not None + ] + if len(sphereObjs) == 0: + return "" + + collider = sphereObjs[0].parent.ootActorCollider + name = collider.name + if "Init" in name: + elementsName = name[: name.index("Init")] + "Items" + name[name.index("Init") :] + else: + elementsName = collider.name + "Items" + colliderData = "" + + colliderData += f"static ColliderJntSphElementInit {elementsName}[{len(sphereObjs)}] = {{\n" + for obj in sphereObjs: + if obj.parent is not None and isinstance(obj.parent.data, bpy.types.Armature) and obj.parent_bone != "": + bone = obj.parent.data.bones[obj.parent_bone] + else: + bone = None + + data = "\t{\n" + data += obj.ootActorColliderItem.to_c(2) + data += "\t\t" + getShapeData(obj, bone) + data += "\t},\n" + + colliderData += data + colliderData += "};\n\n" + + # Required to make export use correct shape, otherwise unused so not an issue modifying here + collider.shape = "COLSHAPE_JNTSPH" + + colliderData += f"static ColliderJntSphInit{'Type1' if collider.physics.isType1 else ''} {name} = {{\n" + colliderData += collider.to_c(1) + colliderData += f"\t{len(sphereObjs)},\n" + colliderData += f"\t{elementsName},\n" + colliderData += "};\n\n" + + return colliderData + + +def getColliderDataMesh(colliderObjs: list[bpy.types.Object]) -> str: + meshObjs = [obj for obj in colliderObjs if obj.ootActorCollider.colliderShape == "COLSHAPE_TRIS"] + colliderData = "" + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + transformMatrix = ( + mathutils.Matrix.Diagonal(mathutils.Vector([bpy.context.scene.ootBlenderScale for i in range(3)] + [1])) + @ yUpToZUp.to_matrix().to_4x4().inverted() + ) + + for obj in meshObjs: + collider = obj.ootActorCollider + name = collider.name + if "Init" in name: + elementsName = name[: name.index("Init")] + "Items" + name[name.index("Init") :] + else: + elementsName = collider.name + "Items" + mesh = obj.data + + if not (isinstance(mesh, bpy.types.Mesh) and len(mesh.materials) > 0): + raise PluginError(f"Mesh collider object {obj.name} must have a mesh with at least one material.") + + obj.data.calc_loop_triangles() + meshData = "" + for face in obj.data.loop_triangles: + material = obj.material_slots[face.material_index].material + + tris = [ + ", ".join( + [ + format(value, "0.4f") + "f" + for value in (transformMatrix @ obj.matrix_local @ mesh.vertices[face.vertices[i]].co) + ] + ) + for i in range(3) + ] + + triData = f"{{ {{ {{ {tris[0]} }}, {{ {tris[1]} }}, {{ {tris[2]} }} }} }},\n" + + meshData += "\t{\n" + meshData += material.ootActorColliderItem.to_c(2) + meshData += "\t\t" + triData + meshData += "\t},\n" + + colliderData += ( + f"static ColliderTrisElementInit {elementsName}[{len(mesh.loop_triangles)}] = {{\n{meshData}}};\n\n" + ) + colliderData += f"static ColliderTrisInit{'Type1' if collider.physics.isType1 else ''} {name} = {{\n" + colliderData += collider.to_c(1) + colliderData += f"\t{len(obj.data.loop_triangles)},\n" + colliderData += f"\t{elementsName},\n" + colliderData += "};\n\n" + + return colliderData diff --git a/fast64_internal/oot/actor_collider/importer.py b/fast64_internal/oot/actor_collider/importer.py new file mode 100644 index 000000000..9fb15b277 --- /dev/null +++ b/fast64_internal/oot/actor_collider/importer.py @@ -0,0 +1,526 @@ +from typing import Callable +import bpy, mathutils, os, re, math +from ...utility import PluginError, hexOrDecInt +from ..file_reading import ootGetActorData, ootGetIncludedAssetData, ootGetActorDataPaths, ootGetLinkColliderData +from .properties import ( + OOTActorColliderItemProperty, + OOTActorColliderProperty, + OOTColliderHitboxItemProperty, + OOTColliderHitboxProperty, + OOTColliderHurtboxItemProperty, + OOTColliderHurtboxProperty, + OOTColliderPhysicsItemProperty, + OOTColliderPhysicsProperty, + OOTDamageFlagsProperty, + OOTActorColliderImportExportSettings, +) +from ..oot_utility import getOrderedBoneList, getOOTScale +from .utility import addColliderThenParent, ootEnumColliderType, ootEnumColliderElement, ootEnumHitboxSound + + +# has 1 capture group +def flagRegex(commaTerminating: bool = True) -> str: + return r"\s*([0-9a-zA-Z\_\s\|]*)\s*" + ("," if commaTerminating else ",?") + + +# has {count} capture groups +def flagListRegex(count: int) -> str: + regex = "" + if count < 1: + return "" + for i in range(count - 1): + regex += flagRegex() + regex += flagRegex(False) + return regex + + +# has 3 capture groups +def touchBumpRegex() -> str: + return r"\{\s*" + flagListRegex(3) + r"\s*\}\s*," + + +# has 7 capture groups +def colliderInitRegex() -> str: + return r"\{\s*" + flagListRegex(5) + "(" + flagRegex() + ")?" + r"\s*\}\s*," + + +# has 10 capture groups +def colliderInfoInitRegex() -> str: + return r"\{\s*" + flagRegex() + touchBumpRegex() + touchBumpRegex() + flagListRegex(3) + r"\s*\}\s*," + + +# assumes enums are in numerical order. +def getEnumValue(value: str, enumTuples: list[tuple[str, str, str]]) -> str: + enumList = [i[0] for i in enumTuples] + + if value in enumList: + return value + else: + try: + parsedValue = hexOrDecInt(value) + if parsedValue < len(enumList): + return parsedValue + else: + raise PluginError(f"Out of bounds index: {value}") + except ValueError: + raise PluginError(f"Invalid value: {value}") + + +def parseATFlags(flagData: str, atProp: OOTColliderHitboxProperty): + flags = [flag.strip() for flag in flagData.split("|")] + atProp.enable = "AT_ON" in flags + atProp.alignPlayer = "AT_TYPE_PLAYER" in flags or "AT_TYPE_ALL" in flags + atProp.alignEnemy = "AT_TYPE_ENEMY" in flags or "AT_TYPE_ALL" in flags + atProp.alignOther = "AT_TYPE_OTHER" in flags or "AT_TYPE_ALL" in flags + atProp.alignSelf = "AT_TYPE_SELF" in flags + + +def parseACFlags(flagData: str, acProp: OOTColliderHurtboxProperty): + flags = [flag.strip() for flag in flagData.split("|")] + acProp.enable = "AC_ON" in flags + acProp.attacksBounceOff = "AC_HARD" in flags + acProp.hurtByPlayer = "AC_TYPE_PLAYER" in flags or "AC_TYPE_ALL" in flags + acProp.hurtByEnemy = "AC_TYPE_ENEMY" in flags or "AC_TYPE_ALL" in flags + acProp.hurtByOther = "AC_TYPE_OTHER" in flags or "AC_TYPE_ALL" in flags + acProp.noDamage = "AC_NO_DAMAGE" in flags + + +def parseOCFlags(oc1Data: str, oc2Data: str | None, ocProp: OOTColliderPhysicsProperty): + flags1 = [flag.strip() for flag in oc1Data.split("|")] + ocProp.enable = "OC1_ON" in flags1 + ocProp.noPush = "OC1_NO_PUSH" in flags1 + ocProp.collidesWith.player = "OC1_TYPE_PLAYER" in flags1 or "OC1_TYPE_ALL" in flags1 + ocProp.collidesWith.type1 = "OC1_TYPE_1" in flags1 or "OC1_TYPE_ALL" in flags1 + ocProp.collidesWith.type2 = "OC1_TYPE_2" in flags1 or "OC1_TYPE_ALL" in flags1 + + if oc2Data is not None: + flags2 = [flag.strip() for flag in oc2Data.split("|")] + ocProp.isCollider.player = "OC2_TYPE_PLAYER" in flags2 + ocProp.isCollider.type1 = "OC2_TYPE_1" in flags2 + ocProp.isCollider.type2 = "OC2_TYPE_2" in flags2 + ocProp.skipHurtboxCheck = "OC2_FIRST_ONLY" in flags2 + ocProp.unk1 = "OC2_UNK1" in flags2 + ocProp.unk2 = "OC2_UNK2" in flags2 + + +def parseColliderInit(dataList: list[str], colliderProp: OOTActorColliderProperty): + colliderProp.colliderType = getEnumValue(dataList[0].strip(), ootEnumColliderType) + parseATFlags(dataList[1], colliderProp.hitbox) + parseACFlags(dataList[2], colliderProp.hurtbox) + parseOCFlags(dataList[3], dataList[4], colliderProp.physics) + + +def parseDamageFlags(flags: int, flagProp: OOTDamageFlagsProperty): + flagProp.dekuNut = flags & (1 << 0) != 0 + flagProp.dekuStick = flags & (1 << 1) != 0 + flagProp.slingshot = flags & (1 << 2) != 0 + flagProp.explosive = flags & (1 << 3) != 0 + flagProp.boomerang = flags & (1 << 4) != 0 + flagProp.arrowNormal = flags & (1 << 5) != 0 + flagProp.hammerSwing = flags & (1 << 6) != 0 + flagProp.hookshot = flags & (1 << 7) != 0 + flagProp.slashKokiriSword = flags & (1 << 8) != 0 + flagProp.slashMasterSword = flags & (1 << 9) != 0 + flagProp.slashGiantSword = flags & (1 << 10) != 0 + flagProp.arrowFire = flags & (1 << 11) != 0 + flagProp.arrowIce = flags & (1 << 12) != 0 + flagProp.arrowLight = flags & (1 << 13) != 0 + flagProp.arrowUnk1 = flags & (1 << 14) != 0 + flagProp.arrowUnk2 = flags & (1 << 15) != 0 + flagProp.arrowUnk3 = flags & (1 << 16) != 0 + flagProp.magicFire = flags & (1 << 17) != 0 + flagProp.magicIce = flags & (1 << 18) != 0 + flagProp.magicLight = flags & (1 << 19) != 0 + flagProp.shield = flags & (1 << 20) != 0 + flagProp.mirrorRay = flags & (1 << 21) != 0 + flagProp.spinKokiriSword = flags & (1 << 22) != 0 + flagProp.spinGiantSword = flags & (1 << 23) != 0 + flagProp.spinMasterSword = flags & (1 << 24) != 0 + flagProp.jumpKokiriSword = flags & (1 << 25) != 0 + flagProp.jumpGiantSword = flags & (1 << 26) != 0 + flagProp.jumpMasterSword = flags & (1 << 27) != 0 + flagProp.unknown1 = flags & (1 << 28) != 0 + flagProp.unblockable = flags & (1 << 29) != 0 + flagProp.hammerJump = flags & (1 << 30) != 0 + flagProp.unknown2 = flags & (1 << 31) != 0 + + +def parseTouch(dataList: list[str], touch: OOTColliderHitboxItemProperty, startIndex: int): + dmgFlags = int(dataList[startIndex + 1].strip(), 16) + parseDamageFlags(dmgFlags, touch.damageFlags) + touch.effect = hexOrDecInt(dataList[startIndex + 2]) + touch.damage = hexOrDecInt(dataList[startIndex + 3]) + + flags = [flag.strip() for flag in dataList[startIndex + 7].split("|")] + touch.enable = "TOUCH_ON" in flags + + for flag in flags: + if flag in [i[0] for i in ootEnumHitboxSound]: + touch.soundEffect = flag + + touch.drawHitmarksForEveryCollision = "TOUCH_AT_HITMARK" in flags + touch.closestBumper = "TOUCH_NEAREST" in flags + touch.unk7 = "TOUCH_UNK7" in flags + + +def parseBump(dataList: list[str], bump: OOTColliderHurtboxItemProperty, startIndex: int): + dmgFlags = int(dataList[startIndex + 4].strip(), 16) + parseDamageFlags(dmgFlags, bump.damageFlags) + bump.effect = hexOrDecInt(dataList[startIndex + 5]) + bump.defense = hexOrDecInt(dataList[startIndex + 6]) + + flags = [flag.strip() for flag in dataList[startIndex + 8].split("|")] + bump.enable = "BUMP_ON" in flags + bump.hookable = "BUMP_HOOKABLE" in flags + bump.giveInfoToHit = "BUMP_NO_AT_INFO" not in flags + bump.takesDamage = "BUMP_NO_DAMAGE" not in flags + bump.hasSound = "BUMP_NO_SWORD_SFX" not in flags + bump.hasHitmark = "BUMP_NO_HITMARK" not in flags + + +def parseObjectElement(dataList: list[str], objectElem: OOTColliderPhysicsItemProperty, startIndex: int): + flags = [flag.strip() for flag in dataList[startIndex + 9].split("|")] + objectElem.enable = "OCELEM_ON" in flags + objectElem.unk3 = "OCELEM_UNK3" in flags + + +def parseColliderInfoInit(dataList: list[str], colliderItemProp: OOTActorColliderItemProperty, startIndex: int): + colliderItemProp.element = getEnumValue(dataList[startIndex], ootEnumColliderElement) + parseTouch(dataList, colliderItemProp.touch, startIndex) + parseBump(dataList, colliderItemProp.bump, startIndex) + parseObjectElement(dataList, colliderItemProp.objectElem, startIndex) + + +def parseColliderData( + geometryName: str, + basePath: str, + overlayName: str, + isLink: bool, + parentObj: bpy.types.Object, + colliderSettings: OOTActorColliderImportExportSettings, +): + if not isLink: + actorData = ootGetActorData(basePath, overlayName) + currentPaths = ootGetActorDataPaths(basePath, overlayName) + else: + actorData = ootGetLinkColliderData(basePath) + currentPaths = [os.path.join(basePath, f"src/code/z_player_lib.c")] + actorData = ootGetIncludedAssetData(basePath, currentPaths, actorData) + actorData + + if colliderSettings.chooseSpecific: + filterNameFunc = lambda geometryName, name: name in [ + value.strip() for value in colliderSettings.specificColliders.split(",") if value.strip() != "" + ] + + else: + filterNameFunc = noFilter + if overlayName == "ovl_Boss_Sst": + filterNameFunc = filterForSst + elif overlayName == "ovl_Boss_Va": + filterNameFunc = filterForVa + + if colliderSettings.cylinder: + parseCylinderColliders(actorData, parentObj, geometryName, filterNameFunc) + + if colliderSettings.jointSphere: + parseJointSphereColliders( + actorData, parentObj, geometryName, filterNameFunc, colliderSettings.parentJointSpheresToBone + ) + + if colliderSettings.mesh: + parseMeshColliders(actorData, parentObj, geometryName, filterNameFunc) + + if colliderSettings.quad: + parseQuadColliders(actorData, parentObj, geometryName, filterNameFunc) + + +def noFilter(name: str, colliderName: str): + return True + + +def filterForSst(geometryName: str, colliderName: str): + if "Hand" in geometryName: + return "Hand" in colliderName + elif "Head" in geometryName: + return "Head" in colliderName + else: + return False + + +def filterForVa(geometryName: str, colliderName: str): + if "BodySkel" in geometryName: + return colliderName == "sCylinderInit" + elif "SupportSkel" in geometryName: + return colliderName == "sJntSphInitSupport" + elif "ZapperSkel" in geometryName: + return colliderName == "sQuadInit" + elif "StumpSkel" in geometryName: + return False + elif "BariSkel" in geometryName: + return colliderName == "sJntSphInitBari" or colliderName == "sQuadInit" + else: + return False + + +def parseCylinderColliders( + data: str, parentObj: bpy.types.Object, geometryName: str | None, filterNameFunc: Callable[[str, str], bool] +): + handledColliders = [] + for match in re.finditer( + r"ColliderCylinderInit(Type1)?\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + + name = match.group(2) + colliderData = match.group(3) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 16 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + obj = addColliderThenParent("COLSHAPE_CYLINDER", parentObj, None) + parseColliderInit(dataList, obj.ootActorCollider) + parseColliderInfoInit(dataList, obj.ootActorColliderItem, 6) + + obj.name = f"Collider {name}" + obj.ootActorCollider.name = name + + radius = hexOrDecInt(dataList[16]) / bpy.context.scene.ootBlenderScale + height = hexOrDecInt(dataList[17]) / bpy.context.scene.ootBlenderScale + yShift = hexOrDecInt(dataList[18]) / bpy.context.scene.ootBlenderScale + position = [hexOrDecInt(value) / bpy.context.scene.ootBlenderScale for value in dataList[19:22]] + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + location = mathutils.Vector((0, yShift, 0)) + mathutils.Vector(position) + obj.matrix_local = mathutils.Matrix.Diagonal( + mathutils.Vector((radius, radius, height / 2, 1)) + ) @ mathutils.Matrix.Translation(yUpToZUp @ location) + + +def parseJointSphereColliders( + data: str, + parentObj: bpy.types.Object, + geometryName: str | None, + filterNameFunc: Callable[[str, str], bool], + parentToBones: bool, +): + handledColliders = [] + for match in re.finditer( + r"ColliderJntSphInit\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + name = match.group(1) + colliderData = match.group(2) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 2 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + itemsName = dataList[7] + + parentObj.ootActorCollider.name = name + + parseColliderInit(dataList, parentObj.ootActorCollider) + parseJointSphereCollidersItems(data, parentObj, itemsName, name, parentToBones) + + +def parseJointSphereCollidersItems( + data: str, parentObj: bpy.types.Object, itemsName: str, name: str, parentToBones: bool +): + match = re.search( + r"ColliderJntSphElementInit\s*" + re.escape(itemsName) + r"\s*\[\s*[0-9A-Fa-fx]*\s*\]\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ) + + if match is None: + raise PluginError(f"Could not find {itemsName}.") + + matchData = match.group(1) + + dataList = [item.strip() for item in matchData.replace("{", "").replace("}", "").split(",") if item.strip() != ""] + if len(dataList) % 16 != 0: + raise PluginError(f"{itemsName} has unexpected struct format.") + + willParentToBones = isinstance(parentObj.data, bpy.types.Armature) and parentToBones + + if willParentToBones: + boneList = getOrderedBoneList(parentObj) + else: + boneList = None + + count = int(round(len(dataList) / 16)) + for item in [dataList[16 * i : 16 * (i + 1)] for i in range(count)]: + + # Why subtract 1??? + # in SkelAnime_InitFlex: skelAnime->limbCount = skeletonHeader->sh.limbCount + 1; + # possibly? + # Note: works with king dodongo, not with ganon2 + # Note: king dodongo count = numElements - 1 + limb = hexOrDecInt(item[10]) - 1 + + location = mathutils.Vector( + [hexOrDecInt(value) / ((getOOTScale(parentObj.ootActorScale))) for value in item[11:14]] + ) + radius = hexOrDecInt(item[14]) / bpy.context.scene.ootBlenderScale + scale = hexOrDecInt(item[15]) / 100 # code defined constant + + obj = addColliderThenParent("COLSHAPE_JNTSPH", parentObj, boneList[limb] if willParentToBones else None) + parseColliderInfoInit(item, obj.ootActorColliderItem, 0) + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + + if willParentToBones: + obj.matrix_world = ( + parentObj.matrix_world + @ parentObj.pose.bones[boneList[limb].name].matrix + @ mathutils.Matrix.Translation(location) + ) + else: + obj.matrix_local = mathutils.Matrix.Translation(yUpToZUp @ location) + obj.ootActorColliderItem.limbOverride = hexOrDecInt(item[10]) + + obj.scale.x = radius * scale + obj.scale.y = radius * scale + obj.scale.z = radius * scale + + +def parseMeshColliders( + data: str, parentObj: bpy.types.Object, geometryName: str | None, filterNameFunc: Callable[[str, str], bool] +): + handledColliders = [] + for match in re.finditer( + r"ColliderTrisInit(Type1)?\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + name = match.group(2) + colliderData = match.group(3) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 2 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + itemsName = dataList[7] + + obj = addColliderThenParent("COLSHAPE_TRIS", parentObj, None) + obj.name = f"Collider {name}" + obj.ootActorCollider.name = name + parseColliderInit(dataList, obj.ootActorCollider) + parseMeshCollidersItems(data, obj, itemsName, name) + + +def parseMeshCollidersItems(data: str, obj: bpy.types.Object, itemsName: str, name: str): + match = re.search( + r"ColliderTrisElementInit\s*" + re.escape(itemsName) + r"\s*\[\s*[0-9A-Fa-fx]*\s*\]\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ) + + if match is None: + raise PluginError(f"Could not find {itemsName}.") + + matchData = match.group(1) + + dataList = [item.strip() for item in matchData.replace("{", "").replace("}", "").split(",") if item.strip() != ""] + if len(dataList) % 19 != 0: + raise PluginError(f"{itemsName} has unexpected struct format.") + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + materialDict = {} # collider item hash : material index + vertList = [] + materialIndexList = [] + count = int(round(len(dataList) / 19)) + for item in [dataList[19 * i : 19 * (i + 1)] for i in range(count)]: + colliderHash = tuple(item[:10]) + if colliderHash not in materialDict: + material = bpy.data.materials.new(f"{name} Collider Material") + obj.data.materials.append(material) + materialDict[colliderHash] = material + parseColliderInfoInit(item, material.ootActorColliderItem, 0) + else: + material = materialDict[colliderHash] + + verts = [ + [ + float(value[:-1] if value[-1] == "f" else value) / bpy.context.scene.ootBlenderScale + for value in item[3 * i + 10 : 3 * i + 13] + ] + for i in range(3) + ] + for i in range(3): + transformedVert = yUpToZUp @ mathutils.Vector(verts[i]) + vertList.append(transformedVert[:]) + materialIndexList.append(obj.data.materials[:].index(material)) + + triangleCount = int(len(vertList) / 3) + faces = [[3 * i + j for j in range(3)] for i in range(triangleCount)] + obj.data.from_pydata(vertices=vertList, edges=[], faces=faces) + for i in range(triangleCount): + obj.data.polygons[i].material_index = materialIndexList[i] + + +def parseQuadColliders( + data: str, parentObj: bpy.types.Object, geometryName: str | None, filterNameFunc: Callable[[str, str], bool] +): + handledColliders = [] + for match in re.finditer( + r"ColliderQuadInit(Type1)?\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + name = match.group(2) + colliderData = match.group(3) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 16 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + obj = addColliderThenParent("COLSHAPE_QUAD", parentObj, None) + parseColliderInit(dataList, obj.ootActorCollider) + parseColliderInfoInit(dataList, obj.ootActorColliderItem, 6) + + obj.name = f"Collider {name}" + obj.ootActorCollider.name = name diff --git a/fast64_internal/oot/actor_collider/operators.py b/fast64_internal/oot/actor_collider/operators.py new file mode 100644 index 000000000..8c3db81c2 --- /dev/null +++ b/fast64_internal/oot/actor_collider/operators.py @@ -0,0 +1,108 @@ +import bpy +from ...utility import PluginError, raisePluginError, copyPropertyGroup +from .utility import updateColliderOnObj, addColliderThenParent, ootEnumColliderShape +from bpy.utils import register_class, unregister_class + + +class OOT_AddActorCollider(bpy.types.Operator): + bl_idname = "object.oot_add_actor_collider_operator" + bl_label = "Add Actor Collider" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + shape: bpy.props.EnumProperty(items=ootEnumColliderShape) + parentToBone: bpy.props.BoolProperty(default=False) + + def execute(self, context): + try: + activeObj = bpy.context.view_layer.objects.active + selectedObjs = bpy.context.selected_objects + + if activeObj is None: + raise PluginError("No object selected.") + + if context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + + if self.parentToBone and self.shape == "COLSHAPE_JNTSPH": + if isinstance(activeObj.data, bpy.types.Armature): + selectedBones = [bone for bone in activeObj.data.bones if bone.select] + if len(selectedBones) == 0: + raise PluginError("Cannot add joint spheres since no bones are selected on armature.") + for bone in selectedBones: + addColliderThenParent(self.shape, activeObj, bone, self.shape != "COLSHAPE_TRIS") + else: + raise PluginError("Non armature object selected.") + else: + addColliderThenParent(self.shape, activeObj, None, self.shape != "COLSHAPE_TRIS") + + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + return {"FINISHED"} + + +class OOT_CopyColliderProperties(bpy.types.Operator): + bl_idname = "object.oot_copy_collider_properties_operator" + bl_label = "Copy Collider Properties" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + try: + activeObj = bpy.context.view_layer.objects.active + selectedObjs = [obj for obj in bpy.context.selected_objects if obj.ootGeometryType == "Actor Collider"] + + if activeObj is None: + raise PluginError("No object selected.") + + if activeObj.ootGeometryType != "Actor Collider": + raise PluginError("Active object is not an actor collider.") + + if context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + + if ( + activeObj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH" + and activeObj.parent is not None + and isinstance(activeObj.parent.data, bpy.types.Armature) + ): + parentCollider = activeObj.parent.ootActorCollider + else: + parentCollider = activeObj.ootActorCollider + + for obj in selectedObjs: + if ( + obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH" + and obj.parent is not None + and isinstance(obj.parent.data, bpy.types.Armature) + ): + copyPropertyGroup(parentCollider, obj.parent.ootActorCollider) + else: + copyPropertyGroup(parentCollider, obj.ootActorCollider) + copyPropertyGroup(activeObj.ootActorColliderItem, obj.ootActorColliderItem) + + updateColliderOnObj(obj) + + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + return {"FINISHED"} + + +actor_collider_ops_classes = ( + OOT_AddActorCollider, + OOT_CopyColliderProperties, +) + + +def actor_collider_ops_register(): + for cls in actor_collider_ops_classes: + register_class(cls) + + +def actor_collider_ops_unregister(): + for cls in reversed(actor_collider_ops_classes): + unregister_class(cls) diff --git a/fast64_internal/oot/actor_collider/panels.py b/fast64_internal/oot/actor_collider/panels.py new file mode 100644 index 000000000..043adb6db --- /dev/null +++ b/fast64_internal/oot/actor_collider/panels.py @@ -0,0 +1,67 @@ +import bpy +from .utility import shapeNameToSimpleName +from bpy.utils import register_class, unregister_class + + +class OOT_ActorColliderPanel(bpy.types.Panel): + bl_label = "OOT Actor Collider Inspector" + bl_idname = "OBJECT_PT_OOT_Actor_Collider_Inspector" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_options = {"HIDE_HEADER"} + + @classmethod + def poll(cls, context: bpy.types.Context): + return context.scene.gameEditorMode == "OOT" and ( + context.object is not None and isinstance(context.object.data, bpy.types.Mesh) + ) + + def draw(self, context: bpy.types.Context): + obj = context.object + if obj.ootGeometryType == "Actor Collider": + box = self.layout.box().column() + name = shapeNameToSimpleName(obj.ootActorCollider.colliderShape) + box.box().label(text=f"OOT Actor {name} Collider Inspector") + obj.ootActorCollider.draw(obj, box) + obj.ootActorColliderItem.draw(obj, box) + + +def isActorCollider(context: bpy.types.Context) -> bool: + return ( + (context.object is not None and isinstance(context.object.data, bpy.types.Mesh)) + and context.object.ootGeometryType == "Actor Collider" + and context.material is not None + ) + + +class OOT_ActorColliderMaterialPanel(bpy.types.Panel): + bl_label = "OOT Actor Collider Material Inspector" + bl_idname = "OBJECT_PT_OOT_Actor_Collider_Material_Inspector" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {"HIDE_HEADER"} + + @classmethod + def poll(cls, context: bpy.types.Context): + return context.scene.gameEditorMode == "OOT" and isActorCollider(context) + + def draw(self, context: bpy.types.Context): + material = context.material + box = self.layout.box().column() + box.box().label(text=f"OOT Actor Mesh Collider Inspector") + material.ootActorColliderItem.draw(None, box) + + +actor_collider_panel_classes = (OOT_ActorColliderPanel, OOT_ActorColliderMaterialPanel) + + +def actor_collider_panel_register(): + for cls in actor_collider_panel_classes: + register_class(cls) + + +def actor_collider_panel_unregister(): + for cls in reversed(actor_collider_panel_classes): + unregister_class(cls) diff --git a/fast64_internal/oot/actor_collider/properties.py b/fast64_internal/oot/actor_collider/properties.py new file mode 100644 index 000000000..1b1340ad7 --- /dev/null +++ b/fast64_internal/oot/actor_collider/properties.py @@ -0,0 +1,639 @@ +import bpy, os +from bpy.utils import register_class, unregister_class +from ...utility import prop_split +from .utility import ( + updateCollider, + ootEnumColliderShape, + ootEnumColliderType, + ootEnumColliderElement, + ootEnumHitboxSound, +) + + +class OOTActorColliderImportExportSettings(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Actor Colliders", default=False) + chooseSpecific: bpy.props.BoolProperty(name="Choose Specific Colliders") + specificColliders: bpy.props.StringProperty(name="Colliders (Comma Separated List)") + jointSphere: bpy.props.BoolProperty(name="Joint Sphere", default=True) + cylinder: bpy.props.BoolProperty(name="Cylinder", default=True) + mesh: bpy.props.BoolProperty(name="Mesh", default=True) + quad: bpy.props.BoolProperty(name="Quad", default=True) + parentJointSpheresToBone: bpy.props.BoolProperty(name="Parent Joint Spheres To Bones", default=True) + + def draw(self, layout: bpy.types.UILayout, title: str, isImport: bool): + col = layout.column() + col.prop(self, "enable", text=title) + if self.enable: + col.prop(self, "chooseSpecific") + if self.chooseSpecific: + col.prop(self, "specificColliders") + row = col.row(align=True) + row.prop(self, "jointSphere", text="Joint Sphere", toggle=1) + row.prop(self, "cylinder", text="Cylinder", toggle=1) + row.prop(self, "mesh", text="Mesh", toggle=1) + row.prop(self, "quad", text="Quad", toggle=1) + + if isImport: + col.prop(self, "parentJointSpheresToBone") + + return col + + +# Defaults are from DMG_DEFAULT. +class OOTDamageFlagsProperty(bpy.types.PropertyGroup): + expandTab: bpy.props.BoolProperty(default=False, name="Damage Flags") + dekuNut: bpy.props.BoolProperty(default=True, name="Deku Nut") + dekuStick: bpy.props.BoolProperty(default=True, name="Deku Stick") + slingshot: bpy.props.BoolProperty(default=True, name="Slingshot") + explosive: bpy.props.BoolProperty(default=True, name="Bomb") + boomerang: bpy.props.BoolProperty(default=True, name="Boomerang") + arrowNormal: bpy.props.BoolProperty(default=True, name="Normal") + hammerSwing: bpy.props.BoolProperty(default=True, name="Hammer Swing") + hookshot: bpy.props.BoolProperty(default=True, name="Hookshot") + slashKokiriSword: bpy.props.BoolProperty(default=True, name="Kokiri") + slashMasterSword: bpy.props.BoolProperty(default=True, name="Master") + slashGiantSword: bpy.props.BoolProperty(default=True, name="Giant") + arrowFire: bpy.props.BoolProperty(default=True, name="Fire") + arrowIce: bpy.props.BoolProperty(default=True, name="Ice") + arrowLight: bpy.props.BoolProperty(default=True, name="Light") + arrowUnk1: bpy.props.BoolProperty(default=True, name="Unk1") + arrowUnk2: bpy.props.BoolProperty(default=True, name="Unk2") + arrowUnk3: bpy.props.BoolProperty(default=True, name="Unk3") + magicFire: bpy.props.BoolProperty(default=True, name="Fire") + magicIce: bpy.props.BoolProperty(default=True, name="Ice") + magicLight: bpy.props.BoolProperty(default=True, name="Light") + shield: bpy.props.BoolProperty(default=False, name="Shield") + mirrorRay: bpy.props.BoolProperty(default=False, name="Mirror Ray") + spinKokiriSword: bpy.props.BoolProperty(default=True, name="Kokiri") + spinGiantSword: bpy.props.BoolProperty(default=True, name="Giant") + spinMasterSword: bpy.props.BoolProperty(default=True, name="Master") + jumpKokiriSword: bpy.props.BoolProperty(default=True, name="Kokiri") + jumpGiantSword: bpy.props.BoolProperty(default=True, name="Giant") + jumpMasterSword: bpy.props.BoolProperty(default=True, name="Master") + unknown1: bpy.props.BoolProperty(default=True, name="Unknown 1") + unblockable: bpy.props.BoolProperty(default=True, name="Unblockable") + hammerJump: bpy.props.BoolProperty(default=True, name="Hammer Jump") + unknown2: bpy.props.BoolProperty(default=True, name="Unknown 2") + + def draw(self, layout: bpy.types.UILayout): + layout.prop(self, "expandTab", text="Damage Flags", icon="TRIA_DOWN" if self.expandTab else "TRIA_RIGHT") + + if self.expandTab: + row = layout.row(align=True) + row.prop(self, "dekuNut", toggle=1) + row.prop(self, "dekuStick", toggle=1) + row.prop(self, "slingshot", toggle=1) + + row = layout.row(align=True) + row.prop(self, "explosive", toggle=1) + row.prop(self, "boomerang", toggle=1) + row.prop(self, "hookshot", toggle=1) + + row = layout.row(align=True) + row.prop(self, "hammerSwing", toggle=1) + row.prop(self, "hammerJump", toggle=1) + + row = layout.row(align=True) + row.label(text="Slash") + row.prop(self, "slashKokiriSword", toggle=1) + row.prop(self, "slashMasterSword", toggle=1) + row.prop(self, "slashGiantSword", toggle=1) + + row = layout.row(align=True) + row.label(text="Spin") + row.prop(self, "spinKokiriSword", toggle=1) + row.prop(self, "spinMasterSword", toggle=1) + row.prop(self, "spinGiantSword", toggle=1) + + row = layout.row(align=True) + row.label(text="Jump") + row.prop(self, "jumpKokiriSword", toggle=1) + row.prop(self, "jumpMasterSword", toggle=1) + row.prop(self, "jumpGiantSword", toggle=1) + + row = layout.row(align=True) + row.label(text="Arrow") + row.prop(self, "arrowNormal", toggle=1) + row.prop(self, "arrowFire", toggle=1) + row.prop(self, "arrowIce", toggle=1) + row.prop(self, "arrowLight", toggle=1) + + row = layout.row(align=True) + row.label(text="Arrow Unknown") + row.prop(self, "arrowUnk1", toggle=1) + row.prop(self, "arrowUnk2", toggle=1) + row.prop(self, "arrowUnk3", toggle=1) + + row = layout.row(align=True) + row.label(text="Magic") + row.prop(self, "magicFire", toggle=1) + row.prop(self, "magicIce", toggle=1) + row.prop(self, "magicLight", toggle=1) + + row = layout.row(align=True) + row.prop(self, "shield", toggle=1) + row.prop(self, "mirrorRay", toggle=1) + + row = layout.row(align=True) + row.prop(self, "unblockable", toggle=1) + row.prop(self, "unknown1", toggle=1) + row.prop(self, "unknown2", toggle=1) + + def to_c(self): + flags = ( + ((1 if self.dekuNut else 0) << 0) + | ((1 if self.dekuStick else 0) << 1) + | ((1 if self.slingshot else 0) << 2) + | ((1 if self.explosive else 0) << 3) + | ((1 if self.boomerang else 0) << 4) + | ((1 if self.arrowNormal else 0) << 5) + | ((1 if self.hammerSwing else 0) << 6) + | ((1 if self.hookshot else 0) << 7) + | ((1 if self.slashKokiriSword else 0) << 8) + | ((1 if self.slashMasterSword else 0) << 9) + | ((1 if self.slashGiantSword else 0) << 10) + | ((1 if self.arrowFire else 0) << 11) + | ((1 if self.arrowIce else 0) << 12) + | ((1 if self.arrowLight else 0) << 13) + | ((1 if self.arrowUnk1 else 0) << 14) + | ((1 if self.arrowUnk2 else 0) << 15) + | ((1 if self.arrowUnk3 else 0) << 16) + | ((1 if self.magicFire else 0) << 17) + | ((1 if self.magicIce else 0) << 18) + | ((1 if self.magicLight else 0) << 19) + | ((1 if self.shield else 0) << 20) + | ((1 if self.mirrorRay else 0) << 21) + | ((1 if self.spinKokiriSword else 0) << 22) + | ((1 if self.spinGiantSword else 0) << 23) + | ((1 if self.spinMasterSword else 0) << 24) + | ((1 if self.jumpKokiriSword else 0) << 25) + | ((1 if self.jumpGiantSword else 0) << 26) + | ((1 if self.jumpMasterSword else 0) << 27) + | ((1 if self.unknown1 else 0) << 28) + | ((1 if self.unblockable else 0) << 29) + | ((1 if self.hammerJump else 0) << 30) + | ((1 if self.unknown2 else 0) << 31) + ) + return format(flags, "#010x") + + +# AT +class OOTColliderHitboxProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Hitbox (AT)", update=updateCollider, default=False) + alignPlayer: bpy.props.BoolProperty(name="Player", default=False) + alignEnemy: bpy.props.BoolProperty(name="Enemy", default=True) + alignOther: bpy.props.BoolProperty(name="Other", default=False) + alignSelf: bpy.props.BoolProperty(name="Self", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + alignToggles = layout.row(align=True) + alignToggles.label(text="Aligned") + alignToggles.prop(self, "alignPlayer", toggle=1) + alignToggles.prop(self, "alignEnemy", toggle=1) + alignToggles.prop(self, "alignOther", toggle=1) + alignToggles.prop(self, "alignSelf", toggle=1) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c(self): + flagList = [] + flagList.append("AT_ON") if self.enable else None + if self.alignPlayer and self.alignEnemy and self.alignOther: + flagList.append("AT_TYPE_ALL") + else: + flagList.append("AT_TYPE_PLAYER") if self.alignPlayer else None + flagList.append("AT_TYPE_ENEMY") if self.alignEnemy else None + flagList.append("AT_TYPE_OTHER") if self.alignOther else None + flagList.append("AT_TYPE_SELF") if self.alignSelf else None + + flagList = ["AT_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + +# AC +class OOTColliderHurtboxProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Hurtbox (AC)", update=updateCollider, default=True) + attacksBounceOff: bpy.props.BoolProperty(name="Attacks Bounce Off") + hurtByPlayer: bpy.props.BoolProperty(name="Player", default=True) + hurtByEnemy: bpy.props.BoolProperty(name="Enemy", default=False) + hurtByOther: bpy.props.BoolProperty(name="Other", default=False) + noDamage: bpy.props.BoolProperty(name="Doesn't Take Damage", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "attacksBounceOff") + layout.prop(self, "noDamage") + hurtToggles = layout.row(align=True) + hurtToggles.label(text="Hurt By") + hurtToggles.prop(self, "hurtByPlayer", toggle=1) + hurtToggles.prop(self, "hurtByEnemy", toggle=1) + hurtToggles.prop(self, "hurtByOther", toggle=1) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c(self): + flagList = [] + flagList.append("AC_ON") if self.enable else None + flagList.append("AC_HARD") if self.attacksBounceOff else None + if self.hurtByPlayer and self.hurtByEnemy and self.hurtByOther: + flagList.append("AC_TYPE_ALL") + else: + flagList.append("AC_TYPE_PLAYER") if self.hurtByPlayer else None + flagList.append("AC_TYPE_ENEMY") if self.hurtByEnemy else None + flagList.append("AC_TYPE_OTHER") if self.hurtByOther else None + flagList.append("AC_NO_DAMAGE") if self.noDamage else None + + flagList = ["AC_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + +class OOTColliderLayers(bpy.types.PropertyGroup): + player: bpy.props.BoolProperty(name="Player", default=False) + type1: bpy.props.BoolProperty(name="Type 1", default=True) + type2: bpy.props.BoolProperty(name="Type 2", default=False) + + def draw(self, layout: bpy.types.UILayout, name: str): + collisionLayers = layout.row(align=True) + collisionLayers.label(text=name) + collisionLayers.prop(self, "player", toggle=1) + collisionLayers.prop(self, "type1", toggle=1) + collisionLayers.prop(self, "type2", toggle=1) + + +# OC +class OOTColliderPhysicsProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Physics (OC)", update=updateCollider, default=True) + noPush: bpy.props.BoolProperty(name="Don't Push Others") + collidesWith: bpy.props.PointerProperty(type=OOTColliderLayers) + isCollider: bpy.props.PointerProperty(type=OOTColliderLayers) + skipHurtboxCheck: bpy.props.BoolProperty(name="Skip Hurtbox Check After First Collision") + isType1: bpy.props.BoolProperty(name="Is Type 1", default=False) + unk1: bpy.props.BoolProperty(name="Unknown 1", default=False) + unk2: bpy.props.BoolProperty(name="Unknown 2", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "noPush") + layout.prop(self, "skipHurtboxCheck") + layout.prop(self, "isType1") + self.collidesWith.draw(layout, "Hits Type") + if not self.isType1: + self.isCollider.draw(layout, "Is Type") + row = layout.row(align=True) + row.prop(self, "unk1") + row.prop(self, "unk2") + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_1(self): + flagList = [] + flagList.append("OC1_ON") if self.enable else None + flagList.append("OC1_NO_PUSH") if self.noPush else None + if self.collidesWith.player and self.collidesWith.type1 and self.collidesWith.type2: + flagList.append("OC1_TYPE_ALL") + else: + flagList.append("OC1_TYPE_PLAYER") if self.collidesWith.player else None + flagList.append("OC1_TYPE_1") if self.collidesWith.type1 else None + flagList.append("OC1_TYPE_2") if self.collidesWith.type2 else None + + flagList = ["OC1_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_2(self): + flagList = [] + flagList.append("OC2_UNK1") if self.unk1 else None + flagList.append("OC2_UNK2") if self.unk2 else None + + flagList.append("OC2_TYPE_PLAYER") if self.isCollider.player else None + flagList.append("OC2_TYPE_1") if self.isCollider.type1 else None + flagList.append("OC2_TYPE_2") if self.isCollider.type2 else None + + flagList.append("OC2_FIRST_ONLY") if self.skipHurtboxCheck else None + + flagList = ["OC2_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + +# Touch +class OOTColliderHitboxItemProperty(bpy.types.PropertyGroup): + # Flags + enable: bpy.props.BoolProperty(name="Touch") + soundEffect: bpy.props.EnumProperty(name="Sound Effect", items=ootEnumHitboxSound) + drawHitmarksForEveryCollision: bpy.props.BoolProperty(name="Draw Hitmarks For Every Collision") + closestBumper: bpy.props.BoolProperty(name="Only Collide With Closest Bumper (Quads)") + + # ColliderTouch + damageFlags: bpy.props.PointerProperty(type=OOTDamageFlagsProperty, name="Damage Flags") + effect: bpy.props.IntProperty(min=0, max=255, name="Effect") + damage: bpy.props.IntProperty(min=0, max=255, name="Damage") + unk7: bpy.props.BoolProperty(name="Unknown 7") + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + prop_split(layout, self, "soundEffect", "Sound Effect") + layout.prop(self, "drawHitmarksForEveryCollision") + layout.prop(self, "closestBumper") + layout.prop(self, "unk7") + prop_split(layout, self, "effect", "Effect") + prop_split(layout, self, "damage", "Damage") + self.damageFlags.draw(layout) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_flags(self): + flagList = [] + flagList.append("TOUCH_ON") if self.enable else None + flagList.append("TOUCH_NEAREST") if self.closestBumper else None + flagList.append(self.soundEffect) + + flagList.append("TOUCH_AT_HITMARK") if self.drawHitmarksForEveryCollision else None + flagList.append("TOUCH_UNK7") if self.unk7 else None + + # note that TOUCH_SFX_NORMAL is the same as 0, but since it is an enum it would usually be included anyway + flagList = ( + ["TOUCH_NONE"] + if len(flagList) == 0 or (len(flagList) == 1 and flagList[0] == "TOUCH_SFX_NORMAL") + else flagList + ) + + return " | ".join(flagList) + + def to_c_damage_flags(self): + flagList = [self.damageFlags.to_c()] + flagList.append(format(self.effect, "#04x")) + flagList.append(format(self.damage, "#04x")) + + return "{ " + ", ".join(flagList) + " }" + + +# Bump +class OOTColliderHurtboxItemProperty(bpy.types.PropertyGroup): + # Flags + enable: bpy.props.BoolProperty(name="Bump") + hookable: bpy.props.BoolProperty(name="Hookable") + giveInfoToHit: bpy.props.BoolProperty(name="Give Info To Hit") + takesDamage: bpy.props.BoolProperty(name="Damageable", default=True) + hasSound: bpy.props.BoolProperty(name="Has SFX", default=True) + hasHitmark: bpy.props.BoolProperty(name="Has Hitmark", default=True) + + # ColliderBumpInit + damageFlags: bpy.props.PointerProperty(type=OOTDamageFlagsProperty, name="Damage Flags") + effect: bpy.props.IntProperty(min=0, max=255, name="Effect") + defense: bpy.props.IntProperty(min=0, max=255, name="Damage") + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "hookable") + layout.prop(self, "giveInfoToHit") + row = layout.row(align=True) + row.prop(self, "takesDamage", toggle=1) + row.prop(self, "hasSound", toggle=1) + row.prop(self, "hasHitmark", toggle=1) + prop_split(layout, self, "effect", "Effect") + prop_split(layout, self, "defense", "Defense") + self.damageFlags.draw(layout) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_flags(self): + flagList = [] + flagList.append("BUMP_ON") if self.enable else None + flagList.append("BUMP_HOOKABLE") if self.hookable else None + flagList.append("BUMP_NO_AT_INFO") if not self.giveInfoToHit else None + flagList.append("BUMP_NO_DAMAGE") if not self.takesDamage else None + flagList.append("BUMP_NO_SWORD_SFX") if not self.hasSound else None + flagList.append("BUMP_NO_HITMARK") if not self.hasHitmark else None + flagList.append("BUMP_DRAW_HITMARK") if self.hookable else None + + flagList = ["BUMP_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + def to_c_damage_flags(self): + flagList = [self.damageFlags.to_c()] + flagList.append(format(self.effect, "#04x")) + flagList.append(format(self.defense, "#04x")) + + return "{ " + ", ".join(flagList) + " }" + + +# OCElem +class OOTColliderPhysicsItemProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Object Element") + unk3: bpy.props.BoolProperty(name="Unknown 3", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "unk3") + + def to_c_flags(self): + if not self.enable: + return "OCELEM_NONE" + + flagList = ["OCELEM_ON"] + flagList.append("OCELEM_UNK3") if self.unk3 else None + + return " | ".join(flagList) + + +# ColliderInit is for entire collection. +# ColliderInfoInit is for each item of a collection. + +# Triangle/Cylinder will use their own object for ColliderInit. +# Joint Sphere will use armature object for ColliderInit. + + +class OOTActorColliderProperty(bpy.types.PropertyGroup): + # ColliderInit + colliderShape: bpy.props.EnumProperty( + items=ootEnumColliderShape, name="Shape", default="COLSHAPE_CYLINDER", update=updateCollider + ) + colliderType: bpy.props.EnumProperty(items=ootEnumColliderType, name="Hit Reaction") + hitbox: bpy.props.PointerProperty(type=OOTColliderHitboxProperty, name="Hitbox (AT)") + hurtbox: bpy.props.PointerProperty(type=OOTColliderHurtboxProperty, name="Hurtbox (AC)") + physics: bpy.props.PointerProperty(type=OOTColliderPhysicsProperty, name="Physics (OC)") + name: bpy.props.StringProperty(name="Struct Name", default="sColliderInit") + + def draw(self, obj: bpy.types.Object, layout: bpy.types.UILayout): + if obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH": + if obj.parent is not None: + collider = obj.parent.ootActorCollider + layout.label(text="Joint Shared", icon="INFO") + prop_split(layout, collider, "name", "Struct Name") + prop_split(layout, collider, "colliderType", "Collider Type") + collider.hitbox.draw(layout) + collider.hurtbox.draw(layout) + collider.physics.draw(layout) + else: + layout.label(text="Joint sphere colliders must be parented to a bone or object.", icon="ERROR") + + else: + prop_split(layout, self, "name", "Struct Name") + if obj.ootActorCollider.colliderShape == "COLSHAPE_QUAD": + layout.label(text="Geometry is ignored and zeroed.", icon="INFO") + layout.label(text="Only properties are exported.") + prop_split(layout, self, "colliderType", "Collider Type") + self.hitbox.draw(layout) + self.hurtbox.draw(layout) + self.physics.draw(layout) + + def to_c(self, tabDepth: int): + indent = "\t" * tabDepth + nextIndent = "\t" * (tabDepth + 1) + + physics2 = f"{nextIndent}{self.physics.to_c_2()},\n" if not self.physics.isType1 else "" + + data = ( + f"{indent}{{\n" + f"{nextIndent}{self.colliderType},\n" + f"{nextIndent}{self.hitbox.to_c()},\n" + f"{nextIndent}{self.hurtbox.to_c()},\n" + f"{nextIndent}{self.physics.to_c_1()},\n" + f"{physics2}" + f"{nextIndent}{self.colliderShape},\n" + f"{indent}}},\n" + ) + + return data + + +class OOTActorColliderItemProperty(bpy.types.PropertyGroup): + # ColliderInfoInit + element: bpy.props.EnumProperty(items=ootEnumColliderElement, name="Element Type") + limbOverride: bpy.props.IntProperty(min=0, max=256, name="Limb Index") + touch: bpy.props.PointerProperty(type=OOTColliderHitboxItemProperty, name="Touch") + bump: bpy.props.PointerProperty(type=OOTColliderHurtboxItemProperty, name="Bump") + objectElem: bpy.props.PointerProperty(type=OOTColliderPhysicsItemProperty, name="Object Element") + + # obj is None when using mesh collider, where property is on material + def draw(self, obj: bpy.types.Object | None, layout: bpy.types.UILayout): + if obj is not None and obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH": + layout.label(text="Joint Specific", icon="INFO") + if not ( + obj.parent is not None and isinstance(obj.parent.data, bpy.types.Armature) and obj.parent_bone != "" + ): + prop_split(layout, self, "limbOverride", "Limb Index") + + if obj is not None and obj.ootActorCollider.colliderShape == "COLSHAPE_TRIS": + layout = layout.column() + layout.label(text="Touch/bump defined in materials.", icon="INFO") + layout.label(text="Materials will not be visualized.") + else: + layout = layout.column() + prop_split(layout, self, "element", "Element Type") + self.touch.draw(layout) + self.bump.draw(layout) + self.objectElem.draw(layout) + + def to_c(self, tabDepth: int): + indent = "\t" * tabDepth + nextIndent = "\t" * (tabDepth + 1) + + data = ( + f"{indent}{{\n" + f"{nextIndent}{self.element},\n" + f"{nextIndent}{self.touch.to_c_damage_flags()},\n" + f"{nextIndent}{self.bump.to_c_damage_flags()},\n" + f"{nextIndent}{self.touch.to_c_flags()},\n" + f"{nextIndent}{self.bump.to_c_flags()},\n" + f"{nextIndent}{self.objectElem.to_c_flags()},\n" + f"{indent}}},\n" + ) + + return data + + +def drawColliderVisibilityOperators(layout: bpy.types.UILayout): + col = layout.column() + col.label(text="Toggle Visibility (Excluding Selected)") + row = col.row(align=True) + visibilitySettings = bpy.context.scene.ootColliderVisibility + row.prop(visibilitySettings, "jointSphere", text="Joint Sphere", toggle=1) + row.prop(visibilitySettings, "cylinder", text="Cylinder", toggle=1) + row.prop(visibilitySettings, "mesh", text="Mesh", toggle=1) + row.prop(visibilitySettings, "quad", text="Quad", toggle=1) + + +def updateVisibilityJointSphere(self, context): + updateVisibilityCollider("COLSHAPE_JNTSPH", self.jointSphere) + + +def updateVisibilityCylinder(self, context): + updateVisibilityCollider("COLSHAPE_CYLINDER", self.cylinder) + + +def updateVisibilityMesh(self, context): + updateVisibilityCollider("COLSHAPE_TRIS", self.mesh) + + +def updateVisibilityQuad(self, context): + updateVisibilityCollider("COLSHAPE_QUAD", self.quad) + + +def updateVisibilityCollider(shapeName: str, visibility: bool) -> None: + selectedObjs = bpy.context.selected_objects + for obj in bpy.data.objects: + if ( + isinstance(obj.data, bpy.types.Mesh) + and obj.ootGeometryType == "Actor Collider" + and obj.ootActorCollider.colliderShape == shapeName + and obj not in selectedObjs + ): + obj.hide_set(not visibility) + + +class OOTColliderVisibilitySettings(bpy.types.PropertyGroup): + jointSphere: bpy.props.BoolProperty(name="Joint Sphere", default=True, update=updateVisibilityJointSphere) + cylinder: bpy.props.BoolProperty(name="Cylinder", default=True, update=updateVisibilityCylinder) + mesh: bpy.props.BoolProperty(name="Mesh", default=True, update=updateVisibilityMesh) + quad: bpy.props.BoolProperty(name="Quad", default=True, update=updateVisibilityQuad) + + +actor_collider_props_classes = ( + OOTColliderLayers, + OOTDamageFlagsProperty, + OOTColliderHitboxItemProperty, + OOTColliderHurtboxItemProperty, + OOTColliderPhysicsItemProperty, + OOTColliderHitboxProperty, + OOTColliderHurtboxProperty, + OOTColliderPhysicsProperty, + OOTActorColliderProperty, + OOTActorColliderItemProperty, + OOTColliderVisibilitySettings, + OOTActorColliderImportExportSettings, +) + + +def actor_collider_props_register(): + for cls in actor_collider_props_classes: + register_class(cls) + + bpy.types.Object.ootActorCollider = bpy.props.PointerProperty(type=OOTActorColliderProperty) + bpy.types.Object.ootActorColliderItem = bpy.props.PointerProperty(type=OOTActorColliderItemProperty) + bpy.types.Material.ootActorColliderItem = bpy.props.PointerProperty(type=OOTActorColliderItemProperty) + bpy.types.Scene.ootColliderLibVer = bpy.props.IntProperty(default=1) + bpy.types.Scene.ootColliderVisibility = bpy.props.PointerProperty(type=OOTColliderVisibilitySettings) + + +def actor_collider_props_unregister(): + for cls in reversed(actor_collider_props_classes): + unregister_class(cls) + + del bpy.types.Object.ootActorCollider + del bpy.types.Object.ootActorColliderItem + del bpy.types.Scene.ootColliderLibVer + del bpy.types.Scene.ootColliderVisibility diff --git a/fast64_internal/oot/actor_collider/utility.py b/fast64_internal/oot/actor_collider/utility.py new file mode 100644 index 000000000..5cc124a49 --- /dev/null +++ b/fast64_internal/oot/actor_collider/utility.py @@ -0,0 +1,243 @@ +from ...utility import PluginError, parentObject +from ..oot_f3d_writer import getColliderMat +import bpy, math, mathutils + +ootEnumColliderShape = [ + ("COLSHAPE_JNTSPH", "Joint Sphere", "Joint Sphere"), + ("COLSHAPE_CYLINDER", "Cylinder", "Cylinder"), + ("COLSHAPE_TRIS", "Triangles", "Triangles"), + ("COLSHAPE_QUAD", "Quad (Properties Only)", "Quad"), +] + +ootEnumColliderType = [ + ("COLTYPE_HIT0", "Blue Blood, White Hitmark", "Blue Blood, White Hitmark"), + ("COLTYPE_HIT1", "No Blood, Dust Hitmark", "No Blood, Dust Hitmark"), + ("COLTYPE_HIT2", "Green Blood, Dust Hitmark", "Green Blood, Dust Hitmark"), + ("COLTYPE_HIT3", "No Blood, White Hitmark", "No Blood, White Hitmark"), + ("COLTYPE_HIT4", "Water Burst, No hitmark", "Water Burst, No hitmark"), + ("COLTYPE_HIT5", "No blood, Red Hitmark", "No blood, Red Hitmark"), + ("COLTYPE_HIT6", "Green Blood, White Hitmark", "Green Blood, White Hitmark"), + ("COLTYPE_HIT7", "Red Blood, White Hitmark", "Red Blood, White Hitmark"), + ("COLTYPE_HIT8", "Blue Blood, Red Hitmark", "Blue Blood, Red Hitmark"), + ("COLTYPE_METAL", "Metal", "Metal"), + ("COLTYPE_NONE", "None", "None"), + ("COLTYPE_WOOD", "Wood", "Wood"), + ("COLTYPE_HARD", "Hard", "Hard"), + ("COLTYPE_TREE", "Tree", "Tree"), +] + +ootEnumColliderElement = [ + ("ELEMTYPE_UNK0", "Element 0", "Element 0"), + ("ELEMTYPE_UNK1", "Element 1", "Element 1"), + ("ELEMTYPE_UNK2", "Element 2", "Element 2"), + ("ELEMTYPE_UNK3", "Element 3", "Element 3"), + ("ELEMTYPE_UNK4", "Element 4", "Element 4"), + ("ELEMTYPE_UNK5", "Element 5", "Element 5"), + ("ELEMTYPE_UNK6", "Element 6", "Element 6"), + ("ELEMTYPE_UNK7", "Element 7", "Element 7"), +] + +ootEnumHitboxSound = [ + ("TOUCH_SFX_NORMAL", "Hurtbox", "Hurtbox"), + ("TOUCH_SFX_HARD", "Hard", "Hard"), + ("TOUCH_SFX_WOOD", "Wood", "Wood"), + ("TOUCH_SFX_NONE", "None", "None"), +] + + +def getGeometryNodes(shapeName: str): + nodesName = shapeNameToBlenderName(shapeName) + if nodesName in bpy.data.node_groups: + return bpy.data.node_groups[nodesName] + else: + node_group = bpy.data.node_groups.new(nodesName, "GeometryNodeTree") + node_group.use_fake_user = True + inNode = node_group.nodes.new("NodeGroupInput") + node_group.inputs.new("NodeSocketGeometry", "Geometry") + node_group.inputs.new("NodeSocketMaterial", "Material") + outNode = node_group.nodes.new("NodeGroupOutput") + node_group.outputs.new("NodeSocketGeometry", "Geometry") + + if nodesName == "oot_collider_sphere": + # Sphere + shape = node_group.nodes.new("GeometryNodeMeshUVSphere") + shape.inputs["Segments"].default_value = 16 + shape.inputs["Rings"].default_value = 8 + shape.inputs["Radius"].default_value = 1 + + # Shade Smooth + smooth = node_group.nodes.new("GeometryNodeSetShadeSmooth") + node_group.links.new(shape.outputs["Mesh"], smooth.inputs["Geometry"]) + lastNode = smooth + + elif nodesName == "oot_collider_cylinder": + + # Cylinder + shape = node_group.nodes.new("GeometryNodeMeshCylinder") + shape.inputs["Vertices"].default_value = 16 + shape.inputs["Radius"].default_value = 1 + shape.inputs["Depth"].default_value = 2 + + # Shade Smooth + smooth = node_group.nodes.new("GeometryNodeSetShadeSmooth") + node_group.links.new(shape.outputs["Mesh"], smooth.inputs["Geometry"]) + node_group.links.new(shape.outputs["Side"], smooth.inputs["Selection"]) + + # Transform + transform = node_group.nodes.new("GeometryNodeTransform") + node_group.links.new(smooth.outputs["Geometry"], transform.inputs["Geometry"]) + transform.inputs["Translation"].default_value[2] = 1 + lastNode = transform + + elif nodesName == "oot_collider_triangles": + lastNode = inNode + + elif nodesName == "oot_collider_quad": + # Grid + shape = node_group.nodes.new("GeometryNodeMeshGrid") + shape.inputs["Size X"].default_value = 2 + shape.inputs["Size Y"].default_value = 2 + shape.inputs["Vertices X"].default_value = 2 + shape.inputs["Vertices Y"].default_value = 2 + + # Transform + transform = node_group.nodes.new("GeometryNodeTransform") + node_group.links.new(shape.outputs["Mesh"], transform.inputs["Geometry"]) + transform.inputs["Rotation"].default_value[0] = math.radians(90) + lastNode = transform + + else: + raise PluginError(f"Could not find node group name: {nodesName}") + + # Set Material + setMat = node_group.nodes.new("GeometryNodeSetMaterial") + node_group.links.new(lastNode.outputs["Geometry"], setMat.inputs["Geometry"]) + node_group.links.new(inNode.outputs["Material"], setMat.inputs["Material"]) + node_group.links.new(setMat.outputs["Geometry"], outNode.inputs["Geometry"]) + + return node_group + + +# Apply geometry nodes for the correct collider shape +def applyColliderGeoNodes(obj: bpy.types.Object, material: bpy.types.Material, shapeName: str) -> None: + if "Collider Shape" not in obj.modifiers: + modifier = obj.modifiers.new("Collider Shape", "NODES") + else: + modifier = obj.modifiers["Collider Shape"] + modifier.node_group = getGeometryNodes(shapeName) + modifier["Input_1"] = material + + +# Update collider callback for collider type property +def updateCollider(self, context: bpy.types.Context) -> None: + updateColliderOnObj(context.object) + + +def updateColliderOnObj(obj: bpy.types.Object, updateJointSiblings: bool = True) -> None: + if obj.ootGeometryType == "Actor Collider": + colliderProp = obj.ootActorCollider + if colliderProp.colliderShape == "COLSHAPE_JNTSPH": + if obj.parent == None: + return + queryProp = obj.parent.ootActorCollider + else: + queryProp = colliderProp + + alpha = 0.7 + # if colliderProp.colliderShape == "COLSHAPE_TRIS": + # material = getColliderMat("oot_collider_cyan", (0, 0.5, 1, alpha)) + if colliderProp.colliderShape == "COLSHAPE_QUAD": + material = getColliderMat("oot_collider_orange", (0.2, 0.05, 0, alpha)) + elif queryProp.hitbox.enable and queryProp.hurtbox.enable: + material = getColliderMat("oot_collider_purple", (0.15, 0, 0.05, alpha)) + elif queryProp.hitbox.enable: + material = getColliderMat("oot_collider_red", (0.2, 0, 0, alpha)) + elif queryProp.hurtbox.enable: + material = getColliderMat("oot_collider_blue", (0, 0, 0.2, alpha)) + else: + material = getColliderMat("oot_collider_white", (0.2, 0.2, 0.2, alpha)) + applyColliderGeoNodes(obj, material, colliderProp.colliderShape) + + if updateJointSiblings and colliderProp.colliderShape == "COLSHAPE_JNTSPH" and obj.parent is not None: + for child in obj.parent.children: + updateColliderOnObj(child, False) + + +def addColliderThenParent( + shapeName: str, obj: bpy.types.Object, bone: bpy.types.Bone | None, notMeshCollider: bool = True +) -> bpy.types.Object: + colliderObj = addCollider(shapeName, notMeshCollider) + if bone is not None: + + # If no active bone is set, then parenting operator fails. + obj.data.bones.active = obj.data.bones[0] + obj.data.bones[0].select = True + + parentObject(obj, colliderObj, "BONE") + colliderObj.parent_bone = bone.name + colliderObj.matrix_world = obj.matrix_world @ obj.pose.bones[bone.name].matrix + else: + parentObject(obj, colliderObj) + # 10 = default value for ootBlenderScale + colliderObj.matrix_local = mathutils.Matrix.Diagonal(colliderObj.matrix_local.decompose()[2].to_4d()) + updateColliderOnObj(colliderObj) + return colliderObj + + +def addCollider(shapeName: str, notMeshCollider: bool) -> bpy.types.Object: + if bpy.context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + + # Mesh shape only matters for Triangle shape, otherwise will be controlled by geometry nodes. + location = mathutils.Vector(bpy.context.scene.cursor.location) + bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align="WORLD", location=location[:]) + planeObj = bpy.context.view_layer.objects.active + if notMeshCollider: + planeObj.data.clear_geometry() + else: + material = bpy.data.materials.new(f"Mesh Collider Material") + planeObj.data.materials.append(material) + planeObj.name = "Collider" + planeObj.ootGeometryType = "Actor Collider" + + if shapeName == "COLSHAPE_CYLINDER": + planeObj.lock_location = (True, True, False) + planeObj.lock_rotation = (True, True, True) + + actorCollider = planeObj.ootActorCollider + actorCollider.colliderShape = shapeName + actorCollider.physics.enable = True + return planeObj + + +def shapeNameToBlenderName(shapeName: str) -> str: + return shapeNameLookup( + shapeName, + { + "COLSHAPE_JNTSPH": "oot_collider_sphere", + "COLSHAPE_CYLINDER": "oot_collider_cylinder", + "COLSHAPE_TRIS": "oot_collider_triangles", + "COLSHAPE_QUAD": "oot_collider_quad", + }, + ) + + +def shapeNameToSimpleName(shapeName: str) -> str: + return shapeNameLookup( + shapeName, + { + "COLSHAPE_JNTSPH": "Sphere", + "COLSHAPE_CYLINDER": "Cylinder", + "COLSHAPE_TRIS": "Mesh", + "COLSHAPE_QUAD": "Quad", + }, + ) + + +def shapeNameLookup(shapeName: str, nameDict: dict[str, str]) -> str: + if shapeName in nameDict: + name = nameDict[shapeName] + return name + else: + raise PluginError(f"Could not find shape name {shapeName} in name dictionary.") diff --git a/fast64_internal/oot/collision/panels.py b/fast64_internal/oot/collision/panels.py index b10bc8c72..615eac273 100644 --- a/fast64_internal/oot/collision/panels.py +++ b/fast64_internal/oot/collision/panels.py @@ -5,6 +5,7 @@ from ..oot_utility import drawEnumWithCustom from .properties import OOTCollisionExportSettings, OOTCameraPositionProperty, OOTMaterialCollisionProperty from .operators import OOT_ExportCollision +from ..actor_collider import isActorCollider class OOT_CameraPosPanel(Panel): @@ -38,7 +39,7 @@ class OOT_CollisionPanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and context.material is not None + return context.scene.gameEditorMode == "OOT" and context.material is not None and not isActorCollider(context) def draw(self, context): box = self.layout.box().column() diff --git a/fast64_internal/oot/f3d/operators.py b/fast64_internal/oot/f3d/operators.py index b153c52fe..c2af2ac8b 100644 --- a/fast64_internal/oot/f3d/operators.py +++ b/fast64_internal/oot/f3d/operators.py @@ -9,12 +9,20 @@ from ...f3d.f3d_gbi import DLFormat, F3D, TextureExportSettings, ScrollMethod from ...f3d.f3d_writer import TriangleConverterInfo, removeDL, saveStaticModel, getInfoDict from ..oot_utility import ootGetObjectPath, getOOTScale -from ..oot_model_classes import OOTF3DContext, ootGetIncludedAssetData +from ..oot_model_classes import OOTF3DContext +from ..file_reading import ootGetIncludedAssetData from ..oot_texture_array import ootReadTextureArrays from ..oot_model_classes import OOTModel, OOTGfxFormatter from ..oot_f3d_writer import ootReadActorScale, writeTextureArraysNew, writeTextureArraysExisting from .properties import OOTDLImportSettings, OOTDLExportSettings +from ..actor_collider import ( + parseColliderData, + getColliderData, + removeExistingColliderData, + writeColliderData, +) + from ..oot_utility import ( OOTObjectCategorizer, ootDuplicateHierarchy, @@ -82,6 +90,10 @@ def ootConvertMeshToC( textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D) data.append(textureArrayData) + if settings.handleColliders.enable: + colliderData = getColliderData(originalObj) + data.append(colliderData) + filename = settings.filename if settings.isCustomFilename else name writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) @@ -93,6 +105,12 @@ def ootConvertMeshToC( sourcePath = os.path.join(path, folderName + ".c") removeDL(sourcePath, headerPath, name) + if settings.handleColliders.enable: + removeExistingColliderData( + bpy.context.scene.ootDecompPath, settings.actorOverlayName, False, colliderData.source + ) + writeColliderData(originalObj, bpy.context.scene.ootDecompPath, settings.actorOverlayName, False) + class OOT_ImportDL(Operator): # set bl_ properties @@ -145,6 +163,11 @@ def execute(self, context): ) obj.ootActorScale = scale / context.scene.ootBlenderScale + if settings.handleColliders.enable: + parseColliderData( + settings.name, basePath, settings.actorOverlayName, False, obj, settings.handleColliders + ) + self.report({"INFO"}, "Success!") return {"FINISHED"} diff --git a/fast64_internal/oot/f3d/panels.py b/fast64_internal/oot/f3d/panels.py index 70eff9046..fa4aede48 100644 --- a/fast64_internal/oot/f3d/panels.py +++ b/fast64_internal/oot/f3d/panels.py @@ -9,6 +9,7 @@ OOTDynamicMaterialProperty, OOTDefaultRenderModesProperty, ) +from ..actor_collider import isActorCollider class OOT_DisplayListPanel(Panel): @@ -22,7 +23,9 @@ class OOT_DisplayListPanel(Panel): @classmethod def poll(cls, context): return context.scene.gameEditorMode == "OOT" and ( - context.object is not None and isinstance(context.object.data, Mesh) + context.object is not None + and isinstance(context.object.data, Mesh) + and not context.object.ootGeometryType == "Actor Collider" ) def draw(self, context): @@ -53,7 +56,7 @@ class OOT_MaterialPanel(Panel): @classmethod def poll(cls, context): - return context.material is not None and context.scene.gameEditorMode == "OOT" + return context.material is not None and context.scene.gameEditorMode == "OOT" and not isActorCollider(context) def draw(self, context): layout = self.layout diff --git a/fast64_internal/oot/f3d/properties.py b/fast64_internal/oot/f3d/properties.py index 747674608..f094c6492 100644 --- a/fast64_internal/oot/f3d/properties.py +++ b/fast64_internal/oot/f3d/properties.py @@ -3,6 +3,12 @@ from bpy.utils import register_class, unregister_class from ...f3d.f3d_parser import ootEnumDrawLayers from ...utility import prop_split +from ..actor_collider import OOTActorColliderImportExportSettings + +ootEnumGeometryType = [ + ("Regular", "Regular", "Regular"), + ("Actor Collider", "Actor Collider", "Actor Collider"), +] class OOTDLExportSettings(PropertyGroup): @@ -20,6 +26,7 @@ class OOTDLExportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) customAssetIncludeDir: StringProperty( name="Asset Include Directory", default="assets/objects/gameplay_keep", @@ -42,6 +49,7 @@ def draw_props(self, layout: UILayout): box = layout.box().column() prop_split(box, self, "flipbookArrayIndex2D", "Flipbook Index") + self.handleColliders.draw(layout, "Export Actor Colliders", False) prop_split(layout, self, "drawLayer", "Export Draw Layer") layout.prop(self, "isCustom") layout.prop(self, "removeVanillaData") @@ -58,6 +66,7 @@ class OOTDLImportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) autoDetectActorScale: BoolProperty(name="Auto Detect Actor Scale", default=True) actorScale: FloatProperty(name="Actor Scale", min=0, default=100) @@ -75,6 +84,7 @@ def draw_props(self, layout: UILayout): if self.flipbookUses2DArray: box = layout.box().column() prop_split(box, self, "flipbookArrayIndex2D", "Flipbook Index") + self.handleColliders.draw(layout, "Import Actor Colliders", True) prop_split(layout, self, "drawLayer", "Import Draw Layer") layout.prop(self, "isCustom") @@ -198,6 +208,7 @@ def f3d_props_register(): World.ootDefaultRenderModes = PointerProperty(type=OOTDefaultRenderModesProperty) Material.ootMaterial = PointerProperty(type=OOTDynamicMaterialProperty) Object.ootObjectMenu = EnumProperty(items=ootEnumObjectMenu) + Object.ootGeometryType = EnumProperty(items=ootEnumGeometryType, name="Geometry Type") def f3d_props_unregister(): @@ -206,3 +217,4 @@ def f3d_props_unregister(): del Material.ootMaterial del Object.ootObjectMenu + del Object.ootGeometryType diff --git a/fast64_internal/oot/file_reading.py b/fast64_internal/oot/file_reading.py new file mode 100644 index 000000000..9e22e1009 --- /dev/null +++ b/fast64_internal/oot/file_reading.py @@ -0,0 +1,91 @@ +import os, re +from ..utility import getImportData + + +def getNonLinkActorFilepath(basePath: str, overlayName: str, checkDataPath: bool = False) -> str: + actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") + actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here + + if checkDataPath and os.path.exists(actorFileDataPath): + actorFilePath = actorFileDataPath + + return actorFilePath + + +def getLinkColliderFilepath(basePath: str) -> str: + return os.path.join(basePath, f"src/overlays/actors/ovl_player_actor/z_player.c") + + +def getLinkTextureFilepath(basePath: str) -> str: + return os.path.join(basePath, f"src/code/z_player_lib.c") + + # read included asset data + + +def ootGetIncludedAssetData(basePath: str, currentPaths: list[str], data: str) -> str: + includeData = "" + searchedPaths = currentPaths[:] + + print("Included paths:") + + # search assets + for includeMatch in re.finditer(r"\#include\s*\"(assets/objects/(.*?))\.h\"", data): + path = os.path.join(basePath, includeMatch.group(1) + ".c") + if path in searchedPaths: + continue + searchedPaths.append(path) + subIncludeData = getImportData([path]) + "\n" + includeData += subIncludeData + print(path) + + for subIncludeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", subIncludeData): + subPath = os.path.join(os.path.dirname(path), subIncludeMatch.group(1) + ".c") + if subPath in searchedPaths: + continue + searchedPaths.append(subPath) + print(subPath) + includeData += getImportData([subPath]) + "\n" + + # search same directory c includes, both in current path and in included object files + # these are usually fast64 exported files + for includeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", data): + sameDirPaths = [ + os.path.join(os.path.dirname(currentPath), includeMatch.group(1) + ".c") for currentPath in currentPaths + ] + sameDirPathsToSearch = [] + for sameDirPath in sameDirPaths: + if sameDirPath not in searchedPaths: + sameDirPathsToSearch.append(sameDirPath) + + for sameDirPath in sameDirPathsToSearch: + print(sameDirPath) + + includeData += getImportData(sameDirPathsToSearch) + "\n" + return includeData + + +def ootGetActorDataPaths(basePath: str, overlayName: str) -> list[str]: + actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") + actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here + + return [actorFileDataPath, actorFilePath] + + +# read actor data +def ootGetActorData(basePath: str, overlayName: str) -> str: + actorData = getImportData(ootGetActorDataPaths(basePath, overlayName)) + return actorData + + +def ootGetLinkTextureData(basePath: str) -> str: + linkFilePath = os.path.join(basePath, f"src/code/z_player_lib.c") + actorData = getImportData([linkFilePath]) + + return actorData + + +def ootGetLinkColliderData(basePath: str) -> str: + linkFilePath = os.path.join(basePath, f"src/overlays/actors/ovl_player_actor/z_player.c") + actorData = getImportData([linkFilePath]) + + return actorData diff --git a/fast64_internal/oot/oot_anim.py b/fast64_internal/oot/oot_anim.py index 49fc71b9c..72fd54e80 100644 --- a/fast64_internal/oot/oot_anim.py +++ b/fast64_internal/oot/oot_anim.py @@ -2,7 +2,7 @@ from ..utility import CData, PluginError, toAlnum, hexOrDecInt from ..f3d.f3d_parser import getImportData from .skeleton.exporter import ootConvertArmatureToSkeletonWithoutMesh -from .oot_model_classes import ootGetIncludedAssetData +from .file_reading import ootGetIncludedAssetData from ..utility_anim import ( ValueFrameData, diff --git a/fast64_internal/oot/oot_f3d_writer.py b/fast64_internal/oot/oot_f3d_writer.py index 8207f23c1..01427159f 100644 --- a/fast64_internal/oot/oot_f3d_writer.py +++ b/fast64_internal/oot/oot_f3d_writer.py @@ -12,12 +12,8 @@ saveMeshByFaces, ) -from .oot_model_classes import ( - OOTTriangleConverterInfo, - OOTModel, - ootGetActorData, - ootGetLinkData, -) +from .oot_model_classes import OOTTriangleConverterInfo, OOTModel +from .file_reading import ootGetActorData, getNonLinkActorFilepath, ootGetLinkTextureData # Creates a semi-transparent solid color material (cached) @@ -207,23 +203,13 @@ def writeTextureArraysNew(fModel: OOTModel, arrayIndex: int): return textureArrayData -def getActorFilepath(basePath: str, overlayName: str | None, isLink: bool, checkDataPath: bool = False): - if isLink: - actorFilePath = os.path.join(basePath, f"src/code/z_player_lib.c") - else: - actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") - actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here - - if checkDataPath and os.path.exists(actorFileDataPath): - actorFilePath = actorFileDataPath - - return actorFilePath - - def writeTextureArraysExisting( exportPath: str, overlayName: str, isLink: bool, flipbookArrayIndex2D: int, fModel: OOTModel ): - actorFilePath = getActorFilepath(exportPath, overlayName, isLink, True) + if not isLink: + actorFilePath = getNonLinkActorFilepath(exportPath, overlayName, True) + else: + actorFilePath = os.path.join(exportPath, f"src/code/z_player_lib.c") if not os.path.exists(actorFilePath): print(f"{actorFilePath} not found, ignoring texture array writing.") @@ -331,7 +317,7 @@ def ootReadActorScale(basePath: str, overlayName: str, isLink: bool) -> float: if not isLink: actorData = ootGetActorData(basePath, overlayName) else: - actorData = ootGetLinkData(basePath) + actorData = ootGetLinkTextureData(basePath) chainInitMatch = re.search(r"CHAIN_VEC3F_DIV1000\s*\(\s*scale\s*,\s*(.*?)\s*,", actorData, re.DOTALL) if chainInitMatch is not None: diff --git a/fast64_internal/oot/oot_model_classes.py b/fast64_internal/oot/oot_model_classes.py index ed6816432..52dbdf300 100644 --- a/fast64_internal/oot/oot_model_classes.py +++ b/fast64_internal/oot/oot_model_classes.py @@ -35,69 +35,6 @@ ) -# read included asset data -def ootGetIncludedAssetData(basePath: str, currentPaths: list[str], data: str) -> str: - includeData = "" - searchedPaths = currentPaths[:] - - print("Included paths:") - - # search assets - for includeMatch in re.finditer(r"\#include\s*\"(assets/objects/(.*?))\.h\"", data): - path = os.path.join(basePath, includeMatch.group(1) + ".c") - if path in searchedPaths: - continue - searchedPaths.append(path) - subIncludeData = getImportData([path]) + "\n" - includeData += subIncludeData - print(path) - - for subIncludeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", subIncludeData): - subPath = os.path.join(os.path.dirname(path), subIncludeMatch.group(1) + ".c") - if subPath in searchedPaths: - continue - searchedPaths.append(subPath) - print(subPath) - includeData += getImportData([subPath]) + "\n" - - # search same directory c includes, both in current path and in included object files - # these are usually fast64 exported files - for includeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", data): - sameDirPaths = [ - os.path.join(os.path.dirname(currentPath), includeMatch.group(1) + ".c") for currentPath in currentPaths - ] - sameDirPathsToSearch = [] - for sameDirPath in sameDirPaths: - if sameDirPath not in searchedPaths: - sameDirPathsToSearch.append(sameDirPath) - - for sameDirPath in sameDirPathsToSearch: - print(sameDirPath) - - includeData += getImportData(sameDirPathsToSearch) + "\n" - return includeData - - -def ootGetActorDataPaths(basePath: str, overlayName: str) -> list[str]: - actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") - actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here - - return [actorFileDataPath, actorFilePath] - - -# read actor data -def ootGetActorData(basePath: str, overlayName: str) -> str: - actorData = getImportData(ootGetActorDataPaths(basePath, overlayName)) - return actorData - - -def ootGetLinkData(basePath: str) -> str: - linkFilePath = os.path.join(basePath, f"src/code/z_player_lib.c") - actorData = getImportData([linkFilePath]) - - return actorData - - class OOTModel(FModel): def __init__(self, f3dType, isHWv1, name, DLFormat, drawLayerOverride): self.drawLayerOverride = drawLayerOverride diff --git a/fast64_internal/oot/oot_texture_array.py b/fast64_internal/oot/oot_texture_array.py index 061d98d38..b2c0e6268 100644 --- a/fast64_internal/oot/oot_texture_array.py +++ b/fast64_internal/oot/oot_texture_array.py @@ -5,10 +5,13 @@ from .oot_model_classes import ( OOTF3DContext, TextureFlipbook, +) + +from .file_reading import ( ootGetActorData, ootGetActorDataPaths, ootGetIncludedAssetData, - ootGetLinkData, + ootGetLinkTextureData, ) # Special cases: @@ -26,7 +29,7 @@ def ootReadTextureArrays( actorData = ootGetActorData(basePath, overlayName) currentPaths = ootGetActorDataPaths(basePath, overlayName) else: - actorData = ootGetLinkData(basePath) + actorData = ootGetLinkTextureData(basePath) currentPaths = [os.path.join(basePath, f"src/code/z_player_lib.c")] actorData = ootGetIncludedAssetData(basePath, currentPaths, actorData) + actorData diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/oot/oot_utility.py index 1d7ca39d4..f4f8658c0 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/oot/oot_utility.py @@ -420,6 +420,18 @@ def getNextBone(boneStack: list[str], armatureObj: bpy.types.Object): return bone, boneStack +def getOrderedBoneList(armatureObj: bpy.types.Object): + startBoneName = getStartBone(armatureObj) + boneList = [] + boneStack = [startBoneName] + + while len(boneStack) > 0: + bone, boneStack = getNextBone(boneStack, armatureObj) + boneList.append(bone) + + return boneList + + def checkForStartBone(armatureObj): pass # if "root" not in armatureObj.data.bones: diff --git a/fast64_internal/oot/skeleton/exporter/functions.py b/fast64_internal/oot/skeleton/exporter/functions.py index 17b026849..b34f533cb 100644 --- a/fast64_internal/oot/skeleton/exporter/functions.py +++ b/fast64_internal/oot/skeleton/exporter/functions.py @@ -7,6 +7,11 @@ from ..properties import OOTSkeletonExportSettings from ..utility import ootDuplicateArmatureAndRemoveRotations, getGroupIndices, ootRemoveSkeleton from .classes import OOTLimb, OOTSkeleton +from ...actor_collider import ( + getColliderData, + removeExistingColliderData, + writeColliderData, +) from ....utility import ( PluginError, @@ -296,6 +301,10 @@ def ootConvertArmatureToC( textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D) data.append(textureArrayData) + if settings.handleColliders.enable: + colliderData = getColliderData(originalArmatureObj) + data.append(colliderData) + writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) if not isCustomExport: @@ -303,3 +312,12 @@ def ootConvertArmatureToC( addIncludeFiles(folderName, path, filename) if removeVanillaData: ootRemoveSkeleton(path, folderName, skeletonName) + + if settings.handleColliders.enable: + colliderData = getColliderData(originalArmatureObj) + removeExistingColliderData( + bpy.context.scene.ootDecompPath, settings.actorOverlayName, isLink, colliderData.source + ) + writeColliderData( + originalArmatureObj, bpy.context.scene.ootDecompPath, settings.actorOverlayName, isLink + ) diff --git a/fast64_internal/oot/skeleton/importer/functions.py b/fast64_internal/oot/skeleton/importer/functions.py index bdf548442..e0eeb4e52 100644 --- a/fast64_internal/oot/skeleton/importer/functions.py +++ b/fast64_internal/oot/skeleton/importer/functions.py @@ -3,12 +3,14 @@ from ....f3d.f3d_parser import getImportData, parseF3D from ....utility import hexOrDecInt, applyRotation from ...oot_f3d_writer import ootReadActorScale -from ...oot_model_classes import OOTF3DContext, ootGetIncludedAssetData +from ...oot_model_classes import OOTF3DContext +from ...file_reading import ootGetIncludedAssetData from ...oot_utility import ootGetObjectPath, getOOTScale from ...oot_texture_array import ootReadTextureArrays from ..constants import ootSkeletonImportDict from ..properties import OOTSkeletonImportSettings from ..utility import ootGetLimb, ootGetLimbs, ootGetSkeleton, applySkeletonRestPose +from ...actor_collider import parseColliderData class OOTDLEntry: @@ -276,6 +278,16 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) f3dContext.deleteMaterialContext() + if importSettings.handleColliders.enable: + parseColliderData( + importSettings.name, + basePath, + overlayName, + isLink, + armatureObj, + importSettings.handleColliders, + ) + if importSettings.applyRestPose and restPoseData is not None: applySkeletonRestPose(restPoseData, armatureObj) if isLOD: diff --git a/fast64_internal/oot/skeleton/properties.py b/fast64_internal/oot/skeleton/properties.py index f0d81ac6e..5ea118cc4 100644 --- a/fast64_internal/oot/skeleton/properties.py +++ b/fast64_internal/oot/skeleton/properties.py @@ -4,6 +4,7 @@ from ...f3d.f3d_material import ootEnumDrawLayers from ...utility import prop_split from .constants import ootEnumSkeletonImportMode +from ..actor_collider import OOTActorColliderImportExportSettings ootEnumBoneType = [ @@ -64,6 +65,7 @@ class OOTSkeletonExportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="ovl_En_GeldB") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) customAssetIncludeDir: StringProperty( name="Asset Include Directory", default="assets/objects/object_geldb", @@ -83,6 +85,7 @@ def draw_props(self, layout: UILayout): b = layout.box().column() b.label(icon="LIBRARY_DATA_BROKEN", text="Do not draw anything in SkelAnime") b.label(text="callbacks or cull limbs, will be corrupted.") + self.handleColliders.draw(layout, "Export Actor Colliders", False) layout.prop(self, "isCustom") layout.label(text="Object name used for export.", icon="INFO") layout.prop(self, "isCustomFilename") @@ -119,6 +122,7 @@ class OOTSkeletonImportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="ovl_En_GeldB") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) autoDetectActorScale: BoolProperty(name="Auto Detect Actor Scale", default=True) actorScale: FloatProperty(name="Actor Scale", min=0, default=100) @@ -155,6 +159,7 @@ def draw_props(self, layout: UILayout): ) else: layout.prop(self, "applyRestPose") + self.handleColliders.draw(layout, "Import Actor Colliders", True) oot_skeleton_classes = ( diff --git a/fast64_internal/oot/tools/panel.py b/fast64_internal/oot/tools/panel.py index 1e5151701..f693b65ec 100644 --- a/fast64_internal/oot/tools/panel.py +++ b/fast64_internal/oot/tools/panel.py @@ -9,6 +9,9 @@ OOT_AddPath, ) +from ..actor_collider import OOT_AddActorCollider, OOT_CopyColliderProperties, drawColliderVisibilityOperators +from ...utility_anim import ArmatureApplyWithMeshOperator + class OoT_ToolsPanel(OOT_Panel): bl_idname = "OOT_PT_tools" @@ -23,6 +26,29 @@ def draw(self, context): col.operator(OOT_AddCutscene.bl_idname) col.operator(OOT_AddPath.bl_idname) + col.label(text="") + col.label(text="Armatures") + col.operator(ArmatureApplyWithMeshOperator.bl_idname) + + col.label(text="") + col.label(text="Actor Colliders") + col.label(text="Do not scale armatures with joint sphere colliders.", icon="ERROR") + col.label(text="Applying scale will mess up joint sphere translations.") + addOp = col.operator(OOT_AddActorCollider.bl_idname, text="Add Joint Sphere Collider (Bones)") + addOp.shape = "COLSHAPE_JNTSPH" + addOp.parentToBone = True + + col.operator( + OOT_AddActorCollider.bl_idname, text="Add Joint Sphere Collider (Object)" + ).shape = "COLSHAPE_JNTSPH" + col.operator(OOT_AddActorCollider.bl_idname, text="Add Cylinder Collider").shape = "COLSHAPE_CYLINDER" + col.operator(OOT_AddActorCollider.bl_idname, text="Add Mesh Collider").shape = "COLSHAPE_TRIS" + col.operator(OOT_AddActorCollider.bl_idname, text="Add Quad Collider (Properties Only)").shape = "COLSHAPE_QUAD" + + drawColliderVisibilityOperators(col) + + col.operator(OOT_CopyColliderProperties.bl_idname, text="Copy Collider Properties (From Active To Selected)") + oot_operator_panel_classes = [ OoT_ToolsPanel, diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 84f2abecc..3120d54e2 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -50,6 +50,15 @@ class VertexWeightError(PluginError): ] +def getImportData(filepaths: list[str]) -> str: + data = "" + for path in filepaths: + if os.path.exists(path): + data += readFile(path) + + return data + + def isPowerOf2(n): return (n & (n - 1) == 0) and n != 0 @@ -124,13 +133,13 @@ def selectSingleObject(obj: bpy.types.Object): bpy.context.view_layer.objects.active = obj -def parentObject(parent, child): +def parentObject(parent, child, type="OBJECT"): bpy.ops.object.select_all(action="DESELECT") child.select_set(True) parent.select_set(True) bpy.context.view_layer.objects.active = parent - bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + bpy.ops.object.parent_set(type=type, keep_transform=True) def getFMeshName(vertexGroup, namePrefix, drawLayer, isSkinned): @@ -816,6 +825,7 @@ def get_obj_temp_mesh(obj): if o.get("temp_export") and o.get("instanced_mesh_name") == obj.get("instanced_mesh_name"): return o + def apply_objects_modifiers_and_transformations(allObjs: Iterable[bpy.types.Object]): # first apply modifiers so that any objects that affect each other are taken into consideration for selectedObj in allObjs: @@ -834,6 +844,7 @@ def apply_objects_modifiers_and_transformations(allObjs: Iterable[bpy.types.Obje bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) + def duplicateHierarchy(obj, ignoreAttr, includeEmpties, areaIndex): # Duplicate objects to apply scale / modifiers / linked data bpy.ops.object.select_all(action="DESELECT")