diff --git a/__init__.py b/__init__.py index 5a084814d..7fbe27f3e 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,17 @@ import bpy from bpy.utils import register_class, unregister_class +from bpy.path import abspath + from . import addon_updater_ops -from .fast64_internal.utility import prop_split, multilineLabel + +from .fast64_internal.utility import prop_split, multilineLabel, draw_and_check_tab + +from .fast64_internal.repo_settings import ( + draw_repo_settings, + load_repo_settings, + repo_settings_operators_register, + repo_settings_operators_unregister, +) from .fast64_internal.sm64 import sm64_register, sm64_unregister from .fast64_internal.sm64.settings.properties import SM64_Properties @@ -105,13 +115,14 @@ def draw(self, context): prop_split(col, scene, "gameEditorMode", "Game") col.prop(scene, "exportHiddenGeometry") col.prop(scene, "fullTraceback") - prop_split(col, fast64_settings, "anim_range_choice", "Anim Range") - col.separator() + prop_split(col, fast64_settings, "anim_range_choice", "Anim Range") - col.prop(fast64_settings, "auto_pick_texture_format") - if fast64_settings.auto_pick_texture_format: - col.prop(fast64_settings, "prefer_rgba_over_ci") + draw_repo_settings(col.box(), context) + if not fast64_settings.repo_settings_tab: + col.prop(fast64_settings, "auto_pick_texture_format") + if fast64_settings.auto_pick_texture_format: + col.prop(fast64_settings, "prefer_rgba_over_ci") class Fast64_GlobalToolsPanel(bpy.types.Panel): @@ -134,6 +145,10 @@ def draw(self, context): addon_updater_ops.update_notice_box_ui(self, context) +def repo_path_update(self, context): + load_repo_settings(context.scene, abspath(self.repo_settings_path), True) + + class Fast64Settings_Properties(bpy.types.PropertyGroup): """Settings affecting exports for all games found in scene.fast64.settings""" @@ -176,6 +191,14 @@ class Fast64Settings_Properties(bpy.types.PropertyGroup): description="When enabled, fast64 will default colored textures's format to RGBA even if they fit CI requirements, with the exception of textures that would not fit into TMEM otherwise", ) + repo_settings_tab: bpy.props.BoolProperty(default=True, name="Repo Settings") + repo_settings_path: bpy.props.StringProperty(name="Path", subtype="FILE_PATH", update=repo_path_update) + auto_repo_load_settings: bpy.props.BoolProperty( + name="Auto Load Repo's Settings", + description="When enabled, this will make fast64 automatically load repo settings if they are found after picking a decomp path", + default=True, + ) + class Fast64_Properties(bpy.types.PropertyGroup): """ @@ -363,6 +386,8 @@ def register(): sm64_register(True) oot_register(True) + repo_settings_operators_register() + for cls in classes: register_class(cls) @@ -424,6 +449,8 @@ def unregister(): del bpy.types.Bone.fast64 del bpy.types.Object.fast64 + repo_settings_operators_unregister() + for cls in classes: unregister_class(cls) diff --git a/fast64_internal/f3d/f3d_gbi.py b/fast64_internal/f3d/f3d_gbi.py index 8c0c85771..3d21f95cb 100644 --- a/fast64_internal/f3d/f3d_gbi.py +++ b/fast64_internal/f3d/f3d_gbi.py @@ -2328,6 +2328,7 @@ def __init__( self.LODGroups: dict[str, FLODGroup] = {} self.DLFormat: "DLFormat" = DLFormat self.matWriteMethod: GfxMatWriteMethod = matWriteMethod + self.no_light_direction = False self.global_data: FGlobalData = FGlobalData() self.texturesSavedLastExport: int = 0 # hacky @@ -4010,16 +4011,19 @@ def to_binary(self, f3d, segments): class SPLightColor(GbiMacro): # n is macro name (string) n: str - col: int + col: Sequence[int] + + def color_to_int(self): + return self.col[0] * 0x1000000 + self.col[1] * 0x10000 + self.col[2] * 0x100 + 0xFF def to_binary(self, f3d, segments): - return gsMoveWd(f3d.G_MW_LIGHTCOL, f3d.getLightMWO_a(self.n), self.col, f3d) + gsMoveWd( + return gsMoveWd(f3d.G_MW_LIGHTCOL, f3d.getLightMWO_a(self.n), self.color_to_int(), f3d) + gsMoveWd( f3d.G_MW_LIGHTCOL, f3d.getLightMWO_b(self.n), self.col, f3d ) def to_c(self, static=True): header = "gsSPLightColor(" if static else "gSPLightColor(glistp++, " - return header + str(self.n) + ", 0x" + format(self.col, "08X") + ")" + return header + f"{self.n}, 0x" + format(self.color_to_int(), "08X") + ")" @dataclass(unsafe_hash=True) diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index 2563aaa85..660e032f5 100644 --- a/fast64_internal/f3d/f3d_material.py +++ b/fast64_internal/f3d/f3d_material.py @@ -1537,9 +1537,11 @@ def update_fog_nodes(material: Material, context: Context): # rendermodes in code, so to be safe we'll enable fog. Plus we are checking # that fog is enabled in the geometry mode, so if so that's probably the intent. fogBlender.node_tree = bpy.data.node_groups[ - "FogBlender_On" - if shade_alpha_is_fog and is_blender_doing_fog(material.f3d_mat.rdp_settings, True) - else "FogBlender_Off" + ( + "FogBlender_On" + if shade_alpha_is_fog and is_blender_doing_fog(material.f3d_mat.rdp_settings, True) + else "FogBlender_Off" + ) ] if shade_alpha_is_fog: @@ -1571,7 +1573,7 @@ def update_noise_nodes(material: Material): nodes["F3DNoiseFactor"].node_tree = noise_group -def update_combiner_connections(material: Material, context: Context, combiner: (int | None) = None): +def update_combiner_connections(material: Material, context: Context, combiner: int | None = None): f3dMat: "F3DMaterialProperty" = material.f3d_mat update_noise_nodes(material) @@ -2203,10 +2205,6 @@ def load_handler(dummy): bpy.app.handlers.load_post.append(load_handler) -# bpy.context.mode returns the keys here, while the values are required by bpy.ops.object.mode_set -BLENDER_MODE_TO_MODE_SET = {"PAINT_VERTEX": "VERTEX_PAINT", "EDIT_MESH": "EDIT"} -get_mode_set_from_context_mode = lambda mode: BLENDER_MODE_TO_MODE_SET.get(mode, "OBJECT") - SCENE_PROPERTIES_VERSION = 1 @@ -2396,6 +2394,20 @@ def addColorAttributesToModel(obj: Object): bpy.ops.object.mode_set(mode=get_mode_set_from_context_mode(prevMode)) +def add_f3d_mat_to_obj(obj: bpy.types.Object, material, index=None): + # add material to object + if obj is not None: + addColorAttributesToModel(obj) + if index is None: + obj.data.materials.append(material) + if bpy.context.object is not None: + bpy.context.object.active_material_index = len(obj.material_slots) - 1 + else: + obj.material_slots[index].material = material + if bpy.context.object is not None: + bpy.context.object.active_material_index = index + + def createF3DMat(obj: Object | None, preset="Shaded Solid", index=None): # link all node_groups + material from addon's data .blend link_f3d_material_library() @@ -2410,17 +2422,7 @@ def createF3DMat(obj: Object | None, preset="Shaded Solid", index=None): createScenePropertiesForMaterial(material) - # add material to object - if obj is not None: - addColorAttributesToModel(obj) - if index is None: - obj.data.materials.append(material) - if bpy.context.object is not None: - bpy.context.object.active_material_index = len(obj.material_slots) - 1 - else: - obj.material_slots[index].material = material - if bpy.context.object is not None: - bpy.context.object.active_material_index = index + add_f3d_mat_to_obj(obj, material, index) material.is_f3d = True material.mat_ver = 5 @@ -2942,8 +2944,14 @@ class PrimDepthSettings(PropertyGroup): ), ) + def to_dict(self): + return prop_group_to_json(self) + + def from_dict(self, data: dict): + json_to_prop_group(self, data) + def key(self): - return (self.z, self.dz) + return frozenset(self.to_dict().items()) class RDPSettings(PropertyGroup): @@ -3314,65 +3322,184 @@ def blend_inputs(self): def does_blender_use_input(self, setting: str) -> bool: return any(input == setting for input in self.blend_inputs) - def key(self): - setRM = self.set_rendermode - rmAdv = self.rendermode_advanced_enabled - prim = self.g_mdsft_zsrcsel == "G_ZS_PRIM" - return ( - self.g_zbuffer, - self.g_shade, - self.g_cull_front, - self.g_cull_back, - self.g_attroffset_st_enable, - self.g_attroffset_z_enable, - self.g_packed_normals, - self.g_lighttoalpha, - self.g_ambocclusion, - self.g_fog, - self.g_lighting, - self.g_tex_gen, - self.g_tex_gen_linear, - self.g_lod, - self.g_shade_smooth, - self.g_clipping, - self.g_mdsft_alpha_dither, - self.g_mdsft_rgb_dither, - self.g_mdsft_combkey, - self.g_mdsft_textconv, - self.g_mdsft_text_filt, - self.g_mdsft_textlod, - self.g_mdsft_textdetail, - self.g_mdsft_textpersp, - self.g_mdsft_cycletype, - self.g_mdsft_color_dither, - self.g_mdsft_pipeline, - self.g_mdsft_alpha_compare, - self.g_mdsft_zsrcsel, - self.prim_depth.key() if prim else None, - self.clip_ratio, - self.set_rendermode, - self.aa_en if setRM and rmAdv else None, - self.z_cmp if setRM and rmAdv else None, - self.z_upd if setRM and rmAdv else None, - self.im_rd if setRM and rmAdv else None, - self.clr_on_cvg if setRM and rmAdv else None, - self.cvg_dst if setRM and rmAdv else None, - self.zmode if setRM and rmAdv else None, - self.cvg_x_alpha if setRM and rmAdv else None, - self.alpha_cvg_sel if setRM and rmAdv else None, - self.force_bl if setRM and rmAdv else None, - self.blend_p1 if setRM and rmAdv else None, - self.blend_p2 if setRM and rmAdv else None, - self.blend_m1 if setRM and rmAdv else None, - self.blend_m2 if setRM and rmAdv else None, - self.blend_a1 if setRM and rmAdv else None, - self.blend_a2 if setRM and rmAdv else None, - self.blend_b1 if setRM and rmAdv else None, - self.blend_b2 if setRM and rmAdv else None, - self.rendermode_preset_cycle_1 if setRM and not rmAdv else None, - self.rendermode_preset_cycle_2 if setRM and not rmAdv else None, + def attributes_to_dict(self, info: dict): + data = {} + for key, attr, default in info: + value = getattr(self, attr) + if value != default: + data[key] = value + return data + + def attributes_from_dict(self, data: dict, info: dict): + for key, attr, default in info: + setattr(self, attr, data.get(key, default)) + + geo_mode_all_attributes = [ + ("zBuffer", "g_zbuffer", False), + ("shade", "g_shade", False), + ("cullFront", "g_cull_front", False), + ("cullBack", "g_cull_back", False), + ("fog", "g_fog", False), + ("lighting", "g_lighting", False), + ("texGen", "g_tex_gen", False), + ("texGenLinear", "g_tex_gen_linear", False), + ("lod", "g_lod", False), + ("shadeSmooth", "g_shade_smooth", False), + ] + + geo_mode_f3dex_attributes = [ + ("clipping", "g_clipping", False), + ] + + geo_mode_f3dex3_attributes = [ + ("ambientOcclusion", "g_ambocclusion", False), + ("attroffsetZ", "g_attroffset_z_enable", False), + ("attroffsetST", "g_attroffset_st_enable", False), + ("packedNormals", "g_packed_normals", False), + ("lightToAlpha", "g_lighttoalpha", False), + ("specularLighting", "g_lighting_specular", False), + ("fresnelToColor", "g_fresnel_color", False), + ("fresnelToAlpha", "g_fresnel_alpha", False), + ] + geo_mode_attributes = geo_mode_all_attributes + geo_mode_f3dex_attributes + geo_mode_f3dex3_attributes + + def geo_mode_to_dict(self, f3d=None): + f3d = f3d if f3d else get_F3D_GBI() + data = self.attributes_to_dict(self.geo_mode_all_attributes) + if f3d.F3DEX_GBI or f3d.F3DLP_GBI: + data.update(self.attributes_to_dict(self.geo_mode_f3dex_attributes)) + if f3d.F3DEX_GBI_3: + data.update(self.attributes_to_dict(self.geo_mode_f3dex3_attributes)) + return data + + def geo_mode_from_dict(self, data: dict): + self.attributes_from_dict(data, self.geo_mode_attributes) + + other_mode_h_attributes = [ + ("alphaDither", "g_mdsft_alpha_dither", "G_AD_PATTERN"), + ("colorDither", "g_mdsft_rgb_dither", "G_CD_MAGICSQ"), + ("chromaKey", "g_mdsft_combkey", "G_CK_NONE"), + ("textureConvert", "g_mdsft_textconv", "G_TC_CONV"), + ("textureFilter", "g_mdsft_text_filt", "G_TF_POINT"), + # ("lutFormat", "g_mdsft_textlut", "G_TT_NONE") + ("textureLoD", "g_mdsft_textlod", "G_TL_TILE"), + ("textureDetail", "g_mdsft_textdetail", "G_TD_CLAMP"), + ("perspectiveCorrection", "g_mdsft_textpersp", "G_TP_NONE"), + ("cycleType", "g_mdsft_cycletype", "G_CYC_1CYCLE"), + ("pipelineMode", "g_mdsft_pipeline", "G_PM_NPRIMITIVE"), + ] + + def other_mode_h_to_dict(self): + return self.attributes_to_dict(self.other_mode_h_attributes) + + def other_mode_h_from_dict(self, data: dict): + self.attributes_from_dict(data, self.other_mode_h_attributes) + + other_mode_l_attributes = [ + ("alphaCompare", "g_mdsft_alpha_compare", "G_AC_NONE"), + ("zSourceSelection", "g_mdsft_zsrcsel", "G_ZS_PIXEL"), + ] + + rendermode_flag_attributes = [ + ("aa", "aa_en", False), + ("zTest", "z_cmp", False), + ("zWrite", "z_upd", False), + ("colorOnCvg", "clr_on_cvg", False), + ("alphaOnCvg", "alpha_cvg_sel", False), + ("mulCvgXAlpha", "cvg_x_alpha", False), + ("forceBlend", "force_bl", False), + ("readFB", "im_rd", False), + ("cvgDst", "cvg_dst", "CVG_DST_CLAMP"), + ("zMode", "zmode", "ZMODE_OPA"), + ] + + def other_mode_l_to_dict(self): + data = self.attributes_to_dict(self.other_mode_l_attributes) + if self.g_mdsft_zsrcsel == "G_ZS_PRIM": + data["primDepth"] = self.prim_depth.to_dict() + if self.set_rendermode: + two_cycle = self.g_mdsft_cycletype == "G_CYC_2CYCLE" + if self.rendermode_advanced_enabled: + blender_data = [] + for i in range(2 if two_cycle else 1): + num = i + 1 + color_attrs, alpha_attrs = (f"blend_p{num}", f"blend_m{num}"), (f"blend_a{num}", f"blend_b{num}") + blender_data.append( + { + "color": (getattr(self, color_attrs[0]), getattr(self, color_attrs[1])), + "alpha": (getattr(self, alpha_attrs[0]), getattr(self, alpha_attrs[1])), + } + ) + data["renderMode"] = { + "flags": self.attributes_to_dict(self.rendermode_flag_attributes), + "blender": blender_data, + } + else: + data["renderMode"] = { + "presets": [self.rendermode_preset_cycle_1] + + ([self.rendermode_preset_cycle_2] if two_cycle else []) + } + + return data + + def other_mode_l_from_dict(self, data: dict): + self.attributes_from_dict(data, self.other_mode_l_attributes) + self.prim_depth.from_dict(data.get("primDepth", {})) + + render_mode = data.get("renderMode", {}) + blender = render_mode.get("blender", []) + flags = render_mode.get("flags", {}) + if render_mode: + self.set_rendermode = True + if not render_mode.get("presets", None) and (flags or blender): + self.rendermode_advanced_enabled = True + + self.rendermode_preset_cycle_1, self.rendermode_preset_cycle_2 = render_mode.get( + "presets", [self.rendermode_preset_cycle_1, self.rendermode_preset_cycle_2] ) + self.attributes_from_dict(flags, self.rendermode_flag_attributes) + color_attrs = ("blend_p", "blend_m") + alpha_attrs = ("blend_a", "blend_b") + for i, cycle in enumerate(blender * 2 if len(blender) == 1 else blender): + num = str(i + 1) + color_attrs, alpha_attrs = (f"blend_p{num}", f"blend_m{num}"), (f"blend_a{num}", f"blend_b{num}") + self[color_attrs[0]], self[color_attrs[1]] = cycle.get( + "color", [self.get(color_attrs[0]), self.get(color_attrs[1])] + ) + self[alpha_attrs[0]], self[alpha_attrs[1]] = cycle.get( + "alpha", [self.get(alpha_attrs[0]), self.get(alpha_attrs[1])] + ) + + def other_to_dict(self): + data = {} + if self.clip_ratio != 1.0: + data["clipRatio"] = self.clip_ratio + if self.g_mdsft_textlod == "G_TL_LOD" and self.num_textures_mipmapped != 1: + data["mipmapCount"] = self.num_textures_mipmapped + return data + + def other_from_dict(self, data: dict): + self.clip_ratio = data.get("clipRatio", self.clip_ratio) + self.num_textures_mipmapped = data.get("mipmapCount", self.num_textures_mipmapped) + + def to_dict(self, f3d=None): + data = {} + data["geometryMode"] = self.geo_mode_to_dict(f3d) + data["otherModeH"] = self.other_mode_h_to_dict() + data["otherModeL"] = self.other_mode_l_to_dict() + data["other"] = self.other_to_dict() + return data + + def from_dict(self, data: dict): + self.geo_mode_from_dict(data.get("geometryMode", {})) + self.other_mode_h_from_dict(data.get("otherModeH", {})) + self.other_mode_l_from_dict(data.get("otherModeL", {})) + self.other_from_dict(data.get("other", {})) + + def key(self): + return str(self.to_dict().items()) + class DefaultRDPSettingsPanel(Panel): bl_label = "RDP Default Settings" @@ -3505,6 +3632,10 @@ def execute(self, context: Context): return {"FINISHED"} +def getCurrentPresetDir(): + return "f3d/" + bpy.context.scene.gameEditorMode.lower() + + # modules/bpy_types.py -> Menu class MATERIAL_MT_f3d_presets(Menu): bl_label = "F3D Material Presets" @@ -3525,6 +3656,7 @@ def draw(self, _context): props_default = getattr(self, "preset_operator_defaults", None) add_operator = getattr(self, "preset_add_operator", None) presetDir = getCurrentPresetDir() + paths = bpy.utils.preset_paths("f3d/user") if not bpy.context.scene.f3dUserPresetsOnly: paths += bpy.utils.preset_paths(presetDir) @@ -4201,23 +4333,25 @@ def key(self) -> F3DMaterialHash: self.use_large_textures, self.use_cel_shading, self.cel_shading.tintPipeline if self.use_cel_shading else None, - tuple( - [ - ( - c.threshMode, - c.threshold, - c.tintType, - c.tintFixedLevel, - c.tintFixedColor, - c.tintSegmentNum, - c.tintSegmentOffset, - c.tintLightSlot, - ) - for c in self.cel_shading.levels - ] - ) - if self.use_cel_shading - else None, + ( + tuple( + [ + ( + c.threshMode, + c.threshold, + c.tintType, + c.tintFixedLevel, + c.tintFixedColor, + c.tintSegmentNum, + c.tintSegmentOffset, + c.tintLightSlot, + ) + for c in self.cel_shading.levels + ] + ) + if self.use_cel_shading + else None + ), self.use_default_lighting, self.set_blend, self.set_prim, @@ -4243,9 +4377,11 @@ def key(self) -> F3DMaterialHash: round(self.k5, 4) if self.set_k0_5 else None, self.combiner1.key() if self.set_combiner else None, self.combiner2.key() if self.set_combiner else None, - tuple([round(value, 4) for value in (self.ao_ambient, self.ao_directional, self.ao_point)]) - if self.set_ao - else None, + ( + tuple([round(value, 4) for value in (self.ao_ambient, self.ao_directional, self.ao_point)]) + if self.set_ao + else None + ), tuple([round(value, 4) for value in (self.fresnel_lo, self.fresnel_hi)]) if self.set_fresnel else None, tuple([round(value, 4) for value in self.attroffs_st]) if self.set_attroffs_st else None, self.attroffs_z if self.set_attroffs_z else None, @@ -4253,9 +4389,11 @@ def key(self) -> F3DMaterialHash: tuple([round(value, 4) for value in self.fog_position]) if self.set_fog else None, tuple([round(value, 4) for value in self.default_light_color]) if useDefaultLighting else None, self.set_ambient_from_light if useDefaultLighting else None, - tuple([round(value, 4) for value in self.ambient_light_color]) - if useDefaultLighting and not self.set_ambient_from_light - else None, + ( + tuple([round(value, 4) for value in self.ambient_light_color]) + if useDefaultLighting and not self.set_ambient_from_light + else None + ), self.f3d_light1 if not useDefaultLighting else None, self.f3d_light2 if not useDefaultLighting else None, self.f3d_light3 if not useDefaultLighting else None, diff --git a/fast64_internal/f3d/f3d_parser.py b/fast64_internal/f3d/f3d_parser.py index 39039e806..fd74f52f0 100644 --- a/fast64_internal/f3d/f3d_parser.py +++ b/fast64_internal/f3d/f3d_parser.py @@ -189,9 +189,9 @@ def getPosition(vertexBuffer, index): yBytes = vertexBuffer[yStart : yStart + 2] zBytes = vertexBuffer[zStart : zStart + 2] - x = int.from_bytes(xBytes, "big", signed=True) / bpy.context.scene.blenderToSM64Scale - y = int.from_bytes(yBytes, "big", signed=True) / bpy.context.scene.blenderToSM64Scale - z = int.from_bytes(zBytes, "big", signed=True) / bpy.context.scene.blenderToSM64Scale + x = int.from_bytes(xBytes, "big", signed=True) / bpy.context.scene.fast64.sm64.blender_to_sm64_scale + y = int.from_bytes(yBytes, "big", signed=True) / bpy.context.scene.fast64.sm64.blender_to_sm64_scale + z = int.from_bytes(zBytes, "big", signed=True) / bpy.context.scene.fast64.sm64.blender_to_sm64_scale return (x, y, z) diff --git a/fast64_internal/f3d/f3d_writer.py b/fast64_internal/f3d/f3d_writer.py index 87ebdec33..bcc800950 100644 --- a/fast64_internal/f3d/f3d_writer.py +++ b/fast64_internal/f3d/f3d_writer.py @@ -1330,8 +1330,14 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): # Checking for f3dMat.rdp_settings.g_lighting here will prevent accidental exports, # There may be some edge case where this isn't desired. if useDict["Shade"] and f3dMat.rdp_settings.g_lighting and f3dMat.set_lights: - fLights = saveLightsDefinition(fModel, fMaterial, f3dMat, materialName + "_lights") - fMaterial.mat_only_DL.commands.extend([SPSetLights(fLights)]) + if fModel.no_light_direction: + fLights = getLightDefinitions(fModel, f3dMat) + + for i, light in enumerate(fLights.l + [fLights.a]): + fMaterial.mat_only_DL.commands.extend([SPLightColor(f"LIGHT_{i + 1}", light.color)]) + else: + fLights = saveLightsDefinition(fModel, fMaterial, f3dMat, materialName + "_lights") + fMaterial.mat_only_DL.commands.extend([SPSetLights(fLights)]) fMaterial.mat_only_DL.commands.append(DPPipeSync()) fMaterial.revert.commands.append(DPPipeSync()) @@ -1534,11 +1540,7 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): return fMaterial, texDimensions -def saveLightsDefinition(fModel, fMaterial, material, lightsName): - lights = fModel.getLightAndHandleShared(lightsName) - if lights is not None: - return lights - +def getLightDefinitions(fModel, material, lightsName=""): lights = Lights(toAlnum(lightsName), fModel.f3d) if material.use_default_lighting: @@ -1562,6 +1564,16 @@ def saveLightsDefinition(fModel, fMaterial, material, lightsName): if material.f3d_light7 is not None: addLightDefinition(material, material.f3d_light7, lights) + return lights + + +def saveLightsDefinition(fModel, fMaterial, material, lightsName): + lights = fModel.getLightAndHandleShared(lightsName) + if lights is not None: + return lights + + lights = getLightDefinitions(fModel, material, lightsName) + if lightsName in fModel.lights: raise PluginError("Duplicate light name.") fModel.addLight(lightsName, lights, fMaterial) diff --git a/fast64_internal/operators.py b/fast64_internal/operators.py index 0b7d4c946..e4a2041cd 100644 --- a/fast64_internal/operators.py +++ b/fast64_internal/operators.py @@ -1,4 +1,5 @@ import bpy, mathutils, math +from bpy.types import Operator, Context, UILayout from bpy.utils import register_class, unregister_class from .utility import * from .f3d.f3d_material import * @@ -13,11 +14,51 @@ def addMaterialByName(obj, matName, preset): material.name = matName -class AddWaterBox(bpy.types.Operator): - # set bl_ properties +class OperatorBase(Operator): + """Base class for operators, keeps track of context mode and sets it back after running + execute_operator() and catches exceptions for raisePluginError()""" + + context_mode: str = "" + icon = "NONE" + + @classmethod + def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, **op_values): + """Op args are passed to the operator via setattr()""" + icon = icon if icon else cls.icon + op = layout.operator(cls.bl_idname, icon=icon, text=text) + for key, value in op_values.items(): + setattr(op, key, value) + return op + + def execute_operator(self, context: Context): + raise NotImplementedError() + + def execute(self, context: Context): + starting_mode = context.mode + starting_mode_set = get_mode_set_from_context_mode(starting_mode) + starting_object = context.object + try: + if self.context_mode and self.context_mode != starting_mode_set: + bpy.ops.object.mode_set(mode=self.context_mode) + self.execute_operator(context) + return {"FINISHED"} + except Exception as exc: + raisePluginError(self, exc) + return {"CANCELLED"} + finally: + if starting_mode != context.mode: + if starting_mode != "OBJECT" and starting_object: + context.view_layer.objects.active = starting_object + starting_object.select_set(True) + bpy.ops.object.mode_set(mode=starting_mode_set) + + +class AddWaterBox(OperatorBase): bl_idname = "object.add_water_box" bl_label = "Add Water Box" bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "CUBE" scale: bpy.props.FloatProperty(default=10) preset: bpy.props.StringProperty(default="Shaded Solid") @@ -26,10 +67,7 @@ class AddWaterBox(bpy.types.Operator): def setEmptyType(self, emptyObj): return None - def execute(self, context): - if context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - + def execute_operator(self, context): bpy.ops.object.select_all(action="DESELECT") location = mathutils.Vector(bpy.context.scene.cursor.location) @@ -48,7 +86,7 @@ def execute(self, context): parentObject(planeObj, emptyObj) - return {"FINISHED"} + self.report({"INFO"}, "Water box added.") class WarningOperator(bpy.types.Operator): diff --git a/fast64_internal/panels.py b/fast64_internal/panels.py index b1644a0ca..86ab2c6ba 100644 --- a/fast64_internal/panels.py +++ b/fast64_internal/panels.py @@ -1,33 +1,37 @@ import bpy -sm64GoalImport = "Import" # Not in enum, separate UI option - class SM64_Panel(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "SM64" bl_options = {"DEFAULT_CLOSED"} - # goal refers to the selected sm64GoalTypeEnum, a different selection than this goal will filter this panel out - goal = None - # if this is True, the panel is hidden whenever the scene's exportType is not 'C' + + # If bl_context is 'object' and object_type is defined, only show if the object is in the types + bl_context = "" + object_type: list | None = None + + # goal refers to the selected enum_sm64_goal_type in SM64_Properties, + # a different selection than this goal will filter this panel out + goal = "All" + # if this is True, the panel is hidden whenever the scene's export_type is not 'C' decomp_only = False + import_panel = False @classmethod def poll(cls, context): - sm64Props = bpy.context.scene.fast64.sm64 + if cls.bl_context == "object": + if cls.object_type and context.object.type not in cls.object_type: + return False + sm64_props = context.scene.fast64.sm64 if context.scene.gameEditorMode != "SM64": return False - elif not cls.goal: - return True # Panel should always be shown - elif cls.goal == sm64GoalImport: - # Only show if importing is enabled - return sm64Props.showImportingMenus - elif cls.decomp_only and sm64Props.exportType != "C": + elif cls.import_panel and not sm64_props.show_importing_menus: return False - - sceneGoal = sm64Props.goal - return sceneGoal == "All" or sceneGoal == cls.goal + elif cls.decomp_only and sm64_props.export_type != "C": + return False + scene_goal = sm64_props.goal + return scene_goal == "All" or sm64_props.goal == cls.goal or cls.goal == "All" class OOT_Panel(bpy.types.Panel): diff --git a/fast64_internal/repo_settings.py b/fast64_internal/repo_settings.py new file mode 100644 index 000000000..e8a0d3a8e --- /dev/null +++ b/fast64_internal/repo_settings.py @@ -0,0 +1,149 @@ +import json +import os + +from bpy.utils import register_class, unregister_class +from bpy.types import Context, Scene, UILayout +from bpy.props import StringProperty +from bpy.path import abspath + +from .utility import filepath_checks, prop_split, filepath_ui_warnings, draw_and_check_tab +from .operators import OperatorBase +from .f3d.f3d_material import ui_geo_mode, ui_upper_mode, ui_lower_mode, ui_other +from .sm64.settings.repo_settings import load_sm64_repo_settings, save_sm64_repo_settings + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .f3d.f3d_material import RDPSettings + +CUR_VERSION = 1.0 + + +class SaveRepoSettings(OperatorBase): + bl_idname = "scene.save_repo_settings" + bl_label = "Save Repo Settings" + bl_options = {"REGISTER", "UNDO", "PRESET"} + bl_description = "Save repo settings to a file" + icon = "FILE_TICK" + + path: StringProperty(name="Settings File Path", subtype="FILE_PATH") + + def execute_operator(self, context: Context): + save_repo_settings(context.scene, self.path) + self.report({"INFO"}, f"Saved repo settings to {self.path}") + + +class LoadRepoSettings(OperatorBase): + bl_idname = "scene.load_repo_settings" + bl_label = "Load Repo Settings" + bl_options = {"REGISTER", "UNDO", "PRESET"} + bl_description = "Load repo settings to a file" + icon = "IMPORT" + + path: StringProperty(name="Settings File Path", subtype="FILE_PATH") + + def execute_operator(self, context: Context): + load_repo_settings(context.scene, self.path) + self.report({"INFO"}, f"Loaded repo settings from {self.path}") + + +def load_repo_settings(scene: Scene, path: os.PathLike, skip_if_no_auto_load=False): + filepath_checks( + abspath(path), + "Repo settings file path is empty.", + "Repo settings file path {}does not exist.", + "Repo settings file path {}is not a file.", + ) + + try: + with open(abspath(path), "r", encoding="utf-8") as json_file: + data = json.load(json_file) + except Exception as exc: + raise Exception(f"Failed to load repo settings json. ({str(exc)})") from exc + + if skip_if_no_auto_load and not data.get("autoLoad", True): + return + + # Some future proofing + if data.get("version", CUR_VERSION) > CUR_VERSION: # Assuming latest should be fine + raise ValueError( + "This repo settings file is using a version higher than this fast64 version supports.", + ) + + fast64_settings = scene.fast64.settings + fast64_settings.auto_repo_load_settings = data.get("autoLoad", fast64_settings.auto_repo_load_settings) + fast64_settings.auto_pick_texture_format = data.get( + "autoPickTextureFormat", fast64_settings.auto_pick_texture_format + ) + fast64_settings.prefer_rgba_over_ci = data.get("preferRGBAOverCI", fast64_settings.prefer_rgba_over_ci) + scene.f3d_type = data.get("microcode", scene.f3d_type) + scene.saveTextures = data.get("saveTextures", scene.saveTextures) + rdp_defaults: RDPSettings = scene.world.rdp_defaults + rdp_defaults.from_dict(data.get("rdpDefaults", {})) + + if scene.gameEditorMode == "SM64": + load_sm64_repo_settings(scene, data.get("sm64", {})) + + +def save_repo_settings(scene: Scene, path: os.PathLike): + fast64_settings = scene.fast64.settings + data = {} + + data["version"] = CUR_VERSION + data["autoLoad"] = fast64_settings.auto_repo_load_settings + data["microcode"] = scene.f3d_type + data["saveTextures"] = scene.saveTextures + data["autoPickTextureFormat"] = fast64_settings.auto_pick_texture_format + if fast64_settings.auto_pick_texture_format: + data["preferRGBAOverCI"] = fast64_settings.prefer_rgba_over_ci + rdp_defaults: RDPSettings = scene.world.rdp_defaults + data["rdpDefaults"] = rdp_defaults.to_dict() + + if scene.gameEditorMode == "SM64": + data["sm64"] = save_sm64_repo_settings(scene) + + with open(abspath(path), "w", encoding="utf-8") as json_file: + json.dump(data, json_file, indent=2) + + +def draw_repo_settings(layout: UILayout, context: Context): + col = layout.column() + scene = context.scene + fast64_settings = scene.fast64.settings + if not draw_and_check_tab(col, fast64_settings, "repo_settings_tab", icon="PROPERTIES"): + return + prop_split(col, fast64_settings, "repo_settings_path", "Repo Settings Path") + path = abspath(fast64_settings.repo_settings_path) + if filepath_ui_warnings(col, path): + LoadRepoSettings.draw_props(col, path=fast64_settings.repo_settings_path) + SaveRepoSettings.draw_props(col, path=path) + + col.prop(fast64_settings, "auto_repo_load_settings") + prop_split(col, scene, "f3d_type", "F3D Microcode") + col.prop(scene, "saveTextures") + col.prop(fast64_settings, "auto_pick_texture_format") + if fast64_settings.auto_pick_texture_format: + col.prop(fast64_settings, "prefer_rgba_over_ci") + col.separator() + + world = scene.world + rdp_defaults = world.rdp_defaults + col.box().label(text="RDP Default Settings", icon="WORLD") + col.label(text="If a material setting is a same as a default setting, then it won't be set.") + ui_geo_mode(rdp_defaults, world, col, True) + ui_upper_mode(rdp_defaults, world, col, True) + ui_lower_mode(rdp_defaults, world, col, True) + ui_other(rdp_defaults, world, col, True) + + +classes = (SaveRepoSettings, LoadRepoSettings) + + +def repo_settings_operators_register(): + for cls in classes: + register_class(cls) + + +def repo_settings_operators_unregister(): + for cls in classes: + unregister_class(cls) diff --git a/fast64_internal/sm64/README.md b/fast64_internal/sm64/README.md index b15f39978..7daa84a4f 100644 --- a/fast64_internal/sm64/README.md +++ b/fast64_internal/sm64/README.md @@ -86,7 +86,10 @@ Basically, Mario's DMA table starts at 0x4EC000. There is an 8 byte header, and Often times it is hard to rig an existing SM64 geolayout, as there are many intermediate non-deform bones and bones don't point to their children. To make this easier you can use the 'Create Animatable Metarig' operator in the SM64 Armature Tools header. This will generate a metarig which can be used with IK. The metarig bones will be placed on armature layers 3 and 4. ## Decomp -To start, set your base decomp folder in SM64 File Settings. This allows the plugin to automatically add headers/includes to the correct locations. You can always choose to export to a custom location, although headers/includes won't be written. +To start, set your base decomp folder in SM64 General Settings. This allows the plugin to automatically add headers/includes to the correct locations. You can always choose to export to a custom location, although headers/includes won't be written. + +## Repo settings +Fast64 can save and load repo settings files. By default, they're named fast64.json. These files have RDP defaults, microcode, and more. They also have game-specific settings (OOT will support these in the future). Fast64 will set the path for the settings and auto-load them if auto-load is enabled as soon as the user picks an sm64 decomp path. ### Decomp Export Types Most exports will let you choose an export type. @@ -140,7 +143,7 @@ The draw function will be in the format "void myfunc(x, y, width, height, s, t)" ### Scrolling Textures in Decomp Scrolling texture settings can be found in the material properties window before the "Geomtry Mode Settings" tab. -If you want to disable scrolling texture code generation, you can do so in the SM64 File Settings. +If you want to disable scrolling texture code generation, you can do so in the SM64 General Settings. This is the process for how scrolling textures is implemented: - Add a sSegmentROMTable to src/game/memory.c/h in order to keep track of which ROM locations are loaded into memory. ROM locations will be stored in this table during segment loading function calls. diff --git a/fast64_internal/sm64/__init__.py b/fast64_internal/sm64/__init__.py index 20daf5589..dc6008d14 100644 --- a/fast64_internal/sm64/__init__.py +++ b/fast64_internal/sm64/__init__.py @@ -8,6 +8,8 @@ from .tools import ( tools_operators_register, tools_operators_unregister, + tools_props_register, + tools_props_unregister, tools_panels_register, tools_panels_unregister, ) @@ -122,9 +124,10 @@ def sm64_panel_unregister(): sm64_anim_panel_unregister() -def sm64_register(registerPanels): +def sm64_register(register_panels: bool): tools_operators_register() - sm64_col_register() # register first, so panel goes above mat panel + tools_props_register() + sm64_col_register() sm64_bone_register() sm64_cam_register() sm64_obj_register() @@ -137,12 +140,13 @@ def sm64_register(registerPanels): sm64_anim_register() settings_props_register() - if registerPanels: + if register_panels: sm64_panel_register() -def sm64_unregister(unregisterPanels): +def sm64_unregister(unregister_panels: bool): tools_operators_unregister() + tools_props_unregister() sm64_col_unregister() sm64_bone_unregister() sm64_cam_unregister() @@ -156,5 +160,5 @@ def sm64_unregister(unregisterPanels): sm64_anim_unregister() settings_props_unregister() - if unregisterPanels: + if unregister_panels: sm64_panel_unregister() diff --git a/fast64_internal/sm64/settings/constants.py b/fast64_internal/sm64/settings/constants.py index 7b33deaff..c3e822815 100644 --- a/fast64_internal/sm64/settings/constants.py +++ b/fast64_internal/sm64/settings/constants.py @@ -1,23 +1,23 @@ -sm64GoalTypeEnum = [ +enum_sm64_goal_type = [ ("All", "All", "All"), - ("Export Object/Actor/Anim", "Export Object/Actor/Anim", "Export Object/Actor/Anim"), - ("Export Level", "Export Level", "Export Level"), - ("Export Displaylist", "Export Displaylist", "Export Displaylist"), - ("Export UI Image", "Export UI Image", "Export UI Image"), + ("Object/Actor/Anim", "Object/Actor/Anim", "Object/Actor/Anim"), + ("Level", "Level", "Level"), + ("Displaylist", "Displaylist", "Displaylist"), + ("UI Image", "UI Image", "UI Image"), ] -enumExportType = [ +enum_export_type = [ ("C", "C", "C"), ("Binary", "Binary", "Binary"), ("Insertable Binary", "Insertable Binary", "Insertable Binary"), ] -enumCompressionFormat = [ +enum_compression_formats = [ ("mio0", "MIO0", "MIO0"), ("yay0", "YAY0", "YAY0"), ] -enumRefreshVer = [ +enum_refresh_versions = [ ("Refresh 3", "Refresh 3", "Refresh 3"), ("Refresh 4", "Refresh 4", "Refresh 4"), ("Refresh 5", "Refresh 5", "Refresh 5"), @@ -29,11 +29,3 @@ ("Refresh 12", "Refresh 12", "Refresh 12"), ("Refresh 13", "Refresh 13", "Refresh 13"), ] - -draw_layer_enums = [ - ("1", "Solid", "0x01"), - ("2", "Solid Decal", "0x02"), - ("4", "Transparent (No Blending)", "0x04"), - ("5", "Transparent (Blending Front)", "0x05"), - ("6", "Transparent (Blending Back)", "0x06"), -] diff --git a/fast64_internal/sm64/settings/panels.py b/fast64_internal/sm64/settings/panels.py index f6fc48fca..97fe73040 100644 --- a/fast64_internal/sm64/settings/panels.py +++ b/fast64_internal/sm64/settings/panels.py @@ -1,57 +1,32 @@ from bpy.utils import register_class, unregister_class +from bpy.types import Context -from ...utility import prop_split from ...panels import SM64_Panel -from .properties import SM64_Properties +from .repo_settings import draw_repo_settings -class SM64_MenuVisibilityPanel(SM64_Panel): - bl_idname = "SM64_PT_menu_visibility_settings" - bl_label = "SM64 Menu Visibility Settings" - bl_options = set() # default to open - bl_order = 0 # force to front +class SM64_GeneralSettingsPanel(SM64_Panel): + bl_idname = "SM64_PT_general_settings" + bl_label = "SM64 General Settings" - def draw(self, context): + def draw(self, context: Context): col = self.layout.column() - col.scale_y = 1.1 # extra padding - sm64Props: SM64_Properties = context.scene.fast64.sm64 + scene = context.scene + sm64_props = scene.fast64.sm64 - prop_split(col, sm64Props, "goal", "Export goal") - prop_split(col, sm64Props, "showImportingMenus", "Show Importing Options") + if sm64_props.export_type == "C": + # If the repo settings tab is open, we pass show_repo_settings as False + # because we want to draw those specfic properties in the repo settings box + draw_repo_settings(scene, col.box()) + col.separator() + sm64_props.draw_props(col, not sm64_props.sm64_repo_settings_tab) + else: + sm64_props.draw_props(col, True) -class SM64_FileSettingsPanel(SM64_Panel): - bl_idname = "SM64_PT_file_settings" - bl_label = "SM64 File Settings" - bl_options = set() - def draw(self, context): - col = self.layout.column() - col.scale_y = 1.1 # extra padding - sm64Props: SM64_Properties = context.scene.fast64.sm64 - - prop_split(col, sm64Props, "exportType", "Export type") - prop_split(col, context.scene, "blenderToSM64Scale", "Blender To SM64 Scale") - - if sm64Props.showImportingMenus: - col.prop(context.scene, "importRom") - - if sm64Props.exportType == "Binary": - col.prop(context.scene, "exportRom") - col.prop(context.scene, "outputRom") - col.prop(context.scene, "extendBank4") - elif sm64Props.exportType == "C": - col.prop(context.scene, "disableScroll") - col.prop(context.scene, "decompPath") - prop_split(col, context.scene, "refreshVer", "Decomp Func Map") - prop_split(col, context.scene, "compressionFormat", "Compression Format") - - -classes = ( - SM64_MenuVisibilityPanel, - SM64_FileSettingsPanel, -) +classes = (SM64_GeneralSettingsPanel,) def settings_panels_register(): diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 65a749717..1871fac22 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -1,63 +1,168 @@ +import os import bpy -from bpy.types import PropertyGroup, Scene -from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty +from bpy.types import PropertyGroup, UILayout, Scene, Context +from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty, PointerProperty +from bpy.path import abspath from bpy.utils import register_class, unregister_class from ...render_settings import on_update_render_settings -from ..sm64_constants import ( - level_enums, - defaultExtendSegment4, +from ...utility import directory_path_checks, directory_ui_warnings, prop_split +from ..sm64_constants import defaultExtendSegment4 +from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings +from ..tools import SM64_AddrConvProperties + +from .constants import ( + enum_refresh_versions, + enum_compression_formats, + enum_export_type, + enum_sm64_goal_type, ) -from .constants import enumRefreshVer, enumExportType, enumCompressionFormat, sm64GoalTypeEnum -def get_legacy_export_type(): - legacy_export_types = ("C", "Binary", "Insertable Binary") - scene = bpy.context.scene - - for exportKey in ["animExportType", "colExportType", "DLExportType", "geoExportType"]: - eType = scene.pop(exportKey, None) - if eType is not None and legacy_export_types[eType] != "C": - return legacy_export_types[eType] - - return "C" +def decomp_path_update(self, context: Context): + fast64_settings = context.scene.fast64.settings + if fast64_settings.repo_settings_path: + return + directory_path_checks(abspath(self.decomp_path)) + fast64_settings.repo_settings_path = os.path.join(abspath(self.decomp_path), "fast64.json") class SM64_Properties(PropertyGroup): """Global SM64 Scene Properties found under scene.fast64.sm64""" version: IntProperty(name="SM64_Properties Version", default=0) - cur_version = 1 # version after property migration + cur_version = 2 # version after property migration # UI Selection - showImportingMenus: BoolProperty(name="Show Importing Menus", default=False) - exportType: EnumProperty(items=enumExportType, name="Export Type", default="C") - goal: EnumProperty(items=sm64GoalTypeEnum, name="Export Goal", default="All") + show_importing_menus: BoolProperty(name="Show Importing Menus", default=False) + export_type: EnumProperty(items=enum_export_type, name="Export Type", default="C") + goal: EnumProperty(items=enum_sm64_goal_type, name="Goal", default="All") + + blender_to_sm64_scale: FloatProperty( + name="Blender To SM64 Scale", + default=100, + update=on_update_render_settings, + ) + import_rom: StringProperty(name="Import ROM", subtype="FILE_PATH") + + export_rom: StringProperty(name="Export ROM", subtype="FILE_PATH") + output_rom: StringProperty(name="Output ROM", subtype="FILE_PATH") + extend_bank_4: BoolProperty( + name="Extend Bank 4 on Export?", + default=True, + description=f"Sets bank 4 range to ({hex(defaultExtendSegment4[0])}, " + f"{hex(defaultExtendSegment4[1])}) and copies data from old bank", + ) + + address_converter: PointerProperty(type=SM64_AddrConvProperties) + # C + decomp_path: StringProperty( + name="Decomp Folder", + subtype="FILE_PATH", + update=decomp_path_update, + ) + sm64_repo_settings_tab: BoolProperty(default=True, name="SM64 Repo Settings") + disable_scroll: BoolProperty(name="Disable Scrolling Textures") + refresh_version: EnumProperty(items=enum_refresh_versions, name="Refresh", default="Refresh 13") + compression_format: EnumProperty( + items=enum_compression_formats, + name="Compression", + default="mio0", + ) + force_extended_ram: BoolProperty( + name="Force Extended Ram", + default=True, + description="USE_EXT_RAM will be defined in include/segments.h on export, increasing the available RAM by 4MB but requiring the expansion pack, this prevents crashes from running out of RAM", + ) + matstack_fix: BoolProperty( + name="Matstack Fix", + description="Exports account for matstack fix requirements", + ) + + @property + def binary_export(self): + return self.export_type in ["Binary", "Insertable Binary"] - # TODO: Utilize these across all exports - # C exporting - # useCustomExportLocation = BoolProperty(name = 'Use Custom Export Path') - # customExportPath: StringProperty(name = 'Custom Export Path', subtype = 'FILE_PATH') - # exportLocation: EnumProperty(items = enumExportHeaderType, name = 'Export Location', default = 'Actor') - # useSelectedObjectName = BoolProperty(name = 'Use Name From Selected Object', default=False) - # exportName: StringProperty(name='Name', default='mario') - # exportGeolayoutName: StringProperty(name='Name', default='mario_geo') + def get_legacy_export_type(self, scene: Scene): + legacy_export_types = ("C", "Binary", "Insertable Binary") - # Actor exports - # exportGroup: StringProperty(name='Group', default='group0') + for export_key in ["animExportType", "colExportType", "DLExportType", "geoExportType"]: + export_type = legacy_export_types[scene.get(export_key, 0)] + if export_type != "C": + return export_type - # Level exports - # exportLevelName: StringProperty(name = 'Level', default = 'bob') - # exportLevelOption: EnumProperty(items = enumLevelNames, name = 'Level', default = 'bob') + return "C" - # Insertable Binary - # exportInsertableBinaryPath: StringProperty(name = 'Filepath', subtype = 'FILE_PATH') + def upgrade_version_1(self, scene: Scene): + old_scene_props_to_new = { + "importRom": "import_rom", + "exportRom": "export_rom", + "outputRom": "output_rom", + "disableScroll": "disable_scroll", + "blenderToSM64Scale": "blender_to_sm64_scale", + "decompPath": "decomp_path", + "extendBank4": "extend_bank_4", + } + for old, new in old_scene_props_to_new.items(): + setattr(self, new, scene.get(old, getattr(self, new))) + + refresh_version = scene.get("refreshVer", None) + if refresh_version is not None: + self.refresh_version = enum_refresh_versions[refresh_version][0] + + self.show_importing_menus = self.get("showImportingMenus", self.show_importing_menus) + + export_type = self.get("exportType", None) + if export_type is not None: + self.export_type = enum_export_type[export_type][0] + + self.version = 2 @staticmethod def upgrade_changed_props(): - if bpy.context.scene.fast64.sm64.version != SM64_Properties.cur_version: - bpy.context.scene.fast64.sm64.exportType = get_legacy_export_type() - bpy.context.scene.fast64.sm64.version = SM64_Properties.cur_version + for scene in bpy.data.scenes: + sm64_props: SM64_Properties = scene.fast64.sm64 + if sm64_props.version == 0: + sm64_props.export_type = sm64_props.get_legacy_export_type(scene) + sm64_props.version = 1 + print("Upgraded global SM64 settings to version 1") + if sm64_props.version == 1: + sm64_props.upgrade_version_1(scene) + print("Upgraded global SM64 settings to version 2") + sm64_props.address_converter.upgrade_changed_props(scene) + + def draw_props(self, layout: UILayout, show_repo_settings: bool = True): + col = layout.column() + + prop_split(col, self, "goal", "Goal") + prop_split(col, self, "export_type", "Export type") + col.separator() + + prop_split(col, self, "blender_to_sm64_scale", "Blender To SM64 Scale") + + if self.export_type == "Binary": + col.prop(self, "export_rom") + export_rom_ui_warnings(col, self.export_rom) + col.prop(self, "output_rom") + col.prop(self, "extend_bank_4") + elif not self.binary_export: + prop_split(col, self, "decomp_path", "Decomp Path") + directory_ui_warnings(col, abspath(self.decomp_path)) + col.separator() + + if not self.binary_export: + col.prop(self, "disable_scroll") + if show_repo_settings: + prop_split(col, self, "compression_format", "Compression Format") + prop_split(col, self, "refresh_version", "Refresh (Function Map)") + col.prop(self, "force_extended_ram") + col.prop(self, "matstack_fix") + col.separator() + + col.prop(self, "show_importing_menus") + if self.show_importing_menus: + prop_split(col, self, "import_rom", "Import ROM") + import_rom_ui_warnings(col, self.import_rom) classes = (SM64_Properties,) @@ -67,45 +172,7 @@ def settings_props_register(): for cls in classes: register_class(cls) - Scene.importRom = StringProperty(name="Import ROM", subtype="FILE_PATH") - Scene.exportRom = StringProperty(name="Export ROM", subtype="FILE_PATH") - Scene.outputRom = StringProperty(name="Output ROM", subtype="FILE_PATH") - Scene.extendBank4 = BoolProperty( - name="Extend Bank 4 on Export?", - default=True, - description="Sets bank 4 range to (" - + hex(defaultExtendSegment4[0]) - + ", " - + hex(defaultExtendSegment4[1]) - + ") and copies data from old bank", - ) - Scene.convertibleAddr = StringProperty(name="Address") - Scene.levelConvert = EnumProperty(items=level_enums, name="Level", default="IC") - Scene.refreshVer = EnumProperty(items=enumRefreshVer, name="Refresh", default="Refresh 13") - Scene.disableScroll = BoolProperty(name="Disable Scrolling Textures") - Scene.blenderToSM64Scale = FloatProperty( - name="Blender To SM64 Scale", default=100, update=on_update_render_settings - ) - Scene.decompPath = StringProperty(name="Decomp Folder", subtype="FILE_PATH") - - Scene.compressionFormat = EnumProperty(items=enumCompressionFormat, name="Compression", default="mio0") - def settings_props_unregister(): for cls in reversed(classes): unregister_class(cls) - - del Scene.importRom - del Scene.exportRom - del Scene.outputRom - del Scene.extendBank4 - - del Scene.convertibleAddr - del Scene.levelConvert - del Scene.refreshVer - - del Scene.disableScroll - - del Scene.blenderToSM64Scale - del Scene.decompPath - del Scene.compressionFormat diff --git a/fast64_internal/sm64/settings/repo_settings.py b/fast64_internal/sm64/settings/repo_settings.py new file mode 100644 index 000000000..f74b7eb3d --- /dev/null +++ b/fast64_internal/sm64/settings/repo_settings.py @@ -0,0 +1,58 @@ +from typing import Any + +from bpy.types import Scene, UILayout + +from ...utility import draw_and_check_tab, prop_split + + +def save_sm64_repo_settings(scene: Scene): + world = scene.world + data: dict[str, Any] = {} + draw_layers: dict[str, Any] = {} + data["draw_layers"] = draw_layers + + for layer in range(8): + draw_layers[layer] = { + "cycle_1": getattr(world, f"draw_layer_{layer}_cycle_1"), + "cycle_2": getattr(world, f"draw_layer_{layer}_cycle_2"), + } + + sm64_props = scene.fast64.sm64 + data["refresh_version"] = sm64_props.refresh_version + data["compression_format"] = sm64_props.compression_format + data["force_extended_ram"] = sm64_props.force_extended_ram + data["matstack_fix"] = sm64_props.matstack_fix + + return data + + +def load_sm64_repo_settings(scene: Scene, data: dict[str, Any]): + world = scene.world + + draw_layers = data.get("draw_layers", {}) + for layer in range(8): + draw_layer = draw_layers.get(str(layer), {}) + if "cycle_1" in draw_layer: + setattr(world, f"draw_layer_{layer}_cycle_1", draw_layer["cycle_1"]) + if "cycle_2" in draw_layer: + setattr(world, f"draw_layer_{layer}_cycle_2", draw_layer["cycle_2"]) + + sm64_props = scene.fast64.sm64 + sm64_props.refresh_version = data.get("refresh_version", sm64_props.refresh_version) + sm64_props.compression_format = data.get("compression_format", sm64_props.compression_format) + sm64_props.force_extended_ram = data.get("force_extended_ram", sm64_props.force_extended_ram) + sm64_props.matstack_fix = data.get("matstack_fix", sm64_props.matstack_fix) + + +def draw_repo_settings(scene: Scene, layout: UILayout): + col = layout.column() + sm64_props = scene.fast64.sm64 + if not draw_and_check_tab(col, sm64_props, "sm64_repo_settings_tab", icon="PROPERTIES"): + return + + prop_split(col, sm64_props, "compression_format", "Compression Format") + prop_split(col, sm64_props, "refresh_version", "Refresh (Function Map)") + col.prop(sm64_props, "force_extended_ram") + col.prop(sm64_props, "matstack_fix") + + col.label(text="See Fast64 repo settings for general settings", icon="INFO") diff --git a/fast64_internal/sm64/sm64_anim.py b/fast64_internal/sm64/sm64_anim.py index fa90cb568..69ef64f49 100644 --- a/fast64_internal/sm64/sm64_anim.py +++ b/fast64_internal/sm64/sm64_anim.py @@ -1,6 +1,6 @@ import bpy, os, copy, shutil, mathutils, math from bpy.utils import register_class, unregister_class -from ..panels import SM64_Panel, sm64GoalImport +from ..panels import SM64_Panel from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 from .sm64_geolayout_bone import animatableBoneTypes @@ -25,7 +25,6 @@ applyRotation, getPathAndLevel, applyBasicTweaks, - checkExpanded, tempName, bytesToHex, prop_split, @@ -47,6 +46,8 @@ marioAnimations, ) +from .sm64_utility import export_rom_checks, import_rom_checks + sm64_anim_types = {"ROTATE", "TRANSLATE"} @@ -617,7 +618,9 @@ def getKeyFramesTranslation(romfile, transformValuesStart, boneIndex): keyframes = [] for frame in range(boneIndex.numFrames): romfile.seek(ptrToValue + frame * 2) - keyframes.append(int.from_bytes(romfile.read(2), "big", signed=True) / bpy.context.scene.blenderToSM64Scale) + keyframes.append( + int.from_bytes(romfile.read(2), "big", signed=True) / bpy.context.scene.fast64.sm64.blender_to_sm64_scale + ) return keyframes @@ -732,7 +735,7 @@ def execute(self, context): # Rotate all armatures 90 degrees applyRotation([armatureObj], math.radians(90), "X") - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": exportPath, levelName = getPathAndLevel( context.scene.animCustomExport, context.scene.animExportPath, @@ -752,7 +755,7 @@ def execute(self, context): levelName, ) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": exportAnimationInsertableBinary( bpy.path.abspath(context.scene.animInsertableBinaryPath), armatureObj, @@ -761,17 +764,17 @@ def execute(self, context): ) self.report({"INFO"}, "Success! Animation at " + context.scene.animInsertableBinaryPath) else: - checkExpanded(bpy.path.abspath(context.scene.exportRom)) - tempROM = tempName(context.scene.outputRom) - romfileExport = open(bpy.path.abspath(context.scene.exportRom), "rb") - shutil.copy(bpy.path.abspath(context.scene.exportRom), bpy.path.abspath(tempROM)) + export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) + tempROM = tempName(context.scene.fast64.sm64.output_rom) + romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") + shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") # Note actual level doesn't matter for Mario, since he is in all of them levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelAnimExport]) segmentData = levelParsed.segmentData - if context.scene.extendBank4: + if context.scene.fast64.sm64.extend_bank_4: ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) DMAAddresses = None @@ -805,9 +808,9 @@ def execute(self, context): segmentedPtr = None romfileOutput.close() - if os.path.exists(bpy.path.abspath(context.scene.outputRom)): - os.remove(bpy.path.abspath(context.scene.outputRom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.outputRom)) + if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): + os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) + os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) if not context.scene.isDMAExport: if context.scene.setAnimListIndex: @@ -858,7 +861,7 @@ def execute(self, context): class SM64_ExportAnimPanel(SM64_Panel): bl_idname = "SM64_PT_export_anim" bl_label = "SM64 Animation Exporter" - goal = "Export Object/Actor/Anim" + goal = "Object/Actor/Anim" # called every frame def draw(self, context): @@ -867,7 +870,7 @@ def draw(self, context): col.prop(context.scene, "loopAnimation") - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": col.prop(context.scene, "animCustomExport") if context.scene.animCustomExport: col.prop(context.scene, "animExportPath") @@ -893,7 +896,7 @@ def draw(self, context): context.scene.animLevelOption, ) - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": col.prop(context.scene, "isDMAExport") col.prop(context.scene, "animInsertableBinaryPath") else: @@ -927,8 +930,8 @@ class SM64_ImportAnimMario(bpy.types.Operator): def execute(self, context): romfileSrc = None try: - checkExpanded(bpy.path.abspath(context.scene.importRom)) - romfileSrc = open(bpy.path.abspath(context.scene.importRom), "rb") + import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) + romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") except Exception as e: raisePluginError(self, e) return {"CANCELLED"} @@ -975,8 +978,8 @@ class SM64_ImportAllMarioAnims(bpy.types.Operator): def execute(self, context): romfileSrc = None try: - checkExpanded(bpy.path.abspath(context.scene.importRom)) - romfileSrc = open(bpy.path.abspath(context.scene.importRom), "rb") + import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) + romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") except Exception as e: raisePluginError(self, e) return {"CANCELLED"} @@ -1004,7 +1007,8 @@ def execute(self, context): class SM64_ImportAnimPanel(SM64_Panel): bl_idname = "SM64_PT_import_anim" bl_label = "SM64 Animation Importer" - goal = sm64GoalImport + goal = "Object/Actor/Anim" + import_panel = True # called every frame def draw(self, context): diff --git a/fast64_internal/sm64/sm64_collision.py b/fast64_internal/sm64/sm64_collision.py index b79e1aedf..e70134e6c 100644 --- a/fast64_internal/sm64/sm64_collision.py +++ b/fast64_internal/sm64/sm64_collision.py @@ -1,7 +1,14 @@ import bpy, shutil, os, math, mathutils from bpy.utils import register_class, unregister_class from io import BytesIO -from .sm64_constants import level_enums, level_pointers, enumLevelNames, insertableBinaryTypes, defaultExtendSegment4 +from .sm64_constants import ( + level_enums, + level_pointers, + enumLevelNames, + insertableBinaryTypes, + defaultExtendSegment4, +) +from .sm64_utility import export_rom_checks from .sm64_objects import SM64_Area, start_process_sm64_objects from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 @@ -25,7 +32,6 @@ getPathAndLevel, applyBasicTweaks, tempName, - checkExpanded, bytesToHex, applyRotation, customExportWarning, @@ -516,7 +522,7 @@ def execute(self, context): # (bpy.context.scene.blenderToSM64Scale)).to_4x4() # finalTransform = mathutils.Matrix.Identity(4) - scaleValue = bpy.context.scene.blenderToSM64Scale + scaleValue = context.scene.fast64.sm64.blender_to_sm64_scale finalTransform = mathutils.Matrix.Diagonal(mathutils.Vector((scaleValue, scaleValue, scaleValue))).to_4x4() except Exception as e: raisePluginError(self, e) @@ -524,7 +530,7 @@ def execute(self, context): try: applyRotation([obj], math.radians(90), "X") - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": exportPath, levelName = getPathAndLevel( context.scene.colCustomExport, context.scene.colExportPath, @@ -547,7 +553,7 @@ def execute(self, context): levelName, ) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": exportCollisionInsertableBinary( obj, finalTransform, @@ -557,17 +563,17 @@ def execute(self, context): ) self.report({"INFO"}, "Success! Collision at " + context.scene.colInsertableBinaryPath) else: - tempROM = tempName(context.scene.outputRom) - checkExpanded(bpy.path.abspath(context.scene.exportRom)) - romfileExport = open(bpy.path.abspath(context.scene.exportRom), "rb") - shutil.copy(bpy.path.abspath(context.scene.exportRom), bpy.path.abspath(tempROM)) + tempROM = tempName(context.scene.fast64.sm64.output_rom) + export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) + romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") + shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.colExportLevel]) segmentData = levelParsed.segmentData - if context.scene.extendBank4: + if context.scene.fast64.sm64.extend_bank_4: ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) addrRange = exportCollisionBinary( @@ -588,9 +594,9 @@ def execute(self, context): romfileOutput.close() - if os.path.exists(bpy.path.abspath(context.scene.outputRom)): - os.remove(bpy.path.abspath(context.scene.outputRom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.outputRom)) + if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): + os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) + os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) self.report( {"INFO"}, @@ -612,7 +618,7 @@ def execute(self, context): applyRotation([obj], math.radians(-90), "X") - if context.scene.fast64.sm64.exportType == "Binary": + if context.scene.fast64.sm64.export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -626,7 +632,7 @@ def execute(self, context): class SM64_ExportCollisionPanel(SM64_Panel): bl_idname = "SM64_PT_export_collision" bl_label = "SM64 Collision Exporter" - goal = "Export Object/Actor/Anim" + goal = "Object/Actor/Anim" # called every frame def draw(self, context): @@ -635,7 +641,7 @@ def draw(self, context): col.prop(context.scene, "colIncludeChildren") - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": col.prop(context.scene, "colExportRooms") col.prop(context.scene, "colCustomExport") if context.scene.colCustomExport: @@ -662,7 +668,7 @@ def draw(self, context): context.scene.colLevelOption, ) - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": col.prop(context.scene, "colInsertableBinaryPath") else: prop_split(col, context.scene, "colStartAddr", "Start Address") diff --git a/fast64_internal/sm64/sm64_constants.py b/fast64_internal/sm64/sm64_constants.py index 321176e30..8f714c771 100644 --- a/fast64_internal/sm64/sm64_constants.py +++ b/fast64_internal/sm64/sm64_constants.py @@ -34,6 +34,7 @@ "Lakitu": [1985520, "CC"], } + level_enums = [ ("HH", "Big Boo's Haunt", "HH"), # Originally Haunted House ("CCM", "Cool Cool Mountain", "CCM"), diff --git a/fast64_internal/sm64/sm64_f3d_parser.py b/fast64_internal/sm64/sm64_f3d_parser.py index 5e191d0ea..6eaabb21e 100644 --- a/fast64_internal/sm64/sm64_f3d_parser.py +++ b/fast64_internal/sm64/sm64_f3d_parser.py @@ -5,9 +5,10 @@ from bpy.utils import register_class, unregister_class from bpy.ops import object from bpy.props import StringProperty, EnumProperty, BoolProperty -from ..panels import SM64_Panel, sm64GoalImport +from ..panels import SM64_Panel from ..f3d.f3d_parser import F3DtoBlenderObject from .sm64_constants import level_enums, level_pointers +from .sm64_utility import import_rom_checks from .sm64_level_parser import parseLevelAtPointer from ..utility import ( @@ -16,7 +17,6 @@ raisePluginError, decodeSegmentedAddr, applyRotation, - checkExpanded, prop_split, ) @@ -38,8 +38,8 @@ def execute(self, context): raisePluginError(self, e) return {"CANCELLED"} try: - checkExpanded(abspath(context.scene.importRom)) - romfileSrc = open(abspath(context.scene.importRom), "rb") + import_rom_checks(abspath(context.scene.fast64.sm64.import_rom)) + romfileSrc = open(abspath(context.scene.fast64.sm64.import_rom), "rb") levelParsed = parseLevelAtPointer(romfileSrc, level_pointers[context.scene.levelDLImport]) segmentData = levelParsed.segmentData start = ( @@ -69,7 +69,7 @@ def execute(self, context): class SM64_ImportDLPanel(SM64_Panel): bl_idname = "SM64_PT_import_dl" bl_label = "SM64 DL Importer" - goal = sm64GoalImport + import_panel = True # called every frame def draw(self, context): diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 059e40815..d756f0f70 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -8,7 +8,7 @@ from ..f3d.f3d_texture_writer import TexInfo from ..f3d.f3d_material import TextureProperty, tmemUsageUI, all_combiner_uses, ui_procAnim from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import starSelectWarning +from .sm64_utility import export_rom_checks, starSelectWarning from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 from typing import Tuple, Union, Iterable @@ -65,7 +65,6 @@ writeInsertableFile, getPathAndLevel, applyBasicTweaks, - checkExpanded, tempName, getAddressFromRAMAddress, bytesToHex, @@ -101,6 +100,7 @@ class SM64Model(FModel): def __init__(self, name, DLFormat, matWriteMethod): FModel.__init__(self, name, DLFormat, matWriteMethod) + self.no_light_direction = bpy.context.scene.fast64.sm64.matstack_fix def getDrawLayerV3(self, obj): return int(obj.draw_layer_static) @@ -570,14 +570,7 @@ def execute(self, context): if obj.type != "MESH": raise PluginError("Object is not a mesh.") - # T, R, S = obj.matrix_world.decompose() - # objTransform = R.to_matrix().to_4x4() @ \ - # Matrix.Diagonal(S).to_4x4() - - # finalTransform = (blenderToSM64Rotation * \ - # (bpy.context.scene.blenderToSM64Scale)).to_4x4() - # finalTransform = Matrix.Identity(4) - scaleValue = bpy.context.scene.blenderToSM64Scale + scaleValue = context.scene.fast64.sm64.blender_to_sm64_scale finalTransform = Matrix.Diagonal(Vector((scaleValue, scaleValue, scaleValue))).to_4x4() except Exception as e: @@ -586,7 +579,7 @@ def execute(self, context): try: applyRotation([obj], radians(90), "X") - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": exportPath, levelName = getPathAndLevel( context.scene.DLCustomExport, context.scene.DLExportPath, @@ -614,7 +607,7 @@ def execute(self, context): starSelectWarning(self, fileStatus) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": exportF3DtoInsertableBinary( bpy.path.abspath(context.scene.DLInsertableBinaryPath), finalTransform, @@ -623,16 +616,16 @@ def execute(self, context): ) self.report({"INFO"}, "Success! DL at " + context.scene.DLInsertableBinaryPath + ".") else: - checkExpanded(bpy.path.abspath(context.scene.exportRom)) - tempROM = tempName(context.scene.outputRom) - romfileExport = open(bpy.path.abspath(context.scene.exportRom), "rb") - shutil.copy(bpy.path.abspath(context.scene.exportRom), bpy.path.abspath(tempROM)) + export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) + tempROM = tempName(context.scene.fast64.sm64.output_rom) + romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") + shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelDLExport]) segmentData = levelParsed.segmentData - if context.scene.extendBank4: + if context.scene.fast64.sm64.extend_bank_4: ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) if context.scene.DLUseBank0: @@ -659,9 +652,9 @@ def execute(self, context): romfileOutput.write(segPointerData) romfileOutput.close() - if os.path.exists(bpy.path.abspath(context.scene.outputRom)): - os.remove(bpy.path.abspath(context.scene.outputRom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.outputRom)) + if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): + os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) + os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) if context.scene.DLUseBank0: self.report( @@ -693,7 +686,7 @@ def execute(self, context): if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") applyRotation([obj], radians(-90), "X") - if context.scene.fast64.sm64.exportType == "Binary": + if context.scene.fast64.sm64.export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -705,14 +698,14 @@ def execute(self, context): class SM64_ExportDLPanel(SM64_Panel): bl_idname = "SM64_PT_export_dl" bl_label = "SM64 DL Exporter" - goal = "Export Displaylist" + goal = "Displaylist" # called every frame def draw(self, context): col = self.layout.column() propsDLE = col.operator(SM64_ExportDL.bl_idname) - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": col.prop(context.scene, "DLExportisStatic") col.prop(context.scene, "DLCustomExport") @@ -745,7 +738,7 @@ def draw(self, context): context.scene.DLLevelOption, ) - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": col.prop(context.scene, "DLInsertableBinaryPath") else: prop_split(col, context.scene, "DLExportStart", "Start Address") @@ -777,9 +770,9 @@ def execute(self, context): if context.scene.TexRectCustomExport: exportPath = context.scene.TexRectExportPath else: - if context.scene.decompPath == "": + if context.scene.fast64.sm64.decomp_path == "": raise PluginError("Decomp path has not been set in File Settings.") - exportPath = context.scene.decompPath + exportPath = context.scene.fast64.sm64.decomp_path if not context.scene.TexRectCustomExport: applyBasicTweaks(exportPath) exportTexRectToC( @@ -814,7 +807,7 @@ def execute(self, context): class ExportTexRectDrawPanel(SM64_Panel): bl_idname = "TEXTURE_PT_export_texrect" bl_label = "SM64 UI Image Exporter" - goal = "Export UI Image" + goal = "UI Image" decomp_only = True # called every frame diff --git a/fast64_internal/sm64/sm64_geolayout_classes.py b/fast64_internal/sm64/sm64_geolayout_classes.py index 3b93d8d53..77792596a 100644 --- a/fast64_internal/sm64/sm64_geolayout_classes.py +++ b/fast64_internal/sm64/sm64_geolayout_classes.py @@ -498,7 +498,7 @@ def walk(node, last_materials): def convertAddrToFunc(addr): if addr == "": raise PluginError("Geolayout node cannot have an empty function name/address.") - refresh_func_map = func_map[bpy.context.scene.refreshVer] + refresh_func_map = func_map[bpy.context.scene.fast64.sm64.refresh_version] if addr.lower() in refresh_func_map: return refresh_func_map[addr.lower()] else: @@ -1133,8 +1133,8 @@ def to_c(self): class CameraNode: def __init__(self, camType, position, lookAt): self.camType = camType - self.position = [int(round(value * bpy.context.scene.blenderToSM64Scale)) for value in position] - self.lookAt = [int(round(value * bpy.context.scene.blenderToSM64Scale)) for value in lookAt] + self.position = [int(round(value * bpy.context.scene.fast64.sm64.blender_to_sm64_scale)) for value in position] + self.lookAt = [int(round(value * bpy.context.scene.fast64.sm64.blender_to_sm64_scale)) for value in lookAt] self.geo_func = "80287D30" self.hasDL = False diff --git a/fast64_internal/sm64/sm64_geolayout_parser.py b/fast64_internal/sm64/sm64_geolayout_parser.py index 34d1b4ae1..db724a4cd 100644 --- a/fast64_internal/sm64/sm64_geolayout_parser.py +++ b/fast64_internal/sm64/sm64_geolayout_parser.py @@ -1,11 +1,12 @@ import bpy, mathutils, math, bmesh, copy from bpy.utils import register_class, unregister_class from ..f3d.f3d_parser import createBlankMaterial, parseF3DBinary -from ..panels import SM64_Panel, sm64GoalImport +from ..panels import SM64_Panel from .sm64_level_parser import parseLevelAtPointer from .sm64_constants import level_pointers, level_enums from .sm64_geolayout_bone import enumShadowType, animatableBoneTypes, enumBoneType from .sm64_geolayout_constants import getGeoLayoutCmdLength, nodeGroupCmds, GEO_BRANCH_STORE +from .sm64_utility import import_rom_checks from ..utility import ( PluginError, @@ -17,7 +18,6 @@ findStartBones, readEulerVectorFromShorts, readFloatFromShort, - checkExpanded, doRotation, prop_split, sm64BoneUp, @@ -1388,7 +1388,7 @@ def parseStartWithRenderArea( commandSize = 4 romfile.seek(currentAddress) command = romfile.read(commandSize) - cullingRadius = int.from_bytes(command[2:4], "big") / bpy.context.scene.blenderToSM64Scale + cullingRadius = int.from_bytes(command[2:4], "big") / bpy.context.scene.fast64.sm64.blender_to_sm64_scale if not ignoreNode: boneName = format(nodeIndex, "03") + "-start_render_area" @@ -1523,7 +1523,6 @@ def execute(self, context): geoImportAddr = context.scene.geoImportAddr generateArmature = context.scene.generateArmature levelGeoImport = context.scene.levelGeoImport - importRom = context.scene.importRom ignoreSwitch = context.scene.ignoreSwitch # finalTransform = mathutils.Matrix.Rotation(math.radians(-90), 4, 'X') @@ -1534,8 +1533,9 @@ def execute(self, context): raisePluginError(self, e) return {"CANCELLED"} try: - romfileSrc = open(bpy.path.abspath(importRom), "rb") - checkExpanded(bpy.path.abspath(importRom)) + import_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.import_rom)) + + romfileSrc = open(bpy.path.abspath(context.scene.fast64.sm64.import_rom), "rb") armatureObj = None @@ -1592,7 +1592,8 @@ def execute(self, context): class SM64_ImportGeolayoutPanel(SM64_Panel): bl_idname = "SM64_PT_import_geolayout" bl_label = "SM64 Geolayout Importer" - goal = sm64GoalImport + goal = "Object/Actor/Anim" + import_panel = True # called every frame def draw(self, context): diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index 8ddc15c16..e246c6f77 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -13,7 +13,7 @@ from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 -from .sm64_utility import starSelectWarning +from .sm64_utility import export_rom_checks, starSelectWarning from ..utility import ( PluginError, @@ -49,7 +49,6 @@ getPathAndLevel, applyBasicTweaks, tempName, - checkExpanded, getAddressFromRAMAddress, prop_split, customExportWarning, @@ -1780,7 +1779,8 @@ def processBone( obj, armatureObj.data, fModel.f3d, - mathutils.Matrix.Scale(bpy.context.scene.blenderToSM64Scale, 4) @ bone.matrix_local.inverted(), + mathutils.Matrix.Scale(bpy.context.scene.fast64.sm64.blender_to_sm64_scale, 4) + @ bone.matrix_local.inverted(), infoDict, ) fMeshes, fSkinnedMeshes, usedDrawLayers = saveModelGivenVertexGroup( @@ -2316,7 +2316,7 @@ def saveModelGivenVertexGroup( print("No vert indices in " + vertexGroup) return None, None, None - transformMatrix = mathutils.Matrix.Scale(bpy.context.scene.blenderToSM64Scale, 4) + transformMatrix = mathutils.Matrix.Scale(bpy.context.scene.fast64.sm64.blender_to_sm64_scale, 4) if parentGroup is None: parentMatrix = transformMatrix else: @@ -2833,7 +2833,7 @@ def execute(self, context): # context.scene.saveCameraSettings else None finalTransform = mathutils.Matrix.Identity(4) - scaleValue = bpy.context.scene.blenderToSM64Scale + scaleValue = context.scene.fast64.sm64.blender_to_sm64_scale finalTransform = mathutils.Matrix.Diagonal(mathutils.Vector((scaleValue, scaleValue, scaleValue))).to_4x4() except Exception as e: raisePluginError(self, e) @@ -2847,7 +2847,7 @@ def execute(self, context): saveTextures = bpy.context.scene.saveTextures - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": exportPath, levelName = getPathAndLevel( context.scene.geoCustomExport, context.scene.geoExportPath, @@ -2873,7 +2873,7 @@ def execute(self, context): DLFormat.Static, ) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": exportGeolayoutObjectInsertableBinary( obj, finalTransform, @@ -2882,17 +2882,17 @@ def execute(self, context): ) self.report({"INFO"}, "Success! Data at " + context.scene.geoInsertableBinaryPath) else: - tempROM = tempName(context.scene.outputRom) - checkExpanded(bpy.path.abspath(context.scene.exportRom)) - romfileExport = open(bpy.path.abspath(context.scene.exportRom), "rb") - shutil.copy(bpy.path.abspath(context.scene.exportRom), bpy.path.abspath(tempROM)) + tempROM = tempName(context.scene.fast64.sm64.output_rom) + export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) + romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") + shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelGeoExport]) segmentData = levelParsed.segmentData - if context.scene.extendBank4: + if context.scene.fast64.sm64.extend_bank_4: ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) exportRange = [int(context.scene.geoExportStart, 16), int(context.scene.geoExportEnd, 16)] @@ -2932,9 +2932,9 @@ def execute(self, context): obj.select_set(True) context.view_layer.objects.active = obj - if os.path.exists(bpy.path.abspath(context.scene.outputRom)): - os.remove(bpy.path.abspath(context.scene.outputRom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.outputRom)) + if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): + os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) + os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) if context.scene.geoUseBank0: self.report( @@ -2972,7 +2972,7 @@ def execute(self, context): self.cleanup_temp_object_data() applyRotation([obj], math.radians(-90), "X") - if context.scene.fast64.sm64.exportType == "Binary": + if context.scene.fast64.sm64.export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -3047,7 +3047,7 @@ def execute(self, context): obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": exportPath, levelName = getPathAndLevel( context.scene.geoCustomExport, context.scene.geoExportPath, @@ -3077,7 +3077,7 @@ def execute(self, context): ) starSelectWarning(self, fileStatus) self.report({"INFO"}, "Success!") - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": exportGeolayoutArmatureInsertableBinary( armatureObj, obj, @@ -3087,17 +3087,17 @@ def execute(self, context): ) self.report({"INFO"}, "Success! Data at " + context.scene.geoInsertableBinaryPath) else: - tempROM = tempName(context.scene.outputRom) - checkExpanded(bpy.path.abspath(context.scene.exportRom)) - romfileExport = open(bpy.path.abspath(context.scene.exportRom), "rb") - shutil.copy(bpy.path.abspath(context.scene.exportRom), bpy.path.abspath(tempROM)) + tempROM = tempName(context.scene.fast64.sm64.output_rom) + export_rom_checks(bpy.path.abspath(context.scene.fast64.sm64.export_rom)) + romfileExport = open(bpy.path.abspath(context.scene.fast64.sm64.export_rom), "rb") + shutil.copy(bpy.path.abspath(context.scene.fast64.sm64.export_rom), bpy.path.abspath(tempROM)) romfileExport.close() romfileOutput = open(bpy.path.abspath(tempROM), "rb+") levelParsed = parseLevelAtPointer(romfileOutput, level_pointers[context.scene.levelGeoExport]) segmentData = levelParsed.segmentData - if context.scene.extendBank4: + if context.scene.fast64.sm64.extend_bank_4: ExtendBank0x04(romfileOutput, segmentData, defaultExtendSegment4) exportRange = [int(context.scene.geoExportStart, 16), int(context.scene.geoExportEnd, 16)] @@ -3139,9 +3139,9 @@ def execute(self, context): armatureObj.select_set(True) context.view_layer.objects.active = armatureObj - if os.path.exists(bpy.path.abspath(context.scene.outputRom)): - os.remove(bpy.path.abspath(context.scene.outputRom)) - os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.outputRom)) + if os.path.exists(bpy.path.abspath(context.scene.fast64.sm64.output_rom)): + os.remove(bpy.path.abspath(context.scene.fast64.sm64.output_rom)) + os.rename(bpy.path.abspath(tempROM), bpy.path.abspath(context.scene.fast64.sm64.output_rom)) if context.scene.geoUseBank0: self.report( @@ -3177,7 +3177,7 @@ def execute(self, context): applyRotation([armatureObj] + linkedArmatures, math.radians(-90), "X") - if context.scene.fast64.sm64.exportType == "Binary": + if context.scene.fast64.sm64.export_type == "Binary": if romfileOutput is not None: romfileOutput.close() if tempROM is not None and os.path.exists(bpy.path.abspath(tempROM)): @@ -3192,7 +3192,7 @@ def execute(self, context): class SM64_ExportGeolayoutPanel(SM64_Panel): bl_idname = "SM64_PT_export_geolayout" bl_label = "SM64 Geolayout Exporter" - goal = "Export Object/Actor/Anim" + goal = "Object/Actor/Anim" # called every frame def draw(self, context): @@ -3200,7 +3200,7 @@ def draw(self, context): propsGeoE = col.operator(SM64_ExportGeolayoutArmature.bl_idname) propsGeoE = col.operator(SM64_ExportGeolayoutObject.bl_idname) - if context.scene.fast64.sm64.exportType == "C": + if context.scene.fast64.sm64.export_type == "C": if context.scene.saveTextures: if context.scene.geoCustomExport: prop_split(col, context.scene, "geoTexDir", "Texture Include Path") @@ -3293,7 +3293,7 @@ def draw(self, context): ) # extendedRAMLabel(col) - elif context.scene.fast64.sm64.exportType == "Insertable Binary": + elif context.scene.fast64.sm64.export_type == "Insertable Binary": col.prop(context.scene, "geoInsertableBinaryPath") else: prop_split(col, context.scene, "geoExportStart", "Start Address") diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index f14cb37a4..6b8c8ed1b 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -837,7 +837,7 @@ def exportLevelC(obj, transformMatrix, levelName, exportDir, savePNG, customExpo cameraVolumeString += "\tNULL_TRIGGER\n};" # Generate levelscript string - compressionFmt = bpy.context.scene.compressionFormat + compressionFmt = bpy.context.scene.fast64.sm64.compression_format replaceSegmentLoad(prevLevelScript, f"_{levelName}_segment_7", f"LOAD_{compressionFmt.upper()}", 0x07) if usesEnvFX: replaceSegmentLoad(prevLevelScript, f"_effect_{compressionFmt}", f"LOAD_{compressionFmt.upper()}", 0x0B) @@ -1154,7 +1154,7 @@ def execute(self, context): raise PluginError("Cannot find level empty.") selectSingleObject(obj) - scaleValue = bpy.context.scene.blenderToSM64Scale + scaleValue = context.scene.fast64.sm64.blender_to_sm64_scale finalTransform = mathutils.Matrix.Diagonal(mathutils.Vector((scaleValue, scaleValue, scaleValue))).to_4x4() except Exception as e: @@ -1169,7 +1169,7 @@ def execute(self, context): levelName = context.scene.levelName triggerName = "sCam" + context.scene.levelName.title().replace(" ", "").replace("_", "") else: - exportPath = bpy.path.abspath(context.scene.decompPath) + exportPath = bpy.path.abspath(context.scene.fast64.sm64.decomp_path) if context.scene.levelOption == "custom": levelName = context.scene.levelName triggerName = "sCam" + context.scene.levelName.title().replace(" ", "").replace("_", "") @@ -1216,7 +1216,7 @@ def execute(self, context): class SM64_ExportLevelPanel(SM64_Panel): bl_idname = "SM64_PT_export_level" bl_label = "SM64 Level Exporter" - goal = "Export Level" + goal = "Level" decomp_only = True # called every frame diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index 83591a7e9..e257b81e4 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -646,7 +646,7 @@ def __init__(self, area, level, permaswap, functionName, position, scale, emptyS # xyz, beginning and end self.begin = (position[0] - scale[0], position[1] - scale[2], position[2] - scale[1]) self.end = (position[0] + scale[0], position[1] + scale[2], position[2] + scale[1]) - camScaleValue = bpy.context.scene.blenderToSM64Scale + camScaleValue = bpy.context.scene.fast64.sm64.blender_to_sm64_scale # xyz for pos and focus obtained from chosen empties or from selected camera (32767 is ignore flag) if camPos != (32767, 32767, 32767): @@ -739,16 +739,11 @@ def exportAreaCommon(areaObj, transformMatrix, geolayout, collision, name): # These are all done in reference to refresh 8 def handleRefreshDiffModelIDs(modelID): - if bpy.context.scene.refreshVer == "Refresh 8" or bpy.context.scene.refreshVer == "Refresh 7": - pass - elif bpy.context.scene.refreshVer == "Refresh 6": + refresh_version = bpy.context.scene.fast64.sm64.refresh_version + if refresh_version == "Refresh 6": if modelID == "MODEL_TWEESTER": modelID = "MODEL_TORNADO" - elif ( - bpy.context.scene.refreshVer == "Refresh 5" - or bpy.context.scene.refreshVer == "Refresh 4" - or bpy.context.scene.refreshVer == "Refresh 3" - ): + elif refresh_version == {"Refresh 3", "Refresh 4", "Refresh 5"}: if modelID == "MODEL_TWEESTER": modelID = "MODEL_TORNADO" elif modelID == "MODEL_WAVE_TRAIL": @@ -761,32 +756,6 @@ def handleRefreshDiffModelIDs(modelID): return modelID -def handleRefreshDiffSpecials(preset): - if ( - bpy.context.scene.refreshVer == "Refresh 8" - or bpy.context.scene.refreshVer == "Refresh 7" - or bpy.context.scene.refreshVer == "Refresh 6" - or bpy.context.scene.refreshVer == "Refresh 5" - or bpy.context.scene.refreshVer == "Refresh 4" - or bpy.context.scene.refreshVer == "Refresh 3" - ): - pass - return preset - - -def handleRefreshDiffMacros(preset): - if ( - bpy.context.scene.refreshVer == "Refresh 8" - or bpy.context.scene.refreshVer == "Refresh 7" - or bpy.context.scene.refreshVer == "Refresh 6" - or bpy.context.scene.refreshVer == "Refresh 5" - or bpy.context.scene.refreshVer == "Refresh 4" - or bpy.context.scene.refreshVer == "Refresh 3" - ): - pass - return preset - - def start_process_sm64_objects(obj, area, transformMatrix, specialsOnly): # spaceRotation = mathutils.Quaternion((1, 0, 0), math.radians(90.0)).to_matrix().to_4x4() @@ -813,7 +782,6 @@ def process_sm64_objects(obj, area, rootMatrix, transformMatrix, specialsOnly): if specialsOnly: if obj.sm64_obj_type == "Special": preset = obj.sm64_special_enum if obj.sm64_special_enum != "Custom" else obj.sm64_obj_preset - preset = handleRefreshDiffSpecials(preset) area.specials.append( SM64_Special_Object( preset, @@ -832,7 +800,7 @@ def process_sm64_objects(obj, area, rootMatrix, transformMatrix, specialsOnly): modelID = obj.sm64_model_enum if obj.sm64_model_enum != "Custom" else obj.sm64_obj_model modelID = handleRefreshDiffModelIDs(modelID) behaviour = ( - func_map[bpy.context.scene.refreshVer][obj.sm64_behaviour_enum] + func_map[bpy.context.scene.fast64.sm64.refresh_version][obj.sm64_behaviour_enum] if obj.sm64_behaviour_enum != "Custom" else obj.sm64_obj_behaviour ) @@ -1033,7 +1001,7 @@ def execute(self, context): context.object.sm64_behaviour_enum = self.sm64_behaviour_enum bpy.context.region.tag_redraw() name = ( - func_map[context.scene.refreshVer][self.sm64_behaviour_enum] + func_map[context.scene.fast64.sm64.refresh_version][self.sm64_behaviour_enum] if self.sm64_behaviour_enum != "Custom" else "Custom" ) @@ -1260,7 +1228,7 @@ def draw(self, context): prop_split(box, levelObj, "backgroundSegment", "Custom Background Segment") segmentExportBox = box.box() segmentExportBox.label( - text=f"Exported Segment: _{levelObj.backgroundSegment}_{context.scene.compressionFormat}SegmentRomStart" + text=f"Exported Segment: _{levelObj.backgroundSegment}_{context.scene.fast64.sm64.compression_format}SegmentRomStart" ) box.prop(obj, "useBackgroundColor") # box.box().label(text = 'Background IDs defined in include/geo_commands.h.') diff --git a/fast64_internal/sm64/sm64_texscroll.py b/fast64_internal/sm64/sm64_texscroll.py index c9e6ec736..f68fe995f 100644 --- a/fast64_internal/sm64/sm64_texscroll.py +++ b/fast64_internal/sm64/sm64_texscroll.py @@ -18,7 +18,7 @@ def readSegmentInfo(baseDir): ldData = ldFile.read() ldFile.close() - compressionFmt = bpy.context.scene.compressionFormat + compressionFmt = bpy.context.scene.fast64.sm64.compression_format segDict = {} for matchResult in re.finditer( "(? 1 else "" + number_part = value[2:] if prefix in bases else value + if not number_part: + raise ValueError("Empty value.") + + base_and_error = bases.get(prefix, decimal) + try: + return int(number_part, base_and_error[0]) + except ValueError as exc: + raise ValueError(f"{value} is not a valid " + base_and_error[1]) from exc + + +def string_int_warning(layout: UILayout, value: str) -> bool: + try: + int_from_str(value) + return True + except Exception as exc: + multilineLabel(layout.box(), str(exc), "ERROR") + return False + + +def string_int_prop(layout: UILayout, data, prop: str, name="", split=True, **prop_kwargs): + if split: + prop_split(layout, data, prop, name, **prop_kwargs) + else: + layout.prop(data, prop, text=name, **prop_kwargs) + return string_int_warning(layout, getattr(data, prop)) diff --git a/fast64_internal/sm64/tools/__init__.py b/fast64_internal/sm64/tools/__init__.py index 8fe5a2236..d2b50fd37 100644 --- a/fast64_internal/sm64/tools/__init__.py +++ b/fast64_internal/sm64/tools/__init__.py @@ -6,3 +6,9 @@ tools_operators_register, tools_operators_unregister, ) + +from .properties import ( + tools_props_register, + tools_props_unregister, + SM64_AddrConvProperties, +) diff --git a/fast64_internal/sm64/tools/operators.py b/fast64_internal/sm64/tools/operators.py index 79bcb8615..ddde8fe60 100644 --- a/fast64_internal/sm64/tools/operators.py +++ b/fast64_internal/sm64/tools/operators.py @@ -1,134 +1,271 @@ import bpy from bpy.utils import register_class, unregister_class -from bpy.types import Operator -from bpy.props import BoolProperty +from bpy.types import Context, Object +from bpy.props import EnumProperty, BoolProperty, IntProperty, FloatProperty, StringProperty from bpy.path import abspath -from ...operators import AddWaterBox -from ...utility import PluginError, checkExpanded, decodeSegmentedAddr, encodeSegmentedAddr, raisePluginError +from ...operators import OperatorBase, AddWaterBox +from ...utility import PluginError, decodeSegmentedAddr, encodeSegmentedAddr +from ...f3d.f3d_material import getDefaultMaterialPreset, createF3DMat, add_f3d_mat_to_obj +from ...utility import parentObject, intToHex, bytesToHex -from ..sm64_constants import level_pointers +from ..sm64_constants import level_pointers, levelIDNames, level_enums +from ..sm64_utility import import_rom_checks, int_from_str from ..sm64_level_parser import parseLevelAtPointer from ..sm64_geolayout_utility import createBoneGroups from ..sm64_geolayout_parser import generateMetarig +enum_address_conversion_options = [ + ("TO_VIR", "Segmented To Virtual", "Convert address from segmented to virtual"), + ("TO_SEG", "Virtual To Segmented", "Convert address from virtual to segmented"), +] -class SM64_AddrConv(Operator): - # set bl_ properties - bl_idname = "object.addr_conv" - bl_label = "Convert Address" + +class SM64_AddrConv(OperatorBase): + bl_idname = "scene.sm64_addr_conv" + bl_label = "Convert SM64 Address" + bl_description = "Converts a segmented address to a virtual address or vice versa" bl_options = {"REGISTER", "UNDO", "PRESET"} - segToVirt: BoolProperty() - - def execute(self, context): - romfileSrc = None - try: - address = int(context.scene.convertibleAddr, 16) - importRom = context.scene.importRom - romfileSrc = open(abspath(importRom), "rb") - checkExpanded(abspath(importRom)) - levelParsed = parseLevelAtPointer(romfileSrc, level_pointers[context.scene.levelConvert]) - segmentData = levelParsed.segmentData - if self.segToVirt: - ptr = decodeSegmentedAddr(address.to_bytes(4, "big"), segmentData) - self.report({"INFO"}, "Virtual pointer is 0x" + format(ptr, "08X")) - else: - ptr = int.from_bytes(encodeSegmentedAddr(address, segmentData), "big") - self.report({"INFO"}, "Segmented pointer is 0x" + format(ptr, "08X")) - romfileSrc.close() - return {"FINISHED"} - except Exception as e: - if romfileSrc is not None: - romfileSrc.close() - raisePluginError(self, e) - return {"CANCELLED"} # must return a set - - -class AddBoneGroups(bpy.types.Operator): - # set bl_ properties + rom: StringProperty(name="ROM", subtype="FILE_PATH") + # Using an enum here looks cleaner when using this as an operator + option: EnumProperty(name="Conversion type", items=enum_address_conversion_options) + level: EnumProperty(items=level_enums, name="Level", default="IC") + addr: StringProperty(name="Address") + clipboard: BoolProperty(name="Copy to clipboard", default=True) + result: StringProperty(name="Result") + + def execute_operator(self, context: Context): + addr = int_from_str(self.addr) + import_rom_path = abspath(self.rom) + import_rom_checks(import_rom_path) + with open(import_rom_path, "rb") as romfile: + level_parsed = parseLevelAtPointer(romfile, level_pointers[self.level]) + segment_data = level_parsed.segmentData + if self.option == "TO_VIR": + result = intToHex(decodeSegmentedAddr(addr.to_bytes(4, "big"), segment_data)) + self.report({"INFO"}, f"Virtual pointer is {result}") + elif self.option == "TO_SEG": + result = bytesToHex(encodeSegmentedAddr(addr, segment_data)) + self.report({"INFO"}, f"Segmented pointer is {result}") + else: + raise NotImplementedError(f"Non implement conversion option {self.option}") + self.result = result + if self.clipboard: + context.window_manager.clipboard = result + + +class SM64_AddBoneGroups(OperatorBase): bl_description = ( "Add bone groups respresenting other node types in " + "SM64 geolayouts (ex. Shadow, Switch, Function)." ) bl_idname = "object.add_bone_groups" bl_label = "Add Bone Groups" bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "GROUP_BONE" - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - try: - if context.mode != "OBJECT" and context.mode != "POSE": - raise PluginError("Operator can only be used in object or pose mode.") - elif context.mode == "POSE": - bpy.ops.object.mode_set(mode="OBJECT") - - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - elif type(context.selected_objects[0].data) is not bpy.types.Armature: - raise PluginError("Armature not selected.") - - armatureObj = context.selected_objects[0] - createBoneGroups(armatureObj) - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} + def execute_operator(self, context: Context): + if len(context.selected_objects) == 0: + raise PluginError("Armature not selected.") + elif len(context.selected_objects) > 1: + raise PluginError("More than one object selected.") + elif context.selected_objects[0].type != "ARMATURE": + raise PluginError("Selected object is not an armature.") + + armature_obj: Object = context.selected_objects[0] + createBoneGroups(armature_obj) self.report({"INFO"}, "Created bone groups.") - return {"FINISHED"} # must return a set -class CreateMetarig(bpy.types.Operator): - # set bl_ properties +class SM64_CreateMetarig(OperatorBase): bl_description = ( "SM64 imported armatures are usually not good for " + "rigging. There are often intermediate bones between deform bones " + "and they don't usually point to their children. This operator " + "creates a metarig on armature layer 4 useful for IK." ) - bl_idname = "object.create_metarig" + bl_idname = "object.sm64_create_metarig" bl_label = "Create Animatable Metarig" bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "BONE_DATA" - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - try: - if context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - - if len(context.selected_objects) == 0: - raise PluginError("Armature not selected.") - elif type(context.selected_objects[0].data) is not bpy.types.Armature: - raise PluginError("Armature not selected.") + def execute_operator(self, context: Context): + if len(context.selected_objects) == 0: + raise PluginError("Armature not selected.") + elif len(context.selected_objects) > 1: + raise PluginError("More than one object selected.") + elif context.selected_objects[0].type != "ARMATURE": + raise PluginError("Selected object is not an armature.") - armatureObj = context.selected_objects[0] - generateMetarig(armatureObj) - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} + armature_obj: Object = context.selected_objects[0] + generateMetarig(armature_obj) self.report({"INFO"}, "Created metarig.") - return {"FINISHED"} # must return a set + + +def get_clean_obj_duplicate_name(name: str): + objects = bpy.data.objects + num = 1 + while (num == 1 and name in objects) or f"{name} {num}" in objects: + num += 1 + if num > 1: + name = f"{name} {num}" + return name + + +def create_sm64_empty( + name: str, + obj_type: str, + empty_type: str = "CUBE", + location=(0.0, 0.0, 0.0), + rotation=(0.0, 0.0, 0.0), +) -> Object: + bpy.ops.object.empty_add(type=empty_type, align="CURSOR", location=location, rotation=rotation) + obj = bpy.context.view_layer.objects.active + obj.name, obj.sm64_obj_type = get_clean_obj_duplicate_name(name), obj_type + return obj + + +class SM64_CreateSimpleLevel(OperatorBase): + bl_idname = "scene.sm64_create_simple_level" + bl_label = "Create Level Layout" + bl_description = "Creates a simple SM64 level layout" + "with a user defined area amount and death plane" + bl_options = {"REGISTER", "UNDO", "PRESET"} + context_mode = "OBJECT" + icon = "CUBE" + + area_amount: IntProperty(name="Area Amount", default=1, min=1, max=8) + add_death_plane: BoolProperty(name="Add Death Plane") + set_as_start_level: BoolProperty(name="Set As Start Level") + respawn_in_level: BoolProperty(name="Respawn In The Same Level") + + def execute_operator(self, context: Context): + scene = context.scene + + level_object = create_sm64_empty("Level", "Level Root", "PLAIN_AXES", (0, 0, 0)) + level_object.setAsStartLevel = self.set_as_start_level + + preset = getDefaultMaterialPreset("Shaded Solid") + example_mat = createF3DMat(None, preset) + + example_mat.name = "Grass Example" + example_mat.f3d_mat.default_light_color = (0, 1, 0, 1) + example_mat.collision_type_simple = ( + example_mat.collision_type + ) = example_mat.collision_custom = "SURFACE_NOISE_DEFAULT" + + preset = getDefaultMaterialPreset("Shaded Solid") + + if self.add_death_plane: + death_mat = createF3DMat(None, preset) + death_mat.name = "Death Plane" + death_mat.collision_type_simple = ( + death_mat.collision_type + ) = death_mat.collision_custom = "SURFACE_DEATH_PLANE" + + scale = context.scene.fast64.sm64.blender_to_sm64_scale + mario_scale = (50 / scale, 50 / scale, 160 / 2 / scale) + mario_height = mario_scale[2] + + for i in range(self.area_amount): + y_offset = 4000 / scale * i + location_offset = (0, y_offset, 0) + + area_num = i + 1 + area_object = create_sm64_empty(f"Area {area_num}", "Area Root", "PLAIN_AXES", location_offset) + area_object.areaIndex = area_num + + custom_level_id = "LEVEL_BOB" + for key, value in levelIDNames.items(): + if value == scene.levelName: + custom_level_id = key + + area_object.warpNodes.add() + area_object.warpNodes[-1].warpID = "0x0A" # Spin warp + area_object.warpNodes[-1].destLevel = custom_level_id + area_object.warpNodes[-1].destLevelEnum = scene.levelOption + area_object.warpNodes[-1].destNode = "0x0A" + + area_object.warpNodes.add() + area_object.warpNodes[-1].warpID = "0xF0" # Default + area_object.warpNodes[-1].destLevelEnum = "castle_inside" + area_object.warpNodes[-1].destNode = "0x32" + + area_object.warpNodes.add() + area_object.warpNodes[-1].warpID = "0xF1" # Death + if self.respawn_in_level: + area_object.warpNodes[-1].destLevelEnum = scene.levelOption + area_object.warpNodes[-1].destLevel = custom_level_id + area_object.warpNodes[-1].destNode = "0x0A" + else: + area_object.warpNodes[-1].destLevelEnum = "castle_inside" + area_object.warpNodes[-1].destNode = "0x64" + + parentObject(level_object, area_object) + + bpy.ops.mesh.primitive_plane_add( + size=1000 / scale, align="CURSOR", location=location_offset, rotation=(0, 0, 0) + ) + plane_object = context.view_layer.objects.active + plane_object.name = get_clean_obj_duplicate_name("Level Mesh") + plane_object.data.name = "Mesh" + add_f3d_mat_to_obj(plane_object, example_mat) + parentObject(area_object, plane_object) + + if self.add_death_plane: + bpy.ops.mesh.primitive_plane_add( + size=2500 / scale, align="CURSOR", location=(0, y_offset, -2500 / scale), rotation=(0, 0, 0) + ) + death_plane_obj = context.view_layer.objects.active + death_plane_obj.name = get_clean_obj_duplicate_name("(Collision Only) Death Plane") + death_plane_obj.data.name = "Death Plane" + death_plane_obj.ignore_render = True + add_f3d_mat_to_obj(death_plane_obj, death_mat) + parentObject(area_object, death_plane_obj) + + if i == 0: + mario_start_object = create_sm64_empty( + "Hardcoded Level Start Position", "Mario Start", location=(0, y_offset, mario_height) + ) + mario_start_object.scale = mario_scale + parentObject(area_object, mario_start_object) + + warp_object = create_sm64_empty("Warp", "Object", location=(0, y_offset, mario_height)) + parentObject(area_object, warp_object) + warp_object.scale = mario_scale + warp_object.sm64_behaviour_enum, warp_object.sm64_obj_behaviour = "13002f74", "bhvSpinAirborneWarp" + warp_game_object = warp_object.fast64.sm64.game_object + warp_game_object.use_individual_params = True + warp_game_object.bparam2 = "0x0A" + warp_game_object.bparams = "0x000A0000" + + bpy.ops.object.select_all(action="DESELECT") + level_object.select_set(True) + bpy.context.view_layer.objects.active = level_object class SM64_AddWaterBox(AddWaterBox): bl_idname = "object.sm64_add_water_box" - scale: bpy.props.FloatProperty(default=10) - preset: bpy.props.StringProperty(default="Shaded Solid") - matName: bpy.props.StringProperty(default="sm64_water_mat") + scale: FloatProperty(default=10) + preset: StringProperty(default="Shaded Solid") + matName: StringProperty(default="sm64_water_mat") - def setEmptyType(self, emptyObj): - emptyObj.sm64_obj_type = "Water Box" + def setEmptyType(self, empty_object: Object): + empty_object.sm64_obj_type = "Water Box" classes = ( SM64_AddrConv, - AddBoneGroups, - CreateMetarig, - AddWaterBox, + SM64_CreateSimpleLevel, + SM64_AddBoneGroups, + SM64_CreateMetarig, + SM64_AddWaterBox, ) diff --git a/fast64_internal/sm64/tools/panels.py b/fast64_internal/sm64/tools/panels.py index a8b274076..5a41accbe 100644 --- a/fast64_internal/sm64/tools/panels.py +++ b/fast64_internal/sm64/tools/panels.py @@ -1,41 +1,37 @@ from bpy.utils import register_class, unregister_class -from ...utility import prop_split -from ...utility_anim import ArmatureApplyWithMeshOperator -from ...panels import SM64_Panel, sm64GoalImport +from ...panels import SM64_Panel -from .operators import AddBoneGroups, CreateMetarig, SM64_AddWaterBox, SM64_AddrConv +from .operators import SM64_CreateSimpleLevel, SM64_AddWaterBox, SM64_AddBoneGroups, SM64_CreateMetarig +from typing import TYPE_CHECKING -class SM64_ArmatureToolsPanel(SM64_Panel): - bl_idname = "SM64_PT_armature_tools" +if TYPE_CHECKING: + from ..settings.properties import SM64_Properties + + +class SM64_ToolsPanel(SM64_Panel): + bl_idname = "SM64_PT_tools" bl_label = "SM64 Tools" - # called every frame def draw(self, context): col = self.layout.column() - col.operator(ArmatureApplyWithMeshOperator.bl_idname) - col.operator(AddBoneGroups.bl_idname) - col.operator(CreateMetarig.bl_idname) - col.operator(SM64_AddWaterBox.bl_idname) - + col.label(text="Misc Tools", icon="TOOL_SETTINGS") + SM64_CreateSimpleLevel.draw_props(col) + SM64_AddWaterBox.draw_props(col) -class SM64_AddressConvertPanel(SM64_Panel): - bl_idname = "SM64_PT_addr_conv" - bl_label = "SM64 Address Converter" - goal = sm64GoalImport + col.label(text="Armature Tools", icon="ARMATURE_DATA") + SM64_AddBoneGroups.draw_props(col) + SM64_CreateMetarig.draw_props(col) - def draw(self, context): - col = self.layout.column() - segToVirtOp = col.operator(SM64_AddrConv.bl_idname, text="Convert Segmented To Virtual") - segToVirtOp.segToVirt = True - virtToSegOp = col.operator(SM64_AddrConv.bl_idname, text="Convert Virtual To Segmented") - virtToSegOp.segToVirt = False - prop_split(col, context.scene, "convertibleAddr", "Address") - col.prop(context.scene, "levelConvert") + sm64_props: SM64_Properties = context.scene.fast64.sm64 + if not sm64_props.show_importing_menus: + return + col.label(text="Address Converter", icon="MEMORY") + sm64_props.address_converter.draw_props(col.box(), sm64_props.import_rom) -classes = (SM64_ArmatureToolsPanel, SM64_AddressConvertPanel) +classes = (SM64_ToolsPanel,) def tools_panels_register(): diff --git a/fast64_internal/sm64/tools/properties.py b/fast64_internal/sm64/tools/properties.py new file mode 100644 index 000000000..9384a2239 --- /dev/null +++ b/fast64_internal/sm64/tools/properties.py @@ -0,0 +1,55 @@ +from os import PathLike + +from bpy.path import abspath +from bpy.types import PropertyGroup, UILayout, Scene +from bpy.props import StringProperty, EnumProperty, BoolProperty +from bpy.utils import register_class, unregister_class + +from ...utility import prop_split, intToHex +from ..sm64_utility import string_int_prop, import_rom_ui_warnings +from ..sm64_constants import level_enums + +from .operators import SM64_AddrConv + + +class SM64_AddrConvProperties(PropertyGroup): + rom: StringProperty(name="Import ROM", subtype="FILE_PATH") + address: StringProperty(name="Address") + level: EnumProperty(items=level_enums, name="Level", default="IC") + clipboard: BoolProperty(name="Copy to Clipboard", default=True) + + def upgrade_changed_props(self, scene: Scene): + old_address = scene.pop("convertibleAddr", None) + if old_address is not None: + self.address = intToHex(int(old_address, 16)) + old_level = scene.pop("level", None) + if old_level is not None: + self["level"] = old_level + + def draw_props(self, layout: UILayout, import_rom: PathLike = None): + col = layout.column() + col.label(text="Uses scene import ROM by default", icon="INFO") + prop_split(col, self, "rom", "ROM") + picked_rom = abspath(self.rom if self.rom else import_rom) + if not import_rom_ui_warnings(col, picked_rom): + return + col.prop(self, "level") + if string_int_prop(col, self, "address", "Address"): + col.prop(self, "clipboard") + split = col.split() + args = {"rom": picked_rom, "level": self.level, "addr": self.address, "clipboard": self.clipboard} + SM64_AddrConv.draw_props(split, text="Segmented to Virtual", option="TO_VIR", **args) + SM64_AddrConv.draw_props(split, text="Virtual To Segmented", option="TO_SEG", **args) + + +classes = (SM64_AddrConvProperties,) + + +def tools_props_register(): + for cls in classes: + register_class(cls) + + +def tools_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 02beb9100..ff40a5912 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -2,7 +2,7 @@ from math import pi, ceil, degrees, radians, copysign from mathutils import * from .utility_anim import * -from typing import Callable, Iterable, Any, Tuple, Union +from typing import Callable, Iterable, Any, Optional, Tuple, Union from bpy.types import UILayout CollectionProperty = Any # collection prop as defined by using bpy.props.CollectionProperty @@ -39,6 +39,24 @@ class VertexWeightError(PluginError): ("Level", "Level Data", "Headers are written to a specific level in levels/"), ] +# bpy.context.mode returns the keys here, while the values are required by bpy.ops.object.mode_set +CONTEXT_MODE_TO_MODE_SET = { + "PAINT_VERTEX": "VERTEX_PAINT", + "PAINT_WEIGHT": "WEIGHT_PAINT", + "PAINT_TEXTURE": "TEXTURE_PAINT", + "PARTICLE": "PARTICLE_EDIT", + "EDIT_GREASE_PENCIL": "EDIT_GPENCIL", +} + + +def get_mode_set_from_context_mode(context_mode: str): + if context_mode in CONTEXT_MODE_TO_MODE_SET: + return CONTEXT_MODE_TO_MODE_SET[context_mode] + elif context_mode.startswith("EDIT"): + return "EDIT" + else: + return context_mode + def isPowerOf2(n): return (n & (n - 1) == 0) and n != 0 @@ -394,22 +412,12 @@ def extendedRAMLabel(layout): infoBox.label(text="Extended RAM prevents crashes.") -def checkExpanded(filepath): - size = os.path.getsize(filepath) - if size < 9000000: # check if 8MB - raise PluginError( - "ROM at " - + filepath - + " is too small. You may be using an unexpanded ROM. You can expand a ROM by opening it in SM64 Editor or ROM Manager." - ) - - def getPathAndLevel(customExport, exportPath, levelName, levelOption): if customExport: exportPath = bpy.path.abspath(exportPath) levelName = levelName else: - exportPath = bpy.path.abspath(bpy.context.scene.decompPath) + exportPath = bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) if levelOption == "custom": levelName = levelName else: @@ -468,8 +476,8 @@ def saveDataToFile(filepath, data): def applyBasicTweaks(baseDir): - enableExtendedRAM(baseDir) - return + if bpy.context.scene.fast64.sm64.force_extended_ram: + enableExtendedRAM(baseDir) def enableExtendedRAM(baseDir): @@ -1197,6 +1205,85 @@ def multilineLabel(layout: UILayout, text: str, icon: str = "NONE"): r.scale_y = 0.75 +def draw_and_check_tab( + layout: UILayout, data, proprety: str, text: Optional[str] = None, icon: Optional[str] = None +) -> bool: + row = layout.row(align=True) + tab = getattr(data, proprety) + tria_icon = "TRIA_DOWN" if tab else "TRIA_RIGHT" + if icon is not None: + row.prop(data, proprety, icon=tria_icon, text="") + row.prop(data, proprety, icon=tria_icon if icon is None else icon, text=text) + if tab: + layout.separator() + return tab + + +def run_and_draw_errors(layout: UILayout, func, *args): + try: + func(*args) + return True + except Exception as e: + multilineLabel(layout.box(), str(e), "ERROR") + return False + + +def path_checks(path: str, empty="Empty path.", doesnt_exist="Path {}does not exist.", include_path=True): + path_in_error = f'"{path}" ' if include_path else "" + if path == "": + raise PluginError(empty) + elif not os.path.exists(path): + raise FileNotFoundError(doesnt_exist.format(path_in_error)) + + +def path_ui_warnings(layout: bpy.types.UILayout, path: str, empty="Empty path.", doesnt_exist="Path does not exist."): + return run_and_draw_errors(layout, path_checks, path, empty, doesnt_exist, False) + + +def directory_path_checks( + path: str, + empty="Empty path.", + doesnt_exist="Directory {}does not exist.", + not_a_directory="Path {}is not a folder.", + include_path=True, +): + path_checks(path, empty, doesnt_exist, include_path) + if not os.path.isdir(path): + raise NotADirectoryError(not_a_directory.format(f'"{path}" ' if include_path else "")) + + +def directory_ui_warnings( + layout: bpy.types.UILayout, + path: str, + empty="Empty path.", + doesnt_exist="Directory does not exist.", + not_a_directory="Path is not a folder.", +): + return run_and_draw_errors(layout, directory_path_checks, path, empty, doesnt_exist, not_a_directory, False) + + +def filepath_checks( + path: str, + empty="Empty path.", + doesnt_exist="File {}does not exist.", + not_a_file="Path {}is not a file.", + include_path=True, +): + path_checks(path, empty, doesnt_exist, include_path) + if not os.path.isfile(path): + raise IsADirectoryError(not_a_file.format(f'"{path}" ' if include_path else "")) + + +def filepath_ui_warnings( + layout: bpy.types.UILayout, + path: str, + empty="Empty path.", + doesnt_exist="File does not exist.", + not_a_file="Path is not a file.", +): + return run_and_draw_errors(layout, filepath_checks, path, empty, doesnt_exist, not_a_file, False) + + def toAlnum(name, exceptions=[]): if name is None or name == "": return None @@ -1246,6 +1333,10 @@ def exportColor(lightColor): return [scaleToU8(value) for value in gammaCorrect(lightColor)] +def get_clean_color(srgb: list, include_alpha=False, round_color=True) -> list: + return [round(channel, 4) if round_color else channel for channel in list(srgb[: 4 if include_alpha else 3])] + + def printBlenderMessage(msgSet, message, blenderOp): if blenderOp is not None: blenderOp.report(msgSet, message) @@ -1307,7 +1398,10 @@ def readVectorFromShorts(command, offset): def readFloatFromShort(command, offset): - return int.from_bytes(command[offset : offset + 2], "big", signed=True) / bpy.context.scene.blenderToSM64Scale + return ( + int.from_bytes(command[offset : offset + 2], "big", signed=True) + / bpy.context.scene.fast64.sm64.blender_to_sm64_scale + ) def writeVectorToShorts(command, offset, values): @@ -1317,13 +1411,13 @@ def writeVectorToShorts(command, offset, values): def writeFloatToShort(command, offset, value): - command[offset : offset + 2] = int(round(value * bpy.context.scene.blenderToSM64Scale)).to_bytes( + command[offset : offset + 2] = int(round(value * bpy.context.scene.fast64.sm64.blender_to_sm64_scale)).to_bytes( 2, "big", signed=True ) def convertFloatToShort(value): - return int(round((value * bpy.context.scene.blenderToSM64Scale))) + return int(round((value * bpy.context.scene.fast64.sm64.blender_to_sm64_scale))) def convertEulerFloatToShort(value): @@ -1527,7 +1621,7 @@ def all_values_equal_x(vals: Iterable, test): def get_blender_to_game_scale(context): match context.scene.gameEditorMode: case "SM64": - return context.scene.blenderToSM64Scale + return context.scene.fast64.sm64.blender_to_sm64_scale case "OOT": return context.scene.ootBlenderScale case "F3D": @@ -1622,3 +1716,43 @@ def getTextureSuffixFromFormat(texFmt): ast.BitAnd: operator.and_, ast.BitXor: operator.xor, } + + +def prop_group_to_json(prop_group, blacklist: list[str] = None, whitelist: list[str] = None): + blacklist = ["rna_type", "name"] + (blacklist or []) + + def prop_to_json(prop): + if isinstance(prop, list) or type(prop).__name__ == "bpy_prop_collection_idprop": + prop = list(prop) + for index, value in enumerate(prop): + prop[index] = prop_to_json(value) + return prop + elif isinstance(prop, Color): + return get_clean_color(prop) + elif hasattr(prop, "to_list"): # for IDPropertyArray classes + return prop.to_list() + elif hasattr(prop, "to_dict"): + return prop.to_dict() + else: + return prop + + data = {} + for prop in iter_prop(prop_group): + if prop in blacklist or (whitelist and prop not in whitelist): + continue + value = prop_to_json(getattr(prop_group, prop)) + if value is not None: + data[prop] = value + return data + + +def json_to_prop_group(prop_group, data: dict, blacklist: list[str] = None, whitelist: list[str] = None): + blacklist = ["rna_type", "name"] + (blacklist or []) + for prop in iter_prop(prop_group): + if prop in blacklist or (whitelist and prop not in whitelist): + continue + default = getattr(prop_group, prop) + if hasattr(default, "from_dict"): + default.from_dict(data.get(prop, None)) + else: + setattr(prop_group, prop, data.get(prop, default))