diff --git a/LowPolySkinnedMario.blend b/LowPolySkinnedMario.blend deleted file mode 100644 index f000d488c..000000000 Binary files a/LowPolySkinnedMario.blend and /dev/null differ diff --git a/LowPolySkinnedMario_V5.blend b/LowPolySkinnedMario_V5.blend deleted file mode 100644 index 53dcda582..000000000 Binary files a/LowPolySkinnedMario_V5.blend and /dev/null differ diff --git a/README.md b/README.md index 30fc20dc1..3cb45df09 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ Make sure to save often, as this plugin is prone to crashing when creating mater +### Example models can be found [here](https://github.com/Fast-64/fast64-models) ![alt-text](/images/mat_inspector.png) ### Credits Thanks to anonymous_moose, Cheezepin, Rovert, and especially InTheBeef for testing. -Thanks to InTheBeef for LowPolySkinnedMario. ### Discord Server We have a Discord server for support as well as development [here](https://discord.gg/ny7PDcN2x8). diff --git a/__init__.py b/__init__.py index 82517f8c9..d822f902b 100644 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ from . import addon_updater_ops -from .fast64_internal.utility import prop_split, multilineLabel, draw_and_check_tab +from .fast64_internal.utility import prop_split, multilineLabel, set_prop_if_in_data from .fast64_internal.repo_settings import ( draw_repo_settings, @@ -49,6 +49,7 @@ from .fast64_internal.render_settings import ( Fast64RenderSettings_Properties, + ManualUpdatePreviewOperator, resync_scene_props, on_update_render_settings, ) @@ -56,7 +57,7 @@ # info about add on bl_info = { "name": "Fast64", - "version": (2, 2, 0), + "version": (2, 3, 0), "author": "kurethedead", "location": "3DView", "description": "Plugin for exporting F3D display lists and other game data related to Nintendo 64 games.", @@ -87,7 +88,7 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() col.scale_y = 1.1 # extra padding - prop_split(col, context.scene, "f3d_type", "F3D Microcode") + prop_split(col, context.scene, "f3d_type", "Microcode") col.prop(context.scene, "saveTextures") col.prop(context.scene, "f3d_simple", text="Simple Material UI") col.prop(context.scene, "exportInlineF3D", text="Bleed and Inline Material Exports") @@ -212,6 +213,19 @@ class Fast64Settings_Properties(bpy.types.PropertyGroup): internal_game_update_ver: bpy.props.IntProperty(default=0) + def to_repo_settings(self): + data = {} + data["autoLoad"] = self.auto_repo_load_settings + data["autoPickTextureFormat"] = self.auto_pick_texture_format + if self.auto_pick_texture_format: + data["preferRGBAOverCI"] = self.prefer_rgba_over_ci + return data + + def from_repo_settings(self, data: dict): + set_prop_if_in_data(self, "auto_repo_load_settings", data, "autoLoad") + set_prop_if_in_data(self, "auto_pick_texture_format", data, "autoPickTextureFormat") + set_prop_if_in_data(self, "prefer_rgba_over_ci", data, "preferRGBAOverCI") + class Fast64_Properties(bpy.types.PropertyGroup): """ @@ -311,6 +325,7 @@ def draw(self, context): classes = ( Fast64Settings_Properties, Fast64RenderSettings_Properties, + ManualUpdatePreviewOperator, Fast64_Properties, Fast64_BoneProperties, Fast64_ObjectProperties, @@ -328,9 +343,10 @@ def upgrade_changed_props(): SM64_ObjectProperties.upgrade_changed_props() OOT_ObjectProperties.upgrade_changed_props() for scene in bpy.data.scenes: - if scene.fast64.settings.internal_game_update_ver != 1: - gameEditorUpdate(scene, bpy.context) - scene.fast64.settings.internal_game_update_ver = 1 + settings: Fast64Settings_Properties = scene.fast64.settings + if settings.internal_game_update_ver != 1: + set_game_defaults(scene, False) + settings.internal_game_update_ver = 1 if scene.get("decomp_compatible", False): scene.gameEditorMode = "Homebrew" del scene["decomp_compatible"] @@ -363,23 +379,34 @@ def after_load(_a, _b): upgrade_changed_props() upgrade_scene_props_node() resync_scene_props() + try: + if settings.repo_settings_path: + load_repo_settings(bpy.context.scene, abspath(settings.repo_settings_path), True) + except Exception as exc: + print(exc) -def gameEditorUpdate(self, context): +def set_game_defaults(scene: bpy.types.Scene, set_ucode=True): world_defaults = None - if self.gameEditorMode == "SM64": - self.f3d_type = "F3D" + if scene.gameEditorMode == "SM64": + f3d_type = "F3D" world_defaults = sm64_world_defaults - elif self.gameEditorMode == "OOT": - self.f3d_type = "F3DEX2/LX2" + elif scene.gameEditorMode == "OOT": + f3d_type = "F3DEX2/LX2" world_defaults = oot_world_defaults - elif self.gameEditorMode == "MK64": - self.f3d_type = "F3DEX/LX" - elif self.gameEditorMode == "Homebrew": - self.f3d_type = "F3D" + elif scene.gameEditorMode == "MK64": + f3d_type = "F3DEX/LX" + elif scene.gameEditorMode == "Homebrew": + f3d_type = "F3D" world_defaults = {} # This will set some pretty bad defaults, but trust the user - if self.world is not None: - self.world.rdp_defaults.from_dict(world_defaults) + if set_ucode: + scene.f3d_type = f3d_type + if scene.world is not None: + scene.world.rdp_defaults.from_dict(world_defaults) + + +def gameEditorUpdate(scene: bpy.types.Scene, _context): + set_game_defaults(scene) # called on add-on enabling diff --git a/fast64_internal/f3d/f3d_bleed.py b/fast64_internal/f3d/f3d_bleed.py index 9e6d7da44..0e6faaa9b 100644 --- a/fast64_internal/f3d/f3d_bleed.py +++ b/fast64_internal/f3d/f3d_bleed.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, field +from ..utility import create_or_get_world from .f3d_gbi import ( GfxTag, GfxListTag, @@ -63,7 +64,7 @@ def __init__(self): self.build_default_othermodes() def build_default_geo(self): - defaults = bpy.context.scene.world.rdp_defaults + defaults = create_or_get_world(bpy.context.scene).rdp_defaults setGeo = SPSetGeometryMode([]) clearGeo = SPClearGeometryMode([]) @@ -91,7 +92,7 @@ def place_in_flaglist(flag: bool, enum: str, set_list: SPSetGeometryMode, clear_ self.default_clear_geo = clearGeo def build_default_othermodes(self): - defaults = bpy.context.scene.world.rdp_defaults + defaults = create_or_get_world(bpy.context.scene).rdp_defaults othermode_H = SPSetOtherMode("G_SETOTHERMODE_H", 4, 20 - self.is_f3d_old, []) # if the render mode is set, it will be consider non-default a priori @@ -225,9 +226,8 @@ def bleed_textures(self, cur_fmat: FMaterial, last_mat: FMaterial, bleed_state: for j, cmd in enumerate(cur_fmat.texture_DL.commands): if not cmd: continue # some cmds are None from previous step - if self.bleed_individual_cmd(commands_bled, cmd, bleed_state): - if cmd in last_mat.texture_DL.commands: - commands_bled.commands[j] = None + if self.bleed_individual_cmd(commands_bled, cmd, bleed_state, last_mat.texture_DL.commands) is True: + commands_bled.commands[j] = None # remove Nones from list while None in commands_bled.commands: commands_bled.commands.remove(None) @@ -479,9 +479,15 @@ def bleed_SPSetOtherMode(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: in else: return cmd == self.default_othermode_L - # bleed if there are no tags to scroll and cmd was in last list - def bleed_DPSetTileSize(self, cmd_list: GfxList, cmd: GbiMacro, bleed_state: int, last_cmd_list: GfxList = None): - return cmd.tags != GfxTag.TileScroll0 and cmd.tags != GfxTag.TileScroll1 and cmd in last_cmd_list + # Don´t bleed if the cmd is used for scrolling or if the last cmd's tags are not the same (those are not hashed) + def bleed_DPSetTileSize(self, _cmd_list: GfxList, cmd: GbiMacro, _bleed_state: int, last_cmd_list: GfxList = None): + if cmd.tags == GfxTag.TileScroll0 or cmd.tags == GfxTag.TileScroll1: + return False + if cmd in last_cmd_list: + last_size_cmd = last_cmd_list[last_cmd_list.index(cmd)] + if last_size_cmd.tags == cmd.tags: + return True + return False # At most, only one sync is needed after drawing tris. The f3d writer should # already have placed the appropriate sync type required. If a second sync is diff --git a/fast64_internal/f3d/f3d_enums.py b/fast64_internal/f3d/f3d_enums.py index 993a87316..d9e032c23 100644 --- a/fast64_internal/f3d/f3d_enums.py +++ b/fast64_internal/f3d/f3d_enums.py @@ -378,13 +378,22 @@ } enumF3D = [ - ("F3D", "F3D", "Original microcode used in SM64"), - ("F3DEX/LX", "F3DEX/LX", "F3DEX version 1"), - ("F3DLX.Rej", "F3DLX.Rej", "F3DLX.Rej"), - ("F3DLP.Rej", "F3DLP.Rej", "F3DLP.Rej"), - ("F3DEX2/LX2", "F3DEX2/LX2/ZEX", "Family of microcodes used in later N64 games including OoT and MM"), - ("F3DEX2.Rej/LX2.Rej", "F3DEX2.Rej/LX2.Rej", "Variant of F3DEX2 family using vertex rejection instead of clipping"), - ("F3DEX3", "F3DEX3", "Custom microcode by Sauraen"), + ("", "F3D Family", "", 7), + ("F3D", "F3D", "Original microcode used in SM64", 0), + ("F3DEX/LX", "F3DEX/LX", "F3DEX version 1", 1), + ("F3DLX.Rej", "F3DLX.Rej", "F3DLX.Rej", 2), + ("F3DLP.Rej", "F3DLP.Rej", "F3DLP.Rej", 3), + ("F3DEX2/LX2", "F3DEX2/LX2/ZEX", "Family of microcodes used in later N64 games including OoT and MM", 4), + ( + "F3DEX2.Rej/LX2.Rej", + "F3DEX2.Rej/LX2.Rej", + "Variant of F3DEX2 family using vertex rejection instead of clipping", + 5, + ), + ("F3DEX3", "F3DEX3", "Custom microcode by Sauraen", 6), + ("", "Homebrew", "", 8), + ("RDPQ", "RDPQ", "Base libdragon microcode", 9), + ("T3D", "Tiny3D", "Custom libdragon microcode by HailToDodongo", 10), ] enumLargeEdges = [ diff --git a/fast64_internal/f3d/f3d_gbi.py b/fast64_internal/f3d/f3d_gbi.py index 931b6cd49..ee9325e42 100644 --- a/fast64_internal/f3d/f3d_gbi.py +++ b/fast64_internal/f3d/f3d_gbi.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Sequence, Union, Tuple -from dataclasses import dataclass, fields +from dataclasses import dataclass, fields, field import bpy, os, enum, copy from ..utility import * @@ -70,17 +70,29 @@ class GfxMatWriteMethod(enum.Enum): "F3DEX2/LX2": (32, 32), "F3DEX2.Rej/LX2.Rej": (64, 64), "F3DEX3": (56, 56), + "T3D": (70, 70), } -drawLayerRenderMode = { - 0: ("G_RM_ZB_OPA_SURF", "G_RM_NOOP2"), - 1: ("G_RM_AA_ZB_OPA_SURF", "G_RM_NOOP2"), - 2: ("G_RM_AA_ZB_OPA_DECAL", "G_RM_NOOP2"), - 3: ("G_RM_AA_ZB_OPA_INTER", "G_RM_NOOP2"), - 4: ("G_RM_AA_ZB_TEX_EDGE", "G_RM_NOOP2"), - 5: ("G_RM_AA_ZB_XLU_SURF", "G_RM_NOOP2"), - 6: ("G_RM_AA_ZB_XLU_DECAL", "G_RM_NOOP2"), - 7: ("G_RM_AA_ZB_XLU_INTER", "G_RM_NOOP2"), +sm64_default_draw_layers = { + "0": ("G_RM_ZB_OPA_SURF", "G_RM_NOOP2"), + "1": ("G_RM_AA_ZB_OPA_SURF", "G_RM_NOOP2"), + "2": ("G_RM_AA_ZB_OPA_DECAL", "G_RM_NOOP2"), + "3": ("G_RM_AA_ZB_OPA_INTER", "G_RM_NOOP2"), + "4": ("G_RM_AA_ZB_TEX_EDGE", "G_RM_NOOP2"), + "5": ("G_RM_AA_ZB_XLU_SURF", "G_RM_NOOP2"), + "6": ("G_RM_AA_ZB_XLU_DECAL", "G_RM_NOOP2"), + "7": ("G_RM_AA_ZB_XLU_INTER", "G_RM_NOOP2"), +} + +oot_default_draw_layers = { + "Opaque": ("G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"), + "Transparent": ("G_RM_AA_ZB_XLU_SURF", "G_RM_AA_ZB_XLU_SURF2"), + "Overlay": ("G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"), +} + +default_draw_layers = { + "SM64": sm64_default_draw_layers, + "OOT": oot_default_draw_layers, } CCMUXDict = { @@ -133,6 +145,14 @@ def isUcodeF3DEX3(F3D_VER: str) -> bool: return F3D_VER == "F3DEX3" +def is_ucode_t3d(UCODE_VER: str) -> bool: + return UCODE_VER == "T3D" + + +def is_ucode_f3d(UCODE_VER: str) -> bool: + return UCODE_VER not in {"T3D", "RDPQ"} + + class F3D: """NOTE: do not initialize this class manually! use get_F3D_GBI so that the single instance is cached from the microcode type.""" @@ -143,6 +163,7 @@ def __init__(self, F3D_VER): F3DEX_GBI_3 = self.F3DEX_GBI_3 = isUcodeF3DEX3(F3D_VER) F3DLP_GBI = self.F3DLP_GBI = self.F3DEX_GBI self.F3D_OLD_GBI = not (F3DEX_GBI or F3DEX_GBI_2 or F3DEX_GBI_3) + self.F3D_GBI = is_ucode_f3d(F3D_VER) # F3DEX2 is F3DEX1 and F3DEX3 is F3DEX2, but F3DEX3 is not F3DEX1 if F3DEX_GBI_2: @@ -150,8 +171,12 @@ def __init__(self, F3D_VER): elif F3DEX_GBI_3: F3DEX_GBI_2 = self.F3DEX_GBI_2 = True - self.vert_buffer_size = vertexBufferSize[F3D_VER][0] - self.vert_load_size = vertexBufferSize[F3D_VER][1] + if F3D_VER in vertexBufferSize: + self.vert_buffer_size = vertexBufferSize[F3D_VER][0] + self.vert_load_size = vertexBufferSize[F3D_VER][1] + else: + self.vert_buffer_size = self.vert_load_size = None + self.G_MAX_LIGHTS = 9 if F3DEX_GBI_3 else 7 self.G_INPUT_BUFFER_CMDS = 21 @@ -2320,6 +2345,10 @@ def __init__( self.materialRevert: Union[GfxList, None] = None # F3D library self.f3d: F3D = get_F3D_GBI() + if not self.f3d.F3D_GBI: + raise PluginError( + f"Current microcode {self.f3d.F3D_VER} is not part of the f3d family of microcodes, fast64 cannot export it" + ) # array of FModel self.subModels: list[FModel] = [] self.parentModel: Union[FModel, None] = None @@ -3238,18 +3267,18 @@ def spc(x): # A palette is just a RGBA16 texture with width = 1. +@dataclass class FImage: - def __init__(self, name, fmt, bitSize, width, height, filename): - self.name = name - self.fmt = fmt - self.bitSize = bitSize - self.width = width - self.height = height - self.startAddress = 0 - self.data = bytearray(0) - self.filename = filename - self.converted = False - self.isLargeTexture = False + name: str + fmt: str + bitSize: str + width: int + height: int + filename: str + data: bytearray = field(init=False, compare=False, default_factory=bytearray) + startAddress: int = field(init=False, compare=False, default=0) + isLargeTexture: bool = field(init=False, compare=False, default=False) + converted: bool = field(init=False, compare=False, default=False) def size(self): return len(self.data) @@ -3812,6 +3841,9 @@ def to_binary(self, f3d, segments): self.point ).to_binary(f3d, segments) + def size(self, f3d): + return GFX_SIZE * 2 + @dataclass(unsafe_hash=True) class SPFresnelScale(GbiMacro): @@ -4025,6 +4057,9 @@ def to_c(self, static=True): header = "gsSPLightColor(" if static else "gSPLightColor(glistp++, " return header + f"{self.n}, 0x" + format(self.color_to_int(), "08X") + ")" + def size(self, _f3d): + return GFX_SIZE * 2 + @dataclass(unsafe_hash=True) class SPSetLights(GbiMacro): @@ -4211,7 +4246,7 @@ class SPPerspNormalize(GbiMacro): def to_binary(self, f3d, segments): if f3d.F3DEX_GBI_3: - return gsMoveHalfwd(f3d.G_MW_FX, G_MWO_PERSPNORM, (self.s), f3d) + return gsMoveHalfwd(f3d.G_MW_FX, f3d.G_MWO_PERSPNORM, (self.s), f3d) else: return gsMoveWd(f3d.G_MW_PERSPNORM, 0, (self.s), f3d) @@ -4315,9 +4350,9 @@ class SPSetOtherMode(GbiMacro): def to_binary(self, f3d, segments): data = 0 for flag in self.flagList: - data |= getattr(f3d, flag) if hasattr(f3d, str(flag)) else flag - cmd = getattr(f3d, self.cmd) if hasattr(f3d, str(self.cmd)) else self.cmd - sft = getattr(f3d, self.sft) if hasattr(f3d, str(self.sft)) else self.sft + data |= getattr(f3d, str(flag), flag) + cmd = getattr(f3d, str(self.cmd), self.cmd) + sft = getattr(f3d, str(self.sft), self.sft) return gsSPSetOtherMode(cmd, sft, self.length, data, f3d) @@ -4818,7 +4853,12 @@ class DPSetOtherMode(GbiMacro): mode1: list def to_binary(self, f3d, segments): - words = _SHIFTL(f3d.G_RDPSETOTHERMODE, 24, 8) | _SHIFTL(self.mode0, 0, 24), self.mode1 + mode0 = mode1 = 0 + for mode in self.mode0: + mode0 |= getattr(f3d, str(mode), mode) + for mode in self.mode1: + mode1 |= getattr(f3d, str(mode), mode) + words = _SHIFTL(f3d.G_RDPSETOTHERMODE, 24, 8) | _SHIFTL(mode0, 0, 24), mode1 return words[0].to_bytes(4, "big") + words[1].to_bytes(4, "big") @@ -4841,7 +4881,7 @@ def to_binary(self, f3d, segments): return gsDPLoadTileGeneric(f3d.G_SETTILESIZE, self.tile, self.uls, self.ult, self.lrs, self.lrt) def is_LOADTILE(self, f3d): - return self.t == f3d.G_TX_LOADTILE + return self.tile == f3d.G_TX_LOADTILE @dataclass(unsafe_hash=True) diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index fee2d7094..9033cde50 100644 --- a/fast64_internal/f3d/f3d_material.py +++ b/fast64_internal/f3d/f3d_material.py @@ -31,10 +31,22 @@ from mathutils import Color from .f3d_enums import * -from .f3d_gbi import get_F3D_GBI, GBL_c1, GBL_c2, enumTexScroll, isUcodeF3DEX1 +from .f3d_gbi import ( + get_F3D_GBI, + enumTexScroll, + isUcodeF3DEX1, + isUcodeF3DEX3, + is_ucode_f3d, + is_ucode_t3d, + default_draw_layers, +) from .f3d_material_presets import * from ..utility import * -from ..render_settings import Fast64RenderSettings_Properties, update_scene_props_from_render_settings +from ..render_settings import ( + Fast64RenderSettings_Properties, + update_scene_props_from_render_settings, + ManualUpdatePreviewOperator, +) from .f3d_material_helpers import F3DMaterial_UpdateLock, node_tree_copy from bpy.app.handlers import persistent from typing import Generator, Optional, Tuple, Any, Dict, Union @@ -115,24 +127,14 @@ "Overlay": "1", } -drawLayerSM64Alpha = { - "0": "OPA", - "1": "OPA", - "2": "OPA", - "3": "OPA", - "4": "CLIP", - "5": "XLU", - "6": "XLU", - "7": "XLU", -} -enumF3DMenu = [ - ("Combiner", "Combiner", "Combiner"), - ("Sources", "Sources", "Sources"), - ("Geo", "Geo", "Geo"), - ("Upper", "Upper", "Upper"), - ("Lower", "Lower", "Lower"), -] +def menu_items_enum(_self, context): + items = ["Combiner", "Sources"] + if len(geo_modes_in_ucode(context.scene.f3d_type)) > 1: + items.append("Geo") + items.extend(["Upper", "Lower"]) + return [(item, item, item) for item in items] + enumF3DSource = [ ("None", "None", "None"), @@ -151,6 +153,68 @@ "Shaded Texture": {"SM64": "Shaded Texture", "OOT": "oot_shaded_texture"}, } +F3D_GEO_MODES = { + "zBuffer": "g_zbuffer", + "shade": "g_shade", + "cullFront": "g_cull_front", + "cullBack": "g_cull_back", + "fog": "g_fog", + "lighting": "g_lighting", + "texGen": "g_tex_gen", + "texGenLinear": "g_tex_gen_linear", + "lod": "g_lod", + "shadeSmooth": "g_shade_smooth", +} + +F3DLX_GEO_MODES = { + "clipping": "g_clipping", +} + +F3DEX3_GEO_MODES = { + "ambientOcclusion": "g_ambocclusion", + "attroffsetZ": "g_attroffset_z_enable", + "attroffsetST": "g_attroffset_st_enable", + "packedNormals": "g_packed_normals", + "lightToAlpha": "g_lighttoalpha", + "specularLighting": "g_lighting_specular", + "fresnelToColor": "g_fresnel_color", + "fresnelToAlpha": "g_fresnel_alpha", +} + + +T3D_GEO_MODES = { + "cullFront": "g_cull_front", + "cullBack": "g_cull_back", + "fog": "g_fog", + "texGen": "g_tex_gen", +} + + +def geo_modes_in_ucode(UCODE_VER: str): + geo_modes = {} + if is_ucode_f3d(UCODE_VER): + geo_modes.update(F3D_GEO_MODES) + if isUcodeF3DEX1(UCODE_VER): + geo_modes.update(F3DLX_GEO_MODES) + if isUcodeF3DEX3(UCODE_VER): + geo_modes.update(F3DEX3_GEO_MODES) + if is_ucode_t3d(UCODE_VER): + geo_modes.update(T3D_GEO_MODES) + return geo_modes + + +def sources_in_ucode(UCODE_VER: str): + sources = ["Primitive", "Environment", "Key", "Convert", "Fog"] + if not is_ucode_t3d(UCODE_VER) or is_ucode_f3d(UCODE_VER): + sources.extend(["Lighting", "Clip Ratio"]) + if isUcodeF3DEX3(UCODE_VER): + sources.extend(["AO", "Fresnel", "ST Attr Offset", "Z Attr Offset"]) + return sources + + +def inherit_light_and_fog(): + return is_ucode_t3d(bpy.context.scene.f3d_type) + def getDefaultMaterialPreset(category): game = bpy.context.scene.gameEditorMode @@ -180,17 +244,60 @@ def update_draw_layer(self, context): set_output_node_groups(material) +def get_world_layer_defaults(scene, game_mode: str, layer: str): + world = scene.world + if world is None: + return default_draw_layers.get(game_mode, {}).get(layer, ("", "")) + if game_mode == "SM64": + return ( + getattr(world, f"draw_layer_{layer}_cycle_1", ""), + getattr(world, f"draw_layer_{layer}_cycle_2", ""), + ) + elif game_mode == "OOT": + return ( + getattr(world.ootDefaultRenderModes, f"{layer.lower()}Cycle1", ""), + getattr(world.ootDefaultRenderModes, f"{layer.lower()}Cycle2", ""), + ) + else: + assert ( + False + ), f"game_mode={game_mode} has no draw layer defaults, this function should not have been called at all with it" + + def rendermode_preset_to_advanced(material: bpy.types.Material): """ Set all individual controls for the rendermode from the preset rendermode. """ - settings = material.f3d_mat.rdp_settings + scene = bpy.context.scene + f3d_mat = material.f3d_mat + settings = f3d_mat.rdp_settings f3d = get_F3D_GBI() - if settings.rendermode_advanced_enabled: - # Already in advanced mode, don't overwrite this with the preset + if settings.rendermode_advanced_enabled and settings.set_rendermode: + # Rendermode is being set by the material and in advanced mode, don't overwrite any settings return + cycle_1, cycle_2 = settings.rendermode_preset_cycle_1, settings.rendermode_preset_cycle_2 + if not settings.set_rendermode: + game_mode = scene.gameEditorMode + layer = getattr(f3d_mat.draw_layer, game_mode.lower(), None) + if layer is None: # Game mode has no layer, don´t change anything + return + + possible_cycle_1, possible_cycle_2 = get_world_layer_defaults(scene, game_mode, layer) + if getattr(f3d, possible_cycle_1, None) is not None and getattr(f3d, possible_cycle_2, None) is not None: + cycle_1, cycle_2 = possible_cycle_1, possible_cycle_2 + + # Some presets are not implemented in the blender enum, so print a warning and turn on advanced + try: + settings.rendermode_preset_cycle_1, settings.rendermode_preset_cycle_2 = cycle_1, cycle_2 + settings.rendermode_advanced_enabled = False + except TypeError as exc: + print( + f"Render mode presets {cycle_1} or {cycle_2} probably not included in render mode preset enum:\n{exc}", + ) + settings.rendermode_advanced_enabled = True + def get_with_default(preset, default): # Use the material's settings even if we are not setting rendermode. # This allows the user to enable setting rendermode, set it up as they @@ -199,11 +306,11 @@ def get_with_default(preset, default): is_two_cycle = settings.g_mdsft_cycletype == "G_CYC_2CYCLE" if is_two_cycle: - r1 = get_with_default(settings.rendermode_preset_cycle_1, f3d.G_RM_FOG_SHADE_A) - r2 = get_with_default(settings.rendermode_preset_cycle_2, f3d.G_RM_AA_ZB_OPA_SURF2) + r1 = get_with_default(cycle_1, f3d.G_RM_FOG_SHADE_A) + r2 = get_with_default(cycle_2, f3d.G_RM_AA_ZB_OPA_SURF2) r = r1 | r2 else: - r = get_with_default(settings.rendermode_preset_cycle_1, f3d.G_RM_AA_ZB_OPA_SURF) + r = get_with_default(cycle_1, f3d.G_RM_AA_ZB_OPA_SURF) r1 = r # The cycle 1 bits are copied to the cycle 2 bits at export if in 1-cycle mode # (the hardware requires them to be the same). So, here we also move the cycle 1 @@ -240,14 +347,10 @@ def does_blender_use_mix(settings: "RDPSettings", mix: str, default_for_no_rende return settings.blend_b1 == mix or (is_two_cycle and settings.blend_b2 == mix) -def is_blender_equation_equal( - settings: "RDPSettings", cycle: int, p: str, a: str, m: str, b: str, default_for_no_rendermode: bool = False -) -> bool: +def is_blender_equation_equal(settings: "RDPSettings", cycle: int, p: str, a: str, m: str, b: str) -> bool: assert cycle in {1, 2, -1} # -1 = last cycle if cycle == -1: cycle = 2 if settings.g_mdsft_cycletype == "G_CYC_2CYCLE" else 1 - if not settings.set_rendermode: - return default_for_no_rendermode return ( getattr(settings, f"blend_p{cycle}") == p and getattr(settings, f"blend_a{cycle}") == a @@ -256,7 +359,7 @@ def is_blender_equation_equal( ) -def is_blender_doing_fog(settings: "RDPSettings", default_for_no_rendermode: bool) -> bool: +def is_blender_doing_fog(settings: "RDPSettings") -> bool: return is_blender_equation_equal( settings, # If 2 cycle, fog must be in first cycle. @@ -268,14 +371,12 @@ def is_blender_doing_fog(settings: "RDPSettings", default_for_no_rendermode: boo # is color in and 1-A. "G_BL_CLR_IN", "G_BL_1MA", - default_for_no_rendermode, ) def get_output_method(material: bpy.types.Material) -> str: + rendermode_preset_to_advanced(material) # Make sure advanced settings are updated settings = material.f3d_mat.rdp_settings - if not settings.set_rendermode: - return drawLayerSM64Alpha[material.f3d_mat.draw_layer.sm64] if settings.cvg_x_alpha: return "CLIP" if settings.force_bl and is_blender_equation_equal( @@ -286,11 +387,19 @@ def get_output_method(material: bpy.types.Material) -> str: def update_blend_method(material: Material, context): + blend_mode = get_output_method(material) + if material.f3d_mat.rdp_settings.zmode == "ZMODE_DEC": + blend_mode = "DECAL" if bpy.app.version >= (4, 2, 0): - material.surface_render_method = "BLENDED" - elif get_output_method(material) == "OPA": + if blend_mode == "CLIP": + material.surface_render_method = "DITHERED" + else: + material.surface_render_method = "BLENDED" + elif blend_mode == "OPA": material.blend_method = "OPAQUE" - else: + elif blend_mode == "CLIP": + material.blend_method = "CLIP" + elif blend_mode in {"XLU", "DECAL"}: material.blend_method = "BLEND" @@ -405,6 +514,7 @@ def all_combiner_uses(f3d_mat: "F3DMaterialProperty") -> dict[str, bool]: def ui_geo_mode(settings, dataHolder, layout, useDropdown): + f3d = get_F3D_GBI() inputGroup = layout.column() if useDropdown: inputGroup.prop( @@ -416,7 +526,24 @@ def ui_geo_mode(settings, dataHolder, layout, useDropdown): if not useDropdown or dataHolder.menu_geo: disable_dependent = False # Don't disable dependent props in world defaults + def should_draw(*props): + return any(settings.has_prop_in_ucode(prop) for prop in props) + + def is_on(*props): + return all(settings.is_geo_mode_on(prop) for prop in props) + + def draw_mode(layout: UILayout, *props): + failed = False + for prop in props: + if settings.has_prop_in_ucode(prop): + layout.prop(settings, prop) + else: + failed = True + return not failed + def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], isText: bool) -> UILayout: + if not isText and not should_draw(textOrProp): + return parent.column(align=True) c = parent.column(align=True) if isText: c.label(text=textOrProp) @@ -430,8 +557,6 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], c.enabled = enable or not disable_dependent return c - isF3DEX3 = bpy.context.scene.f3d_type == "F3DEX3" - lightFxPrereq = isF3DEX3 and settings.g_lighting ccWarnings = shadeInCC = False blendWarnings = shadeInBlender = zInBlender = False if isinstance(dataHolder, F3DMaterialProperty): @@ -444,96 +569,103 @@ def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], shadeInBlender = settings.does_blender_use_input("G_BL_A_SHADE") zInBlender = settings.z_cmp or settings.z_upd - inputGroup.prop(settings, "g_shade_smooth") + draw_mode(inputGroup, "g_shade_smooth") c = indentGroup(inputGroup, "g_lighting", False) - if ccWarnings and not shadeInCC and settings.g_lighting and not settings.g_tex_gen: + if ccWarnings and not shadeInCC and is_on("g_lighting") and not is_on("g_tex_gen"): multilineLabel(c, "Shade not used in CC, can disable\nlighting.", icon="INFO") - if isF3DEX3: - c.prop(settings, "g_packed_normals") - c.prop(settings, "g_lighting_specular") - c.prop(settings, "g_ambocclusion") - c.prop(settings, "g_fresnel_color") - d = indentGroup(c, "g_tex_gen", False) - d.prop(settings, "g_tex_gen_linear") - - if lightFxPrereq and settings.g_fresnel_color: + draw_mode(c, "g_packed_normals", "g_lighting_specular", "g_ambocclusion", "g_fresnel_color") + if should_draw("g_tex_gen_linear"): + d = indentGroup(c, "g_tex_gen", False) + else: + draw_mode(c, "g_tex_gen") + d = c + draw_mode(d, "g_tex_gen_linear") + + if is_ucode_t3d(f3d.F3D_VER): + shadeColorLabel = "Lighting * vertex color" + if is_on("g_fresnel_color"): shadeColorLabel = "Fresnel" - elif not settings.g_lighting or (lightFxPrereq and settings.g_lighttoalpha): + elif not is_on("g_lighting") or is_on("g_lighttoalpha"): shadeColorLabel = "Vertex color" - elif lightFxPrereq and settings.g_packed_normals and not settings.g_lighttoalpha: + elif is_on("g_lighting") and is_on("g_packed_normals") and not is_on("g_lighttoalpha"): shadeColorLabel = "Lighting * vertex color" else: shadeColorLabel = "Lighting" - inputGroup.label(text=f"Shade color = {shadeColorLabel}") + inputGroup.column().label(text=f"Shade color = {shadeColorLabel}") + + draw_mode(inputGroup, "g_fog") shadowMapInShadeAlpha = False - if settings.g_fog: + if is_on("g_fog"): shadeAlphaLabel = "Fog" - elif lightFxPrereq and settings.g_fresnel_alpha: + elif is_on("g_lighting", "g_fresnel_alpha"): shadeAlphaLabel = "Fresnel" - elif lightFxPrereq and settings.g_lighttoalpha: + elif is_on("g_lighting", "g_lighttoalpha"): shadeAlphaLabel = "Light intensity" - elif lightFxPrereq and settings.g_ambocclusion: + elif is_on("g_lighting", "g_ambocclusion"): shadeAlphaLabel = "Shadow map / AO in vtx alpha" shadowMapInShadeAlpha = True else: shadeAlphaLabel = "Vtx alpha" - c = indentGroup(inputGroup, f"Shade alpha = {shadeAlphaLabel}:", True) - if isF3DEX3: - lighting_group = c.column(align=True) - lighting_group.enabled = settings.g_lighting or not disable_dependent - lighting_group.prop(settings, "g_lighttoalpha") - lighting_group.prop(settings, "g_fresnel_alpha") - c.prop(settings, "g_fog") - if lightFxPrereq and settings.g_fog and settings.g_fresnel_alpha: - c.label(text="Fog overrides Fresnel Alpha.", icon="ERROR") - if lightFxPrereq and settings.g_fog and settings.g_lighttoalpha: - c.label(text="Fog overrides Light-to-Alpha.", icon="ERROR") - if lightFxPrereq and settings.g_fresnel_alpha and settings.g_lighttoalpha: - c.label(text="Fresnel Alpha overrides Light-to-Alpha.", icon="ERROR") - if shadowMapInShadeAlpha and ccWarnings and ccUse["Shade Alpha"]: - c.label(text="Shadow map = shade alpha used in CC, probably wrong.", icon="INFO") - if settings.g_fog and ccWarnings and ccUse["Shade Alpha"]: - c.label(text="Fog = shade alpha used in CC, probably wrong.", icon="INFO") - if blendWarnings and shadeInBlender and not settings.g_fog: - c.label(text="Rendermode uses shade alpha, probably fog.", icon="INFO") - elif blendWarnings and not shadeInBlender and settings.g_fog: - c.label(text="Fog not used in rendermode / blender, can disable.", icon="INFO") - if isF3DEX3: - c = indentGroup(inputGroup, "Attribute offsets:", True) - c.prop(settings, "g_attroffset_st_enable") - c.prop(settings, "g_attroffset_z_enable") - - c = indentGroup(inputGroup, "Face culling:", True) - c.prop(settings, "g_cull_front") - c.prop(settings, "g_cull_back") - if settings.g_cull_front and settings.g_cull_back: - c.label(text="Nothing will be drawn.", icon="ERROR") - - c = indentGroup(inputGroup, "Disable if not using:", True) - c.prop(settings, "g_zbuffer") - if blendWarnings and not settings.g_zbuffer and zInBlender: - c.label(text="Rendermode / blender using Z, must enable.", icon="ERROR") - elif blendWarnings and settings.g_zbuffer and not zInBlender: - c.label(text="Z is not being used, can disable.", icon="INFO") - c.prop(settings, "g_shade") - if ccWarnings and not settings.g_shade and (shadeInCC or shadeInBlender): - if shadeInCC and shadeInBlender: - where = "CC and blender" - elif shadeInCC: - where = "CC" - else: - where = "rendermode / blender" - c.label(text=f"Shade in use in {where}, must enable.", icon="ERROR") - elif ccWarnings and settings.g_shade and not shadeInCC and not shadeInBlender: - c.label(text="Shade is not being used, can disable.", icon="INFO") + warnings, errors = [], [] + if is_on("g_lighting", "g_fog", "g_fresnel_alpha"): + errors.append("Fog overrides Fresnel Alpha.") + if is_on("g_lighting", "g_fog", "g_lighttoalpha"): + errors.append("Fog overrides Light-to-Alpha.") + if is_on("g_lighting", "g_fresnel_alpha", "g_lighttoalpha"): + errors.append("Fresnel Alpha overrides Light-to-Alpha.") + if shadowMapInShadeAlpha and ccWarnings and ccUse["Shade Alpha"]: + warnings.append("Shadow map = shade alpha used in CC, probably wrong.") + if is_on("g_fog") and ccWarnings and ccUse["Shade Alpha"]: + warnings.append("Fog = shade alpha used in CC, probably wrong.") + if blendWarnings and shadeInBlender and not is_on("g_fog"): + warnings.append("Rendermode uses shade alpha, probably fog.") + elif blendWarnings and not shadeInBlender and is_on("g_fog"): + warnings.append("Fog not used in rendermode / blender, can disable.") + + if should_draw("g_lighttoalpha", "g_fresnel_alpha") or warnings or errors: + c = indentGroup(inputGroup, f"Shade alpha = {shadeAlphaLabel}:", True) + draw_mode(c, "g_lighttoalpha", "g_fresnel_alpha") + if warnings: + multilineLabel(c, "\n".join(warnings), "INFO") + if errors: + multilineLabel(c, "\n".join(errors), "ERROR") + else: + inputGroup.column().label(text=f"Shade alpha = {shadeAlphaLabel}") + + if should_draw("g_attroffset_st_enable", "g_attroffset_z_enable"): + indentGroup(inputGroup, "Attribute offsets:", True) + draw_mode(inputGroup, "g_attroffset_st_enable", "g_attroffset_z_enable") + + if should_draw("g_cull_front", "g_cull_back"): + c = indentGroup(inputGroup, "Face culling:", True) + draw_mode(c, "g_cull_front", "g_cull_back") + if is_on("g_cull_front", "g_cull_back"): + c.label(text="Nothing will be drawn.", icon="ERROR") + + if should_draw("g_zbuffer", "g_shade"): + c = indentGroup(inputGroup, "Disable if not using:", True) + draw_mode(c, "g_zbuffer", "g_shade") + if blendWarnings and not is_on("g_zbuffer") and zInBlender: + c.label(text="Rendermode / blender using Z, must enable.", icon="ERROR") + elif blendWarnings and is_on("g_zbuffer") and not zInBlender: + c.label(text="Z is not being used, can disable.", icon="INFO") + if ccWarnings and not is_on("g_shade") and (shadeInCC or shadeInBlender): + if shadeInCC and shadeInBlender: + where = "CC and blender" + elif shadeInCC: + where = "CC" + else: + where = "rendermode / blender" + c.label(text=f"Shade in use in {where}, must enable.", icon="ERROR") + elif ccWarnings and is_on("g_shade") and not shadeInCC and not shadeInBlender: + c.label(text="Shade is not being used, can disable.", icon="INFO") - c = indentGroup(inputGroup, "Not useful:", True) - c.prop(settings, "g_lod") - if isUcodeF3DEX1(bpy.context.scene.f3d_type): - c.prop(settings, "g_clipping") + if should_draw("g_lod", "g_clipping"): + c = indentGroup(inputGroup, "Not useful:", True) + draw_mode(c, "g_lod", "g_clipping") def ui_upper_mode(settings, dataHolder, layout: UILayout, useDropdown): @@ -605,8 +737,9 @@ def ui_other(settings, dataHolder, layout, useDropdown): dataHolder, "menu_other", text="Other Settings", icon="TRIA_DOWN" if dataHolder.menu_other else "TRIA_RIGHT" ) if not useDropdown or dataHolder.menu_other: - clipRatioGroup = inputGroup.column() - prop_split(clipRatioGroup, settings, "clip_ratio", "Clip Ratio") + if "Clip Ratio" in sources_in_ucode(bpy.context.scene.f3d_type): + clipRatioGroup = inputGroup.column() + prop_split(clipRatioGroup, settings, "clip_ratio", "Clip Ratio") if isinstance(dataHolder, Material) or isinstance(dataHolder, F3DMaterialProperty): blend_color_group = layout.row() @@ -727,6 +860,8 @@ def ui_chroma(self, material, layout, name, setName, setProp, showCheckBox): return inputGroup def ui_lights(self, f3d_mat: "F3DMaterialProperty", layout: UILayout, name, showCheckBox): + if inherit_light_and_fog(): + return inputGroup = layout.row() prop_input_left = inputGroup.column() prop_input = inputGroup.column() @@ -735,9 +870,10 @@ def ui_lights(self, f3d_mat: "F3DMaterialProperty", layout: UILayout, name, show else: prop_input_left.label(text=name) - prop_input_left.enabled = f3d_mat.rdp_settings.g_lighting and f3d_mat.rdp_settings.g_shade + settings = f3d_mat.rdp_settings + prop_input_left.enabled = settings.is_geo_mode_on("g_lighting") and settings.is_geo_mode_on("g_shade") lightSettings: UILayout = prop_input.column() - if f3d_mat.rdp_settings.g_lighting: + if settings.is_geo_mode_on("g_lighting"): prop_input_left.separator(factor=0.25) light_controls = prop_input_left.box() light_controls.enabled = f3d_mat.set_lights @@ -767,8 +903,6 @@ def ui_lights(self, f3d_mat: "F3DMaterialProperty", layout: UILayout, name, show if f3d_mat.f3d_light6 is not None: lightSettings.prop_search(f3d_mat, "f3d_light7", bpy.data, "lights", text="") - prop_input.enabled = f3d_mat.set_lights and f3d_mat.rdp_settings.g_lighting and f3d_mat.rdp_settings.g_shade - return inputGroup def ui_convert(self, material, layout, showCheckBox): @@ -923,7 +1057,8 @@ def ui_draw_layer(self, material, layout, context): prop_split(layout, material.f3d_mat.draw_layer, "oot", "Draw Layer") def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBox: bool) -> None: - if f3dMat.rdp_settings.g_ambocclusion: + sources = sources_in_ucode(bpy.context.scene.f3d_type) + if "AO" in sources and f3dMat.rdp_settings.g_ambocclusion: if showCheckBox or f3dMat.set_ao: inputGroup = inputCol.column() if showCheckBox: @@ -933,7 +1068,7 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo prop_split(inputGroup.row(), f3dMat, "ao_directional", "AO Directional") prop_split(inputGroup.row(), f3dMat, "ao_point", "AO Point") - if f3dMat.rdp_settings.g_fresnel_color or f3dMat.rdp_settings.g_fresnel_alpha: + if "Fresnel" in sources and f3dMat.rdp_settings.g_fresnel_color or f3dMat.rdp_settings.g_fresnel_alpha: if showCheckBox or f3dMat.set_fresnel: inputGroup = inputCol.column() if showCheckBox: @@ -942,7 +1077,7 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo prop_split(inputGroup.row(), f3dMat, "fresnel_lo", "Fresnel Lo") prop_split(inputGroup.row(), f3dMat, "fresnel_hi", "Fresnel Hi") - if f3dMat.rdp_settings.g_attroffset_st_enable: + if "ST Attr Offset" in sources and f3dMat.rdp_settings.g_attroffset_st_enable: if showCheckBox or f3dMat.set_attroffs_st: inputGroup = inputCol.column() if showCheckBox: @@ -950,7 +1085,7 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo if f3dMat.set_attroffs_st: prop_split(inputGroup.row(), f3dMat, "attroffs_st", "ST Attr Offset") - if f3dMat.rdp_settings.g_attroffset_z_enable: + if "Z Attr Offset" in sources and f3dMat.rdp_settings.g_attroffset_z_enable: if showCheckBox or f3dMat.set_attroffs_z: inputGroup = inputCol.column() if showCheckBox: @@ -958,7 +1093,7 @@ def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBo if f3dMat.set_attroffs_z: prop_split(inputGroup.row(), f3dMat, "attroffs_z", "Z Attr Offset") - if f3dMat.rdp_settings.using_fog: + if "Fog" in sources and f3dMat.rdp_settings.using_fog and not inherit_light_and_fog(): if showCheckBox or f3dMat.set_fog: inputGroup = inputCol.column() if showCheckBox: @@ -1082,17 +1217,16 @@ def ui_cel_shading(self, material: Material, layout: UILayout): def checkDrawLayersWarnings(self, f3dMat: "F3DMaterialProperty", useDict: Dict[str, bool], layout: UILayout): settings = f3dMat.rdp_settings - isF3DEX3 = bpy.context.scene.f3d_type == "F3DEX3" - lightFxPrereq = isF3DEX3 and settings.g_lighting + f3d_type = bpy.context.scene.f3d_type anyUseShadeAlpha = useDict["Shade Alpha"] or settings.does_blender_use_input("G_BL_A_SHADE") - g_lighting = settings.g_lighting - g_fog = settings.g_fog - g_packed_normals = lightFxPrereq and settings.g_packed_normals - g_ambocclusion = lightFxPrereq and settings.g_ambocclusion - g_lighttoalpha = lightFxPrereq and settings.g_lighttoalpha - g_fresnel_color = lightFxPrereq and settings.g_fresnel_color - g_fresnel_alpha = lightFxPrereq and settings.g_fresnel_alpha + g_lighting = settings.is_geo_mode_on("g_lighting") or is_ucode_t3d(f3d_type) + g_fog = settings.is_geo_mode_on("g_fog") + g_packed_normals = settings.is_geo_mode_on("g_packed_normals") or is_ucode_t3d(f3d_type) + g_ambocclusion = settings.is_geo_mode_on("g_ambocclusion") + g_lighttoalpha = settings.is_geo_mode_on("g_lighttoalpha") + g_fresnel_color = settings.is_geo_mode_on("g_fresnel_color") + g_fresnel_alpha = settings.is_geo_mode_on("g_fresnel_alpha") usesVertexColor = useDict["Shade"] and (not g_lighting or (g_packed_normals and not g_fresnel_color)) usesVertexAlpha = anyUseShadeAlpha and (g_ambocclusion or not (g_fog or g_lighttoalpha or g_fresnel_alpha)) @@ -1122,6 +1256,7 @@ def checkDrawMixedCIWarning(self, layout, useDict, f3dMat): layout.box().column().label(text="Two CI textures must use the same CI format.", icon="ERROR") def draw_simple(self, f3dMat, material, layout, context): + f3d = get_F3D_GBI() self.ui_uvCheck(layout, context) inputCol = layout.column() @@ -1152,7 +1287,12 @@ def draw_simple(self, f3dMat, material, layout, context): if useDict["Environment"] and f3dMat.set_env: self.ui_env(material, inputCol, False) - showLightProperty = f3dMat.set_lights and f3dMat.rdp_settings.g_lighting and f3dMat.rdp_settings.g_shade + showLightProperty = ( + f3dMat.set_lights + and "Lighting" in sources_in_ucode(bpy.context.scene.f3d_type) + and f3dMat.rdp_settings.is_geo_mode_on("g_lighting") + and f3dMat.rdp_settings.is_geo_mode_on("g_shade") + ) if useDict["Shade"] and showLightProperty: self.ui_lights(f3dMat, inputCol, "Lighting", False) @@ -1168,6 +1308,7 @@ def draw_full(self, f3dMat, material, layout: UILayout, context): layout.row().prop(material, "menu_tab", expand=True) menuTab = material.menu_tab useDict = all_combiner_uses(f3dMat) + f3d = get_F3D_GBI() if menuTab == "Combiner": self.ui_draw_layer(material, layout, context) @@ -1241,7 +1382,7 @@ def drawCCProps(ui: UILayout, combiner: "CombinerProperty", isAlpha: bool, enabl if useDict["Environment"]: self.ui_env(material, inputCol, True) - if useDict["Shade"]: + if useDict["Shade"] and "Lighting" in sources_in_ucode(bpy.context.scene.f3d_type): self.ui_lights(f3dMat, inputCol, "Lighting", True) if useDict["Key"]: @@ -1429,7 +1570,7 @@ def ui_procAnim(material, layout, useTex0, useTex1, title, useDropdown): def update_node_values(self, context, update_preset): - if hasattr(context.scene, "world") and self == context.scene.world.rdp_defaults: + if hasattr(context.scene, "world") and self == create_or_get_world(context.scene).rdp_defaults: pass with F3DMaterial_UpdateLock(get_material_from_context(context)) as material: @@ -1443,10 +1584,24 @@ def update_node_values(self, context, update_preset): def update_all_node_values(material, context): update_node_values_without_preset(material, context) - update_tex_values_and_formats(material, context) + update_tex_values_manual(material, context) update_rendermode_preset(material, context) +def update_all_material_nodes(self, context): + for material in bpy.data.materials: + if material.is_f3d and material.mat_ver >= F3D_MAT_CUR_VERSION: + with context.temp_override(material=material): + update_all_node_values(material, context) + + +def update_world_default_rendermode(self, context): + for material in bpy.data.materials: + if material.is_f3d and material.mat_ver >= F3D_MAT_CUR_VERSION: + with context.temp_override(material=material): + update_rendermode_preset(material, context) + + def update_node_values_with_preset(self, context): update_node_values(self, context, update_preset=True) @@ -1610,7 +1765,7 @@ 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 is_blender_doing_fog(material.f3d_mat.rdp_settings, True) else "FogBlender_Off") + ("FogBlender_On" if is_blender_doing_fog(material.f3d_mat.rdp_settings) else "FogBlender_Off") ] remove_first_link_if_exists(material, fogBlender.inputs["FogAmount"].links) @@ -1619,7 +1774,7 @@ def update_fog_nodes(material: Material, context: Context): else: # If fog is not being calculated, pass in shade alpha material.node_tree.links.new(nodes["Shade Color"].outputs["Alpha"], fogBlender.inputs["FogAmount"]) - if (bpy.context.scene.gameEditorMode == "SM64" and f3dMat.use_global_fog) or not f3dMat.set_fog: # Inherit fog + if (bpy.context.scene.gameEditorMode == "SM64" and f3dMat.use_global_fog) or not f3dMat.set_fog or inherit_light_and_fog(): # Inherit fog link_if_none_exist(material, nodes["SceneProperties"].outputs["FogColor"], nodes["FogColor"].inputs[0]) link_if_none_exist(material, nodes["GlobalFogColor"].outputs[0], fogBlender.inputs["Fog Color"]) link_if_none_exist(material, nodes["SceneProperties"].outputs["FogNear"], nodes["CalcFog"].inputs["FogNear"]) @@ -1682,6 +1837,9 @@ def set_output_node_groups(material: Material): f3dMat: "F3DMaterialProperty" = material.f3d_mat cycle = f3dMat.rdp_settings.g_mdsft_cycletype.lstrip("G_CYC_").rstrip("_CYCLE") output_method = get_output_method(material) + if bpy.app.version < (4, 2, 0) and output_method == "CLIP": + output_method = "XLU" + material.alpha_threshold = 0.125 output_group_name = f"OUTPUT_{cycle}CYCLE_{output_method}" output_group = bpy.data.node_groups[output_group_name] @@ -1716,7 +1874,7 @@ def update_light_colors(material, context): f3dMat.ambient_light_color = new_amb - if f3dMat.set_lights: + if f3dMat.set_lights or inherit_light_and_fog(): remove_first_link_if_exists(material, nodes["Shade Color"].inputs["AmbientColor"].links) remove_first_link_if_exists(material, nodes["Shade Color"].inputs["Light0Color"].links) remove_first_link_if_exists(material, nodes["Shade Color"].inputs["Light1Color"].links) @@ -1770,6 +1928,7 @@ def update_node_values_of_material(material: Material, context): return f3dMat: "F3DMaterialProperty" = material.f3d_mat + settings: RDPSettings = f3dMat.rdp_settings update_combiner_connections(material, context) @@ -1777,8 +1936,8 @@ def update_node_values_of_material(material: Material, context): nodes = material.node_tree.nodes - if f3dMat.rdp_settings.g_lighting and f3dMat.rdp_settings.g_tex_gen: - if f3dMat.rdp_settings.g_tex_gen_linear: + if (settings.is_geo_mode_on("g_lighting") or inherit_light_and_fog()) and settings.is_geo_mode_on("g_tex_gen"): + if settings.is_geo_mode_on("g_tex_gen_linear"): nodes["UV"].node_tree = bpy.data.node_groups["UV_EnvMap_Linear"] else: nodes["UV"].node_tree = bpy.data.node_groups["UV_EnvMap"] @@ -1796,7 +1955,9 @@ def update_node_values_of_material(material: Material, context): "g_fog", "g_lighting", ]: - shdcol_inputs[propName.upper()].default_value = getattr(f3dMat.rdp_settings, propName) + shdcol_inputs[propName.upper()].default_value = f3dMat.rdp_settings.is_geo_mode_on(propName) + if is_ucode_t3d(bpy.context.scene.f3d_type): # Tiny3d always uses lighting * vertex color + shdcol_inputs["G_LIGHTING"].default_value = shdcol_inputs["G_PACKED_NORMALS"].default_value = True shdcol_inputs["AO Ambient"].default_value = f3dMat.ao_ambient shdcol_inputs["AO Directional"].default_value = f3dMat.ao_directional @@ -1826,7 +1987,6 @@ def update_node_values_of_material(material: Material, context): material.use_backface_culling = f3dMat.rdp_settings.g_cull_back update_tex_values_manual(material, context) - update_blend_method(material, context) update_fog_nodes(material, context) @@ -2394,7 +2554,7 @@ def createOrUpdateSceneProperties(): sceneOutputs: NodeGroupOutput = new_group.nodes["Group Output"] renderSettings: "Fast64RenderSettings_Properties" = bpy.context.scene.fast64.renderSettings - update_scene_props_from_render_settings(bpy.context, sceneOutputs, renderSettings) + update_scene_props_from_render_settings(sceneOutputs, renderSettings) def createScenePropertiesForMaterial(material: Material): @@ -3527,6 +3687,12 @@ def blend_inputs(self): yield from self.blend_color_inputs yield from self.blend_alpha_inputs + def has_prop_in_ucode(self, prop: str) -> bool: + return prop in geo_modes_in_ucode(bpy.context.scene.f3d_type).values() + + def is_geo_mode_on(self, prop: str) -> bool: + return getattr(self, prop) if self.has_prop_in_ucode(prop) else False + def does_blender_use_input(self, setting: str) -> bool: return any(input == setting for input in self.blend_inputs) @@ -3542,46 +3708,18 @@ def attributes_from_dict(self, data: dict, info: dict): for key, attr, default in info: setattr(self, attr, data.get(key, default)) - geo_mode_all_attributes = [ - ("zBuffer", "g_zbuffer", False), - ("shade", "g_shade", False), - ("cullFront", "g_cull_front", False), - ("cullBack", "g_cull_back", False), - ("fog", "g_fog", False), - ("lighting", "g_lighting", False), - ("texGen", "g_tex_gen", False), - ("texGenLinear", "g_tex_gen_linear", False), - ("lod", "g_lod", False), - ("shadeSmooth", "g_shade_smooth", False), - ] - - geo_mode_f3dex_attributes = [ - ("clipping", "g_clipping", True), - ] + geo_mode_attributes = {**F3D_GEO_MODES, **F3DLX_GEO_MODES, **F3DEX3_GEO_MODES} - 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)) + def geo_mode_to_dict(self): + data = {} + for key, attr in geo_modes_in_ucode(bpy.context.scene.f3d_type).items(): + if getattr(self, attr): + data[key] = True return data def geo_mode_from_dict(self, data: dict): - self.attributes_from_dict(data, self.geo_mode_attributes) + for key, attr in self.geo_mode_attributes.items(): + setattr(self, attr, data.get(key, False)) other_mode_h_attributes = [ ("alphaDither", "g_mdsft_alpha_dither", "G_AD_DISABLE"), @@ -3694,9 +3832,9 @@ def other_from_dict(self, data: dict): self.clip_ratio = data.get("clipRatio", self.clip_ratio) self.num_textures_mipmapped = data.get("mipmapCount", self.num_textures_mipmapped) - def to_dict(self, f3d=None): + def to_dict(self): data = {} - data["geometryMode"] = self.geo_mode_to_dict(f3d) + data["geometryMode"] = self.geo_mode_to_dict() data["otherModeH"] = self.other_mode_h_to_dict() data["otherModeL"] = self.other_mode_l_to_dict() data["other"] = self.other_to_dict() @@ -4678,6 +4816,18 @@ def draw(self, context): labelbox.label(text="Global Settings") labelbox.ui_units_x = 6 + # Only show the update preview UI if the render engine is EEVEE, + # as there's no point in updating the nodes otherwise. + if context.scene.render.engine in { + "BLENDER_EEVEE", # <4.2 + "BLENDER_EEVEE_NEXT", # 4.2+ + }: + updatePreviewRow = globalSettingsBox.row() + updatePreviewRow.prop(renderSettings, "enableAutoUpdatePreview") + if not renderSettings.enableAutoUpdatePreview: + updatePreviewRow.operator(ManualUpdatePreviewOperator.bl_idname) + globalSettingsBox.separator() + globalSettingsBox.prop(renderSettings, "enableFogPreview") prop_split(globalSettingsBox, renderSettings, "fogPreviewColor", "Fog Color") prop_split(globalSettingsBox, renderSettings, "fogPreviewPosition", "Fog Position") @@ -4820,9 +4970,7 @@ def mat_register(): savePresets() Scene.f3d_type = bpy.props.EnumProperty( - name="F3D Microcode", - items=enumF3D, - default="F3D", + name="Microcode", items=enumF3D, default="F3D", update=update_all_material_nodes ) # RDP Defaults @@ -4837,7 +4985,7 @@ def mat_register(): Material.mat_ver = bpy.props.IntProperty(default=1) Material.f3d_update_flag = bpy.props.BoolProperty() Material.f3d_mat = bpy.props.PointerProperty(type=F3DMaterialProperty) - Material.menu_tab = bpy.props.EnumProperty(items=enumF3DMenu) + Material.menu_tab = bpy.props.EnumProperty(items=menu_items_enum) Scene.f3dUserPresetsOnly = bpy.props.BoolProperty(name="User Presets Only") Scene.f3d_simple = bpy.props.BoolProperty(name="Display Simple", default=True) diff --git a/fast64_internal/f3d/f3d_parser.py b/fast64_internal/f3d/f3d_parser.py index 56ab40ae7..b91154916 100644 --- a/fast64_internal/f3d/f3d_parser.py +++ b/fast64_internal/f3d/f3d_parser.py @@ -463,6 +463,7 @@ def __init__(self, f3d: F3D, basePath: str, materialContext: bpy.types.Material) self.materialContext.f3d_update_flag = True # Don't want visual updates while parsing # If this is not disabled, then tex_scale will auto-update on manual node update. self.materialContext.f3d_mat.scale_autoprop = False + self.draw_layer_prop: str | None = None self.initContext() # This is separate as we want to call __init__ in clearGeometry, but don't want same behaviour for child classes @@ -540,6 +541,8 @@ def clearGeometry(self): savedTlutAppliedTextures = self.tlutAppliedTextures savedImagesDontApplyTlut = self.imagesDontApplyTlut savedLightData = self.lightData + savedMatrixData = self.matrixData + savedLimbToBoneName = self.limbToBoneName self.initContext() @@ -548,6 +551,8 @@ def clearGeometry(self): self.tlutAppliedTextures = savedTlutAppliedTextures self.imagesDontApplyTlut = savedImagesDontApplyTlut self.lightData = savedLightData + self.matrixData = savedMatrixData + self.limbToBoneName = savedLimbToBoneName def clearMaterial(self): mat = self.mat() @@ -1927,7 +1932,6 @@ def parseF3D( transformMatrix: mathutils.Matrix, limbName: str, boneName: str, - drawLayerPropName: str, drawLayer: str, f3dContext: F3DContext, callClearMaterial: bool, @@ -1935,7 +1939,8 @@ def parseF3D( f3dContext.matrixData[limbName] = transformMatrix f3dContext.setCurrentTransform(limbName) f3dContext.limbToBoneName[limbName] = boneName - setattr(f3dContext.mat().draw_layer, drawLayerPropName, drawLayer) + if f3dContext.draw_layer_prop is not None: + setattr(f3dContext.mat().draw_layer, f3dContext.draw_layer_prop, drawLayer) # vertexGroup = getOrMakeVertexGroup(obj, boneName) # groupIndex = vertexGroup.index @@ -1978,6 +1983,8 @@ def parseVertexData(dlData: str, vertexDataName: str, f3dContext: F3DContext): pathMatch = re.search(r'\#include\s*"([^"]*)"', data) if pathMatch is not None: path = pathMatch.group(1) + if bpy.context.scene.gameEditorMode == "OOT": + path = f"{bpy.context.scene.fast64.oot.get_extracted_path()}/{path}" data = readFile(f3dContext.getVTXPathFromInclude(path)) f3d = f3dContext.f3d @@ -2080,6 +2087,8 @@ def parseTextureData(dlData, textureName, f3dContext, imageFormat, imageSize, wi pathMatch = re.search(r'\#include\s*"(.*?)"', data, re.DOTALL) if pathMatch is not None: path = pathMatch.group(1) + if bpy.context.scene.gameEditorMode == "OOT": + path = f"{bpy.context.scene.fast64.oot.get_extracted_path()}/{path}" originalImage = bpy.data.images.load(f3dContext.getImagePathFromInclude(path)) image = originalImage.copy() image.pack() @@ -2271,10 +2280,9 @@ def importMeshC( obj = bpy.data.objects.new(name + "_mesh", mesh) bpy.context.collection.objects.link(obj) - f3dContext.mat().draw_layer.oot = drawLayer transformMatrix = mathutils.Matrix.Scale(1 / scale, 4) - parseF3D(data, name, transformMatrix, name, name, "oot", drawLayer, f3dContext, True) + parseF3D(data, name, transformMatrix, name, name, drawLayer, f3dContext, True) f3dContext.createMesh(obj, removeDoubles, importNormals, callClearMaterial) applyRotation([obj], math.radians(-90), "X") diff --git a/fast64_internal/f3d/f3d_texture_writer.py b/fast64_internal/f3d/f3d_texture_writer.py index d08d80ced..0f912619b 100644 --- a/fast64_internal/f3d/f3d_texture_writer.py +++ b/fast64_internal/f3d/f3d_texture_writer.py @@ -763,14 +763,25 @@ def writeAll( # Assign TMEM addresses sameTextures = ( - self.ti0.useTex - and self.ti1.useTex + (self.ti0.useTex and self.ti1.useTex) + and self.ti0.isTexRef == self.ti1.isTexRef + and self.ti0.tmemSize == self.ti1.tmemSize + and self.ti0.texFormat == self.ti1.texFormat and ( - (not self.ti0.isTexRef and not self.ti1.isTexRef and self.ti0.texProp.tex == self.ti1.texProp.tex) - or ( + ( # not a reference + not self.ti0.isTexRef and self.ti0.texProp.tex == self.ti1.texProp.tex # same image + ) + or ( # reference self.ti0.isTexRef - and self.ti1.isTexRef and self.ti0.texProp.tex_reference == self.ti1.texProp.tex_reference + and self.ti0.texProp.tex_reference_size == self.ti1.texProp.tex_reference_size + and ( # ci format reference + not self.isCI + or ( + self.ti0.texProp.pal_reference == self.ti1.texProp.pal_reference + and self.ti0.texProp.pal_reference_size == self.ti1.texProp.pal_reference_size + ) + ) ) ) ) @@ -779,7 +790,9 @@ def writeAll( self.ti1.texAddr = None # must be set whenever tex 1 used (and loaded or tiled) tmemOccupied = self.texDimensions = None # must be set on all codepaths if sameTextures: - assert self.ti0.tmemSize == self.ti1.tmemSize + assert ( + self.ti0.tmemSize == self.ti1.tmemSize + ), f"Unreachable code path in material {material.name}, same textures (same image or reference) somehow not the same size" tmemOccupied = self.ti0.tmemSize self.ti1.doTexLoad = False self.ti1.texAddr = 0 diff --git a/fast64_internal/f3d/f3d_writer.py b/fast64_internal/f3d/f3d_writer.py index 3bb13826b..4dfd45f34 100644 --- a/fast64_internal/f3d/f3d_writer.py +++ b/fast64_internal/f3d/f3d_writer.py @@ -515,7 +515,7 @@ def addCullCommand(obj, fMesh, transformMatrix, matWriteMethod): ) if matWriteMethod == GfxMatWriteMethod.WriteDifferingAndRevert: - defaults = bpy.context.scene.world.rdp_defaults + defaults = create_or_get_world(bpy.context.scene).rdp_defaults if defaults.g_lighting: cullCommands = [ SPClearGeometryMode(["G_LIGHTING"]), @@ -1321,7 +1321,7 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): fMaterial = fModel.addMaterial(materialName) useDict = all_combiner_uses(f3dMat) - defaults = bpy.context.scene.world.rdp_defaults + defaults = create_or_get_world(bpy.context.scene).rdp_defaults if fModel.f3d.F3DEX_GBI_2: saveGeoModeDefinitionF3DEX2(fMaterial, f3dMat.rdp_settings, defaults, fModel.matWriteMethod) else: diff --git a/fast64_internal/f3d_material_converter.py b/fast64_internal/f3d_material_converter.py index fffb903da..992c2c221 100644 --- a/fast64_internal/f3d_material_converter.py +++ b/fast64_internal/f3d_material_converter.py @@ -186,16 +186,16 @@ def convertAllBSDFtoF3D(objs, renameUV): def convertBSDFtoF3D(obj, index, material, materialDict): if not material.use_nodes: newMaterial = createF3DMat(obj, preset="Shaded Solid", index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = material.diffuse_color + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.default_light_color = material.diffuse_color updateMatWithName(newMaterial, material, materialDict) elif "Principled BSDF" in material.node_tree.nodes: tex0Node = material.node_tree.nodes["Principled BSDF"].inputs["Base Color"] if len(tex0Node.links) == 0: newMaterial = createF3DMat(obj, preset=getDefaultMaterialPreset("Shaded Solid"), index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = tex0Node.default_value + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.default_light_color = tex0Node.default_value updateMatWithName(newMaterial, material, materialDict) else: if isinstance(tex0Node.links[0].from_node, bpy.types.ShaderNodeTexImage): @@ -213,8 +213,8 @@ def convertBSDFtoF3D(obj, index, material, materialDict): else: presetName = getDefaultMaterialPreset("Shaded Texture") newMaterial = createF3DMat(obj, preset=presetName, index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.tex0.tex = tex0Node.links[0].from_node.image + with bpy.context.temp_override(material=newMaterial): + newMaterial.f3d_mat.tex0.tex = tex0Node.links[0].from_node.image updateMatWithName(newMaterial, material, materialDict) else: print("Principled BSDF material does not have an Image Node attached to its Base Color.") diff --git a/fast64_internal/mk64/mk64_model_classes.py b/fast64_internal/mk64/mk64_model_classes.py index 721616cf4..0e19cb8a6 100644 --- a/fast64_internal/mk64/mk64_model_classes.py +++ b/fast64_internal/mk64/mk64_model_classes.py @@ -11,9 +11,9 @@ def course_vertex_format_patterns(): return ( # decomp format r"\{\s*" - r"\{+([^,\}]*),([^,\}]*),([^,\}]*)\}\s*,\s*" + r"\{[\{\s]*([^,\}]*),([^,\}]*),([^,\}]*)\}\s*,\s*" r"\{([^,\}]*),([^,\}]*)\}\s*,\s*" - r"\{MACRO_COLOR_FLAG\(([^,\}]*),([^,\}]*),([^,\}]*),([^,\}])*\),([^,\}]*)\}\s*" + r"\{\s*MACRO_COLOR_FLAG\(([^,\}]*),([^,\}]*),([^,\}]*),([^,\}])*\),([^,\}]*)\}\s*" r"\}" ) diff --git a/fast64_internal/oot/README.md b/fast64_internal/oot/README.md index 99d2fc4b3..abc3f3386 100644 --- a/fast64_internal/oot/README.md +++ b/fast64_internal/oot/README.md @@ -171,7 +171,7 @@ To be able to actually watch your cutscene you need to have a way to trigger it, - ``ENTR_SPOT00_3`` is the Hyrule Field entrance from Lost Woods, see ``entrance_table.h`` to view/add entrances - ``2`` means this cutscene can be watched as child AND as adult - ``EVENTCHKINF_A0`` is the flag set in the ``event_chk_inf`` table, this is a macro but you can use raw hex: ``0xA0`` -- ``gHyruleFieldIntroCs`` is the name of the array with the cutscene commands, as defined in ``assets/scenes/overworld/spot00_scene.c``, ``CutsceneData gHyruleFieldIntroCs[]`` +- ``gHyruleFieldIntroCs`` is the name of the array with the cutscene commands, as defined in ``extracted/VERSION/assets/scenes/overworld/spot00_scene.c``, ``CutsceneData gHyruleFieldIntroCs[]`` 4. Compile the game again and use the entrance you chose for ``sEntranceCutsceneTable`` and your cutscene should play. Alternatively, you can use the map select to watch your cutscene, though note that this won't make it watchable during normal gameplay: diff --git a/fast64_internal/oot/__init__.py b/fast64_internal/oot/__init__.py index 1e9ed07ae..5c4bd90a8 100644 --- a/fast64_internal/oot/__init__.py +++ b/fast64_internal/oot/__init__.py @@ -58,6 +58,20 @@ oot_operator_unregister, ) +oot_versions_items = [ + ("Custom", "Custom", "Custom"), + ("gc-jp", "gc-jp", "gc-jp"), + ("gc-jp-mq", "gc-jp-mq", "gc-jp-mq"), + ("gc-jp-ce", "gc-jp-ce", "gc-jp-ce"), + ("gc-us", "gc-us", "gc-us"), + ("gc-us-mq", "gc-us-mq", "gc-us-mq"), + ("gc-eu", "gc-eu", "gc-eu"), + ("gc-eu-mq", "gc-eu-mq", "gc-eu-mq"), + ("gc-eu-mq-dbg", "gc-eu-mq-dbg", "gc-eu-mq-dbg"), + ("hackeroot-mq", "HackerOoT", "hackeroot-mq"), # TODO: force this value if HackerOoT features are enabled? + ("legacy", "Legacy", "Older Decomp Version"), +] + class OOT_Properties(bpy.types.PropertyGroup): """Global OOT Scene Properties found under scene.fast64.oot""" @@ -75,6 +89,14 @@ class OOT_Properties(bpy.types.PropertyGroup): animExportSettings: bpy.props.PointerProperty(type=OOTAnimExportSettingsProperty) animImportSettings: bpy.props.PointerProperty(type=OOTAnimImportSettingsProperty) collisionExportSettings: bpy.props.PointerProperty(type=OOTCollisionExportSettings) + oot_version: bpy.props.EnumProperty(name="OoT Version", items=oot_versions_items, default="gc-eu-mq-dbg") + oot_version_custom: bpy.props.StringProperty(name="Custom Version") + + def get_extracted_path(self): + if self.oot_version == "legacy": + return "." + else: + return f"extracted/{self.oot_version if self.oot_version != 'Custom' else self.oot_version_custom}" useDecompFeatures: bpy.props.BoolProperty( name="Use decomp for export", description="Use names and macros from decomp when exporting", default=True diff --git a/fast64_internal/oot/animation/operators.py b/fast64_internal/oot/animation/operators.py index 8d11b3e9a..3756b8168 100644 --- a/fast64_internal/oot/animation/operators.py +++ b/fast64_internal/oot/animation/operators.py @@ -19,7 +19,7 @@ def exportAnimationC(armatureObj: bpy.types.Object, settings: OOTAnimExportSettingsProperty): path = bpy.path.abspath(settings.customPath) - exportPath = ootGetObjectPath(settings.isCustom, path, settings.folderName) + exportPath = ootGetObjectPath(settings.isCustom, path, settings.folderName, False) checkEmptyName(settings.folderName) checkEmptyName(armatureObj.name) @@ -66,7 +66,7 @@ def exportAnimationC(armatureObj: bpy.types.Object, settings: OOTAnimExportSetti ootAnim = ootExportNonLinkAnimation(armatureObj, convertTransformMatrix, name) ootAnimC = ootAnim.toC() - path = ootGetPath(exportPath, settings.isCustom, "assets/objects/", settings.folderName, False, False) + path = ootGetPath(exportPath, settings.isCustom, "assets/objects/", settings.folderName, True, False) writeCData(ootAnimC, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) if not settings.isCustom: @@ -79,13 +79,19 @@ def ootImportAnimationC( actorScale: float, ): importPath = bpy.path.abspath(settings.customPath) - filepath = ootGetObjectPath(settings.isCustom, importPath, settings.folderName) + filepath = ootGetObjectPath(settings.isCustom, importPath, settings.folderName, True) if settings.isLink: numLimbs = 21 if not settings.isCustom: basePath = bpy.path.abspath(bpy.context.scene.ootDecompPath) - animFilepath = os.path.join(basePath, "assets/misc/link_animetion/link_animetion.c") - animHeaderFilepath = os.path.join(basePath, "assets/objects/gameplay_keep/gameplay_keep.c") + animFilepath = os.path.join( + basePath, + f"{bpy.context.scene.fast64.oot.get_extracted_path()}/assets/misc/link_animetion/link_animetion.c", + ) + animHeaderFilepath = os.path.join( + basePath, + f"{bpy.context.scene.fast64.oot.get_extracted_path()}/assets/objects/gameplay_keep/gameplay_keep.c", + ) else: animFilepath = filepath animHeaderFilepath = filepath diff --git a/fast64_internal/oot/collision/exporter/to_c/collision.py b/fast64_internal/oot/collision/exporter/to_c/collision.py index 4120776be..563263c5d 100644 --- a/fast64_internal/oot/collision/exporter/to_c/collision.py +++ b/fast64_internal/oot/collision/exporter/to_c/collision.py @@ -273,7 +273,7 @@ def exportCollisionToC( name = toAlnum(originalObj.name) isCustomExport = exportSettings.customExport folderName = exportSettings.folder - exportPath = ootGetObjectPath(isCustomExport, bpy.path.abspath(exportSettings.exportPath), folderName) + exportPath = ootGetObjectPath(isCustomExport, bpy.path.abspath(exportSettings.exportPath), folderName, False) collision = OOTCollision(name) collision.cameraData = OOTCameraData(name) diff --git a/fast64_internal/oot/cutscene/constants.py b/fast64_internal/oot/cutscene/constants.py index b3525246a..d5e155e5b 100644 --- a/fast64_internal/oot/cutscene/constants.py +++ b/fast64_internal/oot/cutscene/constants.py @@ -207,14 +207,14 @@ ] ootCSSingleCommands = [ - "CS_BEGIN_CUTSCENE", - "CS_END", + "CS_HEADER", + "CS_END_OF_SCRIPT", "CS_TRANSITION", "CS_DESTINATION", ] ootCSListAndSingleCommands = ootCSSingleCommands + ootCSListCommands -ootCSListAndSingleCommands.remove("CS_BEGIN_CUTSCENE") +ootCSListAndSingleCommands.remove("CS_HEADER") ootCutsceneCommandsC = ootCSSingleCommands + ootCSListCommands + ootCSListEntryCommands cmdToClass = { diff --git a/fast64_internal/oot/cutscene/exporter/classes.py b/fast64_internal/oot/cutscene/exporter/classes.py index f655b5483..8b42ca5ef 100644 --- a/fast64_internal/oot/cutscene/exporter/classes.py +++ b/fast64_internal/oot/cutscene/exporter/classes.py @@ -458,5 +458,7 @@ def getExportData(self): self.frameCount += self.motionFrameCount - self.frameCount return ( - (indent + f"CS_BEGIN_CUTSCENE({self.entryTotal}, {self.frameCount}),\n") + csData + (indent + "CS_END(),\n") + (indent + f"CS_HEADER({self.entryTotal}, {self.frameCount}),\n") + + csData + + (indent + "CS_END_OF_SCRIPT(),\n") ) diff --git a/fast64_internal/oot/cutscene/importer/classes.py b/fast64_internal/oot/cutscene/importer/classes.py index 121338b11..46e9df3eb 100644 --- a/fast64_internal/oot/cutscene/importer/classes.py +++ b/fast64_internal/oot/cutscene/importer/classes.py @@ -2,7 +2,7 @@ import re from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from bpy.types import Object, Armature from ....utility import PluginError from ..motion.utility import setupCutscene, getBlenderPosition, getInteger @@ -47,8 +47,9 @@ class PropertyData: class CutsceneImport(CutsceneObjectFactory): """This class contains functions to create the new cutscene Blender data""" - filePath: str # used when importing from the panel - fileData: str # used when importing the cutscenes when importing a scene + filePath: Optional[str] # used when importing from the panel + fileData: Optional[str] # used when importing the cutscenes when importing a scene + csName: Optional[str] # used when import a specific cutscene def getCmdParams(self, data: str, cmdName: str, paramNumber: int): """Returns the list of every parameter of the given command""" @@ -68,7 +69,7 @@ def getCmdParams(self, data: str, cmdName: str, paramNumber: int): return params def getNewCutscene(self, csData: str, name: str): - params = self.getCmdParams(csData, "CS_BEGIN_CUTSCENE", Cutscene.paramNumber) + params = self.getCmdParams(csData, "CS_HEADER", Cutscene.paramNumber) return Cutscene(name, getInteger(params[0]), getInteger(params[1])) def getParsedCutscenes(self): @@ -91,6 +92,13 @@ def getParsedCutscenes(self): for oldName in oldNames: fileData = fileData.replace(f"{oldName}(", f"{ootCSLegacyToNewCmdNames[oldName]}(") + # make a list of existing cutscene names, to skip importing them if found + existingCutsceneNames = [ + csObj.name.removeprefix("Cutscene.") + for csObj in bpy.data.objects + if csObj.type == "EMPTY" and csObj.ootEmptyType == "Cutscene" + ] + fileLines: list[str] = [] for line in fileData.split("\n"): fileLines.append(line.strip()) @@ -102,16 +110,22 @@ def getParsedCutscenes(self): for line in fileLines: if not line.startswith("//") and not line.startswith("/*"): if "CutsceneData " in line: + # split with "[" just in case the array has a set size + csName = line.split(" ")[1].split("[")[0] + if csName in existingCutsceneNames: + continue foundCutscene = True if foundCutscene: sLine = line.strip() csCmd = sLine.split("(")[0] if "CutsceneData " not in line and "};" not in line and csCmd not in ootCutsceneCommandsC: - csData[-1] += line + if len(csData) > 0: + csData[-1] += line if len(csData) == 0 or sLine.startswith("CS_") and not sLine.startswith("CS_FLOAT"): - csData.append(line) + if self.csName is None or self.csName == csName: + csData.append(line) if "};" in line: foundCutscene = False @@ -144,7 +158,7 @@ def getParsedCutscenes(self): if curCmd in ootCutsceneCommandsC: line = line.removesuffix(",") + "\n" - if curCmd in ootCSSingleCommands and curCmd != "CS_END": + if curCmd in ootCSSingleCommands and curCmd != "CS_END_OF_SCRIPT": parsedData += line if not cmdListFound and curCmd in ootCSListCommands: @@ -166,7 +180,7 @@ def getParsedCutscenes(self): print(f"{csName}, command:\n{line}") raise PluginError(f"ERROR: Found a list entry outside a list inside ``{csName}``!") - if cmdListFound and nextCmd == "CS_END" or nextCmd in ootCSListAndSingleCommands: + if cmdListFound and nextCmd == "CS_END_OF_SCRIPT" or nextCmd in ootCSListAndSingleCommands: cmdListFound = False parsedCS.append(parsedData) parsedData = "" @@ -199,7 +213,7 @@ def getCutsceneList(self): cmdListName = cmdListData.strip().split("(")[0] # create a new cutscene data - if cmdListName == "CS_BEGIN_CUTSCENE": + if cmdListName == "CS_HEADER": cutscene = self.getNewCutscene(data, parsedCS.csName) # if we have a cutscene, create and add the commands data in it diff --git a/fast64_internal/oot/cutscene/importer/functions.py b/fast64_internal/oot/cutscene/importer/functions.py index 402176e5d..20e562f76 100644 --- a/fast64_internal/oot/cutscene/importer/functions.py +++ b/fast64_internal/oot/cutscene/importer/functions.py @@ -1,10 +1,11 @@ import bpy +from typing import Optional from .classes import CutsceneImport -def importCutsceneData(filePath: str, sceneData: str): +def importCutsceneData(filePath: Optional[str], sceneData: Optional[str], csName: Optional[str] = None): """Initialises and imports the cutscene data from either a file or the scene data""" # NOTE: ``sceneData`` is the data read when importing a scene - csMotionImport = CutsceneImport(filePath, sceneData) + csMotionImport = CutsceneImport(filePath, sceneData, csName) return csMotionImport.setCutsceneData(bpy.context.scene.ootCSNumber) diff --git a/fast64_internal/oot/cutscene/operators.py b/fast64_internal/oot/cutscene/operators.py index 29f727dd4..f956158f4 100644 --- a/fast64_internal/oot/cutscene/operators.py +++ b/fast64_internal/oot/cutscene/operators.py @@ -67,13 +67,13 @@ def insertCutsceneData(filePath: str, csName: str): foundCutscene = True if foundCutscene: - if "CS_BEGIN_CUTSCENE" in line: + if "CS_HEADER" in line: # save the index of the line that contains the entry total and the framecount for later use beginIndex = i # looking at next line to see if we reached the end of the cs script index = i + 1 - if index < len(fileLines) and "CS_END" in fileLines[index]: + if index < len(fileLines) and "CS_END_OF_SCRIPT" in fileLines[index]: # exporting first to get the new framecount and the total of entries values fileLines.insert(index, motionExporter.getExportData()) @@ -90,7 +90,7 @@ def insertCutsceneData(filePath: str, csName: str): frames = re.sub(r"\b([0-9a-fA-F]*)\)", f"{frameCount + motionExporter.frameCount})", beginLine) fileLines[beginIndex] = f"{entries.split(', ')[0]}, {frames.split(', ')[1]}" else: - raise PluginError("ERROR: Can't find `CS_BEGIN_CUTSCENE()` parameters!") + raise PluginError("ERROR: Can't find `CS_HEADER()` parameters!") break fileData = CData() @@ -141,8 +141,8 @@ def execute(self, context): class OOT_ImportCutscene(Operator): bl_idname = "object.oot_import_cutscenes" - bl_label = "Import All Cutscenes" - bl_options = {"REGISTER", "UNDO", "PRESET"} + bl_label = "Import Cutscenes" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): try: @@ -150,7 +150,8 @@ def execute(self, context): object.mode_set(mode="OBJECT") path = abspath(context.scene.ootCutsceneImportPath) - context.scene.ootCSNumber = importCutsceneData(path, None) + csName = context.scene.ootCSImportName if len(context.scene.ootCSImportName) > 0 else None + context.scene.ootCSNumber = importCutsceneData(path, None, csName) self.report({"INFO"}, "Successfully imported cutscenes") return {"FINISHED"} @@ -296,12 +297,16 @@ def cutscene_ops_register(): Scene.ootCutsceneExportPath = StringProperty(name="File", subtype="FILE_PATH") Scene.ootCutsceneImportPath = StringProperty(name="File", subtype="FILE_PATH") Scene.ootCSNumber = IntProperty(default=1, min=0) + Scene.ootCSImportName = StringProperty( + name="CS Name", description="Used to import a single cutscene, can be ``None``" + ) def cutscene_ops_unregister(): for cls in reversed(oot_cutscene_classes): unregister_class(cls) + del Scene.ootCSImportName del Scene.ootCSNumber del Scene.ootCutsceneImportPath del Scene.ootCutsceneExportPath diff --git a/fast64_internal/oot/cutscene/panels.py b/fast64_internal/oot/cutscene/panels.py index 44f3de3e5..5cf84a908 100644 --- a/fast64_internal/oot/cutscene/panels.py +++ b/fast64_internal/oot/cutscene/panels.py @@ -48,9 +48,12 @@ def draw(self, context): importBox = layout.box() importBox.label(text="Cutscene Importer") - prop_split(importBox, context.scene, "ootCutsceneImportPath", "Import From") + prop_split(importBox, context.scene, "ootCSImportName", "Import") + prop_split(importBox, context.scene, "ootCutsceneImportPath", "From") col = importBox.column() + if len(context.scene.ootCSImportName) == 0: + col.label(text="All Cutscenes will be imported.") col.operator(OOT_ImportCutscene.bl_idname) diff --git a/fast64_internal/oot/cutscene_docs.md b/fast64_internal/oot/cutscene_docs.md index e22acb573..c80d5413f 100644 --- a/fast64_internal/oot/cutscene_docs.md +++ b/fast64_internal/oot/cutscene_docs.md @@ -10,8 +10,8 @@ ### Commands More detailed informations and commands' parameters can be found [here](https://github.com/zeldaret/oot/blob/master/include/z64cutscene_commands.h) -- ``CS_BEGIN_CUTSCENE``: defines the beginning of a cutscene script -- ``CS_END``: defines the end of a cutscene script +- ``CS_HEADER``: defines the length and the total number of command entries for a cutscene script +- ``CS_END_OF_SCRIPT``: defines the end of a command list in a cutscene script - ``CS_CAM_POINT``: defines a single camera point, it can be used with any of the "eye" or "at" camera commands - ``CS_CAM_EYE``: defines a single eye point, this feature is not used in the final game and lacks polish - ``CS_CAM_EYE_SPLINE``: declares a list of "eye" camera points that forms a spline diff --git a/fast64_internal/oot/exporter/collision/__init__.py b/fast64_internal/oot/exporter/collision/__init__.py index d2ef96e7d..3a1ad4949 100644 --- a/fast64_internal/oot/exporter/collision/__init__.py +++ b/fast64_internal/oot/exporter/collision/__init__.py @@ -156,14 +156,14 @@ def getCollisionData(dataHolder: Optional[Object], transform: Matrix, useMacros: surfaceType = SurfaceType( colProp.cameraID, colProp.exitID, - int(Utility.getPropValue(colProp, "floorProperty"), base=16), + Utility.getPropValue(colProp, "floorProperty"), 0, # unused? - int(Utility.getPropValue(colProp, "wallSetting"), base=16), - int(Utility.getPropValue(colProp, "floorSetting"), base=16), + Utility.getPropValue(colProp, "wallSetting"), + Utility.getPropValue(colProp, "floorSetting"), colProp.decreaseHeight, colProp.eponaBlock, - int(Utility.getPropValue(colProp, "sound"), base=16), - int(Utility.getPropValue(colProp, "terrain"), base=16), + Utility.getPropValue(colProp, "sound"), + Utility.getPropValue(colProp, "terrain"), colProp.lightingSetting, int(colProp.echo, base=16), colProp.hookshotable, diff --git a/fast64_internal/oot/exporter/collision/surface.py b/fast64_internal/oot/exporter/collision/surface.py index 67fc810bf..f5bf0d571 100644 --- a/fast64_internal/oot/exporter/collision/surface.py +++ b/fast64_internal/oot/exporter/collision/surface.py @@ -9,16 +9,16 @@ class SurfaceType: # surface type 0 bgCamIndex: int exitIndex: int - floorType: int + floorType: str unk18: int # unused? - wallType: int - floorProperty: int + wallType: str + floorProperty: str isSoft: bool isHorseBlocked: bool # surface type 1 - material: int - floorEffect: int + material: str + floorEffect: str lightSetting: int echo: int canHookshot: bool diff --git a/fast64_internal/oot/exporter/cutscene/__init__.py b/fast64_internal/oot/exporter/cutscene/__init__.py index f1cca53b6..a9d8c5b1a 100644 --- a/fast64_internal/oot/exporter/cutscene/__init__.py +++ b/fast64_internal/oot/exporter/cutscene/__init__.py @@ -83,10 +83,10 @@ def getC(self): csData.source = ( declarationBase + " = {\n" - + (indent + f"CS_BEGIN_CUTSCENE({self.totalEntries}, {self.frameCount}),\n") + + (indent + f"CS_HEADER({self.totalEntries}, {self.frameCount}),\n") + (self.data.destination.getCmd() if self.data.destination is not None else "") + "".join(entry.getCmd() for curList in dataListNames for entry in getattr(self.data, curList)) - + (indent + "CS_END(),\n") + + (indent + "CS_END_OF_SCRIPT(),\n") + "};\n\n" ) diff --git a/fast64_internal/oot/exporter/room/header.py b/fast64_internal/oot/exporter/room/header.py index 433659b3b..bcc275f73 100644 --- a/fast64_internal/oot/exporter/room/header.py +++ b/fast64_internal/oot/exporter/room/header.py @@ -137,9 +137,16 @@ class RoomActors: actorList: list[Actor] @staticmethod - def new(name: str, sceneObj: Optional[Object], roomObj: Optional[Object], transform: Matrix, headerIndex: int): + def new( + name: str, + sceneObj: Optional[Object], + roomObj: Optional[Object], + transform: Matrix, + headerIndex: int, + room_index: int, + ): actorList: list[Actor] = [] - actorObjList = getObjectList(sceneObj.children_recursive, "EMPTY", "Actor", parentObj=roomObj) + actorObjList = getObjectList(sceneObj.children, "EMPTY", "Actor", parentObj=roomObj, room_index=room_index) for obj in actorObjList: actorProp = obj.ootActorProperty if not Utility.isCurrentHeaderValid(actorProp.headerSettings, headerIndex): @@ -228,7 +235,7 @@ def new( name, RoomInfos.new(props), RoomObjects.new(f"{name}_objectList", props), - RoomActors.new(f"{name}_actorList", sceneObj, roomObj, transform, headerIndex), + RoomActors.new(f"{name}_actorList", sceneObj, roomObj, transform, headerIndex, props.roomIndex), ) def getHeaderDefines(self): diff --git a/fast64_internal/oot/f3d/operators.py b/fast64_internal/oot/f3d/operators.py index f62653c54..5d8cab7d8 100644 --- a/fast64_internal/oot/f3d/operators.py +++ b/fast64_internal/oot/f3d/operators.py @@ -119,13 +119,13 @@ def execute(self, context): flipbookUses2DArray = settings.flipbookUses2DArray flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None - paths = [ootGetObjectPath(isCustomImport, importPath, folderName)] - data = getImportData(paths) + paths = [ootGetObjectPath(isCustomImport, importPath, folderName, True)] + filedata = getImportData(paths) f3dContext = OOTF3DContext(get_F3D_GBI(), [name], basePath) scale = getOOTScale(settings.actorScale) if not isCustomImport: - data = ootGetIncludedAssetData(basePath, paths, data) + data + filedata = ootGetIncludedAssetData(basePath, paths, filedata) + filedata if overlayName is not None: ootReadTextureArrays(basePath, overlayName, name, f3dContext, False, flipbookArrayIndex2D) @@ -133,7 +133,7 @@ def execute(self, context): scale = ootReadActorScale(basePath, overlayName, False) obj = importMeshC( - data, + filedata, name, scale, removeDoubles, diff --git a/fast64_internal/oot/f3d/panels.py b/fast64_internal/oot/f3d/panels.py index 94cb0242e..30b0bff20 100644 --- a/fast64_internal/oot/f3d/panels.py +++ b/fast64_internal/oot/f3d/panels.py @@ -94,7 +94,10 @@ def poll(cls, context): return context.scene.gameEditorMode == "OOT" def draw(self, context): - ootDefaultRenderModeProp: OOTDefaultRenderModesProperty = context.scene.world.ootDefaultRenderModes + world = context.scene.world + if not world: + return + ootDefaultRenderModeProp: OOTDefaultRenderModesProperty = world.ootDefaultRenderModes ootDefaultRenderModeProp.draw_props(self.layout) diff --git a/fast64_internal/oot/f3d/properties.py b/fast64_internal/oot/f3d/properties.py index 747674608..d4c0e4772 100644 --- a/fast64_internal/oot/f3d/properties.py +++ b/fast64_internal/oot/f3d/properties.py @@ -1,6 +1,9 @@ +import bpy + from bpy.types import PropertyGroup, Object, World, Material, UILayout from bpy.props import PointerProperty, StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty from bpy.utils import register_class, unregister_class +from ...f3d.f3d_material import update_world_default_rendermode from ...f3d.f3d_parser import ootEnumDrawLayers from ...utility import prop_split @@ -149,12 +152,12 @@ def draw_props(self, layout: UILayout, mat: Object, drawLayer: str): class OOTDefaultRenderModesProperty(PropertyGroup): expandTab: BoolProperty() - opaqueCycle1: StringProperty(default="G_RM_AA_ZB_OPA_SURF") - opaqueCycle2: StringProperty(default="G_RM_AA_ZB_OPA_SURF2") - transparentCycle1: StringProperty(default="G_RM_AA_ZB_XLU_SURF") - transparentCycle2: StringProperty(default="G_RM_AA_ZB_XLU_SURF2") - overlayCycle1: StringProperty(default="G_RM_AA_ZB_OPA_SURF") - overlayCycle2: StringProperty(default="G_RM_AA_ZB_OPA_SURF2") + opaqueCycle1: StringProperty(default="G_RM_AA_ZB_OPA_SURF", update=update_world_default_rendermode) + opaqueCycle2: StringProperty(default="G_RM_AA_ZB_OPA_SURF2", update=update_world_default_rendermode) + transparentCycle1: StringProperty(default="G_RM_AA_ZB_XLU_SURF", update=update_world_default_rendermode) + transparentCycle2: StringProperty(default="G_RM_AA_ZB_XLU_SURF2", update=update_world_default_rendermode) + overlayCycle1: StringProperty(default="G_RM_AA_ZB_OPA_SURF", update=update_world_default_rendermode) + overlayCycle2: StringProperty(default="G_RM_AA_ZB_OPA_SURF2", update=update_world_default_rendermode) def draw_props(self, layout: UILayout): inputGroup = layout.column() diff --git a/fast64_internal/oot/file_settings.py b/fast64_internal/oot/file_settings.py index 1f451becd..b6bd246c6 100644 --- a/fast64_internal/oot/file_settings.py +++ b/fast64_internal/oot/file_settings.py @@ -18,6 +18,11 @@ def draw(self, context): prop_split(col, context.scene, "ootBlenderScale", "OOT Scene Scale") prop_split(col, context.scene, "ootDecompPath", "Decomp Path") + + prop_split(col, context.scene.fast64.oot, "oot_version", "OoT Version") + if context.scene.fast64.oot.oot_version == "Custom": + prop_split(col, context.scene.fast64.oot, "oot_version_custom", "Custom Version") + col.prop(context.scene.fast64.oot, "headerTabAffectsVisibility") col.prop(context.scene.fast64.oot, "hackerFeaturesEnabled") diff --git a/fast64_internal/oot/importer/scene.py b/fast64_internal/oot/importer/scene.py index 386b15568..38541609a 100644 --- a/fast64_internal/oot/importer/scene.py +++ b/fast64_internal/oot/importer/scene.py @@ -84,7 +84,7 @@ def parseScene( subfolder = None else: if option == "Custom": - subfolder = "assets/scenes/" + settings.subFolder + "/" + subfolder = f"{bpy.context.scene.fast64.oot.get_extracted_path()}/assets/scenes/{settings.subFolder}/" else: sceneName = sceneNameFromID(option) subfolder = None @@ -94,9 +94,16 @@ def parseScene( if settings.isCustomDest is not None: importSubdir = subfolder if not settings.isCustomDest and subfolder is None: - importSubdir = os.path.dirname(getSceneDirFromLevelName(sceneName)) + "/" - - sceneFolderPath = ootGetPath(importPath, settings.isCustomDest, importSubdir, sceneName, False, True) + importSubdir = os.path.dirname(getSceneDirFromLevelName(sceneName, True)) + "/" + + sceneFolderPath = ootGetPath( + importPath if settings.isCustomDest else f"{importPath}/{bpy.context.scene.fast64.oot.get_extracted_path()}/", + settings.isCustomDest, + importSubdir, + sceneName, + False, + True, + ) filePath = os.path.join(sceneFolderPath, f"{sceneName}_scene.c") sceneData = readFile(filePath) diff --git a/fast64_internal/oot/oot_model_classes.py b/fast64_internal/oot/oot_model_classes.py index 59b5b5253..ce3fe2f45 100644 --- a/fast64_internal/oot/oot_model_classes.py +++ b/fast64_internal/oot/oot_model_classes.py @@ -2,7 +2,7 @@ from typing import Union from ..f3d.f3d_parser import F3DContext, F3DTextureReference, getImportData from ..f3d.f3d_material import TextureProperty, createF3DMat, texFormatOf, texBitSizeF3D -from ..utility import PluginError, CData, hexOrDecInt, getNameFromPath, getTextureSuffixFromFormat, toAlnum +from ..utility import PluginError, hexOrDecInt, create_or_get_world from ..f3d.flipbook import TextureFlipbook, FlipbookProperty, usesFlipbook, ootFlipbookReferenceIsValid from ..f3d.f3d_writer import VertexGroupInfo, TriangleConverterInfo @@ -118,7 +118,7 @@ def getRenderMode(self, drawLayer): drawLayerUsed = self.drawLayerOverride else: drawLayerUsed = drawLayer - defaultRenderModes = bpy.context.scene.world.ootDefaultRenderModes + defaultRenderModes = create_or_get_world(bpy.context.scene).ootDefaultRenderModes cycle1 = getattr(defaultRenderModes, drawLayerUsed.lower() + "Cycle1") cycle2 = getattr(defaultRenderModes, drawLayerUsed.lower() + "Cycle2") return [cycle1, cycle2] @@ -335,6 +335,7 @@ def __init__(self, f3d, limbList, basePath): materialContext = createF3DMat(None, preset="oot_shaded_solid") # materialContext.f3d_mat.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE" F3DContext.__init__(self, f3d, basePath, materialContext) + self.draw_layer_prop = "oot" def getLimbName(self, index): return self.limbList[index] diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/oot/oot_utility.py index b0b8f3b95..3173cc299 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/oot/oot_utility.py @@ -162,6 +162,7 @@ def isPathObject(obj: bpy.types.Object) -> bool: "testroom", ] +# NOTE: the "extracted/VERSION/" part is added in ``getSceneDirFromLevelName`` when needed ootSceneDirs = { "assets/scenes/dungeons/": ootSceneDungeons, "assets/scenes/indoors/": ootSceneIndoors, @@ -234,7 +235,11 @@ def addIncludeFilesExtension(objectName, objectPath, assetName, extension): if not os.path.exists(objectPath): raise PluginError(objectPath + " does not exist.") path = os.path.join(objectPath, objectName + "." + extension) - data = getDataFromFile(path) + if not os.path.exists(path): + # workaround for exporting to an object that doesn't exist in assets/ + data = "" + else: + data = getDataFromFile(path) if include not in data: data += "\n" + include @@ -243,10 +248,11 @@ def addIncludeFilesExtension(objectName, objectPath, assetName, extension): saveDataToFile(path, data) -def getSceneDirFromLevelName(name): +def getSceneDirFromLevelName(name: str, include_extracted: bool = False): + extracted = bpy.context.scene.fast64.oot.get_extracted_path() if include_extracted else "." for sceneDir, dirLevels in ootSceneDirs.items(): if name in dirLevels: - return sceneDir + name + return f"{extracted}/" + sceneDir + name return None @@ -458,22 +464,34 @@ def checkEmptyName(name): raise PluginError("No name entered for the exporter.") -def ootGetObjectPath(isCustomExport: bool, exportPath: str, folderName: str) -> str: +def ootGetObjectPath(isCustomExport: bool, exportPath: str, folderName: str, include_extracted: bool) -> str: + extracted = bpy.context.scene.fast64.oot.get_extracted_path() if include_extracted else "." + if isCustomExport: filepath = exportPath else: filepath = os.path.join( - ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, False), folderName + ".c" + ootGetPath( + exportPath, + isCustomExport, + f"{extracted}/assets/objects/", + folderName, + False, + False, + ), + folderName + ".c", ) return filepath -def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: str) -> str: +def ootGetObjectHeaderPath(isCustomExport: bool, exportPath: str, folderName: str, include_extracted: bool) -> str: + extracted = bpy.context.scene.fast64.oot.get_extracted_path() if include_extracted else "." if isCustomExport: filepath = exportPath else: filepath = os.path.join( - ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, False), folderName + ".h" + ootGetPath(exportPath, isCustomExport, f"{extracted}/assets/objects/", folderName, False, False), + folderName + ".h", ) return filepath @@ -1044,17 +1062,19 @@ def getObjectList( objType: str, emptyType: Optional[str] = None, splineType: Optional[str] = None, - parentObj: Object = None, + parentObj: Optional[Object] = None, + room_index: Optional[int] = None, ): """ Returns a list containing objects matching ``objType``. Sorts by object name. Parameters: - - ``objList``: the list of objects to iterate through, usually ``obj.children_recursive`` - - ``objType``: the object's type (``EMPTY``, ``CURVE``, etc.) - - ``emptyType``: optional, filters the object by the given empty type - - ``splineType``: optional, filters the object by the given spline type - - ``parentObj``: optional, checks if the found object is parented to ``parentObj`` + - `objList`: the list of objects to iterate through, usually ``obj.children_recursive`` + - `objType`: the object's type (``EMPTY``, ``CURVE``, etc.) + - `emptyType`: optional, filters the object by the given empty type + - `splineType`: optional, filters the object by the given spline type + - `parentObj`: optional, checks if the found object is parented to ``parentObj`` + - `room_index`: optional, the room index """ ret: list[Object] = [] @@ -1068,13 +1088,13 @@ def getObjectList( cond = obj.ootSplineProperty.splineType == splineType if parentObj is not None: - if emptyType == "Actor" and obj.ootEmptyType == "Room": + if emptyType == "Actor" and obj.ootEmptyType == "Room" and obj.ootRoomHeader.roomIndex == room_index: for o in obj.children_recursive: if o.type == objType and o.ootEmptyType == emptyType and o not in ret: ret.append(o) continue else: - cond = cond and obj.parent is not None and obj.parent.name == parentObj.name + cond = cond and obj.parent is not None and obj.parent == parentObj if cond and obj not in ret: ret.append(obj) diff --git a/fast64_internal/oot/props_panel_main.py b/fast64_internal/oot/props_panel_main.py index 11fdb9900..3323825b3 100644 --- a/fast64_internal/oot/props_panel_main.py +++ b/fast64_internal/oot/props_panel_main.py @@ -229,8 +229,8 @@ def draw_props(self, layout: bpy.types.UILayout): col.prop(self, "sizeControlsCull") if not self.sizeControlsCull: prop_split(col, self, "manualRadius", "Radius (OOT Units)") - col.label(text="Meshes generate cull groups automatically.", icon="INFO") - col.label(text="This is only for custom cull group shapes.") + col.label(text="RSP culling is automatic. The 'Custom Cull Group' empty type is for CPU culling.", icon="INFO") + col.label(text="This will create custom cull group shape entries to be used in Cullable rooms.") col.label(text="Use Options -> Transform -> Affect Only -> Parent ", icon="INFO") col.label(text="to move object without affecting children.") diff --git a/fast64_internal/oot/room/properties.py b/fast64_internal/oot/room/properties.py index 7af1e4628..1e9d643d2 100644 --- a/fast64_internal/oot/room/properties.py +++ b/fast64_internal/oot/room/properties.py @@ -172,7 +172,9 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj if self.roomShape == "ROOM_SHAPE_TYPE_IMAGE": self.drawBGImageList(general, objName) if self.roomShape == "ROOM_SHAPE_TYPE_CULLABLE": - general.label(text="Cull regions are generated automatically.", icon="INFO") + general.label(text="The 'Cullable' room shape type is for CPU culling,", icon="INFO") + general.label(text="and requires meshes to be parented to Custom Cull Group empties.") + general.label(text="RSP culling is done automatically regardless of room shape.") prop_split(general, self, "defaultCullDistance", "Default Cull (Blender Units)") # Behaviour behaviourBox = layout.column() diff --git a/fast64_internal/oot/scene/operators.py b/fast64_internal/oot/scene/operators.py index 17e14afdd..72e8b1971 100644 --- a/fast64_internal/oot/scene/operators.py +++ b/fast64_internal/oot/scene/operators.py @@ -222,7 +222,6 @@ class OOT_RemoveScene(Operator): def execute(self, context): settings = context.scene.ootSceneRemoveSettings # Type: OOTRemoveSceneSettingsProperty - levelName = settings.name option = settings.option if settings.customExport: @@ -230,13 +229,14 @@ def execute(self, context): return {"FINISHED"} if option == "Custom": - subfolder = "assets/scenes/" + settings.subFolder + "/" + levelName = settings.name + subfolder = f"assets/scenes/{settings.subFolder}/" else: levelName = sceneNameFromID(option) subfolder = None - removeInfo = RemoveInfo(abspath(context.scene.ootDecompPath), subfolder, levelName) - Files.remove_scene(removeInfo) + # the scene files will be removed from `assets` if it's present + Files.remove_scene(RemoveInfo(abspath(context.scene.ootDecompPath), subfolder, levelName)) self.report({"INFO"}, "Success!") return {"FINISHED"} diff --git a/fast64_internal/oot/scene/panels.py b/fast64_internal/oot/scene/panels.py index 47bdccf09..009c4273b 100644 --- a/fast64_internal/oot/scene/panels.py +++ b/fast64_internal/oot/scene/panels.py @@ -1,7 +1,8 @@ +import bpy import os + from bpy.types import UILayout from bpy.utils import register_class, unregister_class -from ...utility import customExportWarning, prop_split from ...panels import OOT_Panel from ..oot_constants import ootEnumSceneID from ..oot_utility import getEnumName @@ -10,7 +11,6 @@ OOTImportSceneSettingsProperty, OOTRemoveSceneSettingsProperty, OOTBootupSceneOptions, - OOTSceneCommon, ) from .operators import ( @@ -80,17 +80,6 @@ def draw(self, context): removeRow = removeBox.row() removeRow.operator(OOT_RemoveScene.bl_idname, text="Remove Scene") - if removeSettings.option == "Custom": - exportPath = ( - context.scene.ootDecompPath + f"assets/scenes/{removeSettings.subFolder}/{removeSettings.name}/" - ) - - if not os.path.exists(exportPath): - removeRow.enabled = False - removeBox.label(text="This path doesn't exist.") - else: - removeRow.enabled = True - classes = (OOT_ExportScenePanel,) diff --git a/fast64_internal/oot/skeleton/exporter/functions.py b/fast64_internal/oot/skeleton/exporter/functions.py index 171113483..3c508f7d7 100644 --- a/fast64_internal/oot/skeleton/exporter/functions.py +++ b/fast64_internal/oot/skeleton/exporter/functions.py @@ -132,7 +132,7 @@ def ootProcessBone( def ootConvertArmatureToSkeleton( originalArmatureObj, convertTransformMatrix, - fModel, + fModel: OOTModel, name, convertTextureData, skeletonOnly, @@ -280,7 +280,7 @@ def ootConvertArmatureToC( else: data.source += "\n" - path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, True) + path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, True, True) includeDir = settings.customAssetIncludeDir if settings.isCustom else f"assets/objects/{folderName}" exportData = fModel.to_c( TextureExportSettings(False, savePNG, includeDir, path), OOTGfxFormatter(ScrollMethod.Vertex) diff --git a/fast64_internal/oot/skeleton/importer/functions.py b/fast64_internal/oot/skeleton/importer/functions.py index d2687582b..84699c009 100644 --- a/fast64_internal/oot/skeleton/importer/functions.py +++ b/fast64_internal/oot/skeleton/importer/functions.py @@ -6,7 +6,7 @@ from ....utility import hexOrDecInt, applyRotation, PluginError from ...oot_f3d_writer import ootReadActorScale from ...oot_model_classes import OOTF3DContext, ootGetIncludedAssetData -from ...oot_utility import ootGetObjectPath, getOOTScale, ootGetObjectHeaderPath, ootGetEnums, ootStripComments +from ...oot_utility import OOTEnum, ootGetObjectPath, getOOTScale, ootGetObjectHeaderPath, ootGetEnums, ootStripComments from ...oot_texture_array import ootReadTextureArrays from ..constants import ootSkeletonImportDict from ..properties import OOTSkeletonImportSettings @@ -203,7 +203,6 @@ def ootBuildSkeleton( f3dContext.matrixData[limbName], limbName, boneName, - "oot", drawLayer, f3dContext, True, @@ -259,8 +258,8 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) restPoseData = None filepaths = [ - ootGetObjectPath(isCustomImport, importPath, folderName), - ootGetObjectHeaderPath(isCustomImport, importPath, folderName), + ootGetObjectPath(isCustomImport, importPath, folderName, True), + ootGetObjectHeaderPath(isCustomImport, importPath, folderName, True), ] removeDoubles = importSettings.removeDoubles diff --git a/fast64_internal/oot/skeleton/properties.py b/fast64_internal/oot/skeleton/properties.py index a000d4b9e..daa0e9c43 100644 --- a/fast64_internal/oot/skeleton/properties.py +++ b/fast64_internal/oot/skeleton/properties.py @@ -1,4 +1,6 @@ -from bpy.types import Armature, PropertyGroup, Object, Bone, UILayout +import bpy + +from bpy.types import PropertyGroup, Object, Bone, UILayout from bpy.props import EnumProperty, PointerProperty, StringProperty, FloatProperty, BoolProperty, IntProperty from bpy.utils import register_class, unregister_class from ...f3d.f3d_material import ootEnumDrawLayers diff --git a/fast64_internal/oot/tools/quick_import.py b/fast64_internal/oot/tools/quick_import.py index 179522cd3..0a51344ee 100644 --- a/fast64_internal/oot/tools/quick_import.py +++ b/fast64_internal/oot/tools/quick_import.py @@ -7,6 +7,7 @@ from ..f3d.properties import OOTDLImportSettings from ..skeleton.properties import OOTSkeletonImportSettings from ..animation.properties import OOTAnimImportSettingsProperty +from ..cutscene.importer import importCutsceneData class QuickImportAborted(Exception): @@ -15,6 +16,27 @@ def __init__(self, message): self.message = message +def get_found_defs(path: Path, sym_name: str, sym_def_pattern: re.Pattern[str]): + all_found_defs: dict[Path, list[tuple[str, str]]] = dict() + + for dirpath, _, filenames in os.walk(path): + dirpath_p = Path(dirpath) + for filename in filenames: + file_p = dirpath_p / filename + # Only look into C files + if file_p.suffix != ".c": + continue + source = file_p.read_text() + # Simple check to see if we should look into this file any further + if sym_name not in source: + continue + found_defs = sym_def_pattern.findall(source) + print(file_p, f"{found_defs=}") + all_found_defs[file_p] = found_defs + + return all_found_defs + + def quick_import_exec(context: bpy.types.Context, sym_name: str): sym_name = sym_name.strip() if sym_name == "": @@ -35,12 +57,19 @@ def quick_import_exec(context: bpy.types.Context, sym_name: str): sym_def_pattern = re.compile(rf"([^\s]+)\s+{sym_name}\s*(\[[^\]]*\])?\s*=") - base_dir_p = Path(context.scene.ootDecompPath) + base_dir_p = Path(context.scene.ootDecompPath) / context.scene.fast64.oot.get_extracted_path() assets_objects_dir_p = base_dir_p / "assets" / "objects" + assets_scenes_dir_p = base_dir_p / "assets" / "scenes" + is_sym_object = True + all_found_defs = get_found_defs(assets_objects_dir_p, sym_name, sym_def_pattern) + if len(all_found_defs) == 0: + is_sym_object = False + all_found_defs = get_found_defs(assets_scenes_dir_p, sym_name, sym_def_pattern) + found_dir_p = assets_objects_dir_p if is_sym_object else assets_scenes_dir_p all_found_defs: dict[Path, list[tuple[str, str]]] = dict() - for dirpath, dirnames, filenames in os.walk(assets_objects_dir_p): + for dirpath, dirnames, filenames in os.walk(found_dir_p): dirpath_p = Path(dirpath) for filename in filenames: file_p = dirpath_p / filename @@ -63,32 +92,38 @@ def quick_import_exec(context: bpy.types.Context, sym_name: str): # {Path('.../assets/objects/gameplay_field_keep/gameplay_field_keep.c'): [('SkeletonHeader', '')]} if len(all_found_defs) == 0: - raise QuickImportAborted(f"Couldn't find a definition of {sym_name}") + raise QuickImportAborted(f"Couldn't find a definition of {sym_name}, is the OoT Version correct?") if len(all_found_defs) > 1: raise QuickImportAborted( f"Found definitions of {sym_name} in several files: " - + ", ".join(str(p.relative_to(assets_objects_dir_p)) for p in all_found_defs.keys()) + + ", ".join(str(p.relative_to(found_dir_p)) for p in all_found_defs.keys()) ) assert len(all_found_defs) == 1 sym_file_p, sym_defs = list(all_found_defs.items())[0] if len(sym_defs) > 1: - raise QuickImportAborted( - f"Found several definitions of {sym_name} in {sym_file_p.relative_to(assets_objects_dir_p)}" - ) + raise QuickImportAborted(f"Found several definitions of {sym_name} in {sym_file_p.relative_to(found_dir_p)}") # We found a single definition of the symbol sym_def_type, sym_def_array_decl = sym_defs[0] is_array = sym_def_array_decl != "" - object_name = sym_file_p.relative_to(assets_objects_dir_p).parts[0] + folder_name = sym_file_p.relative_to(found_dir_p).parts[0] + + def raise_only_from_object(type: str): + if not is_sym_object: + raise QuickImportAborted( + f"Can only import {type} from an object ({sym_name} found in {sym_file_p.relative_to(base_dir_p)})" + ) if sym_def_type == "Gfx" and is_array: + raise_only_from_object("Gfx[]") settings: OOTDLImportSettings = context.scene.fast64.oot.DLImportSettings settings.name = sym_name - settings.folder = object_name + settings.folder = folder_name settings.actorOverlayName = "" settings.isCustom = False bpy.ops.object.oot_import_dl() elif sym_def_type in {"SkeletonHeader", "FlexSkeletonHeader"} and not is_array: + raise_only_from_object(sym_def_type) settings: OOTSkeletonImportSettings = context.scene.fast64.oot.skeletonImportSettings settings.isCustom = False if sym_name == "gLinkAdultSkel": @@ -98,19 +133,22 @@ def quick_import_exec(context: bpy.types.Context, sym_name: str): else: settings.mode = "Generic" settings.name = sym_name - settings.folder = object_name + settings.folder = folder_name settings.actorOverlayName = "" bpy.ops.object.oot_import_skeleton() elif sym_def_type == "AnimationHeader" and not is_array: + raise_only_from_object(sym_def_type) settings: OOTAnimImportSettingsProperty = context.scene.fast64.oot.animImportSettings settings.isCustom = False settings.isLink = False settings.animName = sym_name - settings.folderName = object_name + settings.folderName = folder_name bpy.ops.object.oot_import_anim() + elif sym_def_type == "CutsceneData" and is_array: + bpy.context.scene.ootCSNumber = importCutsceneData(f"{sym_file_p}", None, sym_name) else: raise QuickImportAborted( f"Don't know how to import {sym_def_type}" + ("[]" if is_array else "") - + f" (symbol found in {object_name})" + + f" (symbol found in {sym_file_p.relative_to(base_dir_p)})" ) diff --git a/fast64_internal/render_settings.py b/fast64_internal/render_settings.py index 6476f8f1f..29e5a7385 100644 --- a/fast64_internal/render_settings.py +++ b/fast64_internal/render_settings.py @@ -92,45 +92,155 @@ def interpColors(cola, colb, fade): ) -def update_lighting_space(renderSettings: "Fast64RenderSettings_Properties"): - bpy.data.node_groups["GetSpecularNormal"].nodes["GeometryNormal"].node_tree = bpy.data.node_groups[ - "GeometryNormal_WorldSpace" if renderSettings.useWorldSpaceLighting else "GeometryNormal_ViewSpace" - ] +def update_scene_props_from_rs_enableFogPreview( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): + sceneOutputs.inputs["FogEnable"].default_value = int(renderSettings.enableFogPreview) -def update_scene_props_from_render_settings( - context: bpy.types.Context, - sceneOutputs: bpy.types.NodeGroupOutput, - renderSettings: "Fast64RenderSettings_Properties", +def update_scene_props_from_rs_fogPreviewColor( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" ): - sceneOutputs.inputs["FogEnable"].default_value = int(renderSettings.enableFogPreview) sceneOutputs.inputs["FogColor"].default_value = s_rgb_alpha_1_tuple(renderSettings.fogPreviewColor) + + +def update_scene_props_from_rs_clippingPlanes( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["F3D_NearClip"].default_value = float(renderSettings.clippingPlanes[0]) sceneOutputs.inputs["F3D_FarClip"].default_value = float(renderSettings.clippingPlanes[1]) - sceneOutputs.inputs["Blender_Game_Scale"].default_value = float(get_blender_to_game_scale(context)) + + +def update_scene_props_from_rs_fogPreviewPosition( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["FogNear"].default_value = renderSettings.fogPreviewPosition[0] sceneOutputs.inputs["FogFar"].default_value = renderSettings.fogPreviewPosition[1] + +def update_scene_props_from_rs_ambientColor( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["AmbientColor"].default_value = s_rgb_alpha_1_tuple(renderSettings.ambientColor) + + +def update_scene_props_from_rs_light0Color( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["Light0Color"].default_value = s_rgb_alpha_1_tuple(renderSettings.light0Color) + + +def update_scene_props_from_rs_light0Direction( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["Light0Dir"].default_value = renderSettings.light0Direction + + +def update_scene_props_from_rs_light0SpecSize( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["Light0Size"].default_value = renderSettings.light0SpecSize + + +def update_scene_props_from_rs_light1Color( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["Light1Color"].default_value = s_rgb_alpha_1_tuple(renderSettings.light1Color) + + +def update_scene_props_from_rs_light1Direction( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["Light1Dir"].default_value = renderSettings.light1Direction + + +def update_scene_props_from_rs_light1SpecSize( + sceneOutputs: bpy.types.NodeGroupOutput, renderSettings: "Fast64RenderSettings_Properties" +): sceneOutputs.inputs["Light1Size"].default_value = renderSettings.light1SpecSize - update_lighting_space(renderSettings) + +def update_scene_props_from_rs_useWorldSpaceLighting(renderSettings: "Fast64RenderSettings_Properties"): + bpy.data.node_groups["GetSpecularNormal"].nodes["GeometryNormal"].node_tree = bpy.data.node_groups[ + "GeometryNormal_WorldSpace" if renderSettings.useWorldSpaceLighting else "GeometryNormal_ViewSpace" + ] -def on_update_render_preview_nodes(self, context: bpy.types.Context): +def update_scene_props_from_render_settings( + sceneOutputs: bpy.types.NodeGroupOutput, + renderSettings: "Fast64RenderSettings_Properties", +): + update_scene_props_from_rs_enableFogPreview(sceneOutputs, renderSettings) + update_scene_props_from_rs_fogPreviewColor(sceneOutputs, renderSettings) + update_scene_props_from_rs_clippingPlanes(sceneOutputs, renderSettings) + update_scene_props_from_rs_fogPreviewPosition(sceneOutputs, renderSettings) + update_scene_props_from_rs_ambientColor(sceneOutputs, renderSettings) + update_scene_props_from_rs_light0Color(sceneOutputs, renderSettings) + update_scene_props_from_rs_light0Direction(sceneOutputs, renderSettings) + update_scene_props_from_rs_light0SpecSize(sceneOutputs, renderSettings) + update_scene_props_from_rs_light1Color(sceneOutputs, renderSettings) + update_scene_props_from_rs_light1Direction(sceneOutputs, renderSettings) + update_scene_props_from_rs_light1SpecSize(sceneOutputs, renderSettings) + update_scene_props_from_rs_useWorldSpaceLighting(renderSettings) + + # TODO use a callback on the scale props to set this value + sceneOutputs.inputs["Blender_Game_Scale"].default_value = float(get_blender_to_game_scale(bpy.context)) + + +def getSceneOutputs(): sceneProps = bpy.data.node_groups.get("SceneProperties") if sceneProps == None: print("Could not locate SceneProperties!") - return + return None sceneOutputs: bpy.types.NodeGroupOutput = sceneProps.nodes["Group Output"] - renderSettings: "Fast64RenderSettings_Properties" = context.scene.fast64.renderSettings - update_scene_props_from_render_settings(context, sceneOutputs, renderSettings) + return sceneOutputs + + +class ManualUpdatePreviewOperator(bpy.types.Operator): + bl_idname = "view3d.fast64_manual_update_preview" + bl_label = "Update Preview" + bl_description = "Apply the F3D Render Settings to the view" + + def execute(self, context): + sceneOutputs = getSceneOutputs() + renderSettings = bpy.context.scene.fast64.renderSettings + + if sceneOutputs is None: + return {"CANCELLED"} + + update_scene_props_from_render_settings(sceneOutputs, renderSettings) + return {"FINISHED"} + + +def make_callback(update_scene_props_from_rs_func): + def on_update_rs_func(self: "Fast64RenderSettings_Properties", context): + if not self.enableAutoUpdatePreview: + return + sceneOutputs = getSceneOutputs() + if sceneOutputs is not None: + update_scene_props_from_rs_func(sceneOutputs, self) + + return on_update_rs_func + + +# These are all the callbacks that modify values in the scene properties node group +# Since modifying node values turns out to be very slow, +# we need one callback per prop in order to update the specific associated value. +on_update_rs_enableFogPreview = make_callback(update_scene_props_from_rs_enableFogPreview) +on_update_rs_fogPreviewColor = make_callback(update_scene_props_from_rs_fogPreviewColor) +on_update_rs_clippingPlanes = make_callback(update_scene_props_from_rs_clippingPlanes) +on_update_rs_fogPreviewPosition = make_callback(update_scene_props_from_rs_fogPreviewPosition) +on_update_rs_ambientColor = make_callback(update_scene_props_from_rs_ambientColor) +on_update_rs_light0Color = make_callback(update_scene_props_from_rs_light0Color) +on_update_rs_light0Direction = make_callback(update_scene_props_from_rs_light0Direction) +on_update_rs_light0SpecSize = make_callback(update_scene_props_from_rs_light0SpecSize) +on_update_rs_light1Color = make_callback(update_scene_props_from_rs_light1Color) +on_update_rs_light1Direction = make_callback(update_scene_props_from_rs_light1Direction) +on_update_rs_light1SpecSize = make_callback(update_scene_props_from_rs_light1SpecSize) +on_update_rs_useWorldSpaceLighting = make_callback(update_scene_props_from_rs_useWorldSpaceLighting) + +del make_callback def on_update_render_settings(self, context: bpy.types.Context): @@ -147,7 +257,9 @@ def on_update_render_settings(self, context: bpy.types.Context): case _: pass - on_update_render_preview_nodes(self, context) + sceneOutputs = getSceneOutputs() + if sceneOutputs is not None: + update_scene_props_from_render_settings(sceneOutputs, self) def poll_sm64_area(self, object): @@ -161,11 +273,27 @@ def poll_oot_scene(self, object): def resync_scene_props(): if "GetSpecularNormal" in bpy.data.node_groups: # Lighting space needs to be updated due to the nodes being shared and reloaded - update_lighting_space(bpy.context.scene.fast64.renderSettings) + update_scene_props_from_rs_useWorldSpaceLighting(bpy.context.scene.fast64.renderSettings) + + +def on_update_render_settings_enableAutoUpdatePreview(self, context): + # Update on enabling but not disabling + if self.enableAutoUpdatePreview: + on_update_render_settings(self, context) class Fast64RenderSettings_Properties(bpy.types.PropertyGroup): - enableFogPreview: bpy.props.BoolProperty(name="Enable Fog Preview", default=True, update=on_update_render_settings) + enableAutoUpdatePreview: bpy.props.BoolProperty( + name="Auto Update Preview", + description="If enabled, the view will update automatically when changing render settings", + default=True, + update=on_update_render_settings_enableAutoUpdatePreview, + ) + enableFogPreview: bpy.props.BoolProperty( + name="Enable Fog Preview", + default=True, + update=on_update_render_settings, + ) fogPreviewColor: bpy.props.FloatVectorProperty( name="Fog Color", subtype="COLOR", @@ -173,7 +301,7 @@ class Fast64RenderSettings_Properties(bpy.types.PropertyGroup): min=0, max=1, default=(1, 1, 1, 1), - update=on_update_render_preview_nodes, + update=on_update_rs_fogPreviewColor, ) ambientColor: bpy.props.FloatVectorProperty( name="Ambient Light", @@ -182,7 +310,7 @@ class Fast64RenderSettings_Properties(bpy.types.PropertyGroup): min=0, max=1, default=(0.5, 0.5, 0.5, 1), - update=on_update_render_preview_nodes, + update=on_update_rs_ambientColor, ) light0Color: bpy.props.FloatVectorProperty( name="Light 0 Color", @@ -191,7 +319,7 @@ class Fast64RenderSettings_Properties(bpy.types.PropertyGroup): min=0, max=1, default=(1, 1, 1, 1), - update=on_update_render_preview_nodes, + update=on_update_rs_light0Color, ) light0Direction: bpy.props.FloatVectorProperty( name="Light 0 Direction", @@ -200,14 +328,14 @@ class Fast64RenderSettings_Properties(bpy.types.PropertyGroup): min=-1, max=1, default=mathutils.Vector((1.0, -1.0, 1.0)).normalized(), # pre normalized - update=on_update_render_preview_nodes, + update=on_update_rs_light0Direction, ) light0SpecSize: bpy.props.IntProperty( name="Light 0 Specular Size", min=1, max=255, default=3, - update=on_update_render_preview_nodes, + update=on_update_rs_light0SpecSize, ) light1Color: bpy.props.FloatVectorProperty( name="Light 1 Color", @@ -216,7 +344,7 @@ class Fast64RenderSettings_Properties(bpy.types.PropertyGroup): min=0, max=1, default=(0, 0, 0, 1), - update=on_update_render_preview_nodes, + update=on_update_rs_light1Color, ) light1Direction: bpy.props.FloatVectorProperty( name="Light 1 Direction", @@ -225,36 +353,55 @@ class Fast64RenderSettings_Properties(bpy.types.PropertyGroup): min=-1, max=1, default=mathutils.Vector((-1.0, 1.0, -1.0)).normalized(), # pre normalized - update=on_update_render_preview_nodes, + update=on_update_rs_light1Direction, ) light1SpecSize: bpy.props.IntProperty( name="Light 1 Specular Size", min=1, max=255, default=3, - update=on_update_render_preview_nodes, + update=on_update_rs_light1SpecSize, ) useWorldSpaceLighting: bpy.props.BoolProperty( - name="Use World Space Lighting", default=True, update=on_update_render_settings + name="Use World Space Lighting", + default=True, + update=on_update_render_settings, ) # Fog Preview is int because values reflect F3D values fogPreviewPosition: bpy.props.IntVectorProperty( - name="Fog Position", size=2, min=0, max=1000, default=(985, 1000), update=on_update_render_preview_nodes + name="Fog Position", + size=2, + min=0, + max=1000, + default=(985, 1000), + update=on_update_rs_fogPreviewPosition, ) # Clipping planes are float because values reflect F3D values clippingPlanes: bpy.props.FloatVectorProperty( - name="Clipping Planes", size=2, min=0, default=(100, 30000), update=on_update_render_preview_nodes + name="Clipping Planes", + size=2, + min=0, + default=(100, 30000), + update=on_update_rs_clippingPlanes, ) useObjectRenderPreview: bpy.props.BoolProperty( - name="Use Object Preview", default=True, update=on_update_render_settings + name="Use Object Preview", + default=True, + update=on_update_render_settings, ) # SM64 sm64Area: bpy.props.PointerProperty( - name="Area Object", type=bpy.types.Object, update=on_update_sm64_render_settings, poll=poll_sm64_area + name="Area Object", + type=bpy.types.Object, + update=on_update_sm64_render_settings, + poll=poll_sm64_area, ) # OOT ootSceneObject: bpy.props.PointerProperty( - name="Scene Object", type=bpy.types.Object, update=on_update_oot_render_settings, poll=poll_oot_scene + name="Scene Object", + type=bpy.types.Object, + update=on_update_oot_render_settings, + poll=poll_oot_scene, ) ootSceneHeader: bpy.props.IntProperty( name="Header/Setup", diff --git a/fast64_internal/repo_settings.py b/fast64_internal/repo_settings.py index 04418e821..85354df16 100644 --- a/fast64_internal/repo_settings.py +++ b/fast64_internal/repo_settings.py @@ -6,7 +6,7 @@ from bpy.props import StringProperty from bpy.path import abspath -from .utility import filepath_checks, prop_split, filepath_ui_warnings, draw_and_check_tab +from .utility import filepath_checks, prop_split, filepath_ui_warnings, draw_and_check_tab, set_prop_if_in_data from .operators import OperatorBase from .f3d.f3d_material import draw_rdp_world_defaults from .sm64.settings.repo_settings import load_sm64_repo_settings, save_sm64_repo_settings @@ -71,13 +71,10 @@ def load_repo_settings(scene: Scene, path: os.PathLike, skip_if_no_auto_load=Fal ) fast64_settings = scene.fast64.settings - fast64_settings.auto_repo_load_settings = data.get("autoLoad", fast64_settings.auto_repo_load_settings) - fast64_settings.auto_pick_texture_format = data.get( - "autoPickTextureFormat", fast64_settings.auto_pick_texture_format - ) - fast64_settings.prefer_rgba_over_ci = data.get("preferRGBAOverCI", fast64_settings.prefer_rgba_over_ci) - scene.f3d_type = data.get("microcode", scene.f3d_type) - scene.saveTextures = data.get("saveTextures", scene.saveTextures) + fast64_settings.from_repo_settings(data) + set_prop_if_in_data(scene, "f3d_type", data, "microcode") + set_prop_if_in_data(scene, "saveTextures", data, "saveTextures") + rdp_defaults: RDPSettings = scene.world.rdp_defaults rdp_defaults.from_dict(data.get("rdpDefaults", {})) @@ -90,12 +87,10 @@ def save_repo_settings(scene: Scene, path: os.PathLike): data = {} data["version"] = CUR_VERSION - data["autoLoad"] = fast64_settings.auto_repo_load_settings + data.update(fast64_settings.to_repo_settings()) data["microcode"] = scene.f3d_type data["saveTextures"] = scene.saveTextures - data["autoPickTextureFormat"] = fast64_settings.auto_pick_texture_format - if fast64_settings.auto_pick_texture_format: - data["preferRGBAOverCI"] = fast64_settings.prefer_rgba_over_ci + rdp_defaults: RDPSettings = scene.world.rdp_defaults data["rdpDefaults"] = rdp_defaults.to_dict() @@ -119,7 +114,7 @@ def draw_repo_settings(layout: UILayout, context: Context): SaveRepoSettings.draw_props(col, path=path) col.prop(fast64_settings, "auto_repo_load_settings") - prop_split(col, scene, "f3d_type", "F3D Microcode") + prop_split(col, scene, "f3d_type", "Microcode") col.prop(scene, "saveTextures") col.prop(fast64_settings, "auto_pick_texture_format") if fast64_settings.auto_pick_texture_format: diff --git a/fast64_internal/sm64/settings/constants.py b/fast64_internal/sm64/settings/constants.py index d8d70a156..3020ab335 100644 --- a/fast64_internal/sm64/settings/constants.py +++ b/fast64_internal/sm64/settings/constants.py @@ -29,4 +29,6 @@ ("Refresh 12", "Refresh 12", "Refresh 12"), ("Refresh 13", "Refresh 13", "Refresh 13"), ("Refresh 16", "Refresh 16", "Refresh 16"), + ("", "HackerSM64", ""), + ("HackerSM64 2.3.0", "HackerSM64 (v2.3.0)", "HackerSM64 (v2.3.0)"), ] diff --git a/fast64_internal/sm64/settings/panels.py b/fast64_internal/sm64/settings/panels.py index 97fe73040..ba82351a7 100644 --- a/fast64_internal/sm64/settings/panels.py +++ b/fast64_internal/sm64/settings/panels.py @@ -2,8 +2,7 @@ from bpy.types import Context from ...panels import SM64_Panel - -from .repo_settings import draw_repo_settings +from ...utility import draw_and_check_tab class SM64_GeneralSettingsPanel(SM64_Panel): @@ -18,10 +17,12 @@ def draw(self, context: Context): if sm64_props.export_type == "C": # If the repo settings tab is open, we pass show_repo_settings as False # because we want to draw those specfic properties in the repo settings box - draw_repo_settings(scene, col.box()) - col.separator() + box = col.box().column() + if draw_and_check_tab(box, sm64_props, "sm64_repo_settings_tab", icon="PROPERTIES"): + sm64_props.draw_repo_settings(box) + col.separator() - sm64_props.draw_props(col, not sm64_props.sm64_repo_settings_tab) + sm64_props.draw_props(col, not sm64_props.sm64_repo_settings_tab or sm64_props.binary_export) else: sm64_props.draw_props(col, True) diff --git a/fast64_internal/sm64/settings/properties.py b/fast64_internal/sm64/settings/properties.py index 16d406131..64ba59713 100644 --- a/fast64_internal/sm64/settings/properties.py +++ b/fast64_internal/sm64/settings/properties.py @@ -6,7 +6,7 @@ from bpy.utils import register_class, unregister_class from ...render_settings import on_update_render_settings -from ...utility import directory_path_checks, directory_ui_warnings, prop_split, upgrade_old_prop +from ...utility import directory_path_checks, directory_ui_warnings, prop_split, set_prop_if_in_data, upgrade_old_prop from ..sm64_constants import defaultExtendSegment4 from ..sm64_objects import SM64_CombinedObjectProperties from ..sm64_utility import export_rom_ui_warnings, import_rom_ui_warnings @@ -32,7 +32,7 @@ class SM64_Properties(PropertyGroup): """Global SM64 Scene Properties found under scene.fast64.sm64""" version: IntProperty(name="SM64_Properties Version", default=0) - cur_version = 3 # version after property migration + cur_version = 4 # version after property migration # UI Selection show_importing_menus: BoolProperty(name="Show Importing Menus", default=False) @@ -99,12 +99,15 @@ def upgrade_changed_props(): "exportType": "export_type", } old_export_props_to_new = { - "custom_export_name": {"geoLevelName", "colLevelName", "animLevelName"}, + "custom_group_name": {"geoLevelName", "colLevelName", "animLevelName"}, "custom_export_path": {"geoExportPath", "colExportPath", "animExportPath"}, "object_name": {"geoName", "colName", "animName"}, "group_name": {"geoGroupName", "colGroupName", "animGroupName"}, "level_name": {"levelOption", "geoLevelOption", "colLevelOption", "animLevelOption"}, + "custom_level_name": {"levelName", "geoLevelName", "colLevelName", "animLevelName"}, + "non_decomp_level": {"levelCustomExport"}, "export_header_type": {"geoExportHeaderType", "colExportHeaderType", "animExportHeaderType"}, + "custom_include_directory": {"geoTexDir"}, } for scene in bpy.data.scenes: sm64_props: SM64_Properties = scene.fast64.sm64 @@ -131,6 +134,29 @@ def upgrade_changed_props(): upgrade_old_prop(combined_props, new, scene, old) sm64_props.version = SM64_Properties.cur_version + def to_repo_settings(self): + data = {} + data["refresh_version"] = self.refresh_version + data["compression_format"] = self.compression_format + data["force_extended_ram"] = self.force_extended_ram + data["matstack_fix"] = self.matstack_fix + return data + + def from_repo_settings(self, data: dict): + set_prop_if_in_data(self, "refresh_version", data, "refresh_version") + set_prop_if_in_data(self, "compression_format", data, "compression_format") + set_prop_if_in_data(self, "force_extended_ram", data, "force_extended_ram") + set_prop_if_in_data(self, "matstack_fix", data, "matstack_fix") + + def draw_repo_settings(self, layout: UILayout): + col = layout.column() + if not self.binary_export: + col.prop(self, "disable_scroll") + prop_split(col, self, "compression_format", "Compression Format") + prop_split(col, self, "refresh_version", "Refresh (Function Map)") + col.prop(self, "force_extended_ram") + col.prop(self, "matstack_fix") + def draw_props(self, layout: UILayout, show_repo_settings: bool = True): col = layout.column() @@ -150,14 +176,9 @@ def draw_props(self, layout: UILayout, show_repo_settings: bool = True): 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() + if show_repo_settings: + self.draw_repo_settings(col) + col.separator() col.prop(self, "show_importing_menus") if self.show_importing_menus: diff --git a/fast64_internal/sm64/settings/repo_settings.py b/fast64_internal/sm64/settings/repo_settings.py index f74b7eb3d..a32df8621 100644 --- a/fast64_internal/sm64/settings/repo_settings.py +++ b/fast64_internal/sm64/settings/repo_settings.py @@ -2,7 +2,7 @@ from bpy.types import Scene, UILayout -from ...utility import draw_and_check_tab, prop_split +from ...utility import draw_and_check_tab, prop_split, set_prop_if_in_data def save_sm64_repo_settings(scene: Scene): @@ -18,11 +18,7 @@ def save_sm64_repo_settings(scene: Scene): } sm64_props = scene.fast64.sm64 - data["refresh_version"] = sm64_props.refresh_version - data["compression_format"] = sm64_props.compression_format - data["force_extended_ram"] = sm64_props.force_extended_ram - data["matstack_fix"] = sm64_props.matstack_fix - + data.update(sm64_props.to_repo_settings()) return data @@ -33,26 +29,9 @@ def load_sm64_repo_settings(scene: Scene, data: dict[str, Any]): for layer in range(8): draw_layer = draw_layers.get(str(layer), {}) if "cycle_1" in draw_layer: - setattr(world, f"draw_layer_{layer}_cycle_1", draw_layer["cycle_1"]) + set_prop_if_in_data(world, f"draw_layer_{layer}_cycle_1", draw_layer, "cycle_1") if "cycle_2" in draw_layer: - setattr(world, f"draw_layer_{layer}_cycle_2", draw_layer["cycle_2"]) - - sm64_props = scene.fast64.sm64 - sm64_props.refresh_version = data.get("refresh_version", sm64_props.refresh_version) - sm64_props.compression_format = data.get("compression_format", sm64_props.compression_format) - sm64_props.force_extended_ram = data.get("force_extended_ram", sm64_props.force_extended_ram) - sm64_props.matstack_fix = data.get("matstack_fix", sm64_props.matstack_fix) + set_prop_if_in_data(world, f"draw_layer_{layer}_cycle_2", draw_layer, "cycle_2") - -def draw_repo_settings(scene: Scene, layout: UILayout): - col = layout.column() sm64_props = scene.fast64.sm64 - if not draw_and_check_tab(col, sm64_props, "sm64_repo_settings_tab", icon="PROPERTIES"): - return - - prop_split(col, sm64_props, "compression_format", "Compression Format") - prop_split(col, sm64_props, "refresh_version", "Refresh (Function Map)") - col.prop(sm64_props, "force_extended_ram") - col.prop(sm64_props, "matstack_fix") - - col.label(text="See Fast64 repo settings for general settings", icon="INFO") + sm64_props.from_repo_settings(data) diff --git a/fast64_internal/sm64/sm64_collision.py b/fast64_internal/sm64/sm64_collision.py index 0678604dc..6e2907f0c 100644 --- a/fast64_internal/sm64/sm64_collision.py +++ b/fast64_internal/sm64/sm64_collision.py @@ -331,34 +331,31 @@ def exportCollisionC( cDefFile.write(cDefine) cDefFile.close() - if not customExport: - if headerType == "Actor": - # Write to group files - if groupName == "" or groupName is None: - raise PluginError("Actor header type chosen but group name not provided.") - - groupPathC = os.path.join(dirPath, groupName + ".c") - groupPathH = os.path.join(dirPath, groupName + ".h") - - writeIfNotFound(groupPathC, '\n#include "' + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"') - writeIfNotFound(groupPathH, '\n#include "' + name + '/collision_header.h"', "\n#endif") + if headerType == "Actor": + # Write to group files + if groupName == "" or groupName is None: + raise PluginError("Actor header type chosen but group name not provided.") - elif headerType == "Level": - groupPathC = os.path.join(dirPath, "leveldata.c") - groupPathH = os.path.join(dirPath, "header.h") + groupPathC = os.path.join(dirPath, groupName + ".c") + groupPathH = os.path.join(dirPath, groupName + ".h") - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/collision.inc.c"', "") - if writeRoomsFile: - writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"', "") - else: - deleteIfFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"') - writeIfNotFound( - groupPathH, '\n#include "levels/' + levelName + "/" + name + '/collision_header.h"', "\n#endif" - ) + writeIfNotFound(groupPathC, '\n#include "' + name + '/collision.inc.c"', "") + if writeRoomsFile: + writeIfNotFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"', "") + else: + deleteIfFound(groupPathC, '\n#include "' + name + '/rooms.inc.c"') + writeIfNotFound(groupPathH, '\n#include "' + name + '/collision_header.h"', "\n#endif") + + elif headerType == "Level": + groupPathC = os.path.join(dirPath, "leveldata.c") + groupPathH = os.path.join(dirPath, "header.h") + + writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/collision.inc.c"', "") + if writeRoomsFile: + writeIfNotFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"', "") + else: + deleteIfFound(groupPathC, '\n#include "levels/' + levelName + "/" + name + '/rooms.inc.c"') + writeIfNotFound(groupPathH, '\n#include "levels/' + levelName + "/" + name + '/collision_header.h"', "\n#endif") return cDefine @@ -502,12 +499,12 @@ def execute(self, context): applyRotation([obj], math.radians(90), "X") if context.scene.fast64.sm64.export_type == "C": export_path, level_name = getPathAndLevel( - props.export_header_type == "Custom", - props.custom_export_path, - props.custom_export_name, + props.is_actor_custom_export, + props.actor_custom_path, + props.export_level_name, props.level_name, ) - if not props.export_header_type == "Custom": + if not props.is_actor_custom_export: applyBasicTweaks(export_path) exportCollisionC( obj, @@ -516,10 +513,10 @@ def execute(self, context): False, props.include_children, props.obj_name_col, - props.export_header_type == "Custom", + props.is_actor_custom_export, props.export_rooms, props.export_header_type, - props.export_group_name, + props.actor_group_name, level_name, ) self.report({"INFO"}, "Success!") diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 4ccb05256..ab69b095f 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -6,7 +6,13 @@ from ..panels import SM64_Panel from ..f3d.f3d_writer import exportF3DCommon from ..f3d.f3d_texture_writer import TexInfo -from ..f3d.f3d_material import TextureProperty, tmemUsageUI, all_combiner_uses, ui_procAnim +from ..f3d.f3d_material import ( + TextureProperty, + tmemUsageUI, + all_combiner_uses, + ui_procAnim, + update_world_default_rendermode, +) from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup from .sm64_utility import export_rom_checks, starSelectWarning from .sm64_level_parser import parseLevelAtPointer @@ -47,7 +53,6 @@ ) from ..utility import ( - CData, CScrollData, PluginError, raisePluginError, @@ -73,6 +78,7 @@ makeWriteInfoBox, writeBoxExportType, enumExportHeaderType, + create_or_get_world, ) from .sm64_constants import ( @@ -106,8 +112,9 @@ def getDrawLayerV3(self, obj): return int(obj.draw_layer_static) def getRenderMode(self, drawLayer): - cycle1 = getattr(bpy.context.scene.world, "draw_layer_" + str(drawLayer) + "_cycle_1") - cycle2 = getattr(bpy.context.scene.world, "draw_layer_" + str(drawLayer) + "_cycle_2") + world = create_or_get_world(bpy.context.scene) + cycle1 = getattr(world, "draw_layer_" + str(drawLayer) + "_cycle_1") + cycle2 = getattr(world, "draw_layer_" + str(drawLayer) + "_cycle_2") return [cycle1, cycle2] @@ -866,9 +873,10 @@ def poll(cls, context): def draw(self, context): world = context.scene.world - layout = self.layout + if not world: + return - inputGroup = layout.column() + inputGroup = self.layout.column() inputGroup.prop( world, "menu_layers", text="Draw Layers", icon="TRIA_DOWN" if world.menu_layers else "TRIA_RIGHT" ) @@ -940,22 +948,54 @@ def sm64_dl_writer_register(): for cls in sm64_dl_writer_classes: register_class(cls) - bpy.types.World.draw_layer_0_cycle_1 = bpy.props.StringProperty(default="G_RM_ZB_OPA_SURF") - bpy.types.World.draw_layer_0_cycle_2 = bpy.props.StringProperty(default="G_RM_ZB_OPA_SURF2") - bpy.types.World.draw_layer_1_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_SURF") - bpy.types.World.draw_layer_1_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_SURF2") - bpy.types.World.draw_layer_2_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_DECAL") - bpy.types.World.draw_layer_2_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_DECAL2") - bpy.types.World.draw_layer_3_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_INTER") - bpy.types.World.draw_layer_3_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_INTER2") - bpy.types.World.draw_layer_4_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_TEX_EDGE") - bpy.types.World.draw_layer_4_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_TEX_EDGE2") - bpy.types.World.draw_layer_5_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_SURF") - bpy.types.World.draw_layer_5_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_SURF2") - bpy.types.World.draw_layer_6_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_DECAL") - bpy.types.World.draw_layer_6_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_DECAL2") - bpy.types.World.draw_layer_7_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_INTER") - bpy.types.World.draw_layer_7_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_INTER2") + bpy.types.World.draw_layer_0_cycle_1 = bpy.props.StringProperty( + default="G_RM_ZB_OPA_SURF", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_0_cycle_2 = bpy.props.StringProperty( + default="G_RM_ZB_OPA_SURF2", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_1_cycle_1 = bpy.props.StringProperty( + default="G_RM_AA_ZB_OPA_SURF", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_1_cycle_2 = bpy.props.StringProperty( + default="G_RM_AA_ZB_OPA_SURF2", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_2_cycle_1 = bpy.props.StringProperty( + default="G_RM_AA_ZB_OPA_DECAL", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_2_cycle_2 = bpy.props.StringProperty( + default="G_RM_AA_ZB_OPA_DECAL2", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_3_cycle_1 = bpy.props.StringProperty( + default="G_RM_AA_ZB_OPA_INTER", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_3_cycle_2 = bpy.props.StringProperty( + default="G_RM_AA_ZB_OPA_INTER2", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_4_cycle_1 = bpy.props.StringProperty( + default="G_RM_AA_ZB_TEX_EDGE", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_4_cycle_2 = bpy.props.StringProperty( + default="G_RM_AA_ZB_TEX_EDGE2", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_5_cycle_1 = bpy.props.StringProperty( + default="G_RM_AA_ZB_XLU_SURF", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_5_cycle_2 = bpy.props.StringProperty( + default="G_RM_AA_ZB_XLU_SURF2", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_6_cycle_1 = bpy.props.StringProperty( + default="G_RM_AA_ZB_XLU_DECAL", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_6_cycle_2 = bpy.props.StringProperty( + default="G_RM_AA_ZB_XLU_DECAL2", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_7_cycle_1 = bpy.props.StringProperty( + default="G_RM_AA_ZB_XLU_INTER", update=update_world_default_rendermode + ) + bpy.types.World.draw_layer_7_cycle_2 = bpy.props.StringProperty( + default="G_RM_AA_ZB_XLU_INTER2", update=update_world_default_rendermode + ) bpy.types.Scene.DLExportStart = bpy.props.StringProperty(name="Start", default="11D8930") bpy.types.Scene.DLExportEnd = bpy.props.StringProperty(name="End", default="11FFF00") diff --git a/fast64_internal/sm64/sm64_geolayout_bone.py b/fast64_internal/sm64/sm64_geolayout_bone.py index d432210d5..c0ee8e11a 100644 --- a/fast64_internal/sm64/sm64_geolayout_bone.py +++ b/fast64_internal/sm64/sm64_geolayout_bone.py @@ -432,14 +432,9 @@ def draw(self, context): def getSwitchOptionBone(switchArmature): optionBones = [] - if bpy.app.version >= (4, 0, 0): - for bone in switchArmature.data.bones: - if "SwitchOption" in bone.collections: - optionBones.append(bone.name) - else: - for poseBone in switchArmature.pose.bones: - if poseBone.bone_group is not None and poseBone.bone_group.name == "SwitchOption": - optionBones.append(poseBone.name) + for bone in switchArmature.data.bones: + if bone.geo_cmd == "SwitchOption": + optionBones.append(bone.name) if len(optionBones) > 1: raise PluginError("There should only be one switch option bone in " + switchArmature.name + ".") elif len(optionBones) < 1: diff --git a/fast64_internal/sm64/sm64_geolayout_classes.py b/fast64_internal/sm64/sm64_geolayout_classes.py index a0e82306e..1118cf79e 100644 --- a/fast64_internal/sm64/sm64_geolayout_classes.py +++ b/fast64_internal/sm64/sm64_geolayout_classes.py @@ -3,7 +3,6 @@ import bpy from struct import pack from copy import copy -from .sm64_function_map import func_map from ..utility import ( PluginError, @@ -51,6 +50,7 @@ GEO_SETUP_OBJ_RENDER, GEO_SET_BG, ) +from .sm64_utility import convert_addr_to_func drawLayerNames = { 0: "LAYER_FORCE", @@ -512,16 +512,6 @@ def walk(node, last_materials): self.clear_gfx_lists(fModel) -def convertAddrToFunc(addr): - if addr == "": - raise PluginError("Geolayout node cannot have an empty function name/address.") - 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: - return toAlnum(addr) - - # We add Function commands to nonDeformTransformData because any skinned # 0x15 commands should go before them, as they are usually preceding # an empty transform command (of which they modify?) @@ -542,7 +532,7 @@ def to_binary(self, segmentData): return command def to_c(self): - return "GEO_ASM(" + str(self.func_param) + ", " + convertAddrToFunc(self.geo_func) + ")," + return "GEO_ASM(" + str(self.func_param) + ", " + convert_addr_to_func(self.geo_func) + ")," class HeldObjectNode: @@ -570,7 +560,7 @@ def to_c(self): + ", " + str(convertFloatToShort(self.translate[2])) + ", " - + convertAddrToFunc(self.geo_func) + + convert_addr_to_func(self.geo_func) + ")," ) @@ -627,7 +617,7 @@ def to_binary(self, segmentData): return command def to_c(self): - return "GEO_SWITCH_CASE(" + str(self.defaultCase) + ", " + convertAddrToFunc(self.switchFunc) + ")," + return "GEO_SWITCH_CASE(" + str(self.defaultCase) + ", " + convert_addr_to_func(self.switchFunc) + ")," class TranslateRotateNode(BaseDisplayListNode): @@ -1187,7 +1177,7 @@ def to_c(self): + ", " + str(self.lookAt[2]) + ", " - + convertAddrToFunc(self.geo_func) + + convert_addr_to_func(self.geo_func) + ")," ) @@ -1231,7 +1221,7 @@ def to_c(self): if self.isColor: return "GEO_BACKGROUND_COLOR(0x" + format(self.backgroundValue, "04x").upper() + ")," else: - return "GEO_BACKGROUND(" + str(self.backgroundValue) + ", " + convertAddrToFunc(self.geo_func) + ")," + return "GEO_BACKGROUND(" + str(self.backgroundValue) + ", " + convert_addr_to_func(self.geo_func) + ")," class CustomNode: diff --git a/fast64_internal/sm64/sm64_geolayout_writer.py b/fast64_internal/sm64/sm64_geolayout_writer.py index 86ac298af..d1d88eaac 100644 --- a/fast64_internal/sm64/sm64_geolayout_writer.py +++ b/fast64_internal/sm64/sm64_geolayout_writer.py @@ -2849,27 +2849,27 @@ def execute(self, context): if context.scene.fast64.sm64.export_type == "C": export_path, level_name = getPathAndLevel( - props.export_header_type == "Custom", - props.custom_export_path, - props.custom_export_name, + props.is_actor_custom_export, + props.actor_custom_path, + props.export_level_name, props.level_name, ) - if not props.export_header_type == "Custom": + if not props.is_actor_custom_export: applyBasicTweaks(export_path) exportGeolayoutObjectC( obj, final_transform, export_path, - bpy.context.scene.geoTexDir, + props.custom_include_directory, save_textures, save_textures and bpy.context.scene.geoSeparateTextureDef, None, - props.export_group_name, + props.actor_group_name, props.export_header_type, props.obj_name_gfx, props.geo_name, level_name, - props.export_header_type == "Custom", + props.is_actor_custom_export, DLFormat.Static, ) self.report({"INFO"}, "Success!") @@ -3046,30 +3046,30 @@ def execute(self, context): bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) if context.scene.fast64.sm64.export_type == "C": export_path, level_name = getPathAndLevel( - props.export_header_type == "Custom", - props.custom_export_path, - props.custom_export_name, + props.is_actor_custom_export, + props.actor_custom_path, + props.export_level_name, props.level_name, ) save_textures = bpy.context.scene.saveTextures - if not props.export_header_type == "Custom": + if not props.is_actor_custom_export: applyBasicTweaks(export_path) header, fileStatus = exportGeolayoutArmatureC( armatureObj, obj, final_transform, export_path, - bpy.context.scene.geoTexDir, + props.custom_include_directory, save_textures, save_textures and bpy.context.scene.geoSeparateTextureDef, None, - props.export_group_name, + props.actor_group_name, props.export_header_type, props.obj_name_gfx, props.geo_name, level_name, - props.export_header_type == "Custom", + props.is_actor_custom_export, DLFormat.Static, ) starSelectWarning(self, fileStatus) @@ -3254,7 +3254,6 @@ def sm64_geo_writer_register(): bpy.types.Scene.textDumpGeoPath = bpy.props.StringProperty(name="Text Dump Path", subtype="FILE_PATH") bpy.types.Scene.geoUseBank0 = bpy.props.BoolProperty(name="Use Bank 0") bpy.types.Scene.geoRAMAddr = bpy.props.StringProperty(name="RAM Address", default="80000000") - bpy.types.Scene.geoTexDir = bpy.props.StringProperty(name="Include Path", default="actors/mario/") bpy.types.Scene.geoSeparateTextureDef = bpy.props.BoolProperty(name="Save texture.inc.c separately") bpy.types.Scene.geoInsertableBinaryPath = bpy.props.StringProperty(name="Filepath", subtype="FILE_PATH") bpy.types.Scene.geoIsSegPtr = bpy.props.BoolProperty(name="Is Segmented Address") @@ -3284,7 +3283,6 @@ def sm64_geo_writer_unregister(): del bpy.types.Scene.textDumpGeoPath del bpy.types.Scene.geoUseBank0 del bpy.types.Scene.geoRAMAddr - del bpy.types.Scene.geoTexDir del bpy.types.Scene.geoSeparateTextureDef del bpy.types.Scene.geoInsertableBinaryPath del bpy.types.Scene.geoIsSegPtr diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index 5263e62a1..1587b43fe 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -148,22 +148,7 @@ def write(self, filepath): def updateMaskCount(self, levelCount): if len(self.masks) - 1 < int(levelCount / 2): while len(self.masks) - 1 < int(levelCount / 2): - self.masks.append( - [ - "ZOOMOUT_AREA_MASK", - [ - "0", - "0", - "0", - "0", - "0", - "0", - "0", - "0", - ], - "", - ] - ) + self.masks.append(Macro("ZOOMOUT_AREA_MASK", ["0"] * 8, "")) else: self.masks = self.masks[: int(levelCount / 2) + 1] @@ -215,13 +200,13 @@ def getOrMakeMacroByCourseName(self, courseEnum, isBonus): if bonusCourse[1][0] == courseEnum: return bonusCourse if not isBonus: - macroCmd = ["DEFINE_COURSE", [courseEnum, "0x44444440"], ""] + macroCmd = Macro("DEFINE_COURSE", [courseEnum, "0x44444440"], "") self.courses.append(macroCmd) return macroCmd else: - macroCmd = ["DEFINE_BONUS_COURSE", [courseEnum, "0x44444440"], ""] + macroCmd = Macro("DEFINE_BONUS_COURSE", [courseEnum, "0x44444440"], "") self.bonusCourses.append(macroCmd) return macroCmd @@ -255,7 +240,7 @@ def getOrMakeMacroByLevelName(self, levelName): for macro in self.defineMacros: if macro[0] == "DEFINE_LEVEL" and macro[1][3] == levelName: return macro - macroCmd = [ + macroCmd = Macro( "DEFINE_LEVEL", [ '"' + levelName.upper() + '"', @@ -271,7 +256,7 @@ def getOrMakeMacroByLevelName(self, levelName): "_", ], "", - ] + ) self.newLevelAdded = True self.defineMacros.append(macroCmd) return macroCmd @@ -498,24 +483,22 @@ def replaceScriptLoads(levelscript, obj): STRING_TO_MACROS_PATTERN = re.compile( r""" - .*? - (?P\w+) - \s* - \((?P - [^()]* + .*? # match as few chars as possible before macro name + (?P\w+) #group macro name matches 1+ word chars + \s* # allows any number of spaces after macro name + \((?P # group is inside first parenthesis + [^()]* # anything but () (?: # Non-capturing group for 1 depth parentheses - \( - .*? - \) + \(.*?\) # captures parenthesis+any chars inside [^()]* - )* + )* # allows any number of inner parenthesis () )\) - (\s*,\s*|\s*) - (?P - //.*$ + (\s*?,)?[^\n]*? # capture a comma, including white space trailing except for new lines following the comma + (?P # comment group + ([^\n]*?|\s*?\\\s*?\n)//.*$ # two // and any number of chars and str or line end | - /\*.*\*/ - )? + ([^\n]*?|\s*?\\\s*?\n)/\*[\s\S]*?\*/ # a /*, any number of chars (including new line) and a */ + )? # 0 or 1 repetition of comments """, re.VERBOSE | re.MULTILINE, ) @@ -880,7 +863,7 @@ def exportLevelC(obj, transformMatrix, level_name, exportDir, savePNG, customExp else: level_dir = os.path.join(exportDir, "levels/" + level_name) - if customExport or not os.path.exists(os.path.join(level_dir, "script.c")): + if not os.path.exists(os.path.join(level_dir, "script.c")): prev_level_script = LevelScript(level_name) else: prev_level_script = parseLevelScript(level_dir, level_name) @@ -953,8 +936,13 @@ def exportLevelC(obj, transformMatrix, level_name, exportDir, savePNG, customExp if not existingArea: shutil.rmtree(os.path.join(level_dir, folder)) - def include_proto(file_name): - return f'#include "levels/{level_name}/{file_name}"\n' + def include_proto(file_name, new_line_first=False): + include = f'#include "levels/{level_name}/{file_name}"' + if new_line_first: + include = "\n" + include + else: + include += "\n" + return include gfxFormatter = SM64GfxFormatter(ScrollMethod.Vertex) exportData = fModel.to_c(TextureExportSettings(savePNG, savePNG, f"levels/{level_name}", level_dir), gfxFormatter) @@ -974,14 +962,14 @@ def include_proto(file_name): if DLFormat == DLFormat.Static: staticData.append(dynamicData) else: - geoString = writeMaterialFiles( + level_data.geo_data = writeMaterialFiles( exportDir, level_dir, include_proto("header.h"), include_proto("material.inc.h"), dynamicData.header, dynamicData.source, - geoString, + level_data.geo_data, customExport, ) @@ -1094,9 +1082,9 @@ def include_proto(file_name): createHeaderFile(level_name, headerPath) # Write level data - writeIfNotFound(geoPath, include_proto("geo.inc.c"), "") - writeIfNotFound(levelDataPath, include_proto("leveldata.inc.c"), "") - writeIfNotFound(headerPath, include_proto("header.inc.h"), "#endif") + writeIfNotFound(geoPath, include_proto("geo.inc.c", new_line_first=True), "") + writeIfNotFound(levelDataPath, include_proto("leveldata.inc.c", new_line_first=True), "") + writeIfNotFound(headerPath, include_proto("header.inc.h", new_line_first=True), "#endif") if fModel.texturesSavedLastExport == 0: textureIncludePath = os.path.join(level_dir, "texture_include.inc.c") @@ -1217,18 +1205,13 @@ def execute(self, context): applyRotation([obj], math.radians(90), "X") props = context.scene.fast64.sm64.combined_export - export_path, level_name = getPathAndLevel( - props.export_header_type == "Custom", - props.custom_export_path, - props.custom_export_name, - props.level_name, - ) - if props.export_header_type == "Custom": + export_path, level_name = props.base_level_path, props.export_level_name + if props.is_custom_level: triggerName = "sCam" + level_name.title().replace(" ", "").replace("_", "") else: - triggerName = cameraTriggerNames[props.level_name] + triggerName = cameraTriggerNames[level_name] - if props.export_header_type != "Custom": + if not props.non_decomp_level: applyBasicTweaks(export_path) fileStatus = exportLevelC( obj, @@ -1236,7 +1219,7 @@ def execute(self, context): level_name, export_path, context.scene.saveTextures, - props.export_header_type == "Custom", + props.non_decomp_level, triggerName, DLFormat.Static, ) diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index d96ac3401..1e17fe18e 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -1,8 +1,8 @@ import math, bpy, mathutils +import os from bpy.utils import register_class, unregister_class from re import findall, sub from pathlib import Path -from .sm64_function_map import func_map from ..panels import SM64_Panel from ..operators import ObjectDataExporter @@ -10,6 +10,8 @@ PluginError, CData, Vector, + directory_ui_warnings, + filepath_ui_warnings, toAlnum, convertRadiansToS16, checkIdentityRotation, @@ -44,6 +46,7 @@ groupsSeg6, groups_obj_export, ) +from .sm64_utility import convert_addr_to_func from .sm64_spline import ( assertCurveValid, @@ -818,7 +821,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.fast64.sm64.refresh_version][obj.sm64_behaviour_enum] + convert_addr_to_func(obj.sm64_behaviour_enum) if obj.sm64_behaviour_enum != "Custom" else obj.sm64_obj_behaviour ) @@ -854,7 +857,7 @@ def process_sm64_objects(obj, area, rootMatrix, transformMatrix, specialsOnly): SM64_Whirpool(obj.whirlpool_index, obj.whirpool_condition, obj.whirpool_strength, translation) ) elif obj.sm64_obj_type == "Camera Volume": - checkIdentityRotation(obj, rotation, True) + checkIdentityRotation(obj, rotation.to_quaternion(), True) if obj.cameraVolumeGlobal: triggerIndex = -1 else: @@ -871,7 +874,7 @@ def process_sm64_objects(obj, area, rootMatrix, transformMatrix, specialsOnly): ) elif obj.sm64_obj_type == "Puppycam Volume": - checkIdentityRotation(obj, rotation, False) + checkIdentityRotation(obj, rotation.to_quaternion(), False) triggerIndex = area.index puppycamProp = obj.puppycamProp @@ -933,7 +936,7 @@ def process_sm64_objects(obj, area, rootMatrix, transformMatrix, specialsOnly): area.puppycamVolumes.append( PuppycamVolume( triggerIndex, - levelIDNames[bpy.data.scenes["Scene"].levelOption], + levelIDNames[bpy.context.scene.fast64.sm64.export_level_name], puppycamProp.puppycamVolumePermaswap, puppycamProp.puppycamVolumeFunction, translation, @@ -1018,11 +1021,7 @@ class SearchBehaviourEnumOperator(bpy.types.Operator): def execute(self, context): context.object.sm64_behaviour_enum = self.sm64_behaviour_enum bpy.context.region.tag_redraw() - name = ( - func_map[context.scene.fast64.sm64.refresh_version][self.sm64_behaviour_enum] - if self.sm64_behaviour_enum != "Custom" - else "Custom" - ) + name = convert_addr_to_func(self.sm64_behaviour_enum) if self.sm64_behaviour_enum != "Custom" else "Custom" self.report({"INFO"}, "Selected: " + name) return {"FINISHED"} @@ -1560,12 +1559,16 @@ def write_file_lines(self, path, file_lines): # exports the model ID load into the appropriate script.c location def export_script_load(self, context, props): - # check if model_ids.h exists decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) if props.export_header_type == "Level": - script_path = decomp_path / "levels" / f"{props.export_level_name}" / "script.c" + # for some reason full_level_path doesn't work here + if props.non_decomp_level: + levels_path = Path(props.full_level_path) + else: + levels_path = decomp_path / "levels" / props.export_level_name + script_path = levels_path / "script.c" self.export_level_specific_load(script_path, props) - else: + elif props.export_header_type == "Actor": script_path = decomp_path / "levels" / "scripts.c" self.export_group_script_load(script_path, props) @@ -1583,13 +1586,14 @@ def find_export_lines( for j, line in enumerate(file_lines): if start_delim and start_delim in line: search_sig = True + insert_line = j continue if search_sig and match_str and match_str in line: match_line = j break if search_sig and fast64_signature and fast64_signature in line: insert_line = j - if search_sig and alt_condition is not None and alt_condition in line: + if alt_condition is not None and alt_condition in line: alt_insert_line = j if end_delim and end_delim in line: search_sig = False @@ -1597,6 +1601,9 @@ def find_export_lines( # export the model ID to /include/model_ids.h def export_model_id(self, context, props, offset): + # won't find model_ids.h + if props.non_decomp_level: + return # check if model_ids.h exists decomp_path = Path(bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path)) model_ids = decomp_path / "include" / "model_ids.h" @@ -1637,25 +1644,34 @@ def export_group_script_load(self, script_path, props): script_lines = open(script_path, "r").readlines() script_load = f" LOAD_MODEL_FROM_GEO({props.model_id_define}, {props.geo_name}),\n" - if props.group_name != "group0": - script_start = f"const LevelScript script_func_global_{props.group_num}[]" + if props.group_num == 0: + script = "level_main_scripts_entry" else: - script_start = f"const LevelScript level_main_scripts_entry[]" + script = f"script_func_global_{props.group_num}" match_line, sig_insert_line, default_line = self.find_export_lines( script_lines, match_str=f"{props.model_id_define},", - alt_condition="LOAD_MODEL_FROM_GEO", - start_delim=script_start, + start_delim=f"const LevelScript {script}[]", end_delim="};", ) if match_line: script_lines[match_line] = script_load - elif default_line: - script_lines.insert(default_line + 1, script_load) + elif sig_insert_line and props.group_num == 0: + for i, line in enumerate(script_lines[sig_insert_line:]): + if "ALLOC_LEVEL_POOL()" in line: + script_lines.insert(sig_insert_line + i + 1, script_load) + break + elif "FREE_LEVEL_POOL()" in line: + script_lines.insert(sig_insert_line + i, script_load) + break + elif "};" in line: + raise PluginError(f"Could not find FREE_LEVEL_POOL() or ALLOC_LEVEL_POOL() in {script}") + elif sig_insert_line: + script_lines.insert(sig_insert_line + 1, script_load) else: - PluginError(f"Could not find {script_start} in {script_path}") + raise PluginError(f"Could not find {script} in {script_path}") self.write_file_lines(script_path, script_lines) @@ -1673,6 +1689,8 @@ def export_level_specific_load(self, script_path, props): match_str=f"{props.model_id_define},", fast64_signature=f"const LevelScript {fast64_level_script}[]", alt_condition="#include ", + start_delim=f"const LevelScript {fast64_level_script}[]", + end_delim="RETURN()", ) if match_line: @@ -1683,14 +1701,14 @@ def export_level_specific_load(self, script_path, props): export_line = default_line + 1 if default_line else len(script_lines) script_lines.insert(export_line, f"\nconst LevelScript {fast64_level_script}[] = {{\n") script_lines.insert(export_line + 1, script_load) - script_lines.insert(export_line + 2, "};\n") + script_lines.insert(export_line + 2, "\tRETURN(),\n") + script_lines.insert(export_line + 3, "};\n") # jump to custom level script array match_line, sig_insert_line, default_line = self.find_export_lines( script_lines, match_str=f"JUMP_LINK({fast64_level_script})", fast64_signature="JUMP_LINK(", - alt_condition="", start_delim="ALLOC_LEVEL_POOL(", end_delim="AREA(", ) @@ -1745,6 +1763,24 @@ def export_behavior_script(self, context, props): # add at top of bhvs, 3 lines after this is found bhv_data_lines = open(behavior_data, "r").readlines() + + if props.export_header_type == "Actor": + include = f'#include "actors/{toAlnum(props.actor_group_name)}.h"\n' + elif props.export_header_type == "Level" and not props.non_decomp_level: + include = f'#include "levels/{toAlnum(props.export_level_name)}/header.h"\n' + match_line, sig_insert_line, default_line = self.find_export_lines( + bhv_data_lines, + match_str=include, + alt_condition='#include "', + ) + if match_line: + bhv_data_lines[match_line] = include + elif sig_insert_line: + bhv_data_lines.insert(sig_insert_line + 1, include) + else: + export_line = default_line + 1 if default_line else len(bhv_data_lines) + bhv_data_lines.insert(export_line, include) + export_bhv_name = f"const BehaviorScript {props.bhv_name}[] = {{\n" last_bhv_define = "#define SPAWN_WATER_DROPLET(dropletParams)" fast64_sig = "/* fast64 object exports get inserted here */" @@ -1838,7 +1874,6 @@ def execute_col(self, props, obj): # var name is: const GeoLayout _geo[] def execute_gfx(self, props, context, obj, index): try: - print(props.context_obj) if props.export_gfx and props.obj_name_gfx and obj is props.gfx_object: if obj.type == "ARMATURE": bpy.ops.object.sm64_export_geolayout_armature(export_obj=obj.name) @@ -1909,13 +1944,18 @@ def update_or_inherit(new_cmd, index, arg_val, bhv_arg): ) # level export header level_name: bpy.props.EnumProperty(items=enumLevelNames, name="Level", default="bob") + custom_level_name: bpy.props.StringProperty(name="custom") + non_decomp_level: bpy.props.BoolProperty(name="Custom Export Path") + custom_level_path: bpy.props.StringProperty(name="Custom Path", subtype="FILE_PATH") + # actor export header group_name: bpy.props.EnumProperty(name="Group Name", default="group0", items=groups_obj_export) # custom export path, no headers written custom_export_path: bpy.props.StringProperty(name="Custom Path", subtype="FILE_PATH") + custom_include_directory: bpy.props.StringProperty(name="Include directory", subtype="FILE_PATH") # common export opts - custom_export_name: bpy.props.StringProperty(name="custom") # for custom level or custom group + custom_group_name: bpy.props.StringProperty(name="custom") # for custom group model_id: bpy.props.IntProperty( name="Model ID Num", default=0xE2, min=0, description="Export model ID number. A model ID of 0 exports nothing" ) @@ -1985,16 +2025,18 @@ def gfx_object(self): @property def bhv_object(self): - if not self.export_bhv: + if not self.export_bhv or self.export_all_selected: return None - if self.export_all_selected: - return self.context_obj or bpy.context.active_object else: return self.col_object or self.gfx_object or self.context_obj or bpy.context.active_object @property def group_num(self): - if self.group_name == "common0": + """0 represents script_func_global""" + assert self.group_name != "Custom", "Cannot know the group level script num if the group is custom" + if self.group_name in {"common1", "group0"}: + return 0 + elif self.group_name == "common0": return 1 else: return int(self.group_name.removeprefix("group")) + 1 @@ -2019,9 +2061,7 @@ def obj_name_gfx(self): @property def obj_name_bhv(self): - if self.export_all_selected: - return self.filter_name(self.bhv_object.name) - if not self.object_name and not self.bhv_object: + if not self.bhv_object: return "" else: return self.filter_name(self.object_name or self.bhv_object.name) @@ -2044,15 +2084,53 @@ def model_id_define(self): @property def export_level_name(self): - if self.level_name == "Custom": - return self.custom_export_name + if self.level_name == "Custom" or self.non_decomp_level: + return self.custom_level_name return self.level_name @property - def export_group_name(self): + def actor_group_name(self): if self.group_name == "Custom": - return self.custom_export_name - return self.group_name + return self.custom_group_name + else: + return self.group_name + + @property + def is_custom_level(self): + return self.non_decomp_level or self.level_name == "Custom" + + @property + def is_actor_custom_export(self): + if self.non_decomp_level and self.export_header_type == "Level": + return True + elif self.export_header_type == "Custom": + return True + else: + return False + + @property + def actor_custom_path(self): + if self.export_header_type == "Level": + return self.full_level_path + else: + return self.custom_export_path + + @property + def level_directory(self): + if self.non_decomp_level: + return self.custom_level_name + level_name = self.custom_level_name if self.level_name == "Custom" else self.level_name + return os.path.join("/levels/", level_name) + + @property + def base_level_path(self): + if self.non_decomp_level: + return bpy.path.abspath(self.custom_level_path) + return bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + + @property + def full_level_path(self): + return os.path.join(self.base_level_path, self.level_directory) # remove user prefixes/naming that I will be adding, such as _col, _geo etc. def filter_name(self, name): @@ -2075,7 +2153,10 @@ def draw_export_options(self, layout): box = split.box() box.prop(self, "export_gfx", toggle=1) if self.export_gfx: - box.prop(self, "export_script_loads") + if self.export_header_type != "Custom" and not ( + self.export_header_type == "Actor" and self.group_name == "Custom" + ): + box.prop(self, "export_script_loads") if not self.export_all_selected: box.prop(self, "graphics_object", icon_only=True) if self.export_script_loads: @@ -2087,27 +2168,34 @@ def draw_export_options(self, layout): col.prop(self, "export_bhv") self.draw_obj_name(layout) + @property + def actor_names(self) -> list: + return list(dict.fromkeys(filter(None, [self.obj_name_col, self.obj_name_gfx])).keys()) + def draw_level_path(self, layout): - if self.export_header_type == "Custom": - export_path = f"{toAlnum(self.custom_export_path)}/" + if not directory_ui_warnings(layout, bpy.path.abspath(self.base_level_path)): + return + if self.non_decomp_level: + layout.label(text=f"Level export path: {self.full_level_path}") else: - export_path = f"/levels/{toAlnum(self.level_name)}/" - layout.label(text=f"Level export path: {export_path}") + layout.label(text=f"Level export directory: {self.level_directory}") + return True + + def draw_actor_path(self, layout): + actor_path = Path(bpy.context.scene.fast64.sm64.decomp_path) / "actors" + if not filepath_ui_warnings(layout, (actor_path / self.actor_group_name).with_suffix(".c")): + return + export_locations = ",".join({self.obj_name_col, self.obj_name_gfx}) + # can this be more clear? + layout.label(text=f"Actor export path: actors/{export_locations}") + return True def draw_col_names(self, layout): - if self.export_header_type == "Actor": - layout.label(text=f"Collision path: /actors/{toAlnum(self.obj_name_col)}(.c, .h)") - else: - self.draw_level_path(layout) layout.label(text=f"Collision name: {self.collision_name}") if self.export_rooms: layout.label(text=f"Rooms name: {self.collision_name}_rooms") def draw_gfx_names(self, layout): - if self.export_header_type == "Actor": - layout.label(text=f"Geolayout path: /actors/{toAlnum(self.obj_name_gfx)}(.c, .h, _geo.c)") - else: - self.draw_level_path(layout) layout.label(text=f"GeoLayout name: {self.geo_name}") if self.export_script_loads: layout.label(text=f"Model ID: {self.model_id_define}") @@ -2133,15 +2221,20 @@ def draw_bhv_options(self, layout): def draw_props(self, layout): # level exports col = layout.column() - box = col.box() + box = col.box().column() box.operator("object.sm64_export_level", text="Export Level") - prop_split(box, self, "level_name", "Level") - if self.level_name == "Custom": - prop_split(box, self, "custom_export_name", "Level Name") + + box.prop(self, "non_decomp_level") + if self.non_decomp_level: + prop_split(box, self, "custom_level_path", "Custom Path") + else: + prop_split(box, self, "level_name", "Level") + if self.is_custom_level: + prop_split(box, self, "custom_level_name", "Name") self.draw_level_path(box.box()) col.separator() # object exports - box = col.box() + box = col.box().column() if not self.export_col and not self.export_bhv and not self.export_gfx: col = box.column() col.operator("object.sm64_export_combined_object", text="Export Object") @@ -2162,14 +2255,15 @@ def draw_props(self, layout): if self.export_header_type == "Custom": prop_split(box, self, "custom_export_path", "Custom Path") + if bpy.context.scene.saveTextures: + prop_split(box, self, "custom_include_directory", "Texture Include Directory") elif self.export_header_type == "Actor": prop_split(box, self, "group_name", "Group") if self.group_name == "Custom": - prop_split(box, self, "custom_export_name", "Group Name") + prop_split(box, self, "custom_group_name", "Group Name") else: box.label(text="Destination level selection is shared with level export dropdown", icon="PINNED") - prop_split(box, self, "level_name", "Level") # behavior options if self.export_bhv and not self.export_all_selected: self.draw_bhv_options(col) @@ -2177,7 +2271,7 @@ def draw_props(self, layout): # info/warnings if self.export_header_type == "Custom": info_box = box.box() - info_box.label(text="Export will not write any headers or dependencies", icon="ERROR") + info_box.label(text="Export will not write headers, dependencies or script loads", icon="ERROR") if self.export_all_selected: info_box = box.box() @@ -2187,13 +2281,31 @@ def draw_props(self, layout): "Objects will export based on root of parenting hierarchy.\n" "Model IDs will export in order starting from chosen Model ID Num.\n" "Behaviors will not export\n" - "Duplicates objects will be exported! Use with Caution.\n", + "Duplicates objects will be exported! Use with Caution.", icon="ERROR", ) info_box = box.box() info_box.scale_y = 0.5 + if self.export_header_type == "Level": + if not self.draw_level_path(info_box): + return + + elif self.export_header_type == "Actor": + if not self.draw_actor_path(info_box): + return + elif self.export_header_type == "Custom" and bpy.context.scene.saveTextures: + if self.custom_include_directory: + info_box.label(text=f'Include directory "{self.custom_include_directory}"') + else: + actor_names = self.actor_names + joined = ",".join(self.actor_names) + if len(actor_names) > 1: + joined = "{" f"{joined}" "}" + directory = f"{Path(bpy.path.abspath(self.custom_export_path)).name}/{joined}" + info_box.label(text=f'Empty include directory, defaults to "{directory}"') + if self.obj_name_gfx and self.export_gfx: self.draw_gfx_names(info_box) diff --git a/fast64_internal/sm64/sm64_utility.py b/fast64_internal/sm64/sm64_utility.py index 2a708fd2c..c7c55de1f 100644 --- a/fast64_internal/sm64/sm64_utility.py +++ b/fast64_internal/sm64/sm64_utility.py @@ -1,14 +1,9 @@ import os +import bpy from bpy.types import UILayout -from ..utility import ( - PluginError, - filepath_checks, - filepath_ui_warnings, - run_and_draw_errors, - multilineLabel, - prop_split, -) +from ..utility import PluginError, filepath_checks, run_and_draw_errors, multilineLabel, prop_split +from .sm64_function_map import func_map def starSelectWarning(operator, fileStatus): @@ -113,3 +108,17 @@ def string_int_prop(layout: UILayout, data, prop: str, name="", split=True, **pr else: layout.prop(data, prop, text=name, **prop_kwargs) return string_int_warning(layout, getattr(data, prop)) + + +def convert_addr_to_func(addr: str): + if addr == "": + raise PluginError("Empty function name/address.") + refresh_version: str = bpy.context.scene.fast64.sm64.refresh_version + if refresh_version.startswith("HackerSM64"): # hacker uses refresh 13 + refresh_version = "Refresh 13" + assert refresh_version in func_map, "Refresh version not found in function map" + refresh_func_map = func_map[refresh_version] + if addr.lower() in refresh_func_map: + return refresh_func_map[addr.lower()] + else: + return addr diff --git a/fast64_internal/sm64/tools/operators.py b/fast64_internal/sm64/tools/operators.py index ddde8fe60..d8d85fed4 100644 --- a/fast64_internal/sm64/tools/operators.py +++ b/fast64_internal/sm64/tools/operators.py @@ -146,6 +146,7 @@ class SM64_CreateSimpleLevel(OperatorBase): def execute_operator(self, context: Context): scene = context.scene + combined_export = scene.fast64.sm64.combined_export level_object = create_sm64_empty("Level", "Level Root", "PLAIN_AXES", (0, 0, 0)) level_object.setAsStartLevel = self.set_as_start_level @@ -182,13 +183,13 @@ def execute_operator(self, context: Context): custom_level_id = "LEVEL_BOB" for key, value in levelIDNames.items(): - if value == scene.levelName: + if value == combined_export.level_name: 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].destLevelEnum = combined_export.export_level_name area_object.warpNodes[-1].destNode = "0x0A" area_object.warpNodes.add() @@ -199,7 +200,7 @@ def execute_operator(self, context: Context): 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].destLevelEnum = combined_export.export_level_name area_object.warpNodes[-1].destLevel = custom_level_id area_object.warpNodes[-1].destNode = "0x0A" else: diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index ad05ab2d05..702ddd697 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -1,9 +1,10 @@ +from pathlib import Path import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator from math import pi, ceil, degrees, radians, copysign from mathutils import * from .utility_anim import * from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union -from bpy.types import UILayout +from bpy.types import UILayout, Scene, World CollectionProperty = Any # collection prop as defined by using bpy.props.CollectionProperty @@ -418,17 +419,17 @@ def extendedRAMLabel(layout): infoBox.label(text="Extended RAM prevents crashes.") -def getPathAndLevel(customExport, exportPath, levelName, levelOption): - if customExport: - exportPath = bpy.path.abspath(exportPath) - levelName = levelName +def getPathAndLevel(is_custom_export, custom_export_path, custom_level_name, level_enum): + if is_custom_export: + export_path = bpy.path.abspath(custom_export_path) + level_name = custom_level_name else: - exportPath = bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) - if levelOption == "Custom": - levelName = levelName + export_path = bpy.path.abspath(bpy.context.scene.fast64.sm64.decomp_path) + if level_enum == "Custom": + level_name = custom_level_name else: - levelName = levelOption - return exportPath, levelName + level_name = level_enum + return export_path, level_name def findStartBones(armatureObj): @@ -708,6 +709,8 @@ def getExportDir(customExport, dirPath, headerType, levelName, texDir, dirName): elif headerType == "Level": dirPath = os.path.join(dirPath, "levels/" + levelName) texDir = "levels/" + levelName + elif not texDir: + texDir = (Path(dirPath).name / Path(dirName)).as_posix() return dirPath, texDir @@ -788,7 +791,8 @@ def store_original_mtx(): for obj in yield_children(active_obj): # negative scales produce a rotation, we need to remove that since # scales will be applied to the transform for each object - obj["original_mtx"] = Matrix.LocRotScale(obj.location, obj.rotation_euler, None) + loc, rot, _scale = obj.matrix_local.decompose() + obj["original_mtx"] = Matrix.LocRotScale(loc, rot, None) def rotate_bounds(bounds, mtx: mathutils.Matrix): @@ -1868,3 +1872,40 @@ def upgrade_old_prop( print(f"Failed to upgrade {new_prop} from old location {old_loc} with props {old_props}") traceback.print_exc() return False + + +WORLD_WARNING_COUNT = 0 + + +def create_or_get_world(scene: Scene) -> World: + """ + Given a scene, this function will return: + - The world selected in the scene if the scene has a selected world. + - The first world in bpy.data.worlds if the current file has a world. (Which it almost always does because of the f3d nodes library) + - Create a world named "Fast64" and return it if no world exits. + This function does not assign any world to the scene. + """ + global WORLD_WARNING_COUNT + if scene.world: + WORLD_WARNING_COUNT = 0 + return scene.world + elif bpy.data.worlds: + world: World = bpy.data.worlds.values()[0] + if WORLD_WARNING_COUNT < 10: + print(f'No world selected in scene, selected the first one found in this file "{world.name}".') + WORLD_WARNING_COUNT += 1 + return world + else: # Almost never reached because the node library has its own world + WORLD_WARNING_COUNT = 0 + print(f'No world in this file, creating world named "Fast64".') + return bpy.data.worlds.new("Fast64") + + +def set_if_different(owner: object, prop: str, value): + if getattr(owner, prop) != value: + setattr(owner, prop, value) + + +def set_prop_if_in_data(owner: object, prop_name: str, data: dict, data_name: str): + if data_name in data: + set_if_different(owner, prop_name, data[data_name]) diff --git a/mario.blend b/mario.blend deleted file mode 100644 index 8ba071893..000000000 Binary files a/mario.blend and /dev/null differ