From 7bc25a1aef90d0f97cdde09e6e08f3b0675814c2 Mon Sep 17 00:00:00 2001 From: scut Date: Sun, 20 Nov 2022 13:41:16 -0500 Subject: [PATCH 01/11] cleaned up level importer, added type hints, tested for bugs a little bit --- __init__.py | 3 + asdf.txt | 0 fast64_internal/f3d/f3d_import.py | 721 +++++++ fast64_internal/sm64/sm64_level_importer.py | 1862 +++++++++++++++++++ fast64_internal/utility.py | 30 +- 5 files changed, 2610 insertions(+), 6 deletions(-) create mode 100644 asdf.txt create mode 100644 fast64_internal/f3d/f3d_import.py create mode 100644 fast64_internal/sm64/sm64_level_importer.py diff --git a/__init__.py b/__init__.py index afc83898d..6caef91fa 100644 --- a/__init__.py +++ b/__init__.py @@ -10,6 +10,7 @@ from .fast64_internal.sm64.sm64_objects import SM64_ObjectProperties from .fast64_internal.sm64.sm64_geolayout_utility import createBoneGroups from .fast64_internal.sm64.sm64_geolayout_parser import generateMetarig +from .fast64_internal.sm64.sm64_level_importer import sm64_import_register, sm64_import_unregister from .fast64_internal.oot import OOT_Properties, oot_register, oot_unregister from .fast64_internal.oot.oot_level import OOT_ObjectProperties @@ -447,6 +448,7 @@ def register(): bsdf_conv_register() sm64_register(True) oot_register(True) + sm64_import_register() for cls in classes: register_class(cls) @@ -486,6 +488,7 @@ def unregister(): f3d_parser_unregister() sm64_unregister(True) oot_unregister(True) + sm64_import_unregister() mat_unregister() bsdf_conv_unregister() bsdf_conv_panel_unregsiter() diff --git a/asdf.txt b/asdf.txt new file mode 100644 index 000000000..e69de29bb diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py new file mode 100644 index 000000000..66979868c --- /dev/null +++ b/fast64_internal/f3d/f3d_import.py @@ -0,0 +1,721 @@ +# ------------------------------------------------------------------------ +# Header +# ------------------------------------------------------------------------ + +import bpy + +import os, struct, math + +from functools import lru_cache +from pathlib import Path +from mathutils import Vector, Euler, Matrix +from collections import namedtuple +from dataclasses import dataclass +from copy import deepcopy +from re import findall + +from ..utility import hexOrDecInt + +# ------------------------------------------------------------------------ +# Classes +# ------------------------------------------------------------------------ + + +# this will hold tile properties +class Tile: + def __init__(self): + self.Fmt = "RGBA" + self.Siz = "16" + self.Slow = 32 + self.Tlow = 32 + self.Shigh = 32 + self.Thigh = 32 + self.SMask = 5 + self.TMask = 5 + self.SShift = 0 + self.TShift = 0 + self.Sflags = None + self.Tflags = None + + +# this will hold texture properties, dataclass props +# are created in order for me to make comparisons in a set +@dataclass(init=True, eq=True, unsafe_hash=True) +class Texture: + Timg: tuple + Fmt: str + Siz: int + Width: int = 0 + Height: int = 0 + Pal: tuple = None + + def size(self): + return self.Width, self.Height + + +# This is a data storage class and mat to f3dmat converting class +# used when importing for kirby +class Mat: + def __init__(self): + self.TwoCycle = False + self.GeoSet = [] + self.GeoClear = [] + self.tiles = [Tile() for a in range(8)] + self.tex0 = None + self.tex1 = None + self.tx_scr = None + + # calc the hash for an f3d mat and see if its equal to this mats hash + def MatHashF3d(self, f3d): + # texture,1 cycle combiner, render mode, geo modes, some other blender settings, tile size (very important in kirby64) + rdp = f3d.rdp_settings + if f3d.tex0.tex: + T = f3d.tex0.tex_reference + else: + T = "" + F3Dprops = ( + T, + f3d.combiner1.A, + f3d.combiner1.B, + f3d.combiner1.C, + f3d.combiner1.D, + f3d.combiner1.A_alpha, + f3d.combiner1.B_alpha, + f3d.combiner1.C_alpha, + f3d.combiner1.D_alpha, + f3d.rdp_settings.rendermode_preset_cycle_1, + f3d.rdp_settings.rendermode_preset_cycle_2, + f3d.rdp_settings.g_lighting, + f3d.rdp_settings.g_shade, + f3d.rdp_settings.g_shade_smooth, + f3d.rdp_settings.g_zbuffer, + f3d.rdp_settings.g_mdsft_alpha_compare, + f3d.rdp_settings.g_mdsft_zsrcsel, + f3d.rdp_settings.g_mdsft_alpha_dither, + f3d.tex0.S.high, + f3d.tex0.T.high, + f3d.tex0.S.low, + f3d.tex0.T.low, + ) + if hasattr(self, "Combiner"): + MyT = "" + if hasattr(self.tex0, "Timg"): + MyT = str(self.tex0.Timg) + else: + pass + + def EvalGeo(self, mode): + for a in self.GeoSet: + if mode in a.lower(): + return True + for a in self.GeoClear: + if mode in a.lower(): + return False + else: + return True + + chkT = lambda x, y, d: x.__dict__.get(y, d) + rendermode = getattr(self, "RenderMode", ["G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"]) + MyProps = ( + MyT, + *self.Combiner[0:8], + *rendermode, + EvalGeo(self, "g_lighting"), + EvalGeo(self, "g_shade"), + EvalGeo(self, "g_shade_smooth"), + EvalGeo(self, "g_zbuffer"), + chkT(self, "g_mdsft_alpha_compare", "G_AC_NONE"), + chkT(self, "g_mdsft_zsrcsel", "G_ZS_PIXEL"), + chkT(self, "g_mdsft_alpha_dither", "G_AD_NOISE"), + self.tiles[0].Shigh, + self.tiles[0].Thigh, + self.tiles[0].Slow, + self.tiles[0].Tlow, + ) + dupe = hash(MyProps) == hash(F3Dprops) + return dupe + return False + + def MatHash(self, mat): + return False + + def ConvertColor(self, color): + return [int(a) / 255 for a in color] + + def LoadTexture(self, ForceNewTex, path, tex): + png = path / f"bank_{tex.Timg[0]}" / f"{tex.Timg[1]}" + png = (*png.glob("*.png"),) + if png: + i = bpy.data.images.get(str(png[0])) + if not i or ForceNewTex: + return bpy.data.images.load(filepath=str(png[0])) + else: + return i + + def ApplyPBSDFMat(self, mat): + nt = mat.node_tree + nodes = nt.nodes + links = nt.links + pbsdf = nodes.get("Principled BSDF") + if not pbsdf: + return + tex = nodes.new("ShaderNodeTexImage") + links.new(pbsdf.inputs[0], tex.outputs[0]) + links.new(pbsdf.inputs[19], tex.outputs[1]) + i = self.LoadTexture(0, path) + if i: + tex.image = i + + def ApplyMatSettings(self, mat, tex_path): + # if bpy.context.scene.LevelImp.AsObj: + # return self.ApplyPBSDFMat(mat, textures, path, layer) + + f3d = mat.f3d_mat # This is kure's custom property class for materials + + # set color registers if they exist + if hasattr(self, "fog_position"): + f3d.set_fog = True + f3d.use_global_fog = False + f3d.fog_position[0] = eval(self.fog_pos[0]) + f3d.fog_position[1] = eval(self.fog_pos[1]) + if hasattr(self, "fog_color"): + f3d.set_fog = True + f3d.use_global_fog = False + f3d.fog_color = self.ConvertColor(self.fog_color) + if hasattr(self, "light_col"): + # this is a dict but I'll only use the first color for now + f3d.set_lights = True + if self.light_col.get(1): + f3d.default_light_color = self.ConvertColor(eval(self.light_col[1]).to_bytes(4, "big")) + if hasattr(self, "env_color"): + f3d.set_env = True + f3d.env_color = self.ConvertColor(self.env_color[-4:]) + if hasattr(self, "prim_color"): + prim = self.prim_color + f3d.set_prim = True + f3d.prim_lod_min = int(prim[0]) + f3d.prim_lod_frac = int(prim[1]) + f3d.prim_color = self.ConvertColor(prim[-4:]) + # I set these but they aren't properly stored because they're reset by fast64 or something + # its better to have defaults than random 2 cycles + self.SetGeoMode(f3d.rdp_settings, mat) + + if self.TwoCycle: + f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_2CYCLE" + else: + f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE" + + # make combiner custom + f3d.presetName = "Custom" + self.SetCombiner(f3d) + # add tex scroll objects + if self.tx_scr: + scr = self.tx_scr + mat_scr = mat.KCS_tx_scroll + if hasattr(scr, "textures"): + [mat_scr.AddTex(t) for t in scr.textures] + if hasattr(scr, "palettes"): + [mat_scr.AddPal(t) for t in scr.palettes] + # deal with custom render modes + if hasattr(self, "RenderMode"): + self.SetRenderMode(f3d) + # g texture handle + if hasattr(self, "set_tex"): + # not exactly the same but gets the point across maybe? + f3d.tex0.tex_set = self.set_tex + f3d.tex1.tex_set = self.set_tex + # tex scale gets set to 0 when textures are disabled which is automatically done + # often to save processing power between mats or something, or just adhoc bhv + if f3d.rdp_settings.g_tex_gen or any([a < 1 and a > 0 for a in self.tex_scale]): + f3d.scale_autoprop = False + f3d.tex_scale = self.tex_scale + print(self.tex_scale) + if not self.set_tex: + # Update node values + override = bpy.context.copy() + override["material"] = mat + bpy.ops.material.update_f3d_nodes(override) + del override + return + # texture 0 then texture 1 + if self.tex0: + i = self.LoadTexture(0, tex_path, self.tex0) + tex0 = f3d.tex0 + tex0.tex_reference = str(self.tex0.Timg) # setting prop for hash purposes + tex0.tex_set = True + tex0.tex = i + tex0.tex_format = self.EvalFmt(self.tiles[0]) + tex0.autoprop = False + Sflags = self.EvalFlags(self.tiles[0].Sflags) + for f in Sflags: + setattr(tex0.S, f, True) + Tflags = self.EvalFlags(self.tiles[0].Tflags) + for f in Sflags: + setattr(tex0.T, f, True) + tex0.S.low = self.tiles[0].Slow + tex0.T.low = self.tiles[0].Tlow + tex0.S.high = self.tiles[0].Shigh + tex0.T.high = self.tiles[0].Thigh + + tex0.S.mask = self.tiles[0].SMask + tex0.T.mask = self.tiles[0].TMask + if self.tex1: + i = self.LoadTexture(0, tex_path, self.tex1) + tex1 = f3d.tex1 + tex1.tex_reference = str(self.tex1.Timg) # setting prop for hash purposes + tex1.tex_set = True + tex1.tex = i + tex1.tex_format = self.EvalFmt(self.tiles[1]) + Sflags = self.EvalFlags(self.tiles[1].Sflags) + for f in Sflags: + setattr(tex1.S, f, True) + Tflags = self.EvalFlags(self.tiles[1].Tflags) + for f in Sflags: + setattr(tex1.T, f, True) + tex1.S.low = self.tiles[1].Slow + tex1.T.low = self.tiles[1].Tlow + tex1.S.high = self.tiles[1].Shigh + tex1.T.high = self.tiles[1].Thigh + + tex1.S.mask = self.tiles[0].SMask + tex1.T.mask = self.tiles[0].TMask + # Update node values + override = bpy.context.copy() + override["material"] = mat + bpy.ops.material.update_f3d_nodes(override) + del override + + def EvalFlags(self, flags): + if not flags: + return [] + GBIflags = { + "G_TX_NOMIRROR": None, + "G_TX_WRAP": None, + "G_TX_MIRROR": ("mirror"), + "G_TX_CLAMP": ("clamp"), + "0": None, + "1": ("mirror"), + "2": ("clamp"), + "3": ("clamp", "mirror"), + } + x = [] + fsplit = flags.split("|") + for f in fsplit: + z = GBIflags.get(f.strip(), 0) + if z: + x.append(z) + return x + + # only work with macros I can recognize for now + def SetRenderMode(self, f3d): + rdp = f3d.rdp_settings + rdp.set_rendermode = True + # if the enum isn't there, then just print an error for now + try: + rdp.rendermode_preset_cycle_1 = self.RenderMode[0] + rdp.rendermode_preset_cycle_2 = self.RenderMode[1] + # print(f"set render modes with render mode {self.RenderMode}") + except: + print(f"could not set render modes with render mode {self.RenderMode}") + + def SetGeoMode(self, rdp, mat): + # texture gen has a different name than gbi + for a in self.GeoSet: + setattr(rdp, a.replace("G_TEXTURE_GEN", "G_TEX_GEN").lower().strip(), True) + for a in self.GeoClear: + setattr(rdp, a.replace("G_TEXTURE_GEN", "G_TEX_GEN").lower().strip(), False) + + # Very lazy for now + def SetCombiner(self, f3d): + if not hasattr(self, "Combiner"): + f3d.combiner1.A = "TEXEL0" + f3d.combiner1.A_alpha = "0" + f3d.combiner1.C = "SHADE" + f3d.combiner1.C_alpha = "0" + f3d.combiner1.D = "0" + f3d.combiner1.D_alpha = "1" + else: + f3d.combiner1.A = self.Combiner[0] + f3d.combiner1.B = self.Combiner[1] + f3d.combiner1.C = self.Combiner[2] + f3d.combiner1.D = self.Combiner[3] + f3d.combiner1.A_alpha = self.Combiner[4] + f3d.combiner1.B_alpha = self.Combiner[5] + f3d.combiner1.C_alpha = self.Combiner[6] + f3d.combiner1.D_alpha = self.Combiner[7] + f3d.combiner2.A = self.Combiner[8] + f3d.combiner2.B = self.Combiner[9] + f3d.combiner2.C = self.Combiner[10] + f3d.combiner2.D = self.Combiner[11] + f3d.combiner2.A_alpha = self.Combiner[12] + f3d.combiner2.B_alpha = self.Combiner[13] + f3d.combiner2.C_alpha = self.Combiner[14] + f3d.combiner2.D_alpha = self.Combiner[15] + + def EvalFmt(self, tex): + GBIfmts = { + "G_IM_FMT_RGBA": "RGBA", + "RGBA": "RGBA", + "G_IM_FMT_CI": "CI", + "CI": "CI", + "G_IM_FMT_IA": "IA", + "IA": "IA", + "G_IM_FMT_I": "I", + "I": "I", + "0": "RGBA", + "2": "CI", + "3": "IA", + "4": "I", + } + GBIsiz = { + "G_IM_SIZ_4b": "4", + "G_IM_SIZ_8b": "8", + "G_IM_SIZ_16b": "16", + "G_IM_SIZ_32b": "32", + "0": "4", + "1": "8", + "2": "16", + "3": "32", + } + return GBIfmts.get(tex.Fmt, "RGBA") + GBIsiz.get(str(tex.Siz), "16") + + +# handles DL import processing, specifically built to process each cmd into the mat class +# should be inherited into a larger F3d class which wraps DL processing +# does not deal with flow control or gathering the data containers (VB, Geo cls etc.) +class DL: + # the min needed for this class to work for importing + def __init__(self, lastmat=None): + self.VB = {} # vertex buffers + self.Gfx = {} # ptrs: display lists + self.diff = {} # diffuse lights + self.amb = {} # ambient lights + self.Lights = {} # lights + if not lastmat: + self.LastMat = Mat() + self.LastMat.name = 0 + else: + self.LastMat = lastmat + + # DL cmds that control the flow of a DL cannot be handled within a independent #class method without larger context of the total DL + # def gsSPEndDisplayList(): + # return + # def gsSPBranchList(): + # break + # def gsSPDisplayList(): + # continue + # Vertices are not currently handled in the base class + # Triangles + def gsSP2Triangles(self, args): + self.MakeNewMat() + args = [hexOrDecInt(a) for a in args] + Tri1 = self.ParseTri(args[:3]) + Tri2 = self.ParseTri(args[4:7]) + self.Tris.append(Tri1) + self.Tris.append(Tri2) + + def gsSP1Triangle(self, args): + self.MakeNewMat() + args = [hexOrDecInt(a) for a in args] + Tri = self.ParseTri(args[:3]) + self.Tris.append(Tri) + + # materials + # Mats will be placed sequentially. The first item of the list is the triangle number + # The second is the material class + def gsDPSetRenderMode(self, args): + self.NewMat = 1 + self.LastMat.RenderMode = [a.strip() for a in args] + + def gsDPSetFogColor(self, args): + self.NewMat = 1 + self.LastMat.fog_color = args + + def gsSPFogPosition(self, args): + self.NewMat = 1 + self.LastMat.fog_pos = args + + def gsSPLightColor(self, args): + self.NewMat = 1 + if not hasattr(self.LastMat, "light_col"): + self.LastMat.light_col = {} + num = re.search("_\d", args[0]).group()[1] + self.LastMat.light_col[num] = args[-1] + + def gsDPSetPrimColor(self, args): + self.NewMat = 1 + self.LastMat.prim_color = args + + def gsDPSetEnvColor(self, args): + self.NewMat = 1 + self.LastMat.env_color = args + + # multiple geo modes can happen in a row that contradict each other + # this is mostly due to culling wanting diff geo modes than drawing + # but sometimes using the same vertices + def gsSPClearGeometryMode(self, args): + self.NewMat = 1 + args = [a.strip() for a in args[0].split("|")] + for a in args: + if a in self.LastMat.GeoSet: + self.LastMat.GeoSet.remove(a) + self.LastMat.GeoClear.extend(args) + + def gsSPSetGeometryMode(self, args): + self.NewMat = 1 + args = [a.strip() for a in args[0].split("|")] + for a in args: + if a in self.LastMat.GeoClear: + self.LastMat.GeoClear.remove(a) + self.LastMat.GeoSet.extend(args) + + def gsSPGeometryMode(self, args): + self.NewMat = 1 + argsC = [a.strip() for a in args[0].split("|")] + argsS = [a.strip() for a in args[1].split("|")] + for a in argsC: + if a in self.LastMat.GeoSet: + self.LastMat.GeoSet.remove(a) + for a in argsS: + if a in self.LastMat.GeoClear: + self.LastMat.GeoClear.remove(a) + self.LastMat.GeoClear.extend(argsC) + self.LastMat.GeoSet.extend(argsS) + + def gsDPSetCycleType(self, args): + if "G_CYC_1CYCLE" in args[0]: + self.LastMat.TwoCycle = False + if "G_CYC_2CYCLE" in args[0]: + self.LastMat.TwoCycle = True + + def gsDPSetCombineMode(self, args): + self.NewMat = 1 + self.LastMat.Combiner = self.EvalCombiner(args) + + def gsDPSetCombineLERP(self, args): + self.NewMat = 1 + self.LastMat.Combiner = [a.strip() for a in args] + + # root tile, scale and set tex + def gsSPTexture(self, args): + self.NewMat = 1 + macros = { + "G_ON": 2, + "G_OFF": 0, + } + set_tex = macros.get(args[-1].strip()) + if set_tex == None: + set_tex = hexOrDecInt(args[-1].strip()) + self.LastMat.set_tex = set_tex == 2 + self.LastMat.tex_scale = [ + ((0x10000 * (hexOrDecInt(a) < 0)) + hexOrDecInt(a)) / 0xFFFF for a in args[0:2] + ] # signed half to unsigned half + self.LastMat.tile_root = self.EvalTile(args[-2].strip()) # I don't think I'll actually use this + + # last tex is a palette + def gsDPLoadTLUT(self, args): + try: + tex = self.LastMat.loadtex + self.LastMat.pal = tex + except: + print( + "**--Load block before set t img, DL is partial and missing context" + "likely static file meant to be used as a piece of a realtime system.\n" + "No interpretation on file possible**--" + ) + return None + + # tells us what tile the last loaded mat goes into + def gsDPLoadBlock(self, args): + try: + tex = self.LastMat.loadtex + # these values aren't necessary when the texture is already in png format + # tex.dxt = hexOrDecInt(args[4]) + # tex.texels = hexOrDecInt(args[3]) + tile = self.EvalTile(args[0]) + tex.tile = tile + if tile == 7: + self.LastMat.tex0 = tex + elif tile == 6: + self.LastMat.tex1 = tex + except: + print( + "**--Load block before set t img, DL is partial and missing context" + "likely static file meant to be used as a piece of a realtime system.\n" + "No interpretation on file possible**--" + ) + return None + + def gsDPSetTextureImage(self, args): + self.NewMat = 1 + Timg = args[3].strip() + Fmt = args[1].strip() + Siz = args[2].strip() + loadtex = Texture(Timg, Fmt, Siz) + self.LastMat.loadtex = loadtex + + def gsDPSetTileSize(self, args): + self.NewMat = 1 + tile = self.LastMat.tiles[self.EvalTile(args[0])] + tile.Slow = self.EvalImFrac(args[1].strip()) + tile.Tlow = self.EvalImFrac(args[2].strip()) + tile.Shigh = self.EvalImFrac(args[3].strip()) + tile.Thigh = self.EvalImFrac(args[4].strip()) + + def gsDPSetTile(self, args): + self.NewMat = 1 + tile = self.LastMat.tiles[self.EvalTile(args[4].strip())] + tile.Fmt = args[0].strip() + tile.Siz = args[1].strip() + tile.Tflags = args[6].strip() + tile.TMask = self.EvalTile(args[7].strip()) + tile.TShift = self.EvalTile(args[8].strip()) + tile.Sflags = args[9].strip() + tile.SMask = self.EvalTile(args[10].strip()) + tile.SShift = self.EvalTile(args[11].strip()) + + def MakeNewMat(self): + if self.NewMat: + self.NewMat = 0 + self.Mats.append([len(self.Tris) - 1, self.LastMat]) + self.LastMat = deepcopy(self.LastMat) # for safety + self.LastMat.name = self.num + 1 + if self.LastMat.tx_scr: + # I'm clearing here because I did some illegal stuff a bit before, temporary (maybe) + self.LastMat.tx_scr = None + self.num += 1 + + def ParseTri(self, Tri): + return [self.VertBuff[a] for a in Tri] + + def EvalImFrac(self, arg): + if type(arg) == int: + return arg + arg2 = arg.replace("G_TEXTURE_IMAGE_FRAC", "2") + return eval(arg2) + + def EvalTile(self, arg): + # only 0 and 7 have enums, other stuff just uses int (afaik) + Tiles = { + "G_TX_LOADTILE": 7, + "G_TX_RENDERTILE": 0, + "G_TX_NOMASK": 0, + "G_TX_NOLOD": 0, + } + t = Tiles.get(arg) + if t == None: + t = hexOrDecInt(arg) + return t + + def EvalCombiner(self, arg): + # two args + GBI_CC_Macros = { + "G_CC_PRIMITIVE": ["0", "0", "0", "PRIMITIVE", "0", "0", "0", "PRIMITIVE"], + "G_CC_SHADE": ["0", "0", "0", "SHADE", "0", "0", "0", "SHADE"], + "G_CC_MODULATEI": ["TEXEL0", "0", "SHADE", "0", "0", "0", "0", "SHADE"], + "G_CC_MODULATEIDECALA": ["TEXEL0", "0", "SHADE", "0", "0", "0", "0", "TEXEL0"], + "G_CC_MODULATEIFADE": ["TEXEL0", "0", "SHADE", "0", "0", "0", "0", "ENVIRONMENT"], + "G_CC_MODULATERGB": ["TEXEL0", "0", "SHADE", "0", "0", "0", "0", "SHADE"], + "G_CC_MODULATERGBDECALA": ["TEXEL0", "0", "SHADE", "0", "0", "0", "0", "TEXEL0"], + "G_CC_MODULATERGBFADE": ["TEXEL0", "0", "SHADE", "0", "0", "0", "0", "ENVIRONMENT"], + "G_CC_MODULATEIA": ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"], + "G_CC_MODULATEIFADEA": ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "ENVIRONMENT", "0"], + "G_CC_MODULATEFADE": ["TEXEL0", "0", "SHADE", "0", "ENVIRONMENT", "0", "TEXEL0", "0"], + "G_CC_MODULATERGBA": ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"], + "G_CC_MODULATERGBFADEA": ["TEXEL0", "0", "SHADE", "0", "ENVIRONMENT", "0", "TEXEL0", "0"], + "G_CC_MODULATEI_PRIM": ["TEXEL0", "0", "PRIMITIVE", "0", "0", "0", "0", "PRIMITIVE"], + "G_CC_MODULATEIA_PRIM": ["TEXEL0", "0", "PRIMITIVE", "0", "TEXEL0", "0", "PRIMITIVE", "0"], + "G_CC_MODULATEIDECALA_PRIM": ["TEXEL0", "0", "PRIMITIVE", "0", "0", "0", "0", "TEXEL0"], + "G_CC_MODULATERGB_PRIM": ["TEXEL0", "0", "PRIMITIVE", "0", "TEXEL0", "0", "PRIMITIVE", "0"], + "G_CC_MODULATERGBA_PRIM": ["TEXEL0", "0", "PRIMITIVE", "0", "TEXEL0", "0", "PRIMITIVE", "0"], + "G_CC_MODULATERGBDECALA_PRIM": ["TEXEL0", "0", "PRIMITIVE", "0", "0", "0", "0", "TEXEL0"], + "G_CC_FADE": ["SHADE", "0", "ENVIRONMENT", "0", "SHADE", "0", "ENVIRONMENT", "0"], + "G_CC_FADEA": ["TEXEL0", "0", "ENVIRONMENT", "0", "TEXEL0", "0", "ENVIRONMENT", "0"], + "G_CC_DECALRGB": ["0", "0", "0", "TEXEL0", "0", "0", "0", "SHADE"], + "G_CC_DECALRGBA": ["0", "0", "0", "TEXEL0", "0", "0", "0", "TEXEL0"], + "G_CC_DECALFADE": ["0", "0", "0", "TEXEL0", "0", "0", "0", "ENVIRONMENT"], + "G_CC_DECALFADEA": ["0", "0", "0", "TEXEL0", "TEXEL0", "0", "ENVIRONMENT", "0"], + "G_CC_BLENDI": ["ENVIRONMENT", "SHADE", "TEXEL0", "SHADE", "0", "0", "0", "SHADE"], + "G_CC_BLENDIA": ["ENVIRONMENT", "SHADE", "TEXEL0", "SHADE", "TEXEL0", "0", "SHADE", "0"], + "G_CC_BLENDIDECALA": ["ENVIRONMENT", "SHADE", "TEXEL0", "SHADE", "0", "0", "0", "TEXEL0"], + "G_CC_BLENDRGBA": ["TEXEL0", "SHADE", "TEXEL0_ALPHA", "SHADE", "0", "0", "0", "SHADE"], + "G_CC_BLENDRGBDECALA": ["TEXEL0", "SHADE", "TEXEL0_ALPHA", "SHADE", "0", "0", "0", "TEXEL0"], + "G_CC_BLENDRGBFADEA": ["TEXEL0", "SHADE", "TEXEL0_ALPHA", "SHADE", "0", "0", "0", "ENVIRONMENT"], + "G_CC_ADDRGB": ["TEXEL0", "0", "TEXEL0", "SHADE", "0", "0", "0", "SHADE"], + "G_CC_ADDRGBDECALA": ["TEXEL0", "0", "TEXEL0", "SHADE", "0", "0", "0", "TEXEL0"], + "G_CC_ADDRGBFADE": ["TEXEL0", "0", "TEXEL0", "SHADE", "0", "0", "0", "ENVIRONMENT"], + "G_CC_REFLECTRGB": ["ENVIRONMENT", "0", "TEXEL0", "SHADE", "0", "0", "0", "SHADE"], + "G_CC_REFLECTRGBDECALA": ["ENVIRONMENT", "0", "TEXEL0", "SHADE", "0", "0", "0", "TEXEL0"], + "G_CC_HILITERGB": ["PRIMITIVE", "SHADE", "TEXEL0", "SHADE", "0", "0", "0", "SHADE"], + "G_CC_HILITERGBA": ["PRIMITIVE", "SHADE", "TEXEL0", "SHADE", "PRIMITIVE", "SHADE", "TEXEL0", "SHADE"], + "G_CC_HILITERGBDECALA": ["PRIMITIVE", "SHADE", "TEXEL0", "SHADE", "0", "0", "0", "TEXEL0"], + "G_CC_SHADEDECALA": ["0", "0", "0", "SHADE", "0", "0", "0", "TEXEL0"], + "G_CC_SHADEFADEA": ["0", "0", "0", "SHADE", "0", "0", "0", "ENVIRONMENT"], + "G_CC_BLENDPE": ["PRIMITIVE", "ENVIRONMENT", "TEXEL0", "ENVIRONMENT", "TEXEL0", "0", "SHADE", "0"], + "G_CC_BLENDPEDECALA": ["PRIMITIVE", "ENVIRONMENT", "TEXEL0", "ENVIRONMENT", "0", "0", "0", "TEXEL0"], + "_G_CC_BLENDPE": ["ENVIRONMENT", "PRIMITIVE", "TEXEL0", "PRIMITIVE", "TEXEL0", "0", "SHADE", "0"], + "_G_CC_BLENDPEDECALA": ["ENVIRONMENT", "PRIMITIVE", "TEXEL0", "PRIMITIVE", "0", "0", "0", "TEXEL0"], + "_G_CC_TWOCOLORTEX": ["PRIMITIVE", "SHADE", "TEXEL0", "SHADE", "0", "0", "0", "SHADE"], + "_G_CC_SPARSEST": [ + "PRIMITIVE", + "TEXEL0", + "LOD_FRACTION", + "TEXEL0", + "PRIMITIVE", + "TEXEL0", + "LOD_FRACTION", + "TEXEL0", + ], + "G_CC_TEMPLERP": [ + "TEXEL1", + "TEXEL0", + "PRIM_LOD_FRAC", + "TEXEL0", + "TEXEL1", + "TEXEL0", + "PRIM_LOD_FRAC", + "TEXEL0", + ], + "G_CC_TRILERP": [ + "TEXEL1", + "TEXEL0", + "LOD_FRACTION", + "TEXEL0", + "TEXEL1", + "TEXEL0", + "LOD_FRACTION", + "TEXEL0", + ], + "G_CC_INTERFERENCE": ["TEXEL0", "0", "TEXEL1", "0", "TEXEL0", "0", "TEXEL1", "0"], + "G_CC_1CYUV2RGB": ["TEXEL0", "K4", "K5", "TEXEL0", "0", "0", "0", "SHADE"], + "G_CC_YUV2RGB": ["TEXEL1", "K4", "K5", "TEXEL1", "0", "0", "0", "0"], + "G_CC_PASS2": ["0", "0", "0", "COMBINED", "0", "0", "0", "COMBINED"], + "G_CC_MODULATEI2": ["COMBINED", "0", "SHADE", "0", "0", "0", "0", "SHADE"], + "G_CC_MODULATEIA2": ["COMBINED", "0", "SHADE", "0", "COMBINED", "0", "SHADE", "0"], + "G_CC_MODULATERGB2": ["COMBINED", "0", "SHADE", "0", "0", "0", "0", "SHADE"], + "G_CC_MODULATERGBA2": ["COMBINED", "0", "SHADE", "0", "COMBINED", "0", "SHADE", "0"], + "G_CC_MODULATEI_PRIM2": ["COMBINED", "0", "PRIMITIVE", "0", "0", "0", "0", "PRIMITIVE"], + "G_CC_MODULATEIA_PRIM2": ["COMBINED", "0", "PRIMITIVE", "0", "COMBINED", "0", "PRIMITIVE", "0"], + "G_CC_MODULATERGB_PRIM2": ["COMBINED", "0", "PRIMITIVE", "0", "0", "0", "0", "PRIMITIVE"], + "G_CC_MODULATERGBA_PRIM2": ["COMBINED", "0", "PRIMITIVE", "0", "COMBINED", "0", "PRIMITIVE", "0"], + "G_CC_DECALRGB2": ["0", "0", "0", "COMBINED", "0", "0", "0", "SHADE"], + "G_CC_BLENDI2": ["ENVIRONMENT", "SHADE", "COMBINED", "SHADE", "0", "0", "0", "SHADE"], + "G_CC_BLENDIA2": ["ENVIRONMENT", "SHADE", "COMBINED", "SHADE", "COMBINED", "0", "SHADE", "0"], + "G_CC_CHROMA_KEY2": ["TEXEL0", "CENTER", "SCALE", "0", "0", "0", "0", "0"], + "G_CC_HILITERGB2": ["ENVIRONMENT", "COMBINED", "TEXEL0", "COMBINED", "0", "0", "0", "SHADE"], + "G_CC_HILITERGBA2": [ + "ENVIRONMENT", + "COMBINED", + "TEXEL0", + "COMBINED", + "ENVIRONMENT", + "COMBINED", + "TEXEL0", + "COMBINED", + ], + "G_CC_HILITERGBDECALA2": ["ENVIRONMENT", "COMBINED", "TEXEL0", "COMBINED", "0", "0", "0", "TEXEL0"], + "G_CC_HILITERGBPASSA2": ["ENVIRONMENT", "COMBINED", "TEXEL0", "COMBINED", "0", "0", "0", "COMBINED"], + } + return GBI_CC_Macros.get( + arg[0].strip(), ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"] + ) + GBI_CC_Macros.get(arg[1].strip(), ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"]) diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py new file mode 100644 index 000000000..7156bf902 --- /dev/null +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -0,0 +1,1862 @@ +# ------------------------------------------------------------------------ +# Header +# ------------------------------------------------------------------------ +import bpy + +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + PointerProperty, + IntVectorProperty, + BoolVectorProperty, +) +from bpy.types import ( + Panel, + Menu, + Operator, + PropertyGroup, +) +import os, sys, math, re, typing +from array import array +from struct import * +from shutil import copy +from pathlib import Path +from types import ModuleType +from mathutils import Vector, Euler, Matrix, Quaternion +from copy import deepcopy +from dataclasses import dataclass + +# from SM64classes import * + +from ..f3d.f3d_import import * +from ..utility import ( + rotate_quat_n64_to_blender, + parentObject, + GetEnums, +) + +# ------------------------------------------------------------------------ +# Data +# ------------------------------------------------------------------------ + +Num2LevelName = { + 4: "bbh", + 5: "ccm", + 7: "hmc", + 8: "ssl", + 9: "bob", + 10: "sl", + 11: "wdw", + 12: "jrb", + 13: "thi", + 14: "ttc", + 15: "rr", + 16: "castle_grounds", + 17: "bitdw", + 18: "vcutm", + 19: "bitfs", + 20: "sa", + 21: "bits", + 22: "lll", + 23: "ddd", + 24: "wf", + 25: "ending", + 26: "castle_courtyard", + 27: "pss", + 28: "cotmc", + 29: "totwc", + 30: "bowser_1", + 31: "wmotr", + 33: "bowser_2", + 34: "bowser_3", + 36: "ttm", +} + +# Levelname uses a different castle inside name which is dumb +Num2Name = {6: "castle_inside", **Num2LevelName} + + +# Dict converting +Layers = { + "LAYER_FORCE": "0", + "LAYER_OPAQUE": "1", + "LAYER_OPAQUE_DECAL": "2", + "LAYER_OPAQUE_INTER": "3", + "LAYER_ALPHA": "4", + "LAYER_TRANSPARENT": "5", + "LAYER_TRANSPARENT_DECAL": "6", + "LAYER_TRANSPARENT_INTER": "7", +} + + +# ------------------------------------------------------------------------ +# Classes +# ------------------------------------------------------------------------ + + +class Area: + def __init__( + self, + root: bpy.types.Object, + geo: str, + levelRoot: bpy.types.Object, + num: int, + scene: bpy.types.Scene, + col: bpy.types.Collection, + ): + self.root = root + self.geo = geo.strip() + self.num = num + self.scene = scene + # Set level root as parent + parentObject(levelRoot, root) + # set default vars + root.sm64_obj_type = "Area Root" + root.areaIndex = num + self.objects = [] + self.col = col + + def AddWarp(self, args: list[str]): + # set context to the root + bpy.context.view_layer.objects.active = self.root + # call fast64s warp node creation operator + bpy.ops.bone.add_warp_node() + warp = self.root.warpNodes[0] + warp.warpID = args[0] + warp.destNode = args[3] + level = args[1].strip().replace("LEVEL_", "").lower() + if level == "castle": + level = "castle_inside" + if level.isdigit(): + level = Num2Name.get(eval(level)) + if not level: + level = "bob" + warp.destLevelEnum = level + warp.destArea = args[2] + chkpoint = args[-1].strip() + # Sorry for the hex users here + if "WARP_NO_CHECKPOINT" in chkpoint or int(chkpoint.isdigit() * chkpoint + "0") == 0: + warp.warpFlagEnum = "WARP_NO_CHECKPOINT" + else: + warp.warpFlagEnum = "WARP_CHECKPOINT" + + def AddObject(self, args: list[str]): + self.objects.append(args) + + def PlaceObjects(self, col_name: str = None): + if not col_name: + col = self.col + else: + col = CreateCol(self.root.users_collection[0], col_name) + for a in self.objects: + self.PlaceObject(a, col) + + def PlaceObject(self, args: list[str], col: bpy.types.Collection): + Obj = bpy.data.objects.new("Empty", None) + col.objects.link(Obj) + parentObject(self.root, Obj) + Obj.name = "Object {} {}".format(args[8].strip(), args[0].strip()) + Obj.sm64_obj_type = "Object" + Obj.sm64_behaviour_enum = "Custom" + Obj.sm64_obj_behaviour = args[8].strip() + # bparam was changed in newer version of fast64 + if hasattr(Obj, "sm64_obj_bparam"): + Obj.sm64_obj_bparam = args[7] + else: + Obj.fast64.sm64.game_object.bparams = args[7] + Obj.sm64_obj_model = args[0] + loc = [eval(a.strip()) / self.scene.blenderToSM64Scale for a in args[1:4]] + # rotate to fit sm64s axis + loc = [loc[0], -loc[2], loc[1]] + Obj.location = loc + # fast64 just rotations by 90 on x + rot = Euler([math.radians(eval(a.strip())) for a in args[4:7]], "ZXY") + rot = rotate_quat_n64_to_blender(rot).to_euler("XYZ") + Obj.rotation_euler.rotate(rot) + # set act mask + mask = args[-1] + if type(mask) == str and mask.isdigit(): + mask = eval(mask) + form = "sm64_obj_use_act{}" + if mask == 31: + for i in range(1, 7, 1): + setattr(Obj, form.format(i), True) + else: + for i in range(1, 7, 1): + if mask & (1 << i): + setattr(Obj, form.format(i), True) + else: + setattr(Obj, form.format(i), False) + + +class Level: + def __init__(self, scr: list[str], scene: bpy.types.Scene, root: bpy.types.Object): + self.Scripts = FormatDat(scr, "LevelScript", ["(", ")"]) + self.scene = scene + self.Areas = {} + self.CurrArea = None + self.root = root + + def ParseScript(self, entry: str, col: bpy.types.Collection = None): + Start = self.Scripts[entry] + scale = self.scene.blenderToSM64Scale + if not col: + col = self.scene.collection + for l in Start: + args = self.StripArgs(l) + LsW = l.startswith + # Find an area + if LsW("AREA"): + Root = bpy.data.objects.new("Empty", None) + if self.scene.LevelImp.UseCol: + a_col = bpy.data.collections.new(f"{self.scene.LevelImp.Level} area {args[0]}") + col.children.link(a_col) + else: + a_col = col + a_col.objects.link(Root) + Root.name = "{} Area Root {}".format(self.scene.LevelImp.Level, args[0]) + self.Areas[args[0]] = Area(Root, args[1], self.root, int(args[0]), self.scene, a_col) + self.CurrArea = args[0] + continue + # End an area + if LsW("END_AREA"): + self.CurrArea = None + continue + # Jumps are only taken if they're in the script.c file for now + # continues script + elif LsW("JUMP_LINK"): + if self.Scripts.get(args[0]): + self.ParseScript(args[0], col=col) + continue + # ends script, I get arg -1 because sm74 has a different jump cmd + elif LsW("JUMP"): + Nentry = self.Scripts.get(args[-1]) + if Nentry: + self.ParseScript(args[-1], col=col) + # for the sm74 port + if len(args) != 2: + break + # final exit of recursion + elif LsW("EXIT") or l.startswith("RETURN"): + return + # Now deal with data cmds rather than flow control ones + if LsW("WARP_NODE"): + self.Areas[self.CurrArea].AddWarp(args) + continue + if LsW("OBJECT_WITH_ACTS"): + # convert act mask from ORs of act names to a number + mask = args[-1].strip() + if not mask.isdigit(): + mask = mask.replace("ACT_", "") + mask = mask.split("|") + # Attempt for safety I guess + try: + a = 0 + for m in mask: + a += 1 << int(m) + mask = a + except: + mask = 31 + self.Areas[self.CurrArea].AddObject([*args[:-1], mask]) + continue + if LsW("OBJECT"): + # Only difference is act mask, which I set to 31 to mean all acts + self.Areas[self.CurrArea].AddObject([*args, 31]) + continue + # Don't support these for now + if LsW("MACRO_OBJECTS"): + continue + if LsW("TERRAIN_TYPE"): + if not args[0].isdigit(): + self.Areas[self.CurrArea].root.terrainEnum = args[0].strip() + else: + terrains = { + 0: "TERRAIN_GRASS", + 1: "TERRAIN_STONE", + 2: "TERRAIN_SNOW", + 3: "TERRAIN_SAND", + 4: "TERRAIN_SPOOKY", + 5: "TERRAIN_WATER", + 6: "TERRAIN_SLIDE", + 7: "TERRAIN_MASK", + } + try: + num = eval(args[0]) + self.Areas[self.CurrArea].root.terrainEnum = terrains.get(num) + except: + print("could not set terrain") + continue + if LsW("SHOW_DIALOG"): + rt = self.Areas[self.CurrArea].root + rt.showStartDialog = True + rt.startDialog = args[1].strip() + continue + if LsW("TERRAIN"): + self.Areas[self.CurrArea].terrain = args[0].strip() + continue + if LsW("SET_BACKGROUND_MUSIC") or LsW("SET_MENU_MUSIC"): + rt = self.Areas[self.CurrArea].root + rt.musicSeqEnum = "Custom" + rt.music_seq = args[-1].strip() + return self.Areas + + def StripArgs(self, cmd: str): + a = cmd.find("(") + end = cmd.rfind(")") - len(cmd) + return cmd[a + 1 : end].split(",") + + +class Collision: + def __init__(self, col: list[str], scale: float): + self.col = col + self.scale = scale + self.vertices = [] + # key=type,value=tri data + self.tris = {} + self.type = None + self.SpecialObjs = [] + self.Types = [] + self.WaterBox = [] + + def GetCollision(self): + for l in self.col: + args = self.StripArgs(l) + # to avoid catching COL_VERTEX_INIT + if l.startswith("COL_VERTEX") and len(args) == 3: + self.vertices.append([eval(v) / self.scale for v in args]) + continue + if l.startswith("COL_TRI_INIT"): + self.type = args[0] + if not self.tris.get(self.type): + self.tris[self.type] = [] + continue + if l.startswith("COL_TRI") and len(args) > 2: + a = [eval(a) for a in args] + self.tris[self.type].append(a) + continue + if l.startswith("COL_WATER_BOX_INIT"): + continue + if l.startswith("COL_WATER_BOX"): + # id, x1, z1, x2, z2, y + self.WaterBox.append(args) + if l.startswith("SPECIAL_OBJECT"): + self.SpecialObjs.append(args) + # This will keep track of how to assign mats + a = 0 + for k, v in self.tris.items(): + self.Types.append([a, k, v[0]]) + a += len(v) + self.Types.append([a, 0]) + + def StripArgs(self, cmd: str): + a = cmd.find("(") + return cmd[a + 1 : -2].split(",") + + def WriteWaterBoxes( + self, scene: bpy.types.Scene, parent: bpy.types.Object, name: str, col: bpy.types.Collection = None + ): + for i, w in enumerate(self.WaterBox): + Obj = bpy.data.objects.new("Empty", None) + scene.collection.objects.link(Obj) + parentObject(parent, Obj) + Obj.name = "WaterBox_{}_{}".format(name, i) + Obj.sm64_obj_type = "Water Box" + x1 = eval(w[1]) / (self.scale) + x2 = eval(w[3]) / (self.scale) + z1 = eval(w[2]) / (self.scale) + z2 = eval(w[4]) / (self.scale) + y = eval(w[5]) / (self.scale) + Xwidth = abs(x2 - x1) / (2) + Zwidth = abs(z2 - z1) / (2) + loc = [x2 - Xwidth, -(z2 - Zwidth), y - 1] + Obj.location = loc + scale = [Xwidth, Zwidth, 1] + Obj.scale = scale + + def WriteCollision( + self, scene: bpy.types.Scene, name: str, parent: bpy.types.Object, col: bpy.types.Collection = None + ): + if not col: + col = scene.collection + self.WriteWaterBoxes(scene, parent, name, col) + mesh = bpy.data.meshes.new(name + " data") + tris = [] + for t in self.tris.values(): + # deal with special tris + if len(t[0]) > 3: + t = [a[0:3] for a in t] + tris.extend(t) + mesh.from_pydata(self.vertices, [], tris) + + obj = bpy.data.objects.new(name + " Mesh", mesh) + col.objects.link(obj) + obj.ignore_render = True + if parent: + parentObject(parent, obj) + RotateObj(-90, obj, world=1) + polys = obj.data.polygons + x = 0 + bpy.context.view_layer.objects.active = obj + max = len(polys) + for i, p in enumerate(polys): + a = self.Types[x][0] + if i >= a: + bpy.ops.object.create_f3d_mat() # the newest mat should be in slot[-1] + mat = obj.data.materials[x] + mat.collision_type_simple = "Custom" + mat.collision_custom = self.Types[x][1] + mat.name = "Sm64_Col_Mat_{}".format(self.Types[x][1]) + color = ((max - a) / (max), (max + a) / (2 * max - a), a / max, 1) # Just to give some variety + mat.f3d_mat.default_light_color = color + # check for param + if len(self.Types[x][2]) > 3: + mat.use_collision_param = True + mat.collision_param = str(self.Types[x][2][3]) + x += 1 + override = bpy.context.copy() + override["material"] = mat + bpy.ops.material.update_f3d_nodes(override) + p.material_index = x - 1 + return obj + + +class sm64_Mat(Mat): + def LoadTexture(self, ForceNewTex: bool, textures: dict, path: Path, tex: Texture): + if not tex: + return None + Timg = textures.get(tex.Timg)[0].split("/")[-1] + Timg = Timg.replace("#include ", "").replace('"', "").replace("'", "").replace("inc.c", "png") + i = bpy.data.images.get(Timg) + if not i or ForceNewTex: + Timg = textures.get(tex.Timg)[0] + Timg = Timg.replace("#include ", "").replace('"', "").replace("'", "").replace("inc.c", "png") + # deal with duplicate pathing (such as /actors/actors etc.) + Extra = path.relative_to(Path(bpy.context.scene.decompPath)) + for e in Extra.parts: + Timg = Timg.replace(e + "/", "") + # deal with actor import path not working for shared textures + if "textures" in Timg: + fp = Path(bpy.context.scene.decompPath) / Timg + else: + fp = path / Timg + return bpy.data.images.load(filepath=str(fp)) + else: + return i + + def ApplyPBSDFMat(self, mat: bpy.types.Material, textures: dict, path: Path, layer: int, tex0: Texture): + nt = mat.node_tree + nodes = nt.nodes + links = nt.links + pbsdf = nodes.get("Principled BSDF") + if not pbsdf: + return + tex = nodes.new("ShaderNodeTexImage") + links.new(pbsdf.inputs[0], tex.outputs[0]) # base color + i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, tex0) + if i: + tex.image = i + if int(layer) > 4: + mat.blend_method == "BLEND" + + def ApplyMatSettings(self, mat: bpy.types.Material, textures: dict, path: Path, layer: int): + if bpy.context.scene.LevelImp.AsObj: + return self.ApplyPBSDFMat(mat, textures, path, layer, self.tex0) + f3d = mat.f3d_mat # This is kure's custom property class for materials + f3d.draw_layer.sm64 = layer + # set color registers if they exist + if hasattr(self, "fog_position"): + f3d.set_fog = True + f3d.use_global_fog = False + f3d.fog_position[0] = eval(self.fog_pos[0]) + f3d.fog_position[1] = eval(self.fog_pos[1]) + if hasattr(self, "fog_color"): + f3d.set_fog = True + f3d.use_global_fog = False + f3d.fog_color = self.ConvertColor(self.fog_color) + if hasattr(self, "light_col"): + # this is a dict but I'll only use the first color for now + f3d.set_lights = True + if self.light_col.get(1): + f3d.default_light_color = self.ConvertColor(eval(self.light_col[1]).to_bytes(4, "big")) + if hasattr(self, "env_color"): + f3d.set_env = True + f3d.env_color = self.ConvertColor(self.env_color[-4:]) + if hasattr(self, "prim_color"): + prim = self.prim_color + f3d.set_prim = True + f3d.prim_lod_min = int(prim[0]) + f3d.prim_lod_frac = int(prim[1]) + f3d.prim_color = self.ConvertColor(prim[-4:]) + # I set these but they aren't properly stored because they're reset by fast64 or something + # its better to have defaults than random 2 cycles + self.SetGeoMode(f3d.rdp_settings, mat) + + if self.TwoCycle: + f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_2CYCLE" + else: + f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE" + # make combiner custom + f3d.presetName = "Custom" + self.SetCombiner(f3d) + + # deal with custom render modes + if hasattr(self, "RenderMode"): + self.SetRenderMode(f3d) + # g texture handle + if hasattr(self, "set_tex"): + # not exactly the same but gets the point across maybe? + f3d.tex0.tex_set = self.set_tex + f3d.tex1.tex_set = self.set_tex + # tex scale gets set to 0 when textures are disabled which is automatically done + # often to save processing power between mats or something, or just adhoc bhv + if f3d.rdp_settings.g_tex_gen or any([a < 1 and a > 0 for a in self.tex_scale]): + f3d.scale_autoprop = False + f3d.tex_scale = self.tex_scale + print(self.tex_scale) + if not self.set_tex: + # Update node values + override = bpy.context.copy() + override["material"] = mat + bpy.ops.material.update_f3d_nodes(override) + del override + return + # Try to set an image + # texture 0 then texture 1 + if self.tex0: + i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, self.tex0) + tex0 = f3d.tex0 + tex0.tex_reference = str(self.tex0.Timg) # setting prop for hash purposes + tex0.tex_set = True + tex0.tex = i + tex0.tex_format = self.EvalFmt(self.tiles[0]) + tex0.autoprop = False + Sflags = self.EvalFlags(self.tiles[0].Sflags) + for f in Sflags: + setattr(tex0.S, f, True) + Tflags = self.EvalFlags(self.tiles[0].Tflags) + for f in Sflags: + setattr(tex0.T, f, True) + tex0.S.low = self.tiles[0].Slow + tex0.T.low = self.tiles[0].Tlow + tex0.S.high = self.tiles[0].Shigh + tex0.T.high = self.tiles[0].Thigh + + tex0.S.mask = self.tiles[0].SMask + tex0.T.mask = self.tiles[0].TMask + if self.tex1: + i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, self.tex1) + tex1 = f3d.tex1 + tex1.tex_reference = str(self.tex1.Timg) # setting prop for hash purposes + tex1.tex_set = True + tex1.tex = i + tex1.tex_format = self.EvalFmt(self.tiles[1]) + Sflags = self.EvalFlags(self.tiles[1].Sflags) + for f in Sflags: + setattr(tex1.S, f, True) + Tflags = self.EvalFlags(self.tiles[1].Tflags) + for f in Sflags: + setattr(tex1.T, f, True) + tex1.S.low = self.tiles[1].Slow + tex1.T.low = self.tiles[1].Tlow + tex1.S.high = self.tiles[1].Shigh + tex1.T.high = self.tiles[1].Thigh + + tex1.S.mask = self.tiles[0].SMask + tex1.T.mask = self.tiles[0].TMask + # Update node values + override = bpy.context.copy() + override["material"] = mat + bpy.ops.material.update_f3d_nodes(override) + del override + + +class sm64_F3d(DL): + def __init__(self, scene: bpy.types.Scene): + self.VB = {} + self.Gfx = {} + self.diff = {} + self.amb = {} + self.Lights = {} + self.Textures = {} + self.scene = scene + self.num = 0 + + # Textures only contains the texture data found inside the model.inc.c file and the texture.inc.c file + def GetGenericTextures(self, root_path: Path): + for t in [ + "cave.c", + "effect.c", + "fire.c", + "generic.c", + "grass.c", + "inside.c", + "machine.c", + "mountain.c", + "outside.c", + "sky.c", + "snow.c", + "spooky.c", + "water.c", + ]: + t = root_path / "bin" / t + t = open(t, "r") + tex = t.readlines() + # For textures, try u8, and s16 aswell + self.Textures.update(FormatDat(tex, "Texture", [None, None])) + self.Textures.update(FormatDat(tex, "u8", [None, None])) + self.Textures.update(FormatDat(tex, "s16", [None, None])) + t.close() + + # recursively parse the display list in order to return a bunch of model data + def GetDataFromModel(self, start: str): + DL = self.Gfx.get(start) + self.VertBuff = [0] * 32 # If you're doing some fucky shit with a larger vert buffer it sucks to suck I guess + if not DL: + raise Exception("Could not find DL {}".format(start)) + self.Verts = [] + self.Tris = [] + self.UVs = [] + self.VCs = [] + self.Mats = [] + self.LastMat = sm64_Mat() + self.ParseDL(DL) + self.NewMat = 0 + self.StartName = start + return [self.Verts, self.Tris] + + def ParseDL(self, DL: list[str]): + # This will be the equivalent of a giant switch case + x = -1 + while x < len(DL): + # manaual iteration so I can skip certain children efficiently + # manaual iteration so I can skip certain children efficiently if needed + x += 1 + (cmd, args) = self.StripArgs(DL[x]) # each member is a tuple of (cmd, arguments) + LsW = cmd.startswith + # Deal with control flow first + if LsW("gsSPEndDisplayList"): + return + if LsW("gsSPBranchList"): + NewDL = self.Gfx.get(args[0].strip()) + if not DL: + raise Exception( + "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( + NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + ) + ) + self.ParseDL(NewDL) + break + if LsW("gsSPDisplayList"): + NewDL = self.Gfx.get(args[0].strip()) + if not DL: + raise Exception( + "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( + NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + ) + ) + self.ParseDL(NewDL) + continue + # Vertices + if LsW("gsSPVertex"): + # vertex references commonly use pointer arithmatic. I will deal with that case here, but not for other things unless it somehow becomes a problem later + if "+" in args[0]: + ref, add = args[0].split("+") + else: + ref = args[0] + add = "0" + VB = self.VB.get(ref.strip()) + if not VB: + raise Exception( + "Could not find VB {} in levels/{}/{}leveldata.inc.c".format( + ref, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + ) + ) + Verts = VB[ + int(add.strip()) : int(add.strip()) + eval(args[1]) + ] # If you use array indexing here then you deserve to have this not work + Verts = [self.ParseVert(v) for v in Verts] + for k, i in enumerate(range(eval(args[2]), eval(args[1]), 1)): + self.VertBuff[i] = [Verts[k], eval(args[2])] + # These are all independent data blocks in blender + self.Verts.extend([v[0] for v in Verts]) + self.UVs.extend([v[1] for v in Verts]) + self.VCs.extend([v[2] for v in Verts]) + self.LastLoad = eval(args[1]) + continue + # tri and mat DL cmds will be called via parent class + func = getattr(self, cmd, None) + if func: + func(args) + + def MakeNewMat(self): + if self.NewMat: + self.NewMat = 0 + self.Mats.append([len(self.Tris) - 1, self.LastMat]) + self.LastMat = deepcopy(self.LastMat) # for safety + self.LastMat.name = self.num + 1 + self.num += 1 + + # turn member of vtx str arr into vtx args + def ParseVert(self, Vert: str): + v = Vert.replace("{", "").replace("}", "").split(",") + num = lambda x: [eval(a) for a in x] + pos = num(v[:3]) + uv = num(v[4:6]) + vc = num(v[6:10]) + return [pos, uv, vc] + + # given tri args in gbi cmd, give appropriate tri indices in vert list + def ParseTri(self, Tri: list[int]): + L = len(self.Verts) + return [a + L - self.LastLoad for a in Tri] + + def StripArgs(self, cmd: str): + a = cmd.find("(") + return cmd[:a].strip(), cmd[a + 1 : -2].split(",") + + def ApplyDat(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_path: Path): + tris = mesh.polygons + bpy.context.view_layer.objects.active = obj + ind = -1 + new = -1 + UVmap = obj.data.uv_layers.new(name="UVMap") + # I can get the available enums for color attrs with this func + vcol_enums = GetEnums(bpy.types.FloatColorAttribute, "data_type") + # enums were changed in a blender version, this should future proof it a little + if "FLOAT_COLOR" in vcol_enums: + e = "FLOAT_COLOR" + else: + e = "COLOR" + Vcol = obj.data.color_attributes.get("Col") + if not Vcol: + Vcol = obj.data.color_attributes.new(name="Col", type=e, domain="CORNER") + Valph = obj.data.color_attributes.get("Alpha") + if not Valph: + Valph = obj.data.color_attributes.new(name="Alpha", type=e, domain="CORNER") + self.Mats.append([len(tris), 0]) + for i, t in enumerate(tris): + if i > self.Mats[ind + 1][0]: + new = self.Create_new_f3d_mat(self.Mats[ind + 1][1], mesh) + ind += 1 + if not new: + new = len(mesh.materials) - 1 + mat = mesh.materials[new] + mat.name = "sm64 F3D Mat {} {}".format(obj.name, new) + self.Mats[new][1].ApplyMatSettings(mat, self.Textures, tex_path, layer) + else: + # I tried to re use mat slots but it is much slower, and not as accurate + # idk if I was just doing it wrong or the search is that much slower, but this is easier + mesh.materials.append(new) + new = len(mesh.materials) - 1 + # if somehow there is no material assigned to the triangle or something is lost + if new != -1: + t.material_index = new + # Get texture size or assume 32, 32 otherwise + i = mesh.materials[new].f3d_mat.tex0.tex + if not i: + WH = (32, 32) + else: + WH = i.size + # Set UV data and Vertex Color Data + for v, l in zip(t.vertices, t.loop_indices): + uv = self.UVs[v] + vcol = self.VCs[v] + # scale verts + UVmap.data[l].uv = [a * (1 / (32 * b)) if b > 0 else a * 0.001 * 32 for a, b in zip(uv, WH)] + # idk why this is necessary. N64 thing or something? + UVmap.data[l].uv[1] = UVmap.data[l].uv[1] * -1 + 1 + Vcol.data[l].color = [a / 255 for a in vcol] + + # create a new f3d_mat given an sm64_Mat class but don't create copies with same props + def Create_new_f3d_mat(self, mat: sm64_Mat, mesh: bpy.types.Mesh): + if not self.scene.LevelImp.ForceNewTex: + # check if this mat was used already in another mesh (or this mat if DL is garbage or something) + # even looping n^2 is probably faster than duping 3 mats with blender speed + for j, F3Dmat in enumerate(bpy.data.materials): + if F3Dmat.is_f3d: + dupe = mat.MatHashF3d(F3Dmat.f3d_mat) + if dupe: + return F3Dmat + if mesh.materials: + mat = mesh.materials[-1] + new = mat.id_data.copy() # make a copy of the data block + # add a mat slot and add mat to it + mesh.materials.append(new) + else: + if self.scene.LevelImp.AsObj: + NewMat = bpy.data.materials.new(f"sm64 {mesh.name.replace('Data', 'material')}") + mesh.materials.append(NewMat) # the newest mat should be in slot[-1] for the mesh materials + NewMat.use_nodes = True + else: + bpy.ops.object.create_f3d_mat() # the newest mat should be in slot[-1] for the mesh materials + return None + + +# holds model found by geo +@dataclass +class ModelDat: + translate: tuple + rotate: tuple + layer: int + model: str + scale: float = 1.0 + + +class GeoLayout: + def __init__( + self, + GeoLayouts: dict, + root: bpy.types.Object, + scene: bpy.types.Scene, + name, + Aroot: bpy.types.Object, + col: bpy.types.Collection = None, + ): + self.GL = GeoLayouts + self.parent = root + self.models = [] + self.Children = [] + self.scene = scene + self.RenderRange = None + self.Aroot = Aroot # for properties that can only be written to area + self.root = root + self.ParentTransform = [[0, 0, 0], [0, 0, 0]] + self.LastTransform = [[0, 0, 0], [0, 0, 0]] + self.name = name + self.obj = None # last object on this layer of the tree, will become parent of next child + if not col: + self.col = Aroot.users_collection[0] + else: + self.col = col + + def MakeRt(self, name: str, root: bpy.types.Object): + # make an empty node to act as the root of this geo layout + # use this to hold a transform, or an actual cmd, otherwise rt is passed + E = bpy.data.objects.new(name, None) + self.obj = E + self.col.objects.link(E) + parentObject(root, E) + return E + + def ParseLevelGeosStart(self, start: str, scene: bpy.types.Scene): + GL = self.GL.get(start) + if not GL: + raise Exception( + "Could not find geo layout {} from levels/{}/{}geo.c".format( + start, scene.LevelImp.Level, scene.LevelImp.Prefix + ) + ) + self.ParseLevelGeos(GL, 0) + + # So I can start where ever for child nodes + def ParseLevelGeos(self, GL: list[str], depth: int): + # I won't parse the geo layout perfectly. For now I'll just get models. This is mostly because fast64 + # isn't a bijection to geo layouts, the props are sort of handled all over the place + x = -1 + while x < len(GL): + # manaual iteration so I can skip certain children efficiently + x += 1 + (cmd, args) = self.StripArgs(GL[x]) # each member is a tuple of (cmd, arguments) + LsW = cmd.startswith + # Jumps are only taken if they're in the script.c file for now + # continues script + if LsW("GEO_BRANCH_AND_LINK"): + NewGL = self.GL.get(args[0].strip()) + if NewGL: + self.ParseLevelGeos(NewGL, depth) + continue + # continues + elif LsW("GEO_BRANCH"): + NewGL = self.GL.get(args[1].strip()) + if NewGL: + self.ParseLevelGeos(NewGL, depth) + if eval(args[0]): + continue + else: + break + # final exit of recursion + elif LsW("GEO_END") or LsW("GEO_RETURN"): + return + # on an open node, make a child + elif LsW("GEO_CLOSE_NODE"): + # if there is no more open nodes, then parent this to last node + if depth: + return + elif LsW("GEO_OPEN_NODE"): + if self.obj: + GeoChild = GeoLayout(self.GL, self.obj, self.scene, self.name, self.Aroot, col=self.col) + else: + GeoChild = GeoLayout(self.GL, self.root, self.scene, self.name, self.Aroot, col=self.col) + GeoChild.ParentTransform = self.LastTransform + GeoChild.ParseLevelGeos(GL[x + 1 :], depth + 1) + x = self.SkipChildren(GL, x) + self.Children.append(GeoChild) + continue + else: + # things that only need args can be their own functions + func = getattr(self, cmd.strip(), None) + if func: + func(args) + + # Append to models array. Only check this one for now + def GEO_DISPLAY_LIST(self, args: list[str]): + # translation, rotation, layer, model + self.models.append(ModelDat(*self.ParentTransform, *args)) + + # shadows aren't naturally supported but we can emulate them with custom geo cmds + def GEO_SHADOW(self, args: list[str]): + obj = self.MakeRt(self.name + "shadow empty", self.root) + obj.sm64_obj_type = "Custom Geo Command" + obj.customGeoCommand = "GEO_SHADOW" + obj.customGeoCommandArgs = ",".join(args) + + def GEO_ANIMATED_PART(self, args: list[str]): + # layer, translation, DL + layer = args[0] + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + Tlate = [Tlate[0], -Tlate[2], Tlate[1]] + model = args[-1] + self.LastTransform = [Tlate, self.LastTransform[1]] + if model.strip() != "NULL": + self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) + else: + obj = self.MakeRt(self.name + "animated empty", self.root) + obj.location = Tlate + + def GEO_ROTATION_NODE(self, args: list[str]): + obj = self.GEO_ROTATE(args) + if obj: + obj.sm64_obj_type = "Geo Rotation Node" + + def GEO_ROTATE(self, args: list[str]): + layer = args[0] + Rotate = [math.radians(float(a)) for a in [args[1], args[2], args[3]]] + Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") + self.LastTransform = [[0, 0, 0], Rotate] + self.LastTransform = [[0, 0, 0], self.LastTransform[1]] + obj = self.MakeRt(self.name + "rotate", self.root) + obj.rotation_euler = Rotate + obj.sm64_obj_type = "Geo Translate/Rotate" + return obj + + def GEO_ROTATION_NODE_WITH_DL(self, args: list[str]): + obj = self.GEO_ROTATE_WITH_DL(args) + if obj: + obj.sm64_obj_type = "Geo Translate/Rotate" + + def GEO_ROTATE_WITH_DL(self, args: list[str]): + layer = args[0] + Rotate = [math.radians(float(a)) for a in [args[1], args[2], args[3]]] + Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") + self.LastTransform = [[0, 0, 0], Rotate] + model = args[-1] + self.LastTransform = [[0, 0, 0], self.LastTransform[1]] + if model.strip() != "NULL": + self.models.append(ModelDat([0, 0, 0], Rotate, layer, model)) + else: + obj = self.MakeRt(self.name + "rotate", self.root) + obj.rotation_euler = Rotate + obj.sm64_obj_type = "Geo Translate/Rotate" + return obj + + def GEO_TRANSLATE_ROTATE_WITH_DL(self, args: list[str]): + layer = args[0] + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + Tlate = [Tlate[0], -Tlate[2], Tlate[1]] + Rotate = [math.radians(float(a)) for a in [args[4], args[5], args[6]]] + Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") + self.LastTransform = [Tlate, Rotate] + model = args[-1] + self.LastTransform = [Tlate, self.LastTransform[1]] + if model.strip() != "NULL": + self.models.append(ModelDat(Tlate, Rotate, layer, model)) + else: + obj = self.MakeRt(self.name + "translate rotate", self.root) + obj.location = Tlate + obj.rotation_euler = Rotate + obj.sm64_obj_type = "Geo Translate/Rotate" + + def GEO_TRANSLATE_ROTATE(self, args: list[str]): + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + Tlate = [Tlate[0], -Tlate[2], Tlate[1]] + Rotate = [math.radians(float(a)) for a in [args[4], args[5], args[6]]] + Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") + self.LastTransform = [Tlate, Rotate] + obj = self.MakeRt(self.name + "translate", self.root) + obj.location = Tlate + obj.rotation_euler = Rotate + obj.sm64_obj_type = "Geo Translate/Rotate" + + def GEO_TRANSLATE_WITH_DL(self, args: list[str]): + obj = self.GEO_TRANSLATE_NODE_WITH_DL(args) + if obj: + obj.sm64_obj_type = "Geo Translate/Rotate" + + def GEO_TRANSLATE_NODE_WITH_DL(self, args: list[str]): + # translation, layer, model + layer = args[0] + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + Tlate = [Tlate[0], -Tlate[2], Tlate[1]] + model = args[-1] + self.LastTransform = [Tlate, (0, 0, 0)] + if model.strip() != "NULL": + self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) + else: + obj = self.MakeRt(self.name + "translate", self.root) + obj.location = Tlate + obj.rotation_euler = Rotate + obj.sm64_obj_type = "Geo Translate Node" + return obj + + def GEO_TRANSLATE(self, args: list[str]): + obj = self.GEO_TRANSLATE_NODE(args) + if obj: + obj.sm64_obj_type = "Geo Translate/Rotate" + + def GEO_TRANSLATE_NODE(self, args: list[str]): + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + Tlate = [Tlate[0], -Tlate[2], Tlate[1]] + self.LastTransform = [Tlate, self.LastTransform[1]] + obj = self.MakeRt(self.name + "translate", self.root) + obj.location = Tlate + obj.sm64_obj_type = "Geo Translate Node" + return obj + + def GEO_SCALE_WITH_DL(self, args: list[str]): + scale = eval(args[1].strip()) / 0x10000 + model = args[-1] + self.LastTransform = [(0, 0, 0), self.LastTransform[1]] + self.models.append(ModelDat((0, 0, 0), (0, 0, 0), layer, model, scale=scale)) + + def GEO_SCALE(self, args: list[str]): + obj = self.MakeRt(self.name + "scale", self.root) + scale = eval(args[1].strip()) / 0x10000 + obj.scale = (scale, scale, scale) + obj.sm64_obj_type = "Geo Scale" + + def GEO_ASM(self, args: list[str]): + obj = self.MakeRt(self.name + "asm", self.root) + asm = self.obj.fast64.sm64.geo_asm + self.obj.sm64_obj_type = "Geo ASM" + asm.param = args[0].strip() + asm.func = args[1].strip() + + def GEO_SWITCH_CASE(self, args: list[str]): + obj = self.MakeRt(self.name + "switch", self.root) + Switch = self.obj + Switch.sm64_obj_type = "Switch" + Switch.switchParam = eval(args[0]) + Switch.switchFunc = args[1].strip() + + # This has to be applied to meshes + def GEO_RENDER_RANGE(self, args: list[str]): + self.RenderRange = args + + # can only apply type to area root + def GEO_CAMERA(self, args: list[str]): + self.Aroot.camOption = "Custom" + self.Aroot.camType = args[0] + + # Geo backgrounds is pointless because the only background possible is the one + # loaded in the level script. This is the only override + def GEO_BACKGROUND_COLOR(self, args: list[str]): + self.Aroot.areaOverrideBG = True + color = eval(args[0]) + A = color & 1 + B = (color & 0x3E) > 1 + G = (color & (0x3E << 5)) >> 6 + R = (color & (0x3E << 10)) >> 11 + self.Aroot.areaBGColor = (R / 0x1F, G / 0x1F, B / 0x1F, A) + + def SkipChildren(self, GL: list[str], x: int): + open = 0 + opened = 0 + while x < len(GL): + l = GL[x] + if l.startswith("GEO_OPEN_NODE"): + opened = 1 + open += 1 + if l.startswith("GEO_CLOSE_NODE"): + open -= 1 + if open == 0 and opened: + break + x += 1 + return x + + def StripArgs(self, cmd: str): + a = cmd.find("(") + return cmd[:a].strip(), cmd[a + 1 : -2].split(",") + + +# ------------------------------------------------------------------------ +# Functions +# ------------------------------------------------------------------------ + + +# creates a new collection and links it to parent +def CreateCol(parent: bpy.types.Collection, name: str): + col = bpy.data.collections.new(name) + parent.children.link(col) + return col + + +def RotateObj(deg: float, obj: bpy.types.Object, world: bool = 0): + deg = Euler((math.radians(-deg), 0, 0)) + deg = deg.to_quaternion().to_matrix().to_4x4() + if world: + obj.matrix_world = obj.matrix_world @ deg + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(rotation=True) + else: + obj.matrix_basis = obj.matrix_basis @ deg + + +def EvalMacro(line: str): + scene = bpy.context.scene + if scene.LevelImp.Version in line: + return False + if scene.LevelImp.Target in line: + return False + return True + + +# given an aggregate file that imports many files, find files with the name of type +def ParseAggregat(dat: typing.TextIO, filename: str, root_path: Path): + dat.seek(0) # so it may be read multiple times + InlineReg = "/\*((?!\*/).)*\*/" # filter out inline comments + ldat = dat.readlines() + files = [] + # assume this follows naming convention + for l in ldat: + if filename in l: + comment = l.rfind("//") + # double slash terminates line basically + if comment: + l = l[:comment] + # remove inline comments from line + l = re.sub(InlineReg, "", l) + files.append(l.strip()) + # remove include and quotes inefficiently. Now files is a list of relative paths + files = [c.replace("#include ", "").replace('"', "").replace("'", "") for c in files] + # deal with duplicate pathing (such as /actors/actors etc.) + Extra = root_path.relative_to(Path(bpy.context.scene.decompPath)) + for e in Extra.parts: + files = [c.replace(e + "/", "") for c in files] + if files: + return [root_path / c for c in files] + else: + return [] + + +# get all the collision data from a certain path +def FindCollisions(aggregate: Path, lvl: Level, scene: bpy.types.Scene, root_path: Path): + aggregate = open(aggregate, "r") + cols = ParseAggregat(aggregate, "collision.inc.c", root_path) + # catch fast64 includes + fast64 = ParseAggregat(aggregate, "leveldata.inc.c", root_path) + if fast64: + f64dat = open(fast64[0], "r") + cols += ParseAggregat(f64dat, "collision.inc.c", root_path) + aggregate.close() + # search for the area terrain in each file + for k, v in lvl.Areas.items(): + terrain = v.terrain + found = 0 + for c in cols: + if os.path.isfile(c): + c = open(c, "r") + c = c.readlines() + for i, l in enumerate(c): + if terrain in l: + # Trim Collision to be just the lines that have the file + v.ColFile = c[i:] + break + else: + c = None + continue + break + else: + c = None + if not c: + raise Exception( + "Collision {} not found in levels/{}/{}leveldata.c".format( + terrain, scene.LevelImp.Level, scene.LevelImp.Prefix + ) + ) + Collisions = FormatDat(v.ColFile, "Collision", ["(", ")"]) + v.ColFile = Collisions[terrain] + return lvl + + +def WriteLevelCollision(lvl: Level, scene: bpy.types.Scene, cleanup: bool, col_name: str = None): + for k, v in lvl.Areas.items(): + if not col_name: + col = v.root.users_collection[0] + else: + col = CreateCol(v.root.users_collection[0], col_name) + # dat is a class that holds all the collision files data + dat = Collision(v.ColFile, scene.blenderToSM64Scale) + dat.GetCollision() + name = "SM64 {} Area {} Col".format(scene.LevelImp.Level, k) + obj = dat.WriteCollision(scene, name, v.root, col=col) + # final operators to clean stuff up + if cleanup: + obj.data.validate() + obj.data.update(calc_edges=True) + # shade smooth + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.shade_smooth() + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.remove_doubles() + bpy.ops.object.mode_set(mode="OBJECT") + + +# get all the relevant data types cleaned up and organized for the f3d class +def FormatModel(gfx: sm64_F3d, model: list[str]): + # For each data type, make an attribute where it cleans the input of the model files + gfx.VB.update(FormatDat(model, "Vtx", ["{", "}"])) + gfx.Gfx.update(FormatDat(model, "Gfx", ["(", ")"])) + gfx.diff.update(FormatDat(model, "Light_t", [None, None])) + gfx.amb.update(FormatDat(model, "Ambient_t", [None, None])) + gfx.Lights.update(FormatDat(model, "Lights1", [None, None])) + # For textures, try u8, and s16 aswell + gfx.Textures.update(FormatDat(model, "Texture", [None, None])) + gfx.Textures.update(FormatDat(model, "u8", [None, None])) + gfx.Textures.update(FormatDat(model, "s16", [None, None])) + return gfx + + +# Search through a C file to find data of typeName[] and split it into a list +# of macros with all comments removed +def FormatDat(lines: list[str], typeName: str, Delims: list[str]): + # Get a dictionary made up with keys=level script names + # and values as an array of all the cmds inside. + Models = {} + InlineReg = "/\*((?!\*/).)*\*/" # filter out inline comments + regX = "\[[0-9a-fx]*\]" # array bounds, basically [] with any number in it + currScr = 0 # name of current arr of type typeName + skip = 0 # bool to skip during macros + for l in lines: + # remove line comment + comment = l.rfind("//") + if comment: + l = l[:comment] + # check for macro + if "#ifdef" in l: + skip = EvalMacro(l) + if "#elif" in l: + skip = EvalMacro(l) + if "#else" in l: + skip = 0 + continue + # Now Check for level script starts + match = re.search(regX, l, flags=re.IGNORECASE) + if typeName in l and match and not skip: + # get var name, get substring from typename to [] + var = l[l.find(typeName) + len(typeName) : match.span()[0]].strip() + Models[var] = "" + currScr = var + continue + if currScr and not skip: + # remove inline comments from line + l = re.sub(InlineReg, "", l) + # Check for end of Level Script array + if "};" in l: + currScr = 0 + # Add line to dict + else: + Models[currScr] += l + # Now remove newlines from each script, and then split macro ends + # This makes each member of the array a single macro + for script, v in Models.items(): + v = v.replace("\n", "") + arr = [] # arr of macros + buf = "" # buf to put currently processed macro in + x = 0 # cur position in str + stack = 0 # stack cnt of parenthesis + app = 0 # flag to append macro + while x < len(v): + char = v[x] + if char == Delims[0]: + stack += 1 + app = 1 + if char == Delims[1]: + stack -= 1 + if app == 1 and stack == 0: + app = 0 + buf += v[x : x + 2] # get the last parenthesis and comma + arr.append(buf.strip()) + x += 2 + buf = "" + continue + buf += char + x += 1 + # for when the delim characters are nothing + if buf: + arr.append(buf) + Models[script] = arr + return Models + + +# given a geo.c file and a path, return cleaned up geo layouts in a dict +def GetGeoLayouts(geo: typing.TextIO, root_path: Path): + layouts = ParseAggregat(geo, "geo.inc.c", root_path) + if not layouts: + return + # because of fast64, these can be recursively defined (though I expect only a depth of one) + for l in layouts: + geoR = open(l, "r") + layouts += ParseAggregat(geoR, "geo.inc.c", root_path) + GeoLayouts = {} # stores cleaned up geo layout lines + for l in layouts: + l = open(l, "r") + lines = l.readlines() + GeoLayouts.update(FormatDat(lines, "GeoLayout", ["(", ")"])) + return GeoLayouts + + +# Find DL references given a level geo file and a path to a level folder +def FindLvlModels(geo: typing.TextIO, lvl: Level, scene: bpy.types.Scene, root_path: Path, col_name: str = None): + GeoLayouts = GetGeoLayouts(geo, root_path) + for k, v in lvl.Areas.items(): + GL = v.geo + rt = v.root + if col_name: + col = CreateCol(v.root.users_collection[0], col_name) + else: + col = None + Geo = GeoLayout(GeoLayouts, rt, scene, "GeoRoot {} {}".format(scene.LevelImp.Level, k), rt, col=col) + Geo.ParseLevelGeosStart(GL, scene) + v.geo = Geo + return lvl + + +# Parse an aggregate group file or level data file for geo layouts +def FindActModels( + geo: typing.TextIO, + Layout: str, + scene: bpy.types.Scene, + rt: bpy.types.Object, + root_path: Path, + col: bpy.types.Collection = None, +): + GeoLayouts = GetGeoLayouts(geo, root_path) + Geo = GeoLayout(GeoLayouts, rt, scene, "{}".format(Layout), rt, col=col) + Geo.ParseLevelGeosStart(Layout, scene) + return Geo + + +# Parse an aggregate group file or level data file for f3d data +def FindModelDat(aggregate: Path, scene: bpy.types.Scene, root_path: Path): + leveldat = open(aggregate, "r") + models = ParseAggregat(leveldat, "model.inc.c", root_path) + models += ParseAggregat(leveldat, "painting.inc.c", root_path) + # fast64 makes a leveldata.inc.c file and puts custom content there, I want to catch that as well + # this isn't the best way to do this, but I will be lazy here + fast64 = ParseAggregat(leveldat, "leveldata.inc.c", root_path) + if fast64: + f64dat = open(fast64[0], "r") + models += ParseAggregat(f64dat, "model.inc.c", root_path) + # leveldat.seek(0) # so it may be read multiple times + textures = ParseAggregat(leveldat, "texture.inc.c", root_path) # Only deal with textures that are actual .pngs + textures.extend(ParseAggregat(leveldat, "textureNew.inc.c", root_path)) # For RM2C support + # Get all modeldata in the level + Models = sm64_F3d(scene) + for m in models: + md = open(m, "r") + lines = md.readlines() + Models = FormatModel(Models, lines) + # Update file to have texture.inc.c textures, deal with included textures in the model.inc.c files aswell + for t in [*textures, *models]: + t = open(t, "r") + tex = t.readlines() + # For textures, try u8, and s16 aswell + Models.Textures.update(FormatDat(tex, "Texture", [None, None])) + Models.Textures.update(FormatDat(tex, "u8", [None, None])) + Models.Textures.update(FormatDat(tex, "s16", [None, None])) + t.close() + return Models + + +# from a geo layout, create all the mesh's +def ReadGeoLayout( + geo: GeoLayout, scene: bpy.types.Scene, f3d_dat: sm64_F3d, root_path: Path, meshes: dict, cleanup: bool = True +): + if geo.models: + rt = geo.root + col = geo.col + # create a mesh for each one. + for m in geo.models: + name = m.model + " Data" + if name in meshes.keys(): + mesh = meshes[name] + name = 0 + else: + mesh = bpy.data.meshes.new(name) + meshes[name] = mesh + [verts, tris] = f3d_dat.GetDataFromModel(m.model.strip()) + mesh.from_pydata(verts, [], tris) + + obj = bpy.data.objects.new(m.model + " Obj", mesh) + layer = m.layer + if not layer.isdigit(): + layer = Layers.get(layer) + if not layer: + layer = 1 + obj.draw_layer_static = layer + col.objects.link(obj) + parentObject(rt, obj) + RotateObj(-90, obj) + scale = m.scale / scene.blenderToSM64Scale + obj.scale = [scale, scale, scale] + obj.location = m.translate + obj.ignore_collision = True + if name: + f3d_dat.ApplyDat(obj, mesh, layer, root_path) + if cleanup: + # clean up after applying dat + mesh.validate() + mesh.update(calc_edges=True) + # final operators to clean stuff up + # shade smooth + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.shade_smooth() + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.remove_doubles() + bpy.ops.object.mode_set(mode="OBJECT") + if not geo.Children: + return + for g in geo.Children: + ReadGeoLayout(g, scene, f3d_dat, root_path, meshes, cleanup=cleanup) + + +# write the gfx for a level given the level data, and f3d data +def WriteLevelModel(lvl: Level, scene: bpy.types.Scene, root_path: Path, f3d_dat: sm64_F3d, cleanup: bool = True): + for k, v in lvl.Areas.items(): + # Parse the geolayout class I created earlier to look for models + meshes = {} # re use mesh data when the same DL is referenced (bbh is good example) + ReadGeoLayout(v.geo, scene, f3d_dat, root_path, meshes, cleanup=cleanup) + return lvl + + +# given a path, get a level object by parsing the script.c file +def ParseScript(script: Path, scene: bpy.types.Scene, col: bpy.types.Collection = None): + scr = open(script, "r") + Root = bpy.data.objects.new("Empty", None) + if not col: + scene.collection.objects.link(Root) + else: + col.objects.link(Root) + Root.name = "Level Root {}".format(scene.LevelImp.Level) + Root.sm64_obj_type = "Level Root" + # Now parse the script and get data about the level + # Store data in attribute of a level class then assign later and return class + scr = scr.readlines() + lvl = Level(scr, scene, Root) + entry = scene.LevelImp.Entry.format(scene.LevelImp.Level) + lvl.ParseScript(entry, col=col) + return lvl + + +# write the objects from a level object +def WriteObjects(lvl: Level, col_name: str = None): + for area in lvl.Areas.values(): + area.PlaceObjects(col_name=col_name) + + +# import level graphics given geo.c file, and a level object +def ImportLvlVisual( + geo: typing.TextIO, + lvl: Level, + scene: bpy.types.Scene, + root_path: Path, + aggregate: Path, + cleanup: bool = True, + col_name: str = None, +): + lvl = FindLvlModels(geo, lvl, scene, root_path, col_name=col_name) + models = FindModelDat(aggregate, scene, root_path) + # just a try, in case you are importing from something other than base decomp repo (like RM2C output folder) + try: + models.GetGenericTextures(root_path) + except: + print("could not import genric textures, if this errors later from missing textures this may be why") + lvl = WriteLevelModel(lvl, scene, root_path, models, cleanup=cleanup) + return lvl + + +# import level collision given a level script +def ImportLvlCollision( + aggregate: Path, + lvl: Level, + scene: bpy.types.Scene, + root_path: Path, + cleanup: bool, + col_name: str = None, +): + lvl = FindCollisions(aggregate, lvl, scene, root_path) # Now Each area has its collision file nicely formatted + WriteLevelCollision(lvl, scene, cleanup, col_name=col_name) + return lvl + + +# ------------------------------------------------------------------------ +# Operators +# ------------------------------------------------------------------------ + + +class SM64_OT_Act_Import(Operator): + bl_label = "Import Actor" + bl_idname = "wm.sm64_import_actor" + bl_options = {"REGISTER", "UNDO"} + + cleanup: bpy.props.BoolProperty(name="Cleanup Mesh", default=1) + + def execute(self, context): + scene = context.scene + rt_col = context.collection + scene.gameEditorMode = "SM64" + path = Path(scene.decompPath) + folder = path / scene.ActImp.FolderType + Layout = scene.ActImp.GeoLayout + prefix = scene.ActImp.Prefix + # different name schemes and I have no clean way to deal with it + if "actor" in scene.ActImp.FolderType: + geo = folder / (prefix + "_geo.c") + leveldat = folder / (prefix + ".c") + else: + geo = folder / (prefix + "geo.c") + leveldat = folder / (prefix + "leveldata.c") + geo = open(geo, "r") + Root = bpy.data.objects.new("Empty", None) + Root.name = "Actor %s" % scene.ActImp.GeoLayout + rt_col.objects.link(Root) + + Geo = FindActModels( + geo, Layout, scene, Root, folder, col=rt_col + ) # return geo layout class and write the geo layout + models = FindModelDat(leveldat, scene, folder) + # just a try, in case you are importing from not the base decomp repo + try: + models.GetGenericTextures(path) + except: + print("could not import genric textures, if this errors later from missing textures this may be why") + meshes = {} # re use mesh data when the same DL is referenced (bbh is good example) + ReadGeoLayout(Geo, scene, models, folder, meshes, cleanup=self.cleanup) + return {"FINISHED"} + + +class SM64_OT_Lvl_Import(Operator): + bl_label = "Import Level" + bl_idname = "wm.sm64_import_level" + + cleanup = True + + def execute(self, context): + scene = context.scene + + col = context.collection + if scene.LevelImp.UseCol: + obj_col = f"{scene.LevelImp.Level} obj" + gfx_col = f"{scene.LevelImp.Level} gfx" + col_col = f"{scene.LevelImp.Level} col" + else: + obj_col = gfx_col = col_col = None + + scene.gameEditorMode = "SM64" + prefix = scene.LevelImp.Prefix + path = Path(scene.decompPath) + level = path / "levels" / scene.LevelImp.Level + script = level / (prefix + "script.c") + geo = level / (prefix + "geo.c") + leveldat = level / (prefix + "leveldata.c") + geo = open(geo, "r") + lvl = ParseScript(script, scene, col=col) # returns level class + WriteObjects(lvl, col_name=obj_col) + lvl = ImportLvlCollision(leveldat, lvl, scene, path, self.cleanup, col_name=col_col) + lvl = ImportLvlVisual(geo, lvl, scene, path, leveldat, cleanup=self.cleanup, col_name=gfx_col) + return {"FINISHED"} + + +class SM64_OT_Lvl_Gfx_Import(Operator): + bl_label = "Import Gfx" + bl_idname = "wm.sm64_import_level_gfx" + + cleanup = True + + def execute(self, context): + scene = context.scene + + col = context.collection + if scene.LevelImp.UseCol: + gfx_col = f"{scene.LevelImp.Level} gfx" + else: + gfx_col = None + + scene.gameEditorMode = "SM64" + prefix = scene.LevelImp.Prefix + path = Path(scene.decompPath) + level = path / "levels" / scene.LevelImp.Level + script = level / (prefix + "script.c") + geo = level / (prefix + "geo.c") + model = level / (prefix + "leveldata.c") + geo = open(geo, "r") + lvl = ParseScript(script, scene, col=col) # returns level class + lvl = ImportLvlVisual(geo, lvl, scene, path, model, cleanup=self.cleanup, col_name=gfx_col) + return {"FINISHED"} + + +class SM64_OT_Lvl_Col_Import(Operator): + bl_label = "Import Collision" + bl_idname = "wm.sm64_import_level_col" + + cleanup = True + + def execute(self, context): + scene = context.scene + + col = context.collection + if scene.LevelImp.UseCol: + col_col = f"{scene.LevelImp.Level} collision" + else: + col_col = None + + scene.gameEditorMode = "SM64" + prefix = scene.LevelImp.Prefix + path = Path(scene.decompPath) + level = path / "levels" / scene.LevelImp.Level + script = level / (prefix + "script.c") + geo = level / (prefix + "geo.c") + model = level / (prefix + "leveldata.c") + geo = open(geo, "r") + lvl = ParseScript(script, scene, col=col) # returns level class + lvl = ImportLvlCollision(model, lvl, scene, path, self.cleanup, col_name=col_col) + return {"FINISHED"} + + +class SM64_OT_Obj_Import(Operator): + bl_label = "Import Objects" + bl_idname = "wm.sm64_import_object" + + def execute(self, context): + scene = context.scene + + col = context.collection + if scene.LevelImp.UseCol: + obj_col = f"{scene.LevelImp.Level} objs" + else: + obj_col = None + + scene.gameEditorMode = "SM64" + prefix = scene.LevelImp.Prefix + path = Path(scene.decompPath) + level = path / "levels" / scene.LevelImp.Level + script = level / (prefix + "script.c") + lvl = ParseScript(script, scene, col=col) # returns level class + WriteObjects(lvl, col_name=obj_col) + return {"FINISHED"} + + +# ------------------------------------------------------------------------ +# Props +# ------------------------------------------------------------------------ + + +class ActorImport(PropertyGroup): + GeoLayout: StringProperty(name="GeoLayout", description="Name of GeoLayout") + FolderType: EnumProperty( + name="Source", + description="Whether the actor is from a level or from a group", + items=[ + ("actors", "actors", ""), + ("levels", "levels", ""), + ], + ) + Prefix: StringProperty( + name="Prefix", + description="Prefix before expected aggregator files like script.c, leveldata.c and geo.c", + default="", + ) + Version: EnumProperty( + name="Version", + description="Version of the game for any ifdef macros", + items=[ + ("VERSION_US", "VERSION_US", ""), + ("VERSION_JP", "VERSION_JP", ""), + ("VERSION_EU", "VERSION_EU", ""), + ("VERSION_SH", "VERSION_SH", ""), + ], + ) + Target: StringProperty( + name="Target", description="The platform target for any #ifdefs in code", default="TARGET_N64" + ) + + +class LevelImport(PropertyGroup): + Level: EnumProperty( + name="Level", + description="Choose a level", + items=[ + ("bbh", "bbh", ""), + ("ccm", "ccm", ""), + ("hmc", "hmc", ""), + ("ssl", "ssl", ""), + ("bob", "bob", ""), + ("sl", "sl", ""), + ("wdw", "wdw", ""), + ("jrb", "jrb", ""), + ("thi", "thi", ""), + ("ttc", "ttc", ""), + ("rr", "rr", ""), + ("castle_grounds", "castle_grounds", ""), + ("castle_inside", "castle_inside", ""), + ("bitdw", "bitdw", ""), + ("vcutm", "vcutm", ""), + ("bitfs", "bitfs", ""), + ("sa", "sa", ""), + ("bits", "bits", ""), + ("lll", "lll", ""), + ("ddd", "ddd", ""), + ("wf", "wf", ""), + ("ending", "ending", ""), + ("castle_courtyard", "castle_courtyard", ""), + ("pss", "pss", ""), + ("cotmc", "cotmc", ""), + ("totwc", "totwc", ""), + ("bowser_1", "bowser_1", ""), + ("wmotr", "wmotr", ""), + ("bowser_2", "bowser_2", ""), + ("bowser_3", "bowser_3", ""), + ("ttm", "ttm", ""), + ], + ) + Prefix: StringProperty( + name="Prefix", + description="Prefix before expected aggregator files like script.c, leveldata.c and geo.c", + default="", + ) + Entry: StringProperty( + name="Entrypoint", description="The name of the level script entry variable", default="level_{}_entry" + ) + Version: EnumProperty( + name="Version", + description="Version of the game for any ifdef macros", + items=[ + ("VERSION_US", "VERSION_US", ""), + ("VERSION_JP", "VERSION_JP", ""), + ("VERSION_EU", "VERSION_EU", ""), + ("VERSION_SH", "VERSION_SH", ""), + ], + ) + Target: StringProperty( + name="Target", description="The platform target for any #ifdefs in code", default="TARGET_N64" + ) + ForceNewTex: BoolProperty( + name="ForceNewTex", + description="Forcefully load new textures even if duplicate path/name is detected", + default=False, + ) + AsObj: BoolProperty( + name="As OBJ", description="Make new materials as PBSDF so they export to obj format", default=False + ) + UseCol: BoolProperty( + name="Use Col", description="Make new collections to organzie content during imports", default=True + ) + + +# ------------------------------------------------------------------------ +# Panels +# ------------------------------------------------------------------------ + + +class Level_PT_Panel(Panel): + bl_label = "SM64 Level Importer" + bl_idname = "sm64_level_importer" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "SM64 C Importer" + bl_context = "objectmode" + + @classmethod + def poll(self, context): + return context.scene is not None + + def draw(self, context): + layout = self.layout + scene = context.scene + LevelImp = scene.LevelImp + layout.prop(LevelImp, "Level") + layout.prop(LevelImp, "Entry") + layout.prop(LevelImp, "Prefix") + layout.prop(LevelImp, "Version") + layout.prop(LevelImp, "Target") + row = layout.row() + row.prop(LevelImp, "ForceNewTex") + row.prop(LevelImp, "AsObj") + row.prop(LevelImp, "UseCol") + layout.operator("wm.sm64_import_level") + layout.operator("wm.sm64_import_level_gfx") + layout.operator("wm.sm64_import_level_col") + layout.operator("wm.sm64_import_object") + + +class Actor_PT_Panel(Panel): + bl_label = "SM64 Actor Importer" + bl_idname = "sm64_actor_importer" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "SM64 C Importer" + bl_context = "objectmode" + + @classmethod + def poll(self, context): + return context.scene is not None + + def draw(self, context): + layout = self.layout + scene = context.scene + ActImp = scene.ActImp + layout.prop(ActImp, "FolderType") + layout.prop(ActImp, "GeoLayout") + layout.prop(ActImp, "Prefix") + layout.prop(ActImp, "Version") + layout.prop(ActImp, "Target") + layout.operator("wm.sm64_import_actor") + + +classes = ( + LevelImport, + ActorImport, + SM64_OT_Lvl_Import, + SM64_OT_Lvl_Gfx_Import, + SM64_OT_Lvl_Col_Import, + SM64_OT_Obj_Import, + SM64_OT_Act_Import, + Level_PT_Panel, + Actor_PT_Panel, +) + + +def sm64_import_register(): + from bpy.utils import register_class + + for cls in classes: + register_class(cls) + + bpy.types.Scene.LevelImp = PointerProperty(type=LevelImport) + bpy.types.Scene.ActImp = PointerProperty(type=ActorImport) + + +def sm64_import_unregister(): + from bpy.utils import unregister_class + + for cls in reversed(classes): + unregister_class(cls) + del bpy.types.Scene.LevelImp + del bpy.types.Scene.ActImp diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 051af7a8e..4d0bc4710 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -121,13 +121,19 @@ def selectSingleObject(obj: bpy.types.Object): bpy.context.view_layer.objects.active = obj -def parentObject(parent, child): - bpy.ops.object.select_all(action="DESELECT") +def parentObject(parent, child, keep=0): + if not keep: + child.parent = parent + child.matrix_local = child.matrix_parent_inverse + else: + bpy.ops.object.select_all(action="DESELECT") - child.select_set(True) - parent.select_set(True) - bpy.context.view_layer.objects.active = parent - bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + child.select_set(True) + parent.select_set(True) + bpy.context.view_layer.objects.active = parent + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + parent.select_set(False) + child.select_set(False) def getFMeshName(vertexGroup, namePrefix, drawLayer, isSkinned): @@ -1399,6 +1405,18 @@ def rotate_quat_blender_to_n64(rotation: mathutils.Quaternion): return new_rot.to_quaternion() +def rotate_quat_n64_to_blender(rotation: mathutils.Quaternion): + new_rot = transform_mtx_blender_to_n64().inverted() @ rotation.to_matrix().to_4x4() @ transform_mtx_blender_to_n64() + return new_rot.to_quaternion() + + +# this will take a blender property, its enumprop name, and then return a list of the allowed enums +def GetEnums(prop, enum): + enumProp = prop.bl_rna.properties.get(enum) + if enumProp: + return [item.identifier for item in enumProp.enum_items] + + def all_values_equal_x(vals: Iterable, test): return len(set(vals) - set([test])) == 0 From 9a2511bcb555346670f1acd64e96d3b56e7ec25f Mon Sep 17 00:00:00 2001 From: scut Date: Sun, 20 Nov 2022 13:42:45 -0500 Subject: [PATCH 02/11] removed accidentally created file --- asdf.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 asdf.txt diff --git a/asdf.txt b/asdf.txt deleted file mode 100644 index e69de29bb..000000000 From 5bfec0ddce811d1f6a220f8ff5e6d3f2d5b9391e Mon Sep 17 00:00:00 2001 From: scut Date: Wed, 28 Jun 2023 21:45:37 -0400 Subject: [PATCH 03/11] rewrote import plugin to use generic data parser scheme for all parsing. moved c pre parsing and data aggregation functions to a utility file, general cleanup and annotations added --- fast64_internal/f3d/f3d_import.py | 271 +++- fast64_internal/sm64/sm64_constants.py | 7 +- fast64_internal/sm64/sm64_level_importer.py | 1498 +++++++++---------- fast64_internal/utility.py | 18 + fast64_internal/utility_importer.py | 188 +++ 5 files changed, 1082 insertions(+), 900 deletions(-) create mode 100644 fast64_internal/utility_importer.py diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py index 66979868c..b043375ab 100644 --- a/fast64_internal/f3d/f3d_import.py +++ b/fast64_internal/f3d/f3d_import.py @@ -15,6 +15,7 @@ from re import findall from ..utility import hexOrDecInt +from ..utility_importer import * # ------------------------------------------------------------------------ # Classes @@ -160,11 +161,12 @@ def ApplyPBSDFMat(self, mat): if not pbsdf: return tex = nodes.new("ShaderNodeTexImage") - links.new(pbsdf.inputs[0], tex.outputs[0]) - links.new(pbsdf.inputs[19], tex.outputs[1]) - i = self.LoadTexture(0, path) + links.new(pbsdf.inputs[0], tex.outputs[0]) # base color + i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, tex0) if i: tex.image = i + if int(layer) > 4: + mat.blend_method == "BLEND" def ApplyMatSettings(self, mat, tex_path): # if bpy.context.scene.LevelImp.AsObj: @@ -383,96 +385,183 @@ def EvalFmt(self, tex): # handles DL import processing, specifically built to process each cmd into the mat class # should be inherited into a larger F3d class which wraps DL processing # does not deal with flow control or gathering the data containers (VB, Geo cls etc.) -class DL: +class DL(DataParser): # the min needed for this class to work for importing def __init__(self, lastmat=None): - self.VB = {} # vertex buffers - self.Gfx = {} # ptrs: display lists - self.diff = {} # diffuse lights - self.amb = {} # ambient lights - self.Lights = {} # lights + self.Vtx = {} + self.Gfx = {} + self.Light_t = {} + self.Ambient_t = {} + self.Lights1 = {} + self.Textures = {} if not lastmat: self.LastMat = Mat() self.LastMat.name = 0 else: self.LastMat = lastmat - - # DL cmds that control the flow of a DL cannot be handled within a independent #class method without larger context of the total DL - # def gsSPEndDisplayList(): - # return - # def gsSPBranchList(): - # break - # def gsSPDisplayList(): - # continue - # Vertices are not currently handled in the base class - # Triangles - def gsSP2Triangles(self, args): + super().__init__() + + def gsSPEndDisplayList(self, macro: Macro): + return self.break_parse + + def gsSPBranchList(self, macro: Macro): + NewDL = self.Gfx.get(branched_dl := macro.args[0]) + if not NewDL: + raise Exception( + "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( + NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + ) + ) + self.parse_stream(NewDL, branched_dl) + return self.break_parse + + def gsSPDisplayList(self, macro: Macro): + NewDL = self.Gfx.get(branched_dl := macro.args[0]) + if not NewDL: + raise Exception( + "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( + NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + ) + ) + self.parse_stream(NewDL, branched_dl) + return self.continue_parse + + def gsSPEndDisplayList(self, macro: Macro): + return self.break_parse + + def gsSPVertex(self, macro: Macro): + # vertex references commonly use pointer arithmatic. I will deal with that case here, but not for other things unless it somehow becomes a problem later + if "+" in macro.args[0]: + ref, offset = macro.args[0].split("+") + offset = hexOrDecInt(offset) + else: + ref = macro.args[0] + offset = 0 + VB = self.Vtx.get(ref) + if not VB: + raise Exception( + "Could not find VB {} in levels/{}/{}leveldata.inc.c".format( + ref, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + ) + ) + vertex_load_start = hexOrDecInt(macro.args[2]) + vertex_load_length = hexOrDecInt(macro.args[1]) + Verts = VB[ + offset : offset + vertex_load_length + ] # If you use array indexing here then you deserve to have this not work + Verts = [self.ParseVert(v) for v in Verts] + for k, i in enumerate(range(vertex_load_start, vertex_load_length, 1)): + self.VertBuff[i] = [Verts[k], vertex_load_start] + # These are all independent data blocks in blender + self.Verts.extend([v[0] for v in Verts]) + self.UVs.extend([v[1] for v in Verts]) + self.VCs.extend([v[2] for v in Verts]) + self.LastLoad = vertex_load_length + return self.continue_parse + + def gsSP2Triangles(self, macro: Macro): self.MakeNewMat() - args = [hexOrDecInt(a) for a in args] + args = [hexOrDecInt(a) for a in macro.args] Tri1 = self.ParseTri(args[:3]) Tri2 = self.ParseTri(args[4:7]) self.Tris.append(Tri1) self.Tris.append(Tri2) + return self.continue_parse - def gsSP1Triangle(self, args): + def gsSP1Triangle(self, macro: Macro): self.MakeNewMat() - args = [hexOrDecInt(a) for a in args] + args = [hexOrDecInt(a) for a in macro.args] Tri = self.ParseTri(args[:3]) self.Tris.append(Tri) + return self.continue_parse # materials # Mats will be placed sequentially. The first item of the list is the triangle number # The second is the material class - def gsDPSetRenderMode(self, args): + def gsDPSetRenderMode(self, macro: Macro): self.NewMat = 1 - self.LastMat.RenderMode = [a.strip() for a in args] - - def gsDPSetFogColor(self, args): + self.LastMat.RenderMode = [a.strip() for a in macro.args] + return self.continue_parse + + # not finished yet + def gsSPLight(self, macro: Macro): + return self.continue_parse + def gsSPLightColor(self, macro: Macro): + return self.continue_parse + def gsSPSetLights0(self, macro: Macro): + return self.continue_parse + def gsSPSetLights1(self, macro: Macro): + return self.continue_parse + def gsSPSetLights2(self, macro: Macro): + return self.continue_parse + def gsSPSetLights3(self, macro: Macro): + return self.continue_parse + def gsSPSetLights4(self, macro: Macro): + return self.continue_parse + def gsSPSetLights5(self, macro: Macro): + return self.continue_parse + def gsSPSetLights6(self, macro: Macro): + return self.continue_parse + def gsSPSetLights7(self, macro: Macro): + return self.continue_parse + def gsDPSetDepthSource(self, macro: Macro): + return self.continue_parse + def gsSPFogFactor(self, macro: Macro): + return self.continue_parse + + def gsDPSetFogColor(self, macro: Macro): self.NewMat = 1 - self.LastMat.fog_color = args + self.LastMat.fog_color = macro.args + return self.continue_parse - def gsSPFogPosition(self, args): + def gsSPFogPosition(self, macro: Macro): self.NewMat = 1 - self.LastMat.fog_pos = args + self.LastMat.fog_pos = macro.args + return self.continue_parse - def gsSPLightColor(self, args): + def gsSPLightColor(self, macro: Macro): self.NewMat = 1 if not hasattr(self.LastMat, "light_col"): self.LastMat.light_col = {} - num = re.search("_\d", args[0]).group()[1] - self.LastMat.light_col[num] = args[-1] + num = re.search("_\d", macro.args[0]).group()[1] + self.LastMat.light_col[num] = macro.args[-1] + return self.continue_parse - def gsDPSetPrimColor(self, args): + def gsDPSetPrimColor(self, macro: Macro): self.NewMat = 1 - self.LastMat.prim_color = args + self.LastMat.prim_color = macro.args + return self.continue_parse - def gsDPSetEnvColor(self, args): + def gsDPSetEnvColor(self, macro: Macro): self.NewMat = 1 - self.LastMat.env_color = args + self.LastMat.env_color = macro.args + return self.continue_parse # multiple geo modes can happen in a row that contradict each other # this is mostly due to culling wanting diff geo modes than drawing # but sometimes using the same vertices - def gsSPClearGeometryMode(self, args): + def gsSPClearGeometryMode(self, macro: Macro): self.NewMat = 1 - args = [a.strip() for a in args[0].split("|")] + args = [a.strip() for a in macro.args[0].split("|")] for a in args: if a in self.LastMat.GeoSet: self.LastMat.GeoSet.remove(a) self.LastMat.GeoClear.extend(args) + return self.continue_parse - def gsSPSetGeometryMode(self, args): + def gsSPSetGeometryMode(self, macro: Macro): self.NewMat = 1 - args = [a.strip() for a in args[0].split("|")] + args = [a.strip() for a in macro.args[0].split("|")] for a in args: if a in self.LastMat.GeoClear: self.LastMat.GeoClear.remove(a) self.LastMat.GeoSet.extend(args) + return self.continue_parse - def gsSPGeometryMode(self, args): + def gsSPGeometryMode(self, macro: Macro): self.NewMat = 1 - argsC = [a.strip() for a in args[0].split("|")] - argsS = [a.strip() for a in args[1].split("|")] + argsC = [a.strip() for a in macro.args[0].split("|")] + argsS = [a.strip() for a in macro.args[1].split("|")] for a in argsC: if a in self.LastMat.GeoSet: self.LastMat.GeoSet.remove(a) @@ -481,39 +570,44 @@ def gsSPGeometryMode(self, args): self.LastMat.GeoClear.remove(a) self.LastMat.GeoClear.extend(argsC) self.LastMat.GeoSet.extend(argsS) + return self.continue_parse - def gsDPSetCycleType(self, args): - if "G_CYC_1CYCLE" in args[0]: + def gsDPSetCycleType(self, macro: Macro): + if "G_CYC_1CYCLE" in macro.args[0]: self.LastMat.TwoCycle = False - if "G_CYC_2CYCLE" in args[0]: + if "G_CYC_2CYCLE" in macro.args[0]: self.LastMat.TwoCycle = True + return self.continue_parse - def gsDPSetCombineMode(self, args): + def gsDPSetCombineMode(self, macro: Macro): self.NewMat = 1 - self.LastMat.Combiner = self.EvalCombiner(args) + self.LastMat.Combiner = self.EvalCombiner(macro.args) + return self.continue_parse - def gsDPSetCombineLERP(self, args): + def gsDPSetCombineLERP(self, macro: Macro): self.NewMat = 1 - self.LastMat.Combiner = [a.strip() for a in args] + self.LastMat.Combiner = [a.strip() for a in macro.args] + return self.continue_parse # root tile, scale and set tex - def gsSPTexture(self, args): + def gsSPTexture(self, macro: Macro): self.NewMat = 1 macros = { "G_ON": 2, "G_OFF": 0, } - set_tex = macros.get(args[-1].strip()) + set_tex = macros.get(macro.args[-1].strip()) if set_tex == None: - set_tex = hexOrDecInt(args[-1].strip()) + set_tex = hexOrDecInt(macro.args[-1].strip()) self.LastMat.set_tex = set_tex == 2 self.LastMat.tex_scale = [ - ((0x10000 * (hexOrDecInt(a) < 0)) + hexOrDecInt(a)) / 0xFFFF for a in args[0:2] + ((0x10000 * (hexOrDecInt(a) < 0)) + hexOrDecInt(a)) / 0xFFFF for a in macro.args[0:2] ] # signed half to unsigned half - self.LastMat.tile_root = self.EvalTile(args[-2].strip()) # I don't think I'll actually use this + self.LastMat.tile_root = self.EvalTile(macro.args[-2].strip()) # I don't think I'll actually use this + return self.continue_parse # last tex is a palette - def gsDPLoadTLUT(self, args): + def gsDPLoadTLUT(self, macro: Macro): try: tex = self.LastMat.loadtex self.LastMat.pal = tex @@ -524,15 +618,16 @@ def gsDPLoadTLUT(self, args): "No interpretation on file possible**--" ) return None + return self.continue_parse # tells us what tile the last loaded mat goes into - def gsDPLoadBlock(self, args): + def gsDPLoadBlock(self, macro: Macro): try: tex = self.LastMat.loadtex # these values aren't necessary when the texture is already in png format # tex.dxt = hexOrDecInt(args[4]) # tex.texels = hexOrDecInt(args[3]) - tile = self.EvalTile(args[0]) + tile = self.EvalTile(macro.args[0]) tex.tile = tile if tile == 7: self.LastMat.tex0 = tex @@ -545,34 +640,50 @@ def gsDPLoadBlock(self, args): "No interpretation on file possible**--" ) return None + return self.continue_parse - def gsDPSetTextureImage(self, args): + def gsDPSetTextureImage(self, macro: Macro): self.NewMat = 1 - Timg = args[3].strip() - Fmt = args[1].strip() - Siz = args[2].strip() + Timg = macro.args[3].strip() + Fmt = macro.args[1].strip() + Siz = macro.args[2].strip() loadtex = Texture(Timg, Fmt, Siz) self.LastMat.loadtex = loadtex + return self.continue_parse - def gsDPSetTileSize(self, args): + def gsDPSetTileSize(self, macro: Macro): self.NewMat = 1 - tile = self.LastMat.tiles[self.EvalTile(args[0])] - tile.Slow = self.EvalImFrac(args[1].strip()) - tile.Tlow = self.EvalImFrac(args[2].strip()) - tile.Shigh = self.EvalImFrac(args[3].strip()) - tile.Thigh = self.EvalImFrac(args[4].strip()) - - def gsDPSetTile(self, args): + tile = self.LastMat.tiles[self.EvalTile(macro.args[0])] + tile.Slow = self.EvalImFrac(macro.args[1].strip()) + tile.Tlow = self.EvalImFrac(macro.args[2].strip()) + tile.Shigh = self.EvalImFrac(macro.args[3].strip()) + tile.Thigh = self.EvalImFrac(macro.args[4].strip()) + return self.continue_parse + + def gsDPSetTile(self, macro: Macro): self.NewMat = 1 - tile = self.LastMat.tiles[self.EvalTile(args[4].strip())] - tile.Fmt = args[0].strip() - tile.Siz = args[1].strip() - tile.Tflags = args[6].strip() - tile.TMask = self.EvalTile(args[7].strip()) - tile.TShift = self.EvalTile(args[8].strip()) - tile.Sflags = args[9].strip() - tile.SMask = self.EvalTile(args[10].strip()) - tile.SShift = self.EvalTile(args[11].strip()) + tile = self.LastMat.tiles[self.EvalTile(macro.args[4].strip())] + tile.Fmt = macro.args[0].strip() + tile.Siz = macro.args[1].strip() + tile.Tflags = macro.args[6].strip() + tile.TMask = self.EvalTile(macro.args[7].strip()) + tile.TShift = self.EvalTile(macro.args[8].strip()) + tile.Sflags = macro.args[9].strip() + tile.SMask = self.EvalTile(macro.args[10].strip()) + tile.SShift = self.EvalTile(macro.args[11].strip()) + return self.continue_parse + + #syncs need no processing + def gsDPPipeSync(self, macro: Macro): + return self.continue_parse + def gsDPLoadSync(self, macro: Macro): + return self.continue_parse + def gsDPTileSync(self, macro: Macro): + return self.continue_parse + def gsDPFullSync(self, macro: Macro): + return self.continue_parse + def gsDPNoOp(self, macro: Macro): + return self.continue_parse def MakeNewMat(self): if self.NewMat: diff --git a/fast64_internal/sm64/sm64_constants.py b/fast64_internal/sm64/sm64_constants.py index c16864f10..b3bd92b2b 100644 --- a/fast64_internal/sm64/sm64_constants.py +++ b/fast64_internal/sm64/sm64_constants.py @@ -1768,7 +1768,12 @@ def __init__(self, geoAddr, level, switchDict): ("macro_yellow_coin", "Yellow Coin", "Yellow Coin"), ("macro_yellow_coin_2", "Yellow Coin 2", "Yellow Coin 2"), ] - +enumVersionDefs = [ + ("VERSION_US", "VERSION_US", ""), + ("VERSION_JP", "VERSION_JP", ""), + ("VERSION_EU", "VERSION_EU", ""), + ("VERSION_SH", "VERSION_SH", ""), +] # groups you can select for levels groupsSeg5 = [ diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index 7156bf902..6657662d0 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -1,6 +1,9 @@ # ------------------------------------------------------------------------ # Header # ------------------------------------------------------------------------ +from __future__ import annotations + + import bpy from bpy.props import ( @@ -29,14 +32,22 @@ from mathutils import Vector, Euler, Matrix, Quaternion from copy import deepcopy from dataclasses import dataclass +from typing import TextIO # from SM64classes import * from ..f3d.f3d_import import * +from ..utility_importer import * from ..utility import ( rotate_quat_n64_to_blender, + rotate_object, parentObject, GetEnums, + create_collection, +) +from .sm64_constants import ( + enumVersionDefs, + enumLevelNames, ) # ------------------------------------------------------------------------ @@ -120,7 +131,7 @@ def __init__( self.objects = [] self.col = col - def AddWarp(self, args: list[str]): + def add_warp(self, args: list[str]): # set context to the root bpy.context.view_layer.objects.active = self.root # call fast64s warp node creation operator @@ -144,22 +155,22 @@ def AddWarp(self, args: list[str]): else: warp.warpFlagEnum = "WARP_CHECKPOINT" - def AddObject(self, args: list[str]): + def add_object(self, args: list[str]): self.objects.append(args) - def PlaceObjects(self, col_name: str = None): + def place_objects(self, col_name: str = None): if not col_name: col = self.col else: - col = CreateCol(self.root.users_collection[0], col_name) - for a in self.objects: - self.PlaceObject(a, col) + col = create_collection(self.root.users_collection[0], col_name) + for object_args in self.objects: + self.place_object(object_args, col) - def PlaceObject(self, args: list[str], col: bpy.types.Collection): + def place_object(self, args: list[str], col: bpy.types.Collection): Obj = bpy.data.objects.new("Empty", None) col.objects.link(Obj) parentObject(self.root, Obj) - Obj.name = "Object {} {}".format(args[8].strip(), args[0].strip()) + Obj.name = "Object {} {}".format(args[8], args[0]) Obj.sm64_obj_type = "Object" Obj.sm64_behaviour_enum = "Custom" Obj.sm64_obj_behaviour = args[8].strip() @@ -187,179 +198,194 @@ def PlaceObject(self, args: list[str], col: bpy.types.Collection): setattr(Obj, form.format(i), True) else: for i in range(1, 7, 1): - if mask & (1 << i): + if mask & (1 << (i - 1)): setattr(Obj, form.format(i), True) else: setattr(Obj, form.format(i), False) -class Level: - def __init__(self, scr: list[str], scene: bpy.types.Scene, root: bpy.types.Object): - self.Scripts = FormatDat(scr, "LevelScript", ["(", ")"]) +class Level(DataParser): + def __init__(self, script: TextIO, scene: bpy.types.Scene, root: bpy.types.Object): + self.scripts: dict[str, list[str]] = get_data_types_from_file(script, {"LevelScript": ["(", ")"]}) self.scene = scene - self.Areas = {} - self.CurrArea = None + self.areas: dict[Area] = {} + self.cur_area: int = None self.root = root + super().__init__() - def ParseScript(self, entry: str, col: bpy.types.Collection = None): - Start = self.Scripts[entry] + def parse_level_script(self, entry: str, col: bpy.types.Collection = None): + script_stream = self.scripts[entry] scale = self.scene.blenderToSM64Scale if not col: col = self.scene.collection - for l in Start: - args = self.StripArgs(l) - LsW = l.startswith - # Find an area - if LsW("AREA"): - Root = bpy.data.objects.new("Empty", None) - if self.scene.LevelImp.UseCol: - a_col = bpy.data.collections.new(f"{self.scene.LevelImp.Level} area {args[0]}") - col.children.link(a_col) - else: - a_col = col - a_col.objects.link(Root) - Root.name = "{} Area Root {}".format(self.scene.LevelImp.Level, args[0]) - self.Areas[args[0]] = Area(Root, args[1], self.root, int(args[0]), self.scene, a_col) - self.CurrArea = args[0] - continue - # End an area - if LsW("END_AREA"): - self.CurrArea = None - continue - # Jumps are only taken if they're in the script.c file for now - # continues script - elif LsW("JUMP_LINK"): - if self.Scripts.get(args[0]): - self.ParseScript(args[0], col=col) - continue - # ends script, I get arg -1 because sm74 has a different jump cmd - elif LsW("JUMP"): - Nentry = self.Scripts.get(args[-1]) - if Nentry: - self.ParseScript(args[-1], col=col) - # for the sm74 port - if len(args) != 2: - break - # final exit of recursion - elif LsW("EXIT") or l.startswith("RETURN"): - return - # Now deal with data cmds rather than flow control ones - if LsW("WARP_NODE"): - self.Areas[self.CurrArea].AddWarp(args) - continue - if LsW("OBJECT_WITH_ACTS"): - # convert act mask from ORs of act names to a number - mask = args[-1].strip() - if not mask.isdigit(): - mask = mask.replace("ACT_", "") - mask = mask.split("|") - # Attempt for safety I guess - try: - a = 0 - for m in mask: - a += 1 << int(m) - mask = a - except: - mask = 31 - self.Areas[self.CurrArea].AddObject([*args[:-1], mask]) - continue - if LsW("OBJECT"): - # Only difference is act mask, which I set to 31 to mean all acts - self.Areas[self.CurrArea].AddObject([*args, 31]) - continue - # Don't support these for now - if LsW("MACRO_OBJECTS"): - continue - if LsW("TERRAIN_TYPE"): - if not args[0].isdigit(): - self.Areas[self.CurrArea].root.terrainEnum = args[0].strip() - else: - terrains = { - 0: "TERRAIN_GRASS", - 1: "TERRAIN_STONE", - 2: "TERRAIN_SNOW", - 3: "TERRAIN_SAND", - 4: "TERRAIN_SPOOKY", - 5: "TERRAIN_WATER", - 6: "TERRAIN_SLIDE", - 7: "TERRAIN_MASK", - } - try: - num = eval(args[0]) - self.Areas[self.CurrArea].root.terrainEnum = terrains.get(num) - except: - print("could not set terrain") - continue - if LsW("SHOW_DIALOG"): - rt = self.Areas[self.CurrArea].root - rt.showStartDialog = True - rt.startDialog = args[1].strip() - continue - if LsW("TERRAIN"): - self.Areas[self.CurrArea].terrain = args[0].strip() - continue - if LsW("SET_BACKGROUND_MUSIC") or LsW("SET_MENU_MUSIC"): - rt = self.Areas[self.CurrArea].root - rt.musicSeqEnum = "Custom" - rt.music_seq = args[-1].strip() - return self.Areas - - def StripArgs(self, cmd: str): - a = cmd.find("(") - end = cmd.rfind(")") - len(cmd) - return cmd[a + 1 : end].split(",") - - -class Collision: - def __init__(self, col: list[str], scale: float): - self.col = col + self.parse_stream(script_stream, entry, col) + return self.areas + + def AREA(self, macro: Macro, col: bpy.types.Collection): + area_root = bpy.data.objects.new("Empty", None) + if self.scene.LevelImp.UseCol: + area_col = bpy.data.collections.new(f"{self.scene.LevelImp.Level} area {args[0]}") + col.children.link(area_col) + else: + area_col = col + area_col.objects.link(area_root) + area_root.name = f"{self.scene.LevelImp.Level} Area Root {macro.args[0]}" + self.areas[macro.args[0]] = Area( + area_root, macro.args[1], self.root, int(macro.args[0]), self.scene, area_col + ) + self.cur_area = macro.args[0] + return self.continue_parse + + def END_AREA(self, macro: Macro, col: bpy.types.Collection): + self.cur_area = None + return self.continue_parse + + # Jumps are only taken if they're in the script.c file for now + # continues script + def JUMP_LINK(self, macro: Macro, col: bpy.types.Collection): + if self.scripts.get(macro.args[0]): + self.parse_level_script(macro.args[0], col=col) + return self.continue_parse + + # ends script + def JUMP(self, macro: Macro, col: bpy.types.Collection): + new_entry = self.scripts.get(macro.args[-1]) + if new_entry: + self.parse_level_script(macro.args[-1], col=col) + return self.break_parse + + def EXIT(self, macro: Macro, col: bpy.types.Collection): + return self.break_parse + + def RETURN(self, macro: Macro, col: bpy.types.Collection): + return self.break_parse + + # Now deal with data cmds rather than flow control ones + def WARP_NODE(self, macro: Macro, col: bpy.types.Collection): + self.areas[self.cur_area].add_warp(macro.args) + return self.continue_parse + + def OBJECT_WITH_ACTS(self, macro: Macro, col: bpy.types.Collection): + # convert act mask from ORs of act names to a number + mask = macro.args[-1] + if not mask.isdigit(): + mask = mask.replace("ACT_", "") + mask = mask.split("|") + # Attempt for safety I guess + try: + accumulator = 0 + for m in mask: + accumulator += 1 << int(m) + mask = accumulator + except: + mask = 31 + self.areas[self.cur_area].add_object([*macro.args[:-1], mask]) + return self.continue_parse + + def OBJECT(self, macro: Macro, col: bpy.types.Collection): + # Only difference is act mask, which I set to 31 to mean all acts + self.areas[self.cur_area].add_object([*macro.args, 31]) + return self.continue_parse + + def TERRAIN_TYPE(self, macro: Macro, col: bpy.types.Collection): + if not macro.args[0].isdigit(): + self.areas[self.cur_area].root.terrainEnum = macro.args[0] + else: + terrains = { + 0: "TERRAIN_GRASS", + 1: "TERRAIN_STONE", + 2: "TERRAIN_SNOW", + 3: "TERRAIN_SAND", + 4: "TERRAIN_SPOOKY", + 5: "TERRAIN_WATER", + 6: "TERRAIN_SLIDE", + 7: "TERRAIN_MASK", + } + try: + num = eval(macro.args[0]) + self.areas[self.cur_area].root.terrainEnum = terrains.get(num) + except: + print("could not set terrain") + return self.continue_parse + + def SHOW_DIALOG(self, macro: Macro, col: bpy.types.Collection): + root = self.areas[self.cur_area].root + root.showStartDialog = True + root.startDialog = macro.args[1] + return self.continue_parse + + def TERRAIN(self, macro: Macro, col: bpy.types.Collection): + self.areas[self.cur_area].terrain = macro.args[0] + return self.continue_parse + + def SET_BACKGROUND_MUSIC(self, macro: Macro, col: bpy.types.Collection): + return self.generic_music(macro, col) + + def SET_MENU_MUSIC(self, macro: Macro, col: bpy.types.Collection): + return self.generic_music(macro, col) + + def generic_music(self, macro: Macro, col: bpy.types.Collection): + root = self.areas[self.cur_area].root + root.musicSeqEnum = "Custom" + root.music_seq = macro.args[-1] + return self.continue_parse + + # Don't support these for now + def MACRO_OBJECTS(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def MARIO_POS(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + # use group mapping to set groups eventually + def LOAD_MIO0(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def LOAD_MIO0_TEXTURE(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def LOAD_YAY0(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def LOAD_RAW(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + # not useful for bpy, dummy these script cmds + def MARIO(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def INIT_LEVEL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def ALLOC_LEVEL_POOL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def FREE_LEVEL_POOL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def LOAD_MODEL_FROM_GEO(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def LOAD_MODEL_FROM_DL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def CALL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def CALL_LOOP(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def CLEAR_LEVEL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def SLEEP_BEFORE_EXIT(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + +class Collision(DataParser): + def __init__(self, collision: list[str], scale: float): + self.collision = collision self.scale = scale self.vertices = [] # key=type,value=tri data self.tris = {} self.type = None - self.SpecialObjs = [] - self.Types = [] - self.WaterBox = [] - - def GetCollision(self): - for l in self.col: - args = self.StripArgs(l) - # to avoid catching COL_VERTEX_INIT - if l.startswith("COL_VERTEX") and len(args) == 3: - self.vertices.append([eval(v) / self.scale for v in args]) - continue - if l.startswith("COL_TRI_INIT"): - self.type = args[0] - if not self.tris.get(self.type): - self.tris[self.type] = [] - continue - if l.startswith("COL_TRI") and len(args) > 2: - a = [eval(a) for a in args] - self.tris[self.type].append(a) - continue - if l.startswith("COL_WATER_BOX_INIT"): - continue - if l.startswith("COL_WATER_BOX"): - # id, x1, z1, x2, z2, y - self.WaterBox.append(args) - if l.startswith("SPECIAL_OBJECT"): - self.SpecialObjs.append(args) - # This will keep track of how to assign mats - a = 0 - for k, v in self.tris.items(): - self.Types.append([a, k, v[0]]) - a += len(v) - self.Types.append([a, 0]) - - def StripArgs(self, cmd: str): - a = cmd.find("(") - return cmd[a + 1 : -2].split(",") + self.special_objects = [] + self.tri_types = [] + self.water_boxes = [] + super().__init__() def WriteWaterBoxes( self, scene: bpy.types.Scene, parent: bpy.types.Object, name: str, col: bpy.types.Collection = None ): - for i, w in enumerate(self.WaterBox): + for i, w in enumerate(self.water_boxes): Obj = bpy.data.objects.new("Empty", None) scene.collection.objects.link(Obj) parentObject(parent, Obj) @@ -397,25 +423,25 @@ def WriteCollision( obj.ignore_render = True if parent: parentObject(parent, obj) - RotateObj(-90, obj, world=1) + rotate_object(-90, obj, world=1) polys = obj.data.polygons x = 0 bpy.context.view_layer.objects.active = obj max = len(polys) for i, p in enumerate(polys): - a = self.Types[x][0] + a = self.tri_types[x][0] if i >= a: bpy.ops.object.create_f3d_mat() # the newest mat should be in slot[-1] mat = obj.data.materials[x] mat.collision_type_simple = "Custom" - mat.collision_custom = self.Types[x][1] - mat.name = "Sm64_Col_Mat_{}".format(self.Types[x][1]) + mat.collision_custom = self.tri_types[x][1] + mat.name = "Sm64_Col_Mat_{}".format(self.tri_types[x][1]) color = ((max - a) / (max), (max + a) / (2 * max - a), a / max, 1) # Just to give some variety mat.f3d_mat.default_light_color = color # check for param - if len(self.Types[x][2]) > 3: + if len(self.tri_types[x][2]) > 3: mat.use_collision_param = True - mat.collision_param = str(self.Types[x][2][3]) + mat.collision_param = str(self.tri_types[x][2][3]) x += 1 override = bpy.context.copy() override["material"] = mat @@ -423,8 +449,59 @@ def WriteCollision( p.material_index = x - 1 return obj - -class sm64_Mat(Mat): + def GetCollision(self): + self.parse_stream(self.collision, 0) + # This will keep track of how to assign mats + a = 0 + for k, v in self.tris.items(): + self.tri_types.append([a, k, v[0]]) + a += len(v) + self.tri_types.append([a, 0]) + + def COL_VERTEX(self, macro: Macro): + self.vertices.append([eval(v) / self.scale for v in macro.args]) + return self.continue_parse + + def COL_TRI_INIT(self, macro: Macro): + self.type = macro.args[0] + if not self.tris.get(self.type): + self.tris[self.type] = [] + return self.continue_parse + + def COL_TRI(self, macro: Macro): + self.tris[self.type].append([eval(a) for a in macro.args]) + return self.continue_parse + + def COL_WATER_BOX(self, macro: Macro): + # id, x1, z1, x2, z2, y + self.water_boxes.append(macro.args) + return self.continue_parse + + # not written out currently + def SPECIAL_OBJECT(self, macro: Macro): + self.special_objects.append(macro.args) + return self.continue_parse + + def SPECIAL_OBJECT_WITH_YAW(self, macro: Macro): + self.special_objects.append(macro.args) + return self.continue_parse + + # don't do anything to bpy + def COL_WATER_BOX_INIT(self, macro: Macro): + return self.continue_parse + def COL_INIT(self, macro: Macro): + return self.continue_parse + def COL_VERTEX_INIT(self, macro: Macro): + return self.continue_parse + def COL_SPECIAL_INIT(self, macro: Macro): + return self.continue_parse + def COL_TRI_STOP(self, macro: Macro): + return self.continue_parse + def COL_END(self, macro: Macro): + return self.continue_parse + + +class SM64_Material(Mat): def LoadTexture(self, ForceNewTex: bool, textures: dict, path: Path, tex: Texture): if not tex: return None @@ -435,33 +512,18 @@ def LoadTexture(self, ForceNewTex: bool, textures: dict, path: Path, tex: Textur Timg = textures.get(tex.Timg)[0] Timg = Timg.replace("#include ", "").replace('"', "").replace("'", "").replace("inc.c", "png") # deal with duplicate pathing (such as /actors/actors etc.) - Extra = path.relative_to(Path(bpy.context.scene.decompPath)) + Extra = path.relative_to(Path(bpy.path.abspath(bpy.context.scene.decompPath))) for e in Extra.parts: Timg = Timg.replace(e + "/", "") # deal with actor import path not working for shared textures if "textures" in Timg: - fp = Path(bpy.context.scene.decompPath) / Timg + fp = Path(bpy.path.abspath(bpy.context.scene.decompPath)) / Timg else: fp = path / Timg return bpy.data.images.load(filepath=str(fp)) else: return i - def ApplyPBSDFMat(self, mat: bpy.types.Material, textures: dict, path: Path, layer: int, tex0: Texture): - nt = mat.node_tree - nodes = nt.nodes - links = nt.links - pbsdf = nodes.get("Principled BSDF") - if not pbsdf: - return - tex = nodes.new("ShaderNodeTexImage") - links.new(pbsdf.inputs[0], tex.outputs[0]) # base color - i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, tex0) - if i: - tex.image = i - if int(layer) > 4: - mat.blend_method == "BLEND" - def ApplyMatSettings(self, mat: bpy.types.Material, textures: dict, path: Path, layer: int): if bpy.context.scene.LevelImp.AsObj: return self.ApplyPBSDFMat(mat, textures, path, layer, self.tex0) @@ -516,7 +578,6 @@ def ApplyMatSettings(self, mat: bpy.types.Material, textures: dict, path: Path, if f3d.rdp_settings.g_tex_gen or any([a < 1 and a > 0 for a in self.tex_scale]): f3d.scale_autoprop = False f3d.tex_scale = self.tex_scale - print(self.tex_scale) if not self.set_tex: # Update node values override = bpy.context.copy() @@ -574,16 +635,17 @@ def ApplyMatSettings(self, mat: bpy.types.Material, textures: dict, path: Path, del override -class sm64_F3d(DL): +class SM64_F3D(DL): def __init__(self, scene: bpy.types.Scene): - self.VB = {} + self.Vtx = {} self.Gfx = {} - self.diff = {} - self.amb = {} - self.Lights = {} + self.Light_t = {} + self.Ambient_t = {} + self.Lights1 = {} self.Textures = {} self.scene = scene self.num = 0 + super().__init__() # Textures only contains the texture data found inside the model.inc.c file and the texture.inc.c file def GetGenericTextures(self, root_path: Path): @@ -603,12 +665,19 @@ def GetGenericTextures(self, root_path: Path): "water.c", ]: t = root_path / "bin" / t - t = open(t, "r") - tex = t.readlines() + t = open(t, "r", newline='') + tex = t # For textures, try u8, and s16 aswell - self.Textures.update(FormatDat(tex, "Texture", [None, None])) - self.Textures.update(FormatDat(tex, "u8", [None, None])) - self.Textures.update(FormatDat(tex, "s16", [None, None])) + self.Textures.update( + get_data_types_from_file( + tex, + { + "Texture": [None, None], + "u8": [None, None], + "s16": [None, None], + }, + ) + ) t.close() # recursively parse the display list in order to return a bunch of model data @@ -622,76 +691,13 @@ def GetDataFromModel(self, start: str): self.UVs = [] self.VCs = [] self.Mats = [] - self.LastMat = sm64_Mat() - self.ParseDL(DL) + self.LastMat = SM64_Material() + self.parse_stream(DL, start) self.NewMat = 0 self.StartName = start + print(self.Verts, self.Tris, start) return [self.Verts, self.Tris] - def ParseDL(self, DL: list[str]): - # This will be the equivalent of a giant switch case - x = -1 - while x < len(DL): - # manaual iteration so I can skip certain children efficiently - # manaual iteration so I can skip certain children efficiently if needed - x += 1 - (cmd, args) = self.StripArgs(DL[x]) # each member is a tuple of (cmd, arguments) - LsW = cmd.startswith - # Deal with control flow first - if LsW("gsSPEndDisplayList"): - return - if LsW("gsSPBranchList"): - NewDL = self.Gfx.get(args[0].strip()) - if not DL: - raise Exception( - "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( - NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix - ) - ) - self.ParseDL(NewDL) - break - if LsW("gsSPDisplayList"): - NewDL = self.Gfx.get(args[0].strip()) - if not DL: - raise Exception( - "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( - NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix - ) - ) - self.ParseDL(NewDL) - continue - # Vertices - if LsW("gsSPVertex"): - # vertex references commonly use pointer arithmatic. I will deal with that case here, but not for other things unless it somehow becomes a problem later - if "+" in args[0]: - ref, add = args[0].split("+") - else: - ref = args[0] - add = "0" - VB = self.VB.get(ref.strip()) - if not VB: - raise Exception( - "Could not find VB {} in levels/{}/{}leveldata.inc.c".format( - ref, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix - ) - ) - Verts = VB[ - int(add.strip()) : int(add.strip()) + eval(args[1]) - ] # If you use array indexing here then you deserve to have this not work - Verts = [self.ParseVert(v) for v in Verts] - for k, i in enumerate(range(eval(args[2]), eval(args[1]), 1)): - self.VertBuff[i] = [Verts[k], eval(args[2])] - # These are all independent data blocks in blender - self.Verts.extend([v[0] for v in Verts]) - self.UVs.extend([v[1] for v in Verts]) - self.VCs.extend([v[2] for v in Verts]) - self.LastLoad = eval(args[1]) - continue - # tri and mat DL cmds will be called via parent class - func = getattr(self, cmd, None) - if func: - func(args) - def MakeNewMat(self): if self.NewMat: self.NewMat = 0 @@ -771,8 +777,8 @@ def ApplyDat(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_ UVmap.data[l].uv[1] = UVmap.data[l].uv[1] * -1 + 1 Vcol.data[l].color = [a / 255 for a in vcol] - # create a new f3d_mat given an sm64_Mat class but don't create copies with same props - def Create_new_f3d_mat(self, mat: sm64_Mat, mesh: bpy.types.Mesh): + # create a new f3d_mat given an SM64_Material class but don't create copies with same props + def Create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): if not self.scene.LevelImp.ForceNewTex: # check if this mat was used already in another mesh (or this mat if DL is garbage or something) # even looping n^2 is probably faster than duping 3 mats with blender speed @@ -805,156 +811,103 @@ class ModelDat: model: str scale: float = 1.0 - -class GeoLayout: - def __init__( - self, - GeoLayouts: dict, - root: bpy.types.Object, - scene: bpy.types.Scene, - name, - Aroot: bpy.types.Object, - col: bpy.types.Collection = None, - ): - self.GL = GeoLayouts - self.parent = root - self.models = [] - self.Children = [] - self.scene = scene - self.RenderRange = None - self.Aroot = Aroot # for properties that can only be written to area - self.root = root - self.ParentTransform = [[0, 0, 0], [0, 0, 0]] - self.LastTransform = [[0, 0, 0], [0, 0, 0]] - self.name = name - self.obj = None # last object on this layer of the tree, will become parent of next child - if not col: - self.col = Aroot.users_collection[0] +# base class for geo layouts and armatures +class GraphNodes(DataParser): + + def GEO_BRANCH_AND_LINK(self, macro: Macro, depth: int): + new_geo_layout = self.geo_layouts.get(macro.args[0]) + if new_geo_layout: + self.parse_stream(new_geo_layout, depth) + return self.continue_parse + + def GEO_BRANCH(self, macro: Macro, depth: int): + new_geo_layout = self.geo_layouts.get(macro.args[1]) + if new_geo_layout: + self.parse_stream(new_geo_layout, depth) + # arg 0 determines if you return and continue or end after the branch + if eval(macro.args[0]): + return self.continue_parse else: - self.col = col + return self.break_parse - def MakeRt(self, name: str, root: bpy.types.Object): - # make an empty node to act as the root of this geo layout - # use this to hold a transform, or an actual cmd, otherwise rt is passed - E = bpy.data.objects.new(name, None) - self.obj = E - self.col.objects.link(E) - parentObject(root, E) - return E + def GEO_END(self, macro: Macro, depth: int): + return self.break_parse - def ParseLevelGeosStart(self, start: str, scene: bpy.types.Scene): - GL = self.GL.get(start) - if not GL: - raise Exception( - "Could not find geo layout {} from levels/{}/{}geo.c".format( - start, scene.LevelImp.Level, scene.LevelImp.Prefix - ) - ) - self.ParseLevelGeos(GL, 0) + def GEO_RETURN(self, macro: Macro, depth: int): + return self.break_parse - # So I can start where ever for child nodes - def ParseLevelGeos(self, GL: list[str], depth: int): - # I won't parse the geo layout perfectly. For now I'll just get models. This is mostly because fast64 - # isn't a bijection to geo layouts, the props are sort of handled all over the place - x = -1 - while x < len(GL): - # manaual iteration so I can skip certain children efficiently - x += 1 - (cmd, args) = self.StripArgs(GL[x]) # each member is a tuple of (cmd, arguments) - LsW = cmd.startswith - # Jumps are only taken if they're in the script.c file for now - # continues script - if LsW("GEO_BRANCH_AND_LINK"): - NewGL = self.GL.get(args[0].strip()) - if NewGL: - self.ParseLevelGeos(NewGL, depth) - continue - # continues - elif LsW("GEO_BRANCH"): - NewGL = self.GL.get(args[1].strip()) - if NewGL: - self.ParseLevelGeos(NewGL, depth) - if eval(args[0]): - continue - else: - break - # final exit of recursion - elif LsW("GEO_END") or LsW("GEO_RETURN"): - return - # on an open node, make a child - elif LsW("GEO_CLOSE_NODE"): - # if there is no more open nodes, then parent this to last node - if depth: - return - elif LsW("GEO_OPEN_NODE"): - if self.obj: - GeoChild = GeoLayout(self.GL, self.obj, self.scene, self.name, self.Aroot, col=self.col) - else: - GeoChild = GeoLayout(self.GL, self.root, self.scene, self.name, self.Aroot, col=self.col) - GeoChild.ParentTransform = self.LastTransform - GeoChild.ParseLevelGeos(GL[x + 1 :], depth + 1) - x = self.SkipChildren(GL, x) - self.Children.append(GeoChild) - continue - else: - # things that only need args can be their own functions - func = getattr(self, cmd.strip(), None) - if func: - func(args) + def GEO_CLOSE_NODE(self, macro: Macro, depth: int): + # if there is no more open nodes, then parent this to last node + if depth: + return self.break_parse + else: + return self.continue_parse + + def GEO_OPEN_NODE(self, macro: Macro, depth: int): + if self.obj: + GeoChild = GeoLayout(self.geo_layouts, self.obj, self.scene, self.name, self.area_root, col=self.col, geo_parent=self) + else: + GeoChild = GeoLayout(self.geo_layouts, self.root, self.scene, self.name, self.area_root, col=self.col, geo_parent=self) + GeoChild.parent_transform = self.last_transform + GeoChild.stream = self.stream + GeoChild.parse_stream(self.geo_layouts.get(self.stream), self.stream, depth + 1) + # self.head = self.skip_children(self.cur_dat_stream, self.head) + self.children.append(GeoChild) + return self.continue_parse # Append to models array. Only check this one for now - def GEO_DISPLAY_LIST(self, args: list[str]): + def GEO_DISPLAY_LIST(self, macro: Macro, depth: int): # translation, rotation, layer, model - self.models.append(ModelDat(*self.ParentTransform, *args)) + self.models.append(ModelDat(*self.parent_transform, *macro.args)) - # shadows aren't naturally supported but we can emulate them with custom geo cmds - def GEO_SHADOW(self, args: list[str]): + # shadows aren't naturally supported but we can emulate them with custom geo cmds, note: possibly changed with fast64 updates, this is old code + def GEO_SHADOW(self, macro: Macro, depth: int): obj = self.MakeRt(self.name + "shadow empty", self.root) obj.sm64_obj_type = "Custom Geo Command" obj.customGeoCommand = "GEO_SHADOW" - obj.customGeoCommandArgs = ",".join(args) + obj.customGeoCommandArgs = ", ".join(macro.args) - def GEO_ANIMATED_PART(self, args: list[str]): + def GEO_ANIMATED_PART(self, macro: Macro, depth: int): # layer, translation, DL - layer = args[0] - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + layer = macro.args[0] + Tlate = [float(arg) / bpy.context.scene.blenderToSM64Scale for arg in macro.args[1:4]] Tlate = [Tlate[0], -Tlate[2], Tlate[1]] model = args[-1] - self.LastTransform = [Tlate, self.LastTransform[1]] + self.last_transform = [Tlate, self.last_transform[1]] if model.strip() != "NULL": self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) else: obj = self.MakeRt(self.name + "animated empty", self.root) obj.location = Tlate - def GEO_ROTATION_NODE(self, args: list[str]): - obj = self.GEO_ROTATE(args) + def GEO_ROTATION_NODE(self, macro: Macro, depth: int): + obj = self.GEO_ROTATE(macro) if obj: obj.sm64_obj_type = "Geo Rotation Node" - def GEO_ROTATE(self, args: list[str]): - layer = args[0] - Rotate = [math.radians(float(a)) for a in [args[1], args[2], args[3]]] + def GEO_ROTATE(self, macro: Macro, depth: int): + layer = macro.args[0] + Rotate = [math.radians(float(arg)) for arg in macro.args[1:4]] Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.LastTransform = [[0, 0, 0], Rotate] - self.LastTransform = [[0, 0, 0], self.LastTransform[1]] + self.last_transform = [[0, 0, 0], Rotate] + self.last_transform = [[0, 0, 0], self.last_transform[1]] obj = self.MakeRt(self.name + "rotate", self.root) obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate/Rotate" return obj - def GEO_ROTATION_NODE_WITH_DL(self, args: list[str]): - obj = self.GEO_ROTATE_WITH_DL(args) + def GEO_ROTATION_NODE_WITH_DL(self, macro: Macro, depth: int): + obj = self.GEO_ROTATE_WITH_DL(macro) if obj: obj.sm64_obj_type = "Geo Translate/Rotate" - def GEO_ROTATE_WITH_DL(self, args: list[str]): - layer = args[0] - Rotate = [math.radians(float(a)) for a in [args[1], args[2], args[3]]] + def GEO_ROTATE_WITH_DL(self, macro: Macro, depth: int): + layer = macro.args[0] + Rotate = [math.radians(float(arg)) for arg in macro.args[1:4]] Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.LastTransform = [[0, 0, 0], Rotate] + self.last_transform = [[0, 0, 0], Rotate] model = args[-1] - self.LastTransform = [[0, 0, 0], self.LastTransform[1]] + self.last_transform = [[0, 0, 0], self.last_transform[1]] if model.strip() != "NULL": self.models.append(ModelDat([0, 0, 0], Rotate, layer, model)) else: @@ -963,15 +916,15 @@ def GEO_ROTATE_WITH_DL(self, args: list[str]): obj.sm64_obj_type = "Geo Translate/Rotate" return obj - def GEO_TRANSLATE_ROTATE_WITH_DL(self, args: list[str]): - layer = args[0] - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + def GEO_TRANSLATE_ROTATE_WITH_DL(self, macro: Macro, depth: int): + layer = macro.args[0] + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in macro.args[1:4]] Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - Rotate = [math.radians(float(a)) for a in [args[4], args[5], args[6]]] + Rotate = [math.radians(float(arg)) for arg in macro.args[4:7]] Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.LastTransform = [Tlate, Rotate] + self.last_transform = [Tlate, Rotate] model = args[-1] - self.LastTransform = [Tlate, self.LastTransform[1]] + self.last_transform = [Tlate, self.last_transform[1]] if model.strip() != "NULL": self.models.append(ModelDat(Tlate, Rotate, layer, model)) else: @@ -980,29 +933,29 @@ def GEO_TRANSLATE_ROTATE_WITH_DL(self, args: list[str]): obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate/Rotate" - def GEO_TRANSLATE_ROTATE(self, args: list[str]): - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + def GEO_TRANSLATE_ROTATE(self, macro: Macro, depth: int): + Tlate = [float(arg) / bpy.context.scene.blenderToSM64Scale for arg in macro.args[1:4]] Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - Rotate = [math.radians(float(a)) for a in [args[4], args[5], args[6]]] + Rotate = [math.radians(float(arg)) for arg in macro.args[4:7]] Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.LastTransform = [Tlate, Rotate] + self.last_transform = [Tlate, Rotate] obj = self.MakeRt(self.name + "translate", self.root) obj.location = Tlate obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate/Rotate" - def GEO_TRANSLATE_WITH_DL(self, args: list[str]): - obj = self.GEO_TRANSLATE_NODE_WITH_DL(args) + def GEO_TRANSLATE_WITH_DL(self, macro: Macro, depth: int): + obj = self.GEO_TRANSLATE_NODE_WITH_DL(macro) if obj: obj.sm64_obj_type = "Geo Translate/Rotate" - def GEO_TRANSLATE_NODE_WITH_DL(self, args: list[str]): + def GEO_TRANSLATE_NODE_WITH_DL(self, macro: Macro, depth: int): # translation, layer, model - layer = args[0] - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + layer = macro.args[0] + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in macro.args[1:4]] Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - model = args[-1] - self.LastTransform = [Tlate, (0, 0, 0)] + model = macro.args[-1] + self.last_transform = [Tlate, (0, 0, 0)] if model.strip() != "NULL": self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) else: @@ -1012,71 +965,141 @@ def GEO_TRANSLATE_NODE_WITH_DL(self, args: list[str]): obj.sm64_obj_type = "Geo Translate Node" return obj - def GEO_TRANSLATE(self, args: list[str]): - obj = self.GEO_TRANSLATE_NODE(args) + def GEO_TRANSLATE(self, macro: Macro, depth: int): + obj = self.GEO_TRANSLATE_NODE(macro) if obj: obj.sm64_obj_type = "Geo Translate/Rotate" - def GEO_TRANSLATE_NODE(self, args: list[str]): - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in args[1:4]] + def GEO_TRANSLATE_NODE(self, macro: Macro, depth: int): + Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in macro.args[1:4]] Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - self.LastTransform = [Tlate, self.LastTransform[1]] + self.last_transform = [Tlate, self.last_transform[1]] obj = self.MakeRt(self.name + "translate", self.root) obj.location = Tlate obj.sm64_obj_type = "Geo Translate Node" return obj - def GEO_SCALE_WITH_DL(self, args: list[str]): - scale = eval(args[1].strip()) / 0x10000 - model = args[-1] - self.LastTransform = [(0, 0, 0), self.LastTransform[1]] + def GEO_SCALE_WITH_DL(self, macro: Macro, depth: int): + scale = eval(macro.args[1]) / 0x10000 + model = macro.args[-1] + self.last_transform = [(0, 0, 0), self.last_transform[1]] self.models.append(ModelDat((0, 0, 0), (0, 0, 0), layer, model, scale=scale)) - def GEO_SCALE(self, args: list[str]): + def GEO_SCALE(self, macro: Macro, depth: int): obj = self.MakeRt(self.name + "scale", self.root) - scale = eval(args[1].strip()) / 0x10000 + scale = eval(macro.args[1]) / 0x10000 obj.scale = (scale, scale, scale) obj.sm64_obj_type = "Geo Scale" - def GEO_ASM(self, args: list[str]): + def GEO_ASM(self, macro: Macro, depth: int): obj = self.MakeRt(self.name + "asm", self.root) asm = self.obj.fast64.sm64.geo_asm self.obj.sm64_obj_type = "Geo ASM" - asm.param = args[0].strip() - asm.func = args[1].strip() + asm.param = macro.args[0] + asm.func = macro.args[1] - def GEO_SWITCH_CASE(self, args: list[str]): + def GEO_SWITCH_CASE(self, macro: Macro, depth: int): obj = self.MakeRt(self.name + "switch", self.root) Switch = self.obj Switch.sm64_obj_type = "Switch" - Switch.switchParam = eval(args[0]) - Switch.switchFunc = args[1].strip() + Switch.switchParam = eval(macro.args[0]) + Switch.switchFunc = macro.args[1] # This has to be applied to meshes - def GEO_RENDER_RANGE(self, args: list[str]): - self.RenderRange = args + def GEO_RENDER_RANGE(self, macro: Macro, depth: int): + self.render_range = macro.args # can only apply type to area root - def GEO_CAMERA(self, args: list[str]): - self.Aroot.camOption = "Custom" - self.Aroot.camType = args[0] + def GEO_CAMERA(self, macro: Macro, depth: int): + self.area_root.camOption = "Custom" + self.area_root.camType = macro.args[0] + + # make better + def GEO_CAMERA_FRUSTUM_WITH_FUNC(self, macro: Macro, depth: int): + self.area_root.camOption = "Custom" + self.area_root.camType = macro.args[0] # Geo backgrounds is pointless because the only background possible is the one # loaded in the level script. This is the only override - def GEO_BACKGROUND_COLOR(self, args: list[str]): - self.Aroot.areaOverrideBG = True - color = eval(args[0]) + def GEO_BACKGROUND_COLOR(self, macro: Macro, depth: int): + self.area_root.areaOverrideBG = True + color = eval(macro.args[0]) A = color & 1 B = (color & 0x3E) > 1 G = (color & (0x3E << 5)) >> 6 R = (color & (0x3E << 10)) >> 11 - self.Aroot.areaBGColor = (R / 0x1F, G / 0x1F, B / 0x1F, A) + self.area_root.areaBGColor = (R / 0x1F, G / 0x1F, B / 0x1F, A) + + # these have no affect on the bpy + def GEO_BACKGROUND(self, macro: Macro, depth: int): + return self.continue_parse + def GEO_NODE_SCREEN_AREA(self, macro: Macro, depth: int): + return self.continue_parse + def GEO_ZBUFFER(self, macro: Macro, depth: int): + return self.continue_parse + def GEO_NODE_ORTHO(self, macro: Macro, depth: int): + return self.continue_parse + def GEO_RENDER_OBJ(self, macro: Macro, depth: int): + return self.continue_parse + + + +class GeoLayout(GraphNodes): + def __init__( + self, + geo_layouts: dict, + root: bpy.types.Object, + scene: bpy.types.Scene, + name, + area_root: bpy.types.Object, + col: bpy.types.Collection = None, + geo_parent: GeoLayout = None + ): + self.geo_layouts = geo_layouts + self.parent = root + self.models = [] + self.children = [] + self.scene = scene + self.render_range = None + self.area_root = area_root # for properties that can only be written to area + self.root = root + self.parent_transform = [[0, 0, 0], [0, 0, 0]] + self.last_transform = [[0, 0, 0], [0, 0, 0]] + self.name = name + self.obj = None # last object on this layer of the tree, will become parent of next child + if not col: + self.col = area_root.users_collection[0] + else: + self.col = col + super().__init__(parent=geo_parent) - def SkipChildren(self, GL: list[str], x: int): + def MakeRt(self, name: str, root: bpy.types.Object): + # make an empty node to act as the root of this geo layout + # use this to hold a transform, or an actual cmd, otherwise rt is passed + E = bpy.data.objects.new(name, None) + self.obj = E + self.col.objects.link(E) + parentObject(root, E) + return E + + def parse_level_geo(self, start: str, scene: bpy.types.Scene): + geo_layout = self.geo_layouts.get(start) + if not geo_layout: + raise Exception( + "Could not find geo layout {} from levels/{}/{}geo.c".format( + start, scene.LevelImp.Level, scene.LevelImp.Prefix + ) + ) + # This won't parse the geo layout perfectly. For now I'll just get models. This is mostly because fast64 + # isn't a bijection to geo layouts, the props are sort of handled all over the place + self.stream = start + self.parse_stream(geo_layout, start, 0) + + def skip_children(self, geo_layout: list[str], head: int): open = 0 opened = 0 - while x < len(GL): - l = GL[x] + while head < len(geo_layout): + l = geo_layout[head] if l.startswith("GEO_OPEN_NODE"): opened = 1 open += 1 @@ -1084,309 +1107,55 @@ def SkipChildren(self, GL: list[str], x: int): open -= 1 if open == 0 and opened: break - x += 1 - return x + head += 1 + return head - def StripArgs(self, cmd: str): - a = cmd.find("(") - return cmd[:a].strip(), cmd[a + 1 : -2].split(",") + # ------------------------------------------------------------------------ # Functions # ------------------------------------------------------------------------ - -# creates a new collection and links it to parent -def CreateCol(parent: bpy.types.Collection, name: str): - col = bpy.data.collections.new(name) - parent.children.link(col) - return col +# parse aggregate files, and search for sm64 specific fast64 export name schemes +def get_all_aggregates(aggregate: Path, filenames: Union[str, tuple[str]], root_path: Path) -> list[Path]: + with open(aggregate, "r", newline='') as aggregate: + caught_files = parse_aggregate_file(aggregate, filenames, root_path) + # catch fast64 includes + fast64 = parse_aggregate_file(aggregate, "leveldata.inc.c", root_path) + if fast64: + with open(fast64[0], "r", newline='') as fast64_dat: + caught_files.extend(parse_aggregate_file(fast64_dat, filenames, root_path)) + return caught_files -def RotateObj(deg: float, obj: bpy.types.Object, world: bool = 0): - deg = Euler((math.radians(-deg), 0, 0)) - deg = deg.to_quaternion().to_matrix().to_4x4() - if world: - obj.matrix_world = obj.matrix_world @ deg - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - bpy.ops.object.transform_apply(rotation=True) - else: - obj.matrix_basis = obj.matrix_basis @ deg - - -def EvalMacro(line: str): - scene = bpy.context.scene - if scene.LevelImp.Version in line: - return False - if scene.LevelImp.Target in line: - return False - return True - - -# given an aggregate file that imports many files, find files with the name of type -def ParseAggregat(dat: typing.TextIO, filename: str, root_path: Path): - dat.seek(0) # so it may be read multiple times - InlineReg = "/\*((?!\*/).)*\*/" # filter out inline comments - ldat = dat.readlines() - files = [] - # assume this follows naming convention - for l in ldat: - if filename in l: - comment = l.rfind("//") - # double slash terminates line basically - if comment: - l = l[:comment] - # remove inline comments from line - l = re.sub(InlineReg, "", l) - files.append(l.strip()) - # remove include and quotes inefficiently. Now files is a list of relative paths - files = [c.replace("#include ", "").replace('"', "").replace("'", "") for c in files] - # deal with duplicate pathing (such as /actors/actors etc.) - Extra = root_path.relative_to(Path(bpy.context.scene.decompPath)) - for e in Extra.parts: - files = [c.replace(e + "/", "") for c in files] - if files: - return [root_path / c for c in files] +# given a path, get a level object by parsing the script.c file +def parse_level_script(script_file: Path, scene: bpy.types.Scene, col: bpy.types.Collection = None): + Root = bpy.data.objects.new("Empty", None) + if not col: + scene.collection.objects.link(Root) else: - return [] - - -# get all the collision data from a certain path -def FindCollisions(aggregate: Path, lvl: Level, scene: bpy.types.Scene, root_path: Path): - aggregate = open(aggregate, "r") - cols = ParseAggregat(aggregate, "collision.inc.c", root_path) - # catch fast64 includes - fast64 = ParseAggregat(aggregate, "leveldata.inc.c", root_path) - if fast64: - f64dat = open(fast64[0], "r") - cols += ParseAggregat(f64dat, "collision.inc.c", root_path) - aggregate.close() - # search for the area terrain in each file - for k, v in lvl.Areas.items(): - terrain = v.terrain - found = 0 - for c in cols: - if os.path.isfile(c): - c = open(c, "r") - c = c.readlines() - for i, l in enumerate(c): - if terrain in l: - # Trim Collision to be just the lines that have the file - v.ColFile = c[i:] - break - else: - c = None - continue - break - else: - c = None - if not c: - raise Exception( - "Collision {} not found in levels/{}/{}leveldata.c".format( - terrain, scene.LevelImp.Level, scene.LevelImp.Prefix - ) - ) - Collisions = FormatDat(v.ColFile, "Collision", ["(", ")"]) - v.ColFile = Collisions[terrain] - return lvl - - -def WriteLevelCollision(lvl: Level, scene: bpy.types.Scene, cleanup: bool, col_name: str = None): - for k, v in lvl.Areas.items(): - if not col_name: - col = v.root.users_collection[0] - else: - col = CreateCol(v.root.users_collection[0], col_name) - # dat is a class that holds all the collision files data - dat = Collision(v.ColFile, scene.blenderToSM64Scale) - dat.GetCollision() - name = "SM64 {} Area {} Col".format(scene.LevelImp.Level, k) - obj = dat.WriteCollision(scene, name, v.root, col=col) - # final operators to clean stuff up - if cleanup: - obj.data.validate() - obj.data.update(calc_edges=True) - # shade smooth - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - bpy.ops.object.shade_smooth() - bpy.ops.object.mode_set(mode="EDIT") - bpy.ops.mesh.remove_doubles() - bpy.ops.object.mode_set(mode="OBJECT") - - -# get all the relevant data types cleaned up and organized for the f3d class -def FormatModel(gfx: sm64_F3d, model: list[str]): - # For each data type, make an attribute where it cleans the input of the model files - gfx.VB.update(FormatDat(model, "Vtx", ["{", "}"])) - gfx.Gfx.update(FormatDat(model, "Gfx", ["(", ")"])) - gfx.diff.update(FormatDat(model, "Light_t", [None, None])) - gfx.amb.update(FormatDat(model, "Ambient_t", [None, None])) - gfx.Lights.update(FormatDat(model, "Lights1", [None, None])) - # For textures, try u8, and s16 aswell - gfx.Textures.update(FormatDat(model, "Texture", [None, None])) - gfx.Textures.update(FormatDat(model, "u8", [None, None])) - gfx.Textures.update(FormatDat(model, "s16", [None, None])) - return gfx - - -# Search through a C file to find data of typeName[] and split it into a list -# of macros with all comments removed -def FormatDat(lines: list[str], typeName: str, Delims: list[str]): - # Get a dictionary made up with keys=level script names - # and values as an array of all the cmds inside. - Models = {} - InlineReg = "/\*((?!\*/).)*\*/" # filter out inline comments - regX = "\[[0-9a-fx]*\]" # array bounds, basically [] with any number in it - currScr = 0 # name of current arr of type typeName - skip = 0 # bool to skip during macros - for l in lines: - # remove line comment - comment = l.rfind("//") - if comment: - l = l[:comment] - # check for macro - if "#ifdef" in l: - skip = EvalMacro(l) - if "#elif" in l: - skip = EvalMacro(l) - if "#else" in l: - skip = 0 - continue - # Now Check for level script starts - match = re.search(regX, l, flags=re.IGNORECASE) - if typeName in l and match and not skip: - # get var name, get substring from typename to [] - var = l[l.find(typeName) + len(typeName) : match.span()[0]].strip() - Models[var] = "" - currScr = var - continue - if currScr and not skip: - # remove inline comments from line - l = re.sub(InlineReg, "", l) - # Check for end of Level Script array - if "};" in l: - currScr = 0 - # Add line to dict - else: - Models[currScr] += l - # Now remove newlines from each script, and then split macro ends - # This makes each member of the array a single macro - for script, v in Models.items(): - v = v.replace("\n", "") - arr = [] # arr of macros - buf = "" # buf to put currently processed macro in - x = 0 # cur position in str - stack = 0 # stack cnt of parenthesis - app = 0 # flag to append macro - while x < len(v): - char = v[x] - if char == Delims[0]: - stack += 1 - app = 1 - if char == Delims[1]: - stack -= 1 - if app == 1 and stack == 0: - app = 0 - buf += v[x : x + 2] # get the last parenthesis and comma - arr.append(buf.strip()) - x += 2 - buf = "" - continue - buf += char - x += 1 - # for when the delim characters are nothing - if buf: - arr.append(buf) - Models[script] = arr - return Models - - -# given a geo.c file and a path, return cleaned up geo layouts in a dict -def GetGeoLayouts(geo: typing.TextIO, root_path: Path): - layouts = ParseAggregat(geo, "geo.inc.c", root_path) - if not layouts: - return - # because of fast64, these can be recursively defined (though I expect only a depth of one) - for l in layouts: - geoR = open(l, "r") - layouts += ParseAggregat(geoR, "geo.inc.c", root_path) - GeoLayouts = {} # stores cleaned up geo layout lines - for l in layouts: - l = open(l, "r") - lines = l.readlines() - GeoLayouts.update(FormatDat(lines, "GeoLayout", ["(", ")"])) - return GeoLayouts - - -# Find DL references given a level geo file and a path to a level folder -def FindLvlModels(geo: typing.TextIO, lvl: Level, scene: bpy.types.Scene, root_path: Path, col_name: str = None): - GeoLayouts = GetGeoLayouts(geo, root_path) - for k, v in lvl.Areas.items(): - GL = v.geo - rt = v.root - if col_name: - col = CreateCol(v.root.users_collection[0], col_name) - else: - col = None - Geo = GeoLayout(GeoLayouts, rt, scene, "GeoRoot {} {}".format(scene.LevelImp.Level, k), rt, col=col) - Geo.ParseLevelGeosStart(GL, scene) - v.geo = Geo + col.objects.link(Root) + Root.name = f"Level Root {scene.LevelImp.Level}" + Root.sm64_obj_type = "Level Root" + # Now parse the script and get data about the level + # Store data in attribute of a level class then assign later and return class + with open(script_file, "r", newline='') as script_file: + lvl = Level(script_file, scene, Root) + entry = scene.LevelImp.Entry.format(scene.LevelImp.Level) + lvl.parse_level_script(entry, col=col) return lvl -# Parse an aggregate group file or level data file for geo layouts -def FindActModels( - geo: typing.TextIO, - Layout: str, - scene: bpy.types.Scene, - rt: bpy.types.Object, - root_path: Path, - col: bpy.types.Collection = None, -): - GeoLayouts = GetGeoLayouts(geo, root_path) - Geo = GeoLayout(GeoLayouts, rt, scene, "{}".format(Layout), rt, col=col) - Geo.ParseLevelGeosStart(Layout, scene) - return Geo - - -# Parse an aggregate group file or level data file for f3d data -def FindModelDat(aggregate: Path, scene: bpy.types.Scene, root_path: Path): - leveldat = open(aggregate, "r") - models = ParseAggregat(leveldat, "model.inc.c", root_path) - models += ParseAggregat(leveldat, "painting.inc.c", root_path) - # fast64 makes a leveldata.inc.c file and puts custom content there, I want to catch that as well - # this isn't the best way to do this, but I will be lazy here - fast64 = ParseAggregat(leveldat, "leveldata.inc.c", root_path) - if fast64: - f64dat = open(fast64[0], "r") - models += ParseAggregat(f64dat, "model.inc.c", root_path) - # leveldat.seek(0) # so it may be read multiple times - textures = ParseAggregat(leveldat, "texture.inc.c", root_path) # Only deal with textures that are actual .pngs - textures.extend(ParseAggregat(leveldat, "textureNew.inc.c", root_path)) # For RM2C support - # Get all modeldata in the level - Models = sm64_F3d(scene) - for m in models: - md = open(m, "r") - lines = md.readlines() - Models = FormatModel(Models, lines) - # Update file to have texture.inc.c textures, deal with included textures in the model.inc.c files aswell - for t in [*textures, *models]: - t = open(t, "r") - tex = t.readlines() - # For textures, try u8, and s16 aswell - Models.Textures.update(FormatDat(tex, "Texture", [None, None])) - Models.Textures.update(FormatDat(tex, "u8", [None, None])) - Models.Textures.update(FormatDat(tex, "s16", [None, None])) - t.close() - return Models +# write the objects from a level object +def write_level_objects(lvl: Level, col_name: str = None): + for area in lvl.areas.values(): + area.place_objects(col_name=col_name) # from a geo layout, create all the mesh's -def ReadGeoLayout( - geo: GeoLayout, scene: bpy.types.Scene, f3d_dat: sm64_F3d, root_path: Path, meshes: dict, cleanup: bool = True +def write_geo_to_bpy( + geo: GeoLayout, scene: bpy.types.Scene, f3d_dat: SM64_F3D, root_path: Path, meshes: dict, cleanup: bool = True ): if geo.models: rt = geo.root @@ -1412,7 +1181,7 @@ def ReadGeoLayout( obj.draw_layer_static = layer col.objects.link(obj) parentObject(rt, obj) - RotateObj(-90, obj) + rotate_object(-90, obj) scale = m.scale / scene.blenderToSM64Scale obj.scale = [scale, scale, scale] obj.location = m.translate @@ -1431,49 +1200,141 @@ def ReadGeoLayout( bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.remove_doubles() bpy.ops.object.mode_set(mode="OBJECT") - if not geo.Children: + if not geo.children: return - for g in geo.Children: - ReadGeoLayout(g, scene, f3d_dat, root_path, meshes, cleanup=cleanup) + for g in geo.children: + write_geo_to_bpy(g, scene, f3d_dat, root_path, meshes, cleanup=cleanup) # write the gfx for a level given the level data, and f3d data -def WriteLevelModel(lvl: Level, scene: bpy.types.Scene, root_path: Path, f3d_dat: sm64_F3d, cleanup: bool = True): - for k, v in lvl.Areas.items(): - # Parse the geolayout class I created earlier to look for models - meshes = {} # re use mesh data when the same DL is referenced (bbh is good example) - ReadGeoLayout(v.geo, scene, f3d_dat, root_path, meshes, cleanup=cleanup) +def write_level_to_bpy(lvl: Level, scene: bpy.types.Scene, root_path: Path, f3d_dat: SM64_F3D, cleanup: bool = True): + for area in lvl.areas.values(): + print(area, area.geo) + write_geo_to_bpy(area.geo, scene, f3d_dat, root_path, dict(), cleanup=cleanup) return lvl -# given a path, get a level object by parsing the script.c file -def ParseScript(script: Path, scene: bpy.types.Scene, col: bpy.types.Collection = None): - scr = open(script, "r") - Root = bpy.data.objects.new("Empty", None) - if not col: - scene.collection.objects.link(Root) - else: - col.objects.link(Root) - Root.name = "Level Root {}".format(scene.LevelImp.Level) - Root.sm64_obj_type = "Level Root" - # Now parse the script and get data about the level - # Store data in attribute of a level class then assign later and return class - scr = scr.readlines() - lvl = Level(scr, scene, Root) - entry = scene.LevelImp.Entry.format(scene.LevelImp.Level) - lvl.ParseScript(entry, col=col) - return lvl +# given a geo.c file and a path, return cleaned up geo layouts in a dict +def construct_geo_layouts_from_file(geo: TextIO, root_path: Path): + geo_layout_files = get_all_aggregates(geo, "geo.inc.c", root_path) + if not geo_layout_files: + return + # because of fast64, these can be recursively defined (though I expect only a depth of one) + for file in geo_layout_files: + geo_layout_files.extend(get_all_aggregates(file, "geo.inc.c", root_path)) + geo_layout_data = {} # stores cleaned up geo layout lines + for geo_file in geo_layout_files: + with open(geo_file, "r", newline='') as geo_file: + geo_layout_data.update(get_data_types_from_file(geo_file, {"GeoLayout": ["(", ")"]})) + return geo_layout_data -# write the objects from a level object -def WriteObjects(lvl: Level, col_name: str = None): - for area in lvl.Areas.values(): - area.PlaceObjects(col_name=col_name) +# get all the relevant data types cleaned up and organized for the f3d class +def construct_sm64_f3d_data_from_file(gfx: SM64_F3D, model_file: TextIO): + gfx_dat = get_data_types_from_file( + model_file, + { + "Vtx": ["{", "}"], + "Gfx": ["(", ")"], + "Light_t": [None, None], + "Ambient_t": [None, None], + "Lights1": [None, None], + }, + collated=True + ) + for key, value in gfx_dat.items(): + attr = getattr(gfx, key) + attr.update(value) + # For textures, try u8, and s16 aswell + gfx.Textures.update( + get_data_types_from_file( + model_file, + { + "Texture": [None, None], + "u8": [None, None], + "s16": [None, None], + }, + ) + ) + return gfx + + +# Parse an aggregate group file or level data file for f3d data +def construct_model_data_from_file(aggregate: Path, scene: bpy.types.Scene, root_path: Path): + model_files = get_all_aggregates( + aggregate, + ( + "model.inc.c", + "painting.inc.c", + ), + root_path, + ) + texture_files = get_all_aggregates( + aggregate, + ( + "texture.inc.c", + "textureNew.inc.c", + ), + root_path, + ) + # Get all modeldata in the level + sm64_f3d_data = SM64_F3D(scene) + for model_file in model_files: + model_file = open(model_file, "r", newline='') + construct_sm64_f3d_data_from_file(sm64_f3d_data, model_file) + # Update file to have texture.inc.c textures, deal with included textures in the model.inc.c files aswell + for texture_file in [*texture_files, *model_files]: + with open(texture_file, "r", newline='') as texture_file: + # For textures, try u8, and s16 aswell + sm64_f3d_data.Textures.update( + get_data_types_from_file( + texture_file, + { + "Texture": [None, None], + "u8": [None, None], + "s16": [None, None], + }, + ) + ) + return sm64_f3d_data + + +# Parse an aggregate group file or level data file for geo layouts +def find_actor_models_from_geo( + geo: TextIO, + Layout: str, + scene: bpy.types.Scene, + rt: bpy.types.Object, + root_path: Path, + col: bpy.types.Collection = None, +): + GeoLayouts = construct_geo_layouts_from_file(geo, root_path) + Geo = GeoLayout(GeoLayouts, rt, scene, "{}".format(Layout), rt, col=col) + Geo.parse_level_geo(Layout, scene) + return Geo + + +# Find DL references given a level geo file and a path to a level folder +def find_level_models_from_geo( + geo: TextIO, lvl: Level, scene: bpy.types.Scene, root_path: Path, col_name: str = None +): + GeoLayouts = construct_geo_layouts_from_file(geo, root_path) + for area_index, area in lvl.areas.items(): + if col_name: + col = create_collection(area.root.users_collection[0], col_name) + else: + col = None + Geo = GeoLayout( + GeoLayouts, area.root, scene, f"GeoRoot {scene.LevelImp.Level} {area_index}", area.root, col=col + ) + Geo.parse_level_geo(area.geo, scene) + area.geo = Geo + return lvl # import level graphics given geo.c file, and a level object -def ImportLvlVisual( - geo: typing.TextIO, +def import_level_graphics( + geo: TextIO, lvl: Level, scene: bpy.types.Scene, root_path: Path, @@ -1481,19 +1342,62 @@ def ImportLvlVisual( cleanup: bool = True, col_name: str = None, ): - lvl = FindLvlModels(geo, lvl, scene, root_path, col_name=col_name) - models = FindModelDat(aggregate, scene, root_path) + lvl = find_level_models_from_geo(geo, lvl, scene, root_path, col_name=col_name) + models = construct_model_data_from_file(aggregate, scene, root_path) + print(lvl.areas, aggregate) # just a try, in case you are importing from something other than base decomp repo (like RM2C output folder) try: models.GetGenericTextures(root_path) except: print("could not import genric textures, if this errors later from missing textures this may be why") - lvl = WriteLevelModel(lvl, scene, root_path, models, cleanup=cleanup) + lvl = write_level_to_bpy(lvl, scene, root_path, models, cleanup=cleanup) + return lvl + + +# get all the collision data from a certain path +def find_collision_data_from_path(aggregate: Path, lvl: Level, scene: bpy.types.Scene, root_path: Path): + collision_files = get_all_aggregates(aggregate, "collision.inc.c", root_path) + col_data = dict() + for col_file in collision_files: + if not os.path.isfile(col_file): + continue + with open(col_file, "r", newline='') as col_file: + col_data.update(get_data_types_from_file(col_file, {"Collision": ["(", ")"]})) + # search for the area terrain from available collision data + for area in lvl.areas.values(): + area.ColFile = col_data.get(area.terrain, None) + if not area.ColFile: + raise Exception( + f"Collision {area.terrain} not found in levels/{scene.LevelImp.Level}/{scene.LevelImp.Prefix}leveldata.c" + ) return lvl +def write_level_collision_to_bpy(lvl: Level, scene: bpy.types.Scene, cleanup: bool, col_name: str = None): + for area_index, area in lvl.areas.items(): + if not col_name: + col = area.root.users_collection[0] + else: + col = create_collection(area.root.users_collection[0], col_name) + col_parser = Collision(area.ColFile, scene.blenderToSM64Scale) + col_parser.GetCollision() + name = "SM64 {} Area {} Col".format(scene.LevelImp.Level, area_index) + obj = col_parser.WriteCollision(scene, name, area.root, col=col) + # final operators to clean stuff up + if cleanup: + obj.data.validate() + obj.data.update(calc_edges=True) + # shade smooth + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.shade_smooth() + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.remove_doubles() + bpy.ops.object.mode_set(mode="OBJECT") + + # import level collision given a level script -def ImportLvlCollision( +def import_level_collision( aggregate: Path, lvl: Level, scene: bpy.types.Scene, @@ -1501,8 +1405,10 @@ def ImportLvlCollision( cleanup: bool, col_name: str = None, ): - lvl = FindCollisions(aggregate, lvl, scene, root_path) # Now Each area has its collision file nicely formatted - WriteLevelCollision(lvl, scene, cleanup, col_name=col_name) + lvl = find_collision_data_from_path( + aggregate, lvl, scene, root_path + ) # Now Each area has its collision file nicely formatted + write_level_collision_to_bpy(lvl, scene, cleanup, col_name=col_name) return lvl @@ -1522,7 +1428,7 @@ def execute(self, context): scene = context.scene rt_col = context.collection scene.gameEditorMode = "SM64" - path = Path(scene.decompPath) + path = Path(bpy.path.abspath(scene.decompPath)) folder = path / scene.ActImp.FolderType Layout = scene.ActImp.GeoLayout prefix = scene.ActImp.Prefix @@ -1533,22 +1439,22 @@ def execute(self, context): else: geo = folder / (prefix + "geo.c") leveldat = folder / (prefix + "leveldata.c") - geo = open(geo, "r") + geo = open(geo, "r", newline='') Root = bpy.data.objects.new("Empty", None) Root.name = "Actor %s" % scene.ActImp.GeoLayout rt_col.objects.link(Root) - Geo = FindActModels( + Geo = find_actor_models_from_geo( geo, Layout, scene, Root, folder, col=rt_col ) # return geo layout class and write the geo layout - models = FindModelDat(leveldat, scene, folder) + models = construct_model_data_from_file(leveldat, scene, folder) # just a try, in case you are importing from not the base decomp repo try: models.GetGenericTextures(path) except: print("could not import genric textures, if this errors later from missing textures this may be why") meshes = {} # re use mesh data when the same DL is referenced (bbh is good example) - ReadGeoLayout(Geo, scene, models, folder, meshes, cleanup=self.cleanup) + write_geo_to_bpy(Geo, scene, models, folder, meshes, cleanup=self.cleanup) return {"FINISHED"} @@ -1571,16 +1477,15 @@ def execute(self, context): scene.gameEditorMode = "SM64" prefix = scene.LevelImp.Prefix - path = Path(scene.decompPath) + path = Path(bpy.path.abspath(scene.decompPath)) level = path / "levels" / scene.LevelImp.Level script = level / (prefix + "script.c") geo = level / (prefix + "geo.c") leveldat = level / (prefix + "leveldata.c") - geo = open(geo, "r") - lvl = ParseScript(script, scene, col=col) # returns level class - WriteObjects(lvl, col_name=obj_col) - lvl = ImportLvlCollision(leveldat, lvl, scene, path, self.cleanup, col_name=col_col) - lvl = ImportLvlVisual(geo, lvl, scene, path, leveldat, cleanup=self.cleanup, col_name=gfx_col) + lvl = parse_level_script(script, scene, col=col) # returns level class + write_level_objects(lvl, col_name=obj_col) + lvl = import_level_collision(leveldat, lvl, scene, path, self.cleanup, col_name=col_col) + lvl = import_level_graphics(geo, lvl, scene, path, leveldat, cleanup=self.cleanup, col_name=gfx_col) return {"FINISHED"} @@ -1601,14 +1506,13 @@ def execute(self, context): scene.gameEditorMode = "SM64" prefix = scene.LevelImp.Prefix - path = Path(scene.decompPath) + path = Path(bpy.path.abspath(scene.decompPath)) level = path / "levels" / scene.LevelImp.Level script = level / (prefix + "script.c") geo = level / (prefix + "geo.c") model = level / (prefix + "leveldata.c") - geo = open(geo, "r") - lvl = ParseScript(script, scene, col=col) # returns level class - lvl = ImportLvlVisual(geo, lvl, scene, path, model, cleanup=self.cleanup, col_name=gfx_col) + lvl = parse_level_script(script, scene, col=col) # returns level class + lvl = import_level_graphics(geo, lvl, scene, path, model, cleanup=self.cleanup, col_name=gfx_col) return {"FINISHED"} @@ -1629,14 +1533,12 @@ def execute(self, context): scene.gameEditorMode = "SM64" prefix = scene.LevelImp.Prefix - path = Path(scene.decompPath) + path = Path(bpy.path.abspath(scene.decompPath)) level = path / "levels" / scene.LevelImp.Level script = level / (prefix + "script.c") - geo = level / (prefix + "geo.c") model = level / (prefix + "leveldata.c") - geo = open(geo, "r") - lvl = ParseScript(script, scene, col=col) # returns level class - lvl = ImportLvlCollision(model, lvl, scene, path, self.cleanup, col_name=col_col) + lvl = parse_level_script(script, scene, col=col) # returns level class + lvl = import_level_collision(model, lvl, scene, path, self.cleanup, col_name=col_col) return {"FINISHED"} @@ -1655,11 +1557,11 @@ def execute(self, context): scene.gameEditorMode = "SM64" prefix = scene.LevelImp.Prefix - path = Path(scene.decompPath) + path = Path(bpy.path.abspath(scene.decompPath)) level = path / "levels" / scene.LevelImp.Level script = level / (prefix + "script.c") - lvl = ParseScript(script, scene, col=col) # returns level class - WriteObjects(lvl, col_name=obj_col) + lvl = parse_level_script(script, scene, col=col) # returns level class + write_level_objects(lvl, col_name=obj_col) return {"FINISHED"} @@ -1686,12 +1588,7 @@ class ActorImport(PropertyGroup): Version: EnumProperty( name="Version", description="Version of the game for any ifdef macros", - items=[ - ("VERSION_US", "VERSION_US", ""), - ("VERSION_JP", "VERSION_JP", ""), - ("VERSION_EU", "VERSION_EU", ""), - ("VERSION_SH", "VERSION_SH", ""), - ], + items=enumVersionDefs, ) Target: StringProperty( name="Target", description="The platform target for any #ifdefs in code", default="TARGET_N64" @@ -1702,39 +1599,7 @@ class LevelImport(PropertyGroup): Level: EnumProperty( name="Level", description="Choose a level", - items=[ - ("bbh", "bbh", ""), - ("ccm", "ccm", ""), - ("hmc", "hmc", ""), - ("ssl", "ssl", ""), - ("bob", "bob", ""), - ("sl", "sl", ""), - ("wdw", "wdw", ""), - ("jrb", "jrb", ""), - ("thi", "thi", ""), - ("ttc", "ttc", ""), - ("rr", "rr", ""), - ("castle_grounds", "castle_grounds", ""), - ("castle_inside", "castle_inside", ""), - ("bitdw", "bitdw", ""), - ("vcutm", "vcutm", ""), - ("bitfs", "bitfs", ""), - ("sa", "sa", ""), - ("bits", "bits", ""), - ("lll", "lll", ""), - ("ddd", "ddd", ""), - ("wf", "wf", ""), - ("ending", "ending", ""), - ("castle_courtyard", "castle_courtyard", ""), - ("pss", "pss", ""), - ("cotmc", "cotmc", ""), - ("totwc", "totwc", ""), - ("bowser_1", "bowser_1", ""), - ("wmotr", "wmotr", ""), - ("bowser_2", "bowser_2", ""), - ("bowser_3", "bowser_3", ""), - ("ttm", "ttm", ""), - ], + items=enumLevelNames, ) Prefix: StringProperty( name="Prefix", @@ -1747,12 +1612,7 @@ class LevelImport(PropertyGroup): Version: EnumProperty( name="Version", description="Version of the game for any ifdef macros", - items=[ - ("VERSION_US", "VERSION_US", ""), - ("VERSION_JP", "VERSION_JP", ""), - ("VERSION_EU", "VERSION_EU", ""), - ("VERSION_SH", "VERSION_SH", ""), - ], + items=enumVersionDefs, ) Target: StringProperty( name="Target", description="The platform target for any #ifdefs in code", default="TARGET_N64" diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index da8f4047a..67c5ca680 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -210,6 +210,12 @@ def getGroupNameFromIndex(obj, index): return group.name return None +# creates a new collection and links it to parent +def create_collection(parent: bpy.types.Collection, name: str): + col = bpy.data.collections.new(name) + parent.children.link(col) + return col + def copyPropertyCollection(oldProp, newProp): newProp.clear() @@ -1149,6 +1155,18 @@ def doRotation(angle, axis): direction = getDirectionGivenAppVersion() bpy.ops.transform.rotate(value=direction * angle, orient_axis=axis, orient_type="GLOBAL") +# consider checking redundancy of this with above functions? +def rotate_object(deg: float, obj: bpy.types.Object, world: bool = 0): + deg = Euler((math.radians(-deg), 0, 0)) + deg = deg.to_quaternion().to_matrix().to_4x4() + if world: + obj.matrix_world = obj.matrix_world @ deg + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(rotation=True) + else: + obj.matrix_basis = obj.matrix_basis @ deg + def getAddressFromRAMAddress(RAMAddress): addr = RAMAddress - 0x80000000 diff --git a/fast64_internal/utility_importer.py b/fast64_internal/utility_importer.py new file mode 100644 index 000000000..dc0fef4a7 --- /dev/null +++ b/fast64_internal/utility_importer.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import re +import bpy +from functools import partial +from dataclasses import dataclass +from pathlib import Path +from typing import TextIO, Any, Sequence, Union + + +@dataclass +class Macro: + cmd: str + args: list[str] + + # strip each arg + def __post_init__(self): + self.args = [arg.strip() for arg in self.args] + self.cmd = self.cmd.strip() + +@dataclass +class Parser: + cur_stream: Sequence[Any] + head: int = -1 + + def stream(self): + while self.head < len(self.cur_stream) - 1: + self.head += 1 + yield self.cur_stream[self.head] + +# basic methods and utility to parse scripts or data streams of bytecode +class DataParser: + + # parsing flow status codes + continue_parse = 1 + break_parse = 2 + + def __init__(self, parent: DataParser = None): + # for forward referencing scripts, keep track of the stream + if parent: + self.parsed_streams = parent.parsed_streams + else: + self.parsed_streams = dict() + + def parse_stream(self, dat_stream: Sequence[Any], entry_id: Any, *args, **kwargs): + parser = self.parsed_streams.get(entry_id) + if not parser: + self.parsed_streams[entry_id] = (parser := Parser(dat_stream)) + for line in parser.stream(): + cur_macro = self.c_macro_split(line) + func = getattr(self, cur_macro.cmd, None) + if not func: + raise Exception(f"Macro {cur_macro} not found in parser function") + else: + flow_status = func(cur_macro, *args, **kwargs) + if flow_status == self.break_parse: + return + + def get_parser(self, entry_id: Any, relative_offset: int = 0): + parser = self.parsed_streams[entry_id] + parser.head += relative_offset + return parser.cur_stream + + + def c_macro_split(self, macro: str) -> list[str]: + args_start = macro.find("(") + return Macro(macro[:args_start], macro[args_start + 1 : macro.rfind(")")].split(",")) + + +def evaluate_macro(line: str): + scene = bpy.context.scene + if scene.LevelImp.Version in line: + return False + if scene.LevelImp.Target in line: + return False + return True + + +# gets rid of comments, whitespace and macros in a file +def pre_parse_file(file: TextIO) -> list[str]: + multi_line_comment_regx = "/\*[^*]*\*+(?:[^/*][^*]*\*+)*/" + file = re.sub(multi_line_comment_regx, "", file.read()) + skip_macro = 0 # bool to skip during macros + output_lines = [] + for line in file.splitlines(): + # remove line comment + if (comment := line.rfind("//")) > 0: + line = line[: comment] + # check for macro + if "#ifdef" in line: + skip_macro = evaluate_macro(line) + if "#elif" in line: + skip_macro = evaluate_macro(line) + if "#else" in line or "#endif" in line: + skip_macro = 0 + continue + if not skip_macro and line: + output_lines.append(line) + return output_lines + + +# given an aggregate file that imports many files, find files with the name of type +def parse_aggregate_file(dat: TextIO, filenames: Union[str, tuple[str]], root_path: Path) -> list[Path]: + dat.seek(0) # so it may be read multiple times + # make it an iterator even if it isn't on call + if isinstance(filenames, str): + filenames = (filenames,) + file_lines = pre_parse_file(dat) + # remove include and quotes inefficiently from lines with the filenames we are searching for + file_lines = [ + c.replace("#include ", "").replace('"', "").replace("'", "") + for c in file_lines + if any(filename in c for filename in filenames) + ] + # deal with duplicate pathing (such as /actors/actors etc.) + Extra = root_path.relative_to(Path(bpy.path.abspath(bpy.context.scene.decompPath))) + for e in Extra.parts: + files = [c.replace(e + "/", "") for c in file_lines] + if file_lines: + return [root_path / c for c in file_lines] + else: + return [] + + +# Search through a C file to find data of typeName[] and split it into a list +# of data types with all comments removed +def get_data_types_from_file(file: TextIO, type_dict, collated=False): + # from a raw file, create a dict of types. Types should all be arrays + file_lines = pre_parse_file(file) + array_bounds_regx = "\[[0-9a-fx]*\]" # basically [] with any valid number in it + output_variables = {type_name: dict() for type_name in type_dict.keys()} + type_found = None + var_dat_buffer = [] + for line in file_lines: + if type_found: + # Check for end of array + if "};" in line: + output_variables[type_found[0]][type_found[1]] = "".join(var_dat_buffer) + type_found = None + var_dat_buffer = [] + else: + var_dat_buffer.append(line) + continue + match = re.search(array_bounds_regx, line, flags=re.IGNORECASE) + type_collisions = [type_name for type_name in type_dict.keys() if type_name in line] + if match and type_collisions: + # there should ideally only be one collision + type_name = type_collisions[0] + variable_name = line[line.find(type_name) + len(type_name) : match.span()[0]].strip() + type_found = (type_name, variable_name) + # Now remove newlines from each line, and then split macro ends + # This makes each member of the array a single macro or array + for data_type, delimiters in type_dict.items(): + for variable, data in output_variables[data_type].items(): + output_variables[data_type][variable] = format_data_arr(data, delimiters) + + # if collated, organize by data type, otherwise just take the various dicts raw + return output_variables if collated else {vd_key:vd_value for var_dict in output_variables.values() for vd_key, vd_value in var_dict.items() } + + +# takes a raw string representing data and then formats it into an array +def format_data_arr(raw_data: str, delimiters: tuple[str]): + raw_data = raw_data.replace("\n", "") + arr = [] # arr of data in format + buf = "" # buf to put currently processed data in + pos = 0 # cur position in str + stack = 0 # stack cnt of parenthesis + app = 0 # flag to append data + while pos < len(raw_data): + char = raw_data[pos] + if char == delimiters[0]: + stack += 1 + app = 1 + if char == delimiters[1]: + stack -= 1 + if app == 1 and stack == 0: + app = 0 + buf += raw_data[pos : pos + 2] # get the last parenthesis and comma + arr.append(buf.strip()) + pos += 2 + buf = "" + continue + buf += char + pos += 1 + # for when the delim characters are nothing + if buf: + arr.append(buf) + return arr From 3d9a53b5688e12f757295e2b68b75ca7f218d63c Mon Sep 17 00:00:00 2001 From: scut Date: Fri, 30 Jun 2023 16:43:16 -0400 Subject: [PATCH 04/11] made code cleaner and added type hints --- fast64_internal/f3d/f3d_import.py | 342 ++++++++++---------- fast64_internal/sm64/sm64_level_importer.py | 334 ++++++++----------- 2 files changed, 292 insertions(+), 384 deletions(-) diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py index b043375ab..509b0d762 100644 --- a/fast64_internal/f3d/f3d_import.py +++ b/fast64_internal/f3d/f3d_import.py @@ -13,6 +13,14 @@ from dataclasses import dataclass from copy import deepcopy from re import findall +from numbers import Number +from collections.abc import Sequence + +from ..f3d.f3d_material import ( + F3DMaterialProperty, + RDPSettings, + TextureProperty +) from ..utility import hexOrDecInt from ..utility_importer import * @@ -64,10 +72,9 @@ def __init__(self): self.tiles = [Tile() for a in range(8)] self.tex0 = None self.tex1 = None - self.tx_scr = None # calc the hash for an f3d mat and see if its equal to this mats hash - def MatHashF3d(self, f3d): + def mat_hash_f3d(self, f3d: F3DMaterialProperty): # texture,1 cycle combiner, render mode, geo modes, some other blender settings, tile size (very important in kirby64) rdp = f3d.rdp_settings if f3d.tex0.tex: @@ -137,13 +144,13 @@ def EvalGeo(self, mode): return dupe return False - def MatHash(self, mat): + def mat_hash(self, mat: bpy.types.Material): return False - def ConvertColor(self, color): + def convert_color(self, color: Sequence[Number]): return [int(a) / 255 for a in color] - def LoadTexture(self, ForceNewTex, path, tex): + def load_texture(self, ForceNewTex: bool, path: Path, tex: Texture): png = path / f"bank_{tex.Timg[0]}" / f"{tex.Timg[1]}" png = (*png.glob("*.png"),) if png: @@ -153,28 +160,43 @@ def LoadTexture(self, ForceNewTex, path, tex): else: return i - def ApplyPBSDFMat(self, mat): + def apply_PBSDF_Mat(self, mat: bpy.types.Material, tex_path: Path, tex: Texture): nt = mat.node_tree nodes = nt.nodes links = nt.links pbsdf = nodes.get("Principled BSDF") if not pbsdf: return - tex = nodes.new("ShaderNodeTexImage") - links.new(pbsdf.inputs[0], tex.outputs[0]) # base color - i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, tex0) - if i: - tex.image = i - if int(layer) > 4: - mat.blend_method == "BLEND" - - def ApplyMatSettings(self, mat, tex_path): - # if bpy.context.scene.LevelImp.AsObj: - # return self.ApplyPBSDFMat(mat, textures, path, layer) + tex_node = nodes.new("ShaderNodeTexImage") + links.new(pbsdf.inputs[0], tex_node.outputs[0]) # base color + image = self.LoadTexture(0, tex_path, tex) + if image: + tex_node.image = image - f3d = mat.f3d_mat # This is kure's custom property class for materials + def apply_material_settings(self, mat: bpy.types.Material, tex_path: Path): + f3d = mat.f3d_mat - # set color registers if they exist + self.set_register_settings(mat, f3d) + self.set_textures(f3d) + + with bpy.context.temp_override(material=mat): + bpy.ops.material.update_f3d_nodes() + + def set_register_settings(self, mat: bpy.types.Material, f3d: F3DMaterialProperty): + self.set_fog(f3d) + self.set_color_registers(f3d) + self.set_geo_mode(f3d.rdp_settings, mat) + self.set_combiner(f3d) + self.set_render_mode(f3d) + + def set_textures(self, f3d: F3DMaterialProperty, tex_path: Path): + self.set_tex_scale(f3d) + if self.tex0 and self.set_tex: + self.set_tex_settings(f3d.tex0, self.load_texture(0, tex_path, self.tex0), self.tiles[0], self.tex0.Timg) + if self.tex1 and self.set_tex: + self.set_tex_settings(f3d.tex1, self.load_texture(0, tex_path, self.tex1), self.tiles[1], self.tex1.Timg) + + def set_fog(self, f3d: F3DMaterialProperty): if hasattr(self, "fog_position"): f3d.set_fog = True f3d.use_global_fog = False @@ -183,144 +205,72 @@ def ApplyMatSettings(self, mat, tex_path): if hasattr(self, "fog_color"): f3d.set_fog = True f3d.use_global_fog = False - f3d.fog_color = self.ConvertColor(self.fog_color) + f3d.fog_color = self.convert_color(self.fog_color) + + def set_color_registers(self, f3d: F3DMaterialProperty): if hasattr(self, "light_col"): # this is a dict but I'll only use the first color for now f3d.set_lights = True if self.light_col.get(1): - f3d.default_light_color = self.ConvertColor(eval(self.light_col[1]).to_bytes(4, "big")) + f3d.default_light_color = self.convert_color(eval(self.light_col[1]).to_bytes(4, "big")) if hasattr(self, "env_color"): f3d.set_env = True - f3d.env_color = self.ConvertColor(self.env_color[-4:]) + f3d.env_color = self.convert_color(self.env_color[-4:]) if hasattr(self, "prim_color"): prim = self.prim_color f3d.set_prim = True f3d.prim_lod_min = int(prim[0]) f3d.prim_lod_frac = int(prim[1]) - f3d.prim_color = self.ConvertColor(prim[-4:]) - # I set these but they aren't properly stored because they're reset by fast64 or something - # its better to have defaults than random 2 cycles - self.SetGeoMode(f3d.rdp_settings, mat) + f3d.prim_color = self.convert_color(prim[-4:]) - if self.TwoCycle: - f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_2CYCLE" - else: - f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE" - - # make combiner custom - f3d.presetName = "Custom" - self.SetCombiner(f3d) - # add tex scroll objects - if self.tx_scr: - scr = self.tx_scr - mat_scr = mat.KCS_tx_scroll - if hasattr(scr, "textures"): - [mat_scr.AddTex(t) for t in scr.textures] - if hasattr(scr, "palettes"): - [mat_scr.AddPal(t) for t in scr.palettes] - # deal with custom render modes - if hasattr(self, "RenderMode"): - self.SetRenderMode(f3d) - # g texture handle + def set_tex_scale(self, f3d: F3DMaterialProperty): if hasattr(self, "set_tex"): # not exactly the same but gets the point across maybe? f3d.tex0.tex_set = self.set_tex f3d.tex1.tex_set = self.set_tex # tex scale gets set to 0 when textures are disabled which is automatically done # often to save processing power between mats or something, or just adhoc bhv + # though in fast64, we don't want to ever set it to zero if f3d.rdp_settings.g_tex_gen or any([a < 1 and a > 0 for a in self.tex_scale]): f3d.scale_autoprop = False f3d.tex_scale = self.tex_scale - print(self.tex_scale) - if not self.set_tex: - # Update node values - override = bpy.context.copy() - override["material"] = mat - bpy.ops.material.update_f3d_nodes(override) - del override - return - # texture 0 then texture 1 - if self.tex0: - i = self.LoadTexture(0, tex_path, self.tex0) - tex0 = f3d.tex0 - tex0.tex_reference = str(self.tex0.Timg) # setting prop for hash purposes - tex0.tex_set = True - tex0.tex = i - tex0.tex_format = self.EvalFmt(self.tiles[0]) - tex0.autoprop = False - Sflags = self.EvalFlags(self.tiles[0].Sflags) - for f in Sflags: - setattr(tex0.S, f, True) - Tflags = self.EvalFlags(self.tiles[0].Tflags) - for f in Sflags: - setattr(tex0.T, f, True) - tex0.S.low = self.tiles[0].Slow - tex0.T.low = self.tiles[0].Tlow - tex0.S.high = self.tiles[0].Shigh - tex0.T.high = self.tiles[0].Thigh - - tex0.S.mask = self.tiles[0].SMask - tex0.T.mask = self.tiles[0].TMask - if self.tex1: - i = self.LoadTexture(0, tex_path, self.tex1) - tex1 = f3d.tex1 - tex1.tex_reference = str(self.tex1.Timg) # setting prop for hash purposes - tex1.tex_set = True - tex1.tex = i - tex1.tex_format = self.EvalFmt(self.tiles[1]) - Sflags = self.EvalFlags(self.tiles[1].Sflags) - for f in Sflags: - setattr(tex1.S, f, True) - Tflags = self.EvalFlags(self.tiles[1].Tflags) - for f in Sflags: - setattr(tex1.T, f, True) - tex1.S.low = self.tiles[1].Slow - tex1.T.low = self.tiles[1].Tlow - tex1.S.high = self.tiles[1].Shigh - tex1.T.high = self.tiles[1].Thigh - - tex1.S.mask = self.tiles[0].SMask - tex1.T.mask = self.tiles[0].TMask - # Update node values - override = bpy.context.copy() - override["material"] = mat - bpy.ops.material.update_f3d_nodes(override) - del override - - def EvalFlags(self, flags): - if not flags: - return [] - GBIflags = { - "G_TX_NOMIRROR": None, - "G_TX_WRAP": None, - "G_TX_MIRROR": ("mirror"), - "G_TX_CLAMP": ("clamp"), - "0": None, - "1": ("mirror"), - "2": ("clamp"), - "3": ("clamp", "mirror"), - } - x = [] - fsplit = flags.split("|") - for f in fsplit: - z = GBIflags.get(f.strip(), 0) - if z: - x.append(z) - return x - # only work with macros I can recognize for now - def SetRenderMode(self, f3d): + def set_tex_settings(self, tex_prop: TextureProperty, image: bpy.types.Image, tile: Tile, tex_img: Union[Sequence, str]): + tex_prop.tex_reference = str(tex_img) # setting prop for hash purposes + tex_prop.tex_set = True + tex_prop.tex = image + tex_prop.tex_format = self.eval_texture_format(tile) + Sflags = self.eval_tile_flags(tile.Sflags) + for f in Sflags: + setattr(tex_prop.S, f, True) + Tflags = self.eval_tile_flags(tile.Tflags) + for f in Sflags: + setattr(tex_prop.T, f, True) + tex_prop.S.low = tile.Slow + tex_prop.T.low = tile.Tlow + tex_prop.S.high = tile.Shigh + tex_prop.T.high = tile.Thigh + tex_prop.S.mask = tile.SMask + tex_prop.T.mask = tile.TMask + + # rework with combinatoric render mode draft PR + def set_render_mode(self, f3d: F3DMaterialProperty): rdp = f3d.rdp_settings - rdp.set_rendermode = True - # if the enum isn't there, then just print an error for now - try: - rdp.rendermode_preset_cycle_1 = self.RenderMode[0] - rdp.rendermode_preset_cycle_2 = self.RenderMode[1] - # print(f"set render modes with render mode {self.RenderMode}") - except: - print(f"could not set render modes with render mode {self.RenderMode}") - - def SetGeoMode(self, rdp, mat): + if self.TwoCycle: + rdp.g_mdsft_cycletype = "G_CYC_2CYCLE" + else: + rdp.g_mdsft_cycletype = "G_CYC_1CYCLE" + if hasattr(self, "RenderMode"): + rdp.set_rendermode = True + # if the enum isn't there, then just print an error for now + try: + rdp.rendermode_preset_cycle_1 = self.RenderMode[0] + rdp.rendermode_preset_cycle_2 = self.RenderMode[1] + # print(f"set render modes with render mode {self.RenderMode}") + except: + print(f"could not set render modes with render mode {self.RenderMode}") + + def set_geo_mode(self, rdp: RDPSettings, mat: bpy.types.Material): # texture gen has a different name than gbi for a in self.GeoSet: setattr(rdp, a.replace("G_TEXTURE_GEN", "G_TEX_GEN").lower().strip(), True) @@ -328,7 +278,8 @@ def SetGeoMode(self, rdp, mat): setattr(rdp, a.replace("G_TEXTURE_GEN", "G_TEX_GEN").lower().strip(), False) # Very lazy for now - def SetCombiner(self, f3d): + def set_combiner(self, f3d: F3DMaterialProperty): + f3d.presetName = "Custom" if not hasattr(self, "Combiner"): f3d.combiner1.A = "TEXEL0" f3d.combiner1.A_alpha = "0" @@ -354,7 +305,28 @@ def SetCombiner(self, f3d): f3d.combiner2.C_alpha = self.Combiner[14] f3d.combiner2.D_alpha = self.Combiner[15] - def EvalFmt(self, tex): + def eval_tile_flags(self, flags: str): + if not flags: + return [] + GBIflags = { + "G_TX_NOMIRROR": None, + "G_TX_WRAP": None, + "G_TX_MIRROR": ("mirror"), + "G_TX_CLAMP": ("clamp"), + "0": None, + "1": ("mirror"), + "2": ("clamp"), + "3": ("clamp", "mirror"), + } + x = [] + fsplit = flags.split("|") + for f in fsplit: + z = GBIflags.get(f.strip(), 0) + if z: + x.append(z) + return x + + def eval_texture_format(self, tex: Texture): GBIfmts = { "G_IM_FMT_RGBA": "RGBA", "RGBA": "RGBA", @@ -403,7 +375,7 @@ def __init__(self, lastmat=None): def gsSPEndDisplayList(self, macro: Macro): return self.break_parse - + def gsSPBranchList(self, macro: Macro): NewDL = self.Gfx.get(branched_dl := macro.args[0]) if not NewDL: @@ -414,7 +386,7 @@ def gsSPBranchList(self, macro: Macro): ) self.parse_stream(NewDL, branched_dl) return self.break_parse - + def gsSPDisplayList(self, macro: Macro): NewDL = self.Gfx.get(branched_dl := macro.args[0]) if not NewDL: @@ -425,12 +397,12 @@ def gsSPDisplayList(self, macro: Macro): ) self.parse_stream(NewDL, branched_dl) return self.continue_parse - + def gsSPEndDisplayList(self, macro: Macro): return self.break_parse - + def gsSPVertex(self, macro: Macro): - # vertex references commonly use pointer arithmatic. I will deal with that case here, but not for other things unless it somehow becomes a problem later + # vertex references commonly use pointer arithmatic. I will deal with that case here, but not for other things unless it somehow becomes a problem later if "+" in macro.args[0]: ref, offset = macro.args[0].split("+") offset = hexOrDecInt(offset) @@ -449,7 +421,7 @@ def gsSPVertex(self, macro: Macro): Verts = VB[ offset : offset + vertex_load_length ] # If you use array indexing here then you deserve to have this not work - Verts = [self.ParseVert(v) for v in Verts] + Verts = [self.parse_vert(v) for v in Verts] for k, i in enumerate(range(vertex_load_start, vertex_load_length, 1)): self.VertBuff[i] = [Verts[k], vertex_load_start] # These are all independent data blocks in blender @@ -458,20 +430,20 @@ def gsSPVertex(self, macro: Macro): self.VCs.extend([v[2] for v in Verts]) self.LastLoad = vertex_load_length return self.continue_parse - + def gsSP2Triangles(self, macro: Macro): - self.MakeNewMat() + self.make_new_material() args = [hexOrDecInt(a) for a in macro.args] - Tri1 = self.ParseTri(args[:3]) - Tri2 = self.ParseTri(args[4:7]) + Tri1 = self.parse_tri(args[:3]) + Tri2 = self.parse_tri(args[4:7]) self.Tris.append(Tri1) self.Tris.append(Tri2) return self.continue_parse def gsSP1Triangle(self, macro: Macro): - self.MakeNewMat() + self.make_new_material() args = [hexOrDecInt(a) for a in macro.args] - Tri = self.ParseTri(args[:3]) + Tri = self.parse_tri(args[:3]) self.Tris.append(Tri) return self.continue_parse @@ -486,29 +458,40 @@ def gsDPSetRenderMode(self, macro: Macro): # not finished yet def gsSPLight(self, macro: Macro): return self.continue_parse + def gsSPLightColor(self, macro: Macro): return self.continue_parse + def gsSPSetLights0(self, macro: Macro): return self.continue_parse + def gsSPSetLights1(self, macro: Macro): return self.continue_parse + def gsSPSetLights2(self, macro: Macro): return self.continue_parse + def gsSPSetLights3(self, macro: Macro): return self.continue_parse + def gsSPSetLights4(self, macro: Macro): return self.continue_parse + def gsSPSetLights5(self, macro: Macro): return self.continue_parse + def gsSPSetLights6(self, macro: Macro): return self.continue_parse + def gsSPSetLights7(self, macro: Macro): return self.continue_parse + def gsDPSetDepthSource(self, macro: Macro): return self.continue_parse + def gsSPFogFactor(self, macro: Macro): return self.continue_parse - + def gsDPSetFogColor(self, macro: Macro): self.NewMat = 1 self.LastMat.fog_color = macro.args @@ -581,12 +564,12 @@ def gsDPSetCycleType(self, macro: Macro): def gsDPSetCombineMode(self, macro: Macro): self.NewMat = 1 - self.LastMat.Combiner = self.EvalCombiner(macro.args) + self.LastMat.Combiner = self.eval_set_combine_macro(macro.args) return self.continue_parse def gsDPSetCombineLERP(self, macro: Macro): self.NewMat = 1 - self.LastMat.Combiner = [a.strip() for a in macro.args] + self.LastMat.Combiner = macro.args return self.continue_parse # root tile, scale and set tex @@ -596,14 +579,14 @@ def gsSPTexture(self, macro: Macro): "G_ON": 2, "G_OFF": 0, } - set_tex = macros.get(macro.args[-1].strip()) + set_tex = macros.get(macro.args[-1]) if set_tex == None: - set_tex = hexOrDecInt(macro.args[-1].strip()) + set_tex = hexOrDecInt(macro.args[-1]) self.LastMat.set_tex = set_tex == 2 self.LastMat.tex_scale = [ ((0x10000 * (hexOrDecInt(a) < 0)) + hexOrDecInt(a)) / 0xFFFF for a in macro.args[0:2] ] # signed half to unsigned half - self.LastMat.tile_root = self.EvalTile(macro.args[-2].strip()) # I don't think I'll actually use this + self.LastMat.tile_root = self.eval_tile_enum(macro.args[-2]) # I don't think I'll actually use this return self.continue_parse # last tex is a palette @@ -627,7 +610,7 @@ def gsDPLoadBlock(self, macro: Macro): # these values aren't necessary when the texture is already in png format # tex.dxt = hexOrDecInt(args[4]) # tex.texels = hexOrDecInt(args[3]) - tile = self.EvalTile(macro.args[0]) + tile = self.eval_tile_enum(macro.args[0]) tex.tile = tile if tile == 7: self.LastMat.tex0 = tex @@ -644,68 +627,69 @@ def gsDPLoadBlock(self, macro: Macro): def gsDPSetTextureImage(self, macro: Macro): self.NewMat = 1 - Timg = macro.args[3].strip() - Fmt = macro.args[1].strip() - Siz = macro.args[2].strip() + Timg = macro.args[3] + Fmt = macro.args[1] + Siz = macro.args[2] loadtex = Texture(Timg, Fmt, Siz) self.LastMat.loadtex = loadtex return self.continue_parse def gsDPSetTileSize(self, macro: Macro): self.NewMat = 1 - tile = self.LastMat.tiles[self.EvalTile(macro.args[0])] - tile.Slow = self.EvalImFrac(macro.args[1].strip()) - tile.Tlow = self.EvalImFrac(macro.args[2].strip()) - tile.Shigh = self.EvalImFrac(macro.args[3].strip()) - tile.Thigh = self.EvalImFrac(macro.args[4].strip()) + tile = self.LastMat.tiles[self.eval_tile_enum(macro.args[0])] + tile.Slow = self.eval_image_frac(macro.args[1]) + tile.Tlow = self.eval_image_frac(macro.args[2]) + tile.Shigh = self.eval_image_frac(macro.args[3]) + tile.Thigh = self.eval_image_frac(macro.args[4]) return self.continue_parse def gsDPSetTile(self, macro: Macro): self.NewMat = 1 - tile = self.LastMat.tiles[self.EvalTile(macro.args[4].strip())] + tile = self.LastMat.tiles[self.eval_tile_enum(macro.args[4].strip())] tile.Fmt = macro.args[0].strip() tile.Siz = macro.args[1].strip() tile.Tflags = macro.args[6].strip() - tile.TMask = self.EvalTile(macro.args[7].strip()) - tile.TShift = self.EvalTile(macro.args[8].strip()) + tile.TMask = self.eval_tile_enum(macro.args[7]) + tile.TShift = self.eval_tile_enum(macro.args[8]) tile.Sflags = macro.args[9].strip() - tile.SMask = self.EvalTile(macro.args[10].strip()) - tile.SShift = self.EvalTile(macro.args[11].strip()) + tile.SMask = self.eval_tile_enum(macro.args[10]) + tile.SShift = self.eval_tile_enum(macro.args[11]) return self.continue_parse - #syncs need no processing + # syncs need no processing def gsDPPipeSync(self, macro: Macro): return self.continue_parse + def gsDPLoadSync(self, macro: Macro): return self.continue_parse + def gsDPTileSync(self, macro: Macro): return self.continue_parse + def gsDPFullSync(self, macro: Macro): return self.continue_parse + def gsDPNoOp(self, macro: Macro): return self.continue_parse - def MakeNewMat(self): + def make_new_material(self): if self.NewMat: self.NewMat = 0 self.Mats.append([len(self.Tris) - 1, self.LastMat]) self.LastMat = deepcopy(self.LastMat) # for safety self.LastMat.name = self.num + 1 - if self.LastMat.tx_scr: - # I'm clearing here because I did some illegal stuff a bit before, temporary (maybe) - self.LastMat.tx_scr = None self.num += 1 - def ParseTri(self, Tri): + def parse_tri(self, Tri: Sequence[int]): return [self.VertBuff[a] for a in Tri] - def EvalImFrac(self, arg): + def eval_image_frac(self, arg: Union[str, Number]): if type(arg) == int: return arg arg2 = arg.replace("G_TEXTURE_IMAGE_FRAC", "2") return eval(arg2) - def EvalTile(self, arg): + def eval_tile_enum(self, arg: Union[str, Number]): # only 0 and 7 have enums, other stuff just uses int (afaik) Tiles = { "G_TX_LOADTILE": 7, @@ -718,7 +702,7 @@ def EvalTile(self, arg): t = hexOrDecInt(arg) return t - def EvalCombiner(self, arg): + def eval_set_combine_macro(self, arg: str): # two args GBI_CC_Macros = { "G_CC_PRIMITIVE": ["0", "0", "0", "PRIMITIVE", "0", "0", "0", "PRIMITIVE"], @@ -828,5 +812,5 @@ def EvalCombiner(self, arg): "G_CC_HILITERGBPASSA2": ["ENVIRONMENT", "COMBINED", "TEXEL0", "COMBINED", "0", "0", "0", "COMBINED"], } return GBI_CC_Macros.get( - arg[0].strip(), ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"] - ) + GBI_CC_Macros.get(arg[1].strip(), ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"]) + arg[0], ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"] + ) + GBI_CC_Macros.get(arg[1], ["TEXEL0", "0", "SHADE", "0", "TEXEL0", "0", "SHADE", "0"]) diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index 6657662d0..106ccee70 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -230,9 +230,7 @@ def AREA(self, macro: Macro, col: bpy.types.Collection): area_col = col area_col.objects.link(area_root) area_root.name = f"{self.scene.LevelImp.Level} Area Root {macro.args[0]}" - self.areas[macro.args[0]] = Area( - area_root, macro.args[1], self.root, int(macro.args[0]), self.scene, area_col - ) + self.areas[macro.args[0]] = Area(area_root, macro.args[1], self.root, int(macro.args[0]), self.scene, area_col) self.cur_area = macro.args[0] return self.continue_parse @@ -329,42 +327,55 @@ def generic_music(self, macro: Macro, col: bpy.types.Collection): root.musicSeqEnum = "Custom" root.music_seq = macro.args[-1] return self.continue_parse - + # Don't support these for now def MACRO_OBJECTS(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def MARIO_POS(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse - + # use group mapping to set groups eventually def LOAD_MIO0(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def LOAD_MIO0_TEXTURE(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def LOAD_YAY0(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def LOAD_RAW(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse - + # not useful for bpy, dummy these script cmds def MARIO(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def INIT_LEVEL(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def ALLOC_LEVEL_POOL(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def FREE_LEVEL_POOL(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def LOAD_MODEL_FROM_GEO(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def LOAD_MODEL_FROM_DL(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def CALL(self, macro: Macro, col: bpy.types.Collection): - return self.continue_parse + return self.continue_parse + def CALL_LOOP(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def CLEAR_LEVEL(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def SLEEP_BEFORE_EXIT(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse @@ -382,7 +393,7 @@ def __init__(self, collision: list[str], scale: float): self.water_boxes = [] super().__init__() - def WriteWaterBoxes( + def write_water_boxes( self, scene: bpy.types.Scene, parent: bpy.types.Object, name: str, col: bpy.types.Collection = None ): for i, w in enumerate(self.water_boxes): @@ -403,12 +414,12 @@ def WriteWaterBoxes( scale = [Xwidth, Zwidth, 1] Obj.scale = scale - def WriteCollision( + def write_collision( self, scene: bpy.types.Scene, name: str, parent: bpy.types.Object, col: bpy.types.Collection = None ): if not col: col = scene.collection - self.WriteWaterBoxes(scene, parent, name, col) + self.write_water_boxes(scene, parent, name, col) mesh = bpy.data.meshes.new(name + " data") tris = [] for t in self.tris.values(): @@ -443,13 +454,12 @@ def WriteCollision( mat.use_collision_param = True mat.collision_param = str(self.tri_types[x][2][3]) x += 1 - override = bpy.context.copy() - override["material"] = mat - bpy.ops.material.update_f3d_nodes(override) + with bpy.context.temp_override(material=mat): + bpy.ops.material.update_f3d_nodes() p.material_index = x - 1 return obj - def GetCollision(self): + def parse_collision(self): self.parse_stream(self.collision, 0) # This will keep track of how to assign mats a = 0 @@ -481,7 +491,7 @@ def COL_WATER_BOX(self, macro: Macro): def SPECIAL_OBJECT(self, macro: Macro): self.special_objects.append(macro.args) return self.continue_parse - + def SPECIAL_OBJECT_WITH_YAW(self, macro: Macro): self.special_objects.append(macro.args) return self.continue_parse @@ -489,26 +499,31 @@ def SPECIAL_OBJECT_WITH_YAW(self, macro: Macro): # don't do anything to bpy def COL_WATER_BOX_INIT(self, macro: Macro): return self.continue_parse + def COL_INIT(self, macro: Macro): return self.continue_parse + def COL_VERTEX_INIT(self, macro: Macro): return self.continue_parse + def COL_SPECIAL_INIT(self, macro: Macro): return self.continue_parse + def COL_TRI_STOP(self, macro: Macro): return self.continue_parse + def COL_END(self, macro: Macro): return self.continue_parse class SM64_Material(Mat): - def LoadTexture(self, ForceNewTex: bool, textures: dict, path: Path, tex: Texture): + def load_texture(self, ForceNewTex: bool, textures: dict, path: Path, tex: Texture): if not tex: return None Timg = textures.get(tex.Timg)[0].split("/")[-1] Timg = Timg.replace("#include ", "").replace('"', "").replace("'", "").replace("inc.c", "png") - i = bpy.data.images.get(Timg) - if not i or ForceNewTex: + image = bpy.data.images.get(Timg) + if not image or ForceNewTex: Timg = textures.get(tex.Timg)[0] Timg = Timg.replace("#include ", "").replace('"', "").replace("'", "").replace("inc.c", "png") # deal with duplicate pathing (such as /actors/actors etc.) @@ -522,117 +537,51 @@ def LoadTexture(self, ForceNewTex: bool, textures: dict, path: Path, tex: Textur fp = path / Timg return bpy.data.images.load(filepath=str(fp)) else: - return i - - def ApplyMatSettings(self, mat: bpy.types.Material, textures: dict, path: Path, layer: int): + return image + + def apply_PBSDF_Mat(self, mat: bpy.types.Material, textures: dict, tex_path: Path, layer: int, tex: Texture): + nt = mat.node_tree + nodes = nt.nodes + links = nt.links + pbsdf = nodes.get("Principled BSDF") + if not pbsdf: + return + tex_node = nodes.new("ShaderNodeTexImage") + links.new(pbsdf.inputs[0], tex_node.outputs[0]) # base color + image = self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, tex) + if image: + tex_node.image = image + if int(layer) > 4: + mat.blend_method == "BLEND" + + def apply_material_settings(self, mat: bpy.types.Material, textures: dict, tex_path: Path, layer: int): if bpy.context.scene.LevelImp.AsObj: - return self.ApplyPBSDFMat(mat, textures, path, layer, self.tex0) - f3d = mat.f3d_mat # This is kure's custom property class for materials + return self.apply_PBSDF_Mat(mat, textures, tex_path, layer, self.tex0) + + f3d = mat.f3d_mat + f3d.draw_layer.sm64 = layer - # set color registers if they exist - if hasattr(self, "fog_position"): - f3d.set_fog = True - f3d.use_global_fog = False - f3d.fog_position[0] = eval(self.fog_pos[0]) - f3d.fog_position[1] = eval(self.fog_pos[1]) - if hasattr(self, "fog_color"): - f3d.set_fog = True - f3d.use_global_fog = False - f3d.fog_color = self.ConvertColor(self.fog_color) - if hasattr(self, "light_col"): - # this is a dict but I'll only use the first color for now - f3d.set_lights = True - if self.light_col.get(1): - f3d.default_light_color = self.ConvertColor(eval(self.light_col[1]).to_bytes(4, "big")) - if hasattr(self, "env_color"): - f3d.set_env = True - f3d.env_color = self.ConvertColor(self.env_color[-4:]) - if hasattr(self, "prim_color"): - prim = self.prim_color - f3d.set_prim = True - f3d.prim_lod_min = int(prim[0]) - f3d.prim_lod_frac = int(prim[1]) - f3d.prim_color = self.ConvertColor(prim[-4:]) - # I set these but they aren't properly stored because they're reset by fast64 or something - # its better to have defaults than random 2 cycles - self.SetGeoMode(f3d.rdp_settings, mat) - - if self.TwoCycle: - f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_2CYCLE" - else: - f3d.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE" - # make combiner custom - f3d.presetName = "Custom" - self.SetCombiner(f3d) - - # deal with custom render modes - if hasattr(self, "RenderMode"): - self.SetRenderMode(f3d) - # g texture handle - if hasattr(self, "set_tex"): - # not exactly the same but gets the point across maybe? - f3d.tex0.tex_set = self.set_tex - f3d.tex1.tex_set = self.set_tex - # tex scale gets set to 0 when textures are disabled which is automatically done - # often to save processing power between mats or something, or just adhoc bhv - if f3d.rdp_settings.g_tex_gen or any([a < 1 and a > 0 for a in self.tex_scale]): - f3d.scale_autoprop = False - f3d.tex_scale = self.tex_scale - if not self.set_tex: - # Update node values - override = bpy.context.copy() - override["material"] = mat - bpy.ops.material.update_f3d_nodes(override) - del override - return - # Try to set an image - # texture 0 then texture 1 - if self.tex0: - i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, self.tex0) - tex0 = f3d.tex0 - tex0.tex_reference = str(self.tex0.Timg) # setting prop for hash purposes - tex0.tex_set = True - tex0.tex = i - tex0.tex_format = self.EvalFmt(self.tiles[0]) - tex0.autoprop = False - Sflags = self.EvalFlags(self.tiles[0].Sflags) - for f in Sflags: - setattr(tex0.S, f, True) - Tflags = self.EvalFlags(self.tiles[0].Tflags) - for f in Sflags: - setattr(tex0.T, f, True) - tex0.S.low = self.tiles[0].Slow - tex0.T.low = self.tiles[0].Tlow - tex0.S.high = self.tiles[0].Shigh - tex0.T.high = self.tiles[0].Thigh - - tex0.S.mask = self.tiles[0].SMask - tex0.T.mask = self.tiles[0].TMask - if self.tex1: - i = self.LoadTexture(bpy.context.scene.LevelImp.ForceNewTex, textures, path, self.tex1) - tex1 = f3d.tex1 - tex1.tex_reference = str(self.tex1.Timg) # setting prop for hash purposes - tex1.tex_set = True - tex1.tex = i - tex1.tex_format = self.EvalFmt(self.tiles[1]) - Sflags = self.EvalFlags(self.tiles[1].Sflags) - for f in Sflags: - setattr(tex1.S, f, True) - Tflags = self.EvalFlags(self.tiles[1].Tflags) - for f in Sflags: - setattr(tex1.T, f, True) - tex1.S.low = self.tiles[1].Slow - tex1.T.low = self.tiles[1].Tlow - tex1.S.high = self.tiles[1].Shigh - tex1.T.high = self.tiles[1].Thigh - - tex1.S.mask = self.tiles[0].SMask - tex1.T.mask = self.tiles[0].TMask - # Update node values - override = bpy.context.copy() - override["material"] = mat - bpy.ops.material.update_f3d_nodes(override) - del override + self.set_register_settings(mat, f3d) + self.set_textures(f3d, textures, tex_path) + with bpy.context.temp_override(material=mat): + bpy.ops.material.update_f3d_nodes() + + def set_textures(self, f3d: F3DMaterialProperty, textures: dict, tex_path: Path): + self.set_tex_scale(f3d) + if self.tex0 and self.set_tex: + self.set_tex_settings( + f3d.tex0, + self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, self.tex0), + self.tiles[0], + self.tex0.Timg + ) + if self.tex1 and self.set_tex: + self.set_tex_settings( + f3d.tex1, + self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, self.tex1), + self.tiles[1], + self.tex1.Timg + ) class SM64_F3D(DL): @@ -648,7 +597,8 @@ def __init__(self, scene: bpy.types.Scene): super().__init__() # Textures only contains the texture data found inside the model.inc.c file and the texture.inc.c file - def GetGenericTextures(self, root_path: Path): + # this will add all the textures located in the /textures/ folder in decomp + def get_generic_textures(self, root_path: Path): for t in [ "cave.c", "effect.c", @@ -665,7 +615,7 @@ def GetGenericTextures(self, root_path: Path): "water.c", ]: t = root_path / "bin" / t - t = open(t, "r", newline='') + t = open(t, "r", newline="") tex = t # For textures, try u8, and s16 aswell self.Textures.update( @@ -681,7 +631,7 @@ def GetGenericTextures(self, root_path: Path): t.close() # recursively parse the display list in order to return a bunch of model data - def GetDataFromModel(self, start: str): + def get_f3d_data_from_model(self, start: str): DL = self.Gfx.get(start) self.VertBuff = [0] * 32 # If you're doing some fucky shit with a larger vert buffer it sucks to suck I guess if not DL: @@ -695,19 +645,10 @@ def GetDataFromModel(self, start: str): self.parse_stream(DL, start) self.NewMat = 0 self.StartName = start - print(self.Verts, self.Tris, start) return [self.Verts, self.Tris] - def MakeNewMat(self): - if self.NewMat: - self.NewMat = 0 - self.Mats.append([len(self.Tris) - 1, self.LastMat]) - self.LastMat = deepcopy(self.LastMat) # for safety - self.LastMat.name = self.num + 1 - self.num += 1 - # turn member of vtx str arr into vtx args - def ParseVert(self, Vert: str): + def parse_vert(self, Vert: str): v = Vert.replace("{", "").replace("}", "").split(",") num = lambda x: [eval(a) for a in x] pos = num(v[:3]) @@ -716,15 +657,11 @@ def ParseVert(self, Vert: str): return [pos, uv, vc] # given tri args in gbi cmd, give appropriate tri indices in vert list - def ParseTri(self, Tri: list[int]): + def parse_tri(self, Tri: list[int]): L = len(self.Verts) return [a + L - self.LastLoad for a in Tri] - def StripArgs(self, cmd: str): - a = cmd.find("(") - return cmd[:a].strip(), cmd[a + 1 : -2].split(",") - - def ApplyDat(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_path: Path): + def apply_mesh_data(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_path: Path): tris = mesh.polygons bpy.context.view_layer.objects.active = obj ind = -1 @@ -746,13 +683,13 @@ def ApplyDat(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_ self.Mats.append([len(tris), 0]) for i, t in enumerate(tris): if i > self.Mats[ind + 1][0]: - new = self.Create_new_f3d_mat(self.Mats[ind + 1][1], mesh) + new = self.create_new_f3d_mat(self.Mats[ind + 1][1], mesh) ind += 1 if not new: new = len(mesh.materials) - 1 mat = mesh.materials[new] mat.name = "sm64 F3D Mat {} {}".format(obj.name, new) - self.Mats[new][1].ApplyMatSettings(mat, self.Textures, tex_path, layer) + self.Mats[new][1].apply_material_settings(mat, self.Textures, tex_path, layer) else: # I tried to re use mat slots but it is much slower, and not as accurate # idk if I was just doing it wrong or the search is that much slower, but this is easier @@ -778,13 +715,13 @@ def ApplyDat(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_ Vcol.data[l].color = [a / 255 for a in vcol] # create a new f3d_mat given an SM64_Material class but don't create copies with same props - def Create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): + def create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): if not self.scene.LevelImp.ForceNewTex: # check if this mat was used already in another mesh (or this mat if DL is garbage or something) # even looping n^2 is probably faster than duping 3 mats with blender speed for j, F3Dmat in enumerate(bpy.data.materials): if F3Dmat.is_f3d: - dupe = mat.MatHashF3d(F3Dmat.f3d_mat) + dupe = mat.mat_hash_f3d(F3Dmat.f3d_mat) if dupe: return F3Dmat if mesh.materials: @@ -811,9 +748,9 @@ class ModelDat: model: str scale: float = 1.0 + # base class for geo layouts and armatures class GraphNodes(DataParser): - def GEO_BRANCH_AND_LINK(self, macro: Macro, depth: int): new_geo_layout = self.geo_layouts.get(macro.args[0]) if new_geo_layout: @@ -845,13 +782,16 @@ def GEO_CLOSE_NODE(self, macro: Macro, depth: int): def GEO_OPEN_NODE(self, macro: Macro, depth: int): if self.obj: - GeoChild = GeoLayout(self.geo_layouts, self.obj, self.scene, self.name, self.area_root, col=self.col, geo_parent=self) + GeoChild = GeoLayout( + self.geo_layouts, self.obj, self.scene, self.name, self.area_root, col=self.col, geo_parent=self + ) else: - GeoChild = GeoLayout(self.geo_layouts, self.root, self.scene, self.name, self.area_root, col=self.col, geo_parent=self) + GeoChild = GeoLayout( + self.geo_layouts, self.root, self.scene, self.name, self.area_root, col=self.col, geo_parent=self + ) GeoChild.parent_transform = self.last_transform GeoChild.stream = self.stream GeoChild.parse_stream(self.geo_layouts.get(self.stream), self.stream, depth + 1) - # self.head = self.skip_children(self.cur_dat_stream, self.head) self.children.append(GeoChild) return self.continue_parse @@ -862,7 +802,7 @@ def GEO_DISPLAY_LIST(self, macro: Macro, depth: int): # shadows aren't naturally supported but we can emulate them with custom geo cmds, note: possibly changed with fast64 updates, this is old code def GEO_SHADOW(self, macro: Macro, depth: int): - obj = self.MakeRt(self.name + "shadow empty", self.root) + obj = self.make_root_empty(self.name + "shadow empty", self.root) obj.sm64_obj_type = "Custom Geo Command" obj.customGeoCommand = "GEO_SHADOW" obj.customGeoCommandArgs = ", ".join(macro.args) @@ -877,7 +817,7 @@ def GEO_ANIMATED_PART(self, macro: Macro, depth: int): if model.strip() != "NULL": self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) else: - obj = self.MakeRt(self.name + "animated empty", self.root) + obj = self.make_root_empty(self.name + "animated empty", self.root) obj.location = Tlate def GEO_ROTATION_NODE(self, macro: Macro, depth: int): @@ -891,7 +831,7 @@ def GEO_ROTATE(self, macro: Macro, depth: int): Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") self.last_transform = [[0, 0, 0], Rotate] self.last_transform = [[0, 0, 0], self.last_transform[1]] - obj = self.MakeRt(self.name + "rotate", self.root) + obj = self.make_root_empty(self.name + "rotate", self.root) obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate/Rotate" return obj @@ -911,7 +851,7 @@ def GEO_ROTATE_WITH_DL(self, macro: Macro, depth: int): if model.strip() != "NULL": self.models.append(ModelDat([0, 0, 0], Rotate, layer, model)) else: - obj = self.MakeRt(self.name + "rotate", self.root) + obj = self.make_root_empty(self.name + "rotate", self.root) obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate/Rotate" return obj @@ -928,7 +868,7 @@ def GEO_TRANSLATE_ROTATE_WITH_DL(self, macro: Macro, depth: int): if model.strip() != "NULL": self.models.append(ModelDat(Tlate, Rotate, layer, model)) else: - obj = self.MakeRt(self.name + "translate rotate", self.root) + obj = self.make_root_empty(self.name + "translate rotate", self.root) obj.location = Tlate obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate/Rotate" @@ -939,7 +879,7 @@ def GEO_TRANSLATE_ROTATE(self, macro: Macro, depth: int): Rotate = [math.radians(float(arg)) for arg in macro.args[4:7]] Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") self.last_transform = [Tlate, Rotate] - obj = self.MakeRt(self.name + "translate", self.root) + obj = self.make_root_empty(self.name + "translate", self.root) obj.location = Tlate obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate/Rotate" @@ -959,7 +899,7 @@ def GEO_TRANSLATE_NODE_WITH_DL(self, macro: Macro, depth: int): if model.strip() != "NULL": self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) else: - obj = self.MakeRt(self.name + "translate", self.root) + obj = self.make_root_empty(self.name + "translate", self.root) obj.location = Tlate obj.rotation_euler = Rotate obj.sm64_obj_type = "Geo Translate Node" @@ -974,7 +914,7 @@ def GEO_TRANSLATE_NODE(self, macro: Macro, depth: int): Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in macro.args[1:4]] Tlate = [Tlate[0], -Tlate[2], Tlate[1]] self.last_transform = [Tlate, self.last_transform[1]] - obj = self.MakeRt(self.name + "translate", self.root) + obj = self.make_root_empty(self.name + "translate", self.root) obj.location = Tlate obj.sm64_obj_type = "Geo Translate Node" return obj @@ -986,20 +926,20 @@ def GEO_SCALE_WITH_DL(self, macro: Macro, depth: int): self.models.append(ModelDat((0, 0, 0), (0, 0, 0), layer, model, scale=scale)) def GEO_SCALE(self, macro: Macro, depth: int): - obj = self.MakeRt(self.name + "scale", self.root) + obj = self.make_root_empty(self.name + "scale", self.root) scale = eval(macro.args[1]) / 0x10000 obj.scale = (scale, scale, scale) obj.sm64_obj_type = "Geo Scale" def GEO_ASM(self, macro: Macro, depth: int): - obj = self.MakeRt(self.name + "asm", self.root) + obj = self.make_root_empty(self.name + "asm", self.root) asm = self.obj.fast64.sm64.geo_asm self.obj.sm64_obj_type = "Geo ASM" asm.param = macro.args[0] asm.func = macro.args[1] def GEO_SWITCH_CASE(self, macro: Macro, depth: int): - obj = self.MakeRt(self.name + "switch", self.root) + obj = self.make_root_empty(self.name + "switch", self.root) Switch = self.obj Switch.sm64_obj_type = "Switch" Switch.switchParam = eval(macro.args[0]) @@ -1013,7 +953,7 @@ def GEO_RENDER_RANGE(self, macro: Macro, depth: int): def GEO_CAMERA(self, macro: Macro, depth: int): self.area_root.camOption = "Custom" self.area_root.camType = macro.args[0] - + # make better def GEO_CAMERA_FRUSTUM_WITH_FUNC(self, macro: Macro, depth: int): self.area_root.camOption = "Custom" @@ -1029,21 +969,24 @@ def GEO_BACKGROUND_COLOR(self, macro: Macro, depth: int): G = (color & (0x3E << 5)) >> 6 R = (color & (0x3E << 10)) >> 11 self.area_root.areaBGColor = (R / 0x1F, G / 0x1F, B / 0x1F, A) - + # these have no affect on the bpy def GEO_BACKGROUND(self, macro: Macro, depth: int): return self.continue_parse + def GEO_NODE_SCREEN_AREA(self, macro: Macro, depth: int): return self.continue_parse + def GEO_ZBUFFER(self, macro: Macro, depth: int): return self.continue_parse + def GEO_NODE_ORTHO(self, macro: Macro, depth: int): return self.continue_parse + def GEO_RENDER_OBJ(self, macro: Macro, depth: int): return self.continue_parse - class GeoLayout(GraphNodes): def __init__( self, @@ -1053,7 +996,7 @@ def __init__( name, area_root: bpy.types.Object, col: bpy.types.Collection = None, - geo_parent: GeoLayout = None + geo_parent: GeoLayout = None, ): self.geo_layouts = geo_layouts self.parent = root @@ -1073,7 +1016,7 @@ def __init__( self.col = col super().__init__(parent=geo_parent) - def MakeRt(self, name: str, root: bpy.types.Object): + def make_root_empty(self, name: str, root: bpy.types.Object): # make an empty node to act as the root of this geo layout # use this to hold a transform, or an actual cmd, otherwise rt is passed E = bpy.data.objects.new(name, None) @@ -1095,23 +1038,6 @@ def parse_level_geo(self, start: str, scene: bpy.types.Scene): self.stream = start self.parse_stream(geo_layout, start, 0) - def skip_children(self, geo_layout: list[str], head: int): - open = 0 - opened = 0 - while head < len(geo_layout): - l = geo_layout[head] - if l.startswith("GEO_OPEN_NODE"): - opened = 1 - open += 1 - if l.startswith("GEO_CLOSE_NODE"): - open -= 1 - if open == 0 and opened: - break - head += 1 - return head - - - # ------------------------------------------------------------------------ # Functions @@ -1119,12 +1045,12 @@ def skip_children(self, geo_layout: list[str], head: int): # parse aggregate files, and search for sm64 specific fast64 export name schemes def get_all_aggregates(aggregate: Path, filenames: Union[str, tuple[str]], root_path: Path) -> list[Path]: - with open(aggregate, "r", newline='') as aggregate: + with open(aggregate, "r", newline="") as aggregate: caught_files = parse_aggregate_file(aggregate, filenames, root_path) # catch fast64 includes fast64 = parse_aggregate_file(aggregate, "leveldata.inc.c", root_path) if fast64: - with open(fast64[0], "r", newline='') as fast64_dat: + with open(fast64[0], "r", newline="") as fast64_dat: caught_files.extend(parse_aggregate_file(fast64_dat, filenames, root_path)) return caught_files @@ -1140,7 +1066,7 @@ def parse_level_script(script_file: Path, scene: bpy.types.Scene, col: bpy.types Root.sm64_obj_type = "Level Root" # Now parse the script and get data about the level # Store data in attribute of a level class then assign later and return class - with open(script_file, "r", newline='') as script_file: + with open(script_file, "r", newline="") as script_file: lvl = Level(script_file, scene, Root) entry = scene.LevelImp.Entry.format(scene.LevelImp.Level) lvl.parse_level_script(entry, col=col) @@ -1169,7 +1095,7 @@ def write_geo_to_bpy( else: mesh = bpy.data.meshes.new(name) meshes[name] = mesh - [verts, tris] = f3d_dat.GetDataFromModel(m.model.strip()) + [verts, tris] = f3d_dat.get_f3d_data_from_model(m.model.strip()) mesh.from_pydata(verts, [], tris) obj = bpy.data.objects.new(m.model + " Obj", mesh) @@ -1187,7 +1113,7 @@ def write_geo_to_bpy( obj.location = m.translate obj.ignore_collision = True if name: - f3d_dat.ApplyDat(obj, mesh, layer, root_path) + f3d_dat.apply_mesh_data(obj, mesh, layer, root_path) if cleanup: # clean up after applying dat mesh.validate() @@ -1224,7 +1150,7 @@ def construct_geo_layouts_from_file(geo: TextIO, root_path: Path): geo_layout_files.extend(get_all_aggregates(file, "geo.inc.c", root_path)) geo_layout_data = {} # stores cleaned up geo layout lines for geo_file in geo_layout_files: - with open(geo_file, "r", newline='') as geo_file: + with open(geo_file, "r", newline="") as geo_file: geo_layout_data.update(get_data_types_from_file(geo_file, {"GeoLayout": ["(", ")"]})) return geo_layout_data @@ -1240,7 +1166,7 @@ def construct_sm64_f3d_data_from_file(gfx: SM64_F3D, model_file: TextIO): "Ambient_t": [None, None], "Lights1": [None, None], }, - collated=True + collated=True, ) for key, value in gfx_dat.items(): attr = getattr(gfx, key) @@ -1280,11 +1206,11 @@ def construct_model_data_from_file(aggregate: Path, scene: bpy.types.Scene, root # Get all modeldata in the level sm64_f3d_data = SM64_F3D(scene) for model_file in model_files: - model_file = open(model_file, "r", newline='') + model_file = open(model_file, "r", newline="") construct_sm64_f3d_data_from_file(sm64_f3d_data, model_file) # Update file to have texture.inc.c textures, deal with included textures in the model.inc.c files aswell for texture_file in [*texture_files, *model_files]: - with open(texture_file, "r", newline='') as texture_file: + with open(texture_file, "r", newline="") as texture_file: # For textures, try u8, and s16 aswell sm64_f3d_data.Textures.update( get_data_types_from_file( @@ -1315,9 +1241,7 @@ def find_actor_models_from_geo( # Find DL references given a level geo file and a path to a level folder -def find_level_models_from_geo( - geo: TextIO, lvl: Level, scene: bpy.types.Scene, root_path: Path, col_name: str = None -): +def find_level_models_from_geo(geo: TextIO, lvl: Level, scene: bpy.types.Scene, root_path: Path, col_name: str = None): GeoLayouts = construct_geo_layouts_from_file(geo, root_path) for area_index, area in lvl.areas.items(): if col_name: @@ -1347,7 +1271,7 @@ def import_level_graphics( print(lvl.areas, aggregate) # just a try, in case you are importing from something other than base decomp repo (like RM2C output folder) try: - models.GetGenericTextures(root_path) + models.get_generic_textures(root_path) except: print("could not import genric textures, if this errors later from missing textures this may be why") lvl = write_level_to_bpy(lvl, scene, root_path, models, cleanup=cleanup) @@ -1361,7 +1285,7 @@ def find_collision_data_from_path(aggregate: Path, lvl: Level, scene: bpy.types. for col_file in collision_files: if not os.path.isfile(col_file): continue - with open(col_file, "r", newline='') as col_file: + with open(col_file, "r", newline="") as col_file: col_data.update(get_data_types_from_file(col_file, {"Collision": ["(", ")"]})) # search for the area terrain from available collision data for area in lvl.areas.values(): @@ -1380,9 +1304,9 @@ def write_level_collision_to_bpy(lvl: Level, scene: bpy.types.Scene, cleanup: bo else: col = create_collection(area.root.users_collection[0], col_name) col_parser = Collision(area.ColFile, scene.blenderToSM64Scale) - col_parser.GetCollision() + col_parser.parse_collision() name = "SM64 {} Area {} Col".format(scene.LevelImp.Level, area_index) - obj = col_parser.WriteCollision(scene, name, area.root, col=col) + obj = col_parser.write_collision(scene, name, area.root, col=col) # final operators to clean stuff up if cleanup: obj.data.validate() @@ -1439,7 +1363,7 @@ def execute(self, context): else: geo = folder / (prefix + "geo.c") leveldat = folder / (prefix + "leveldata.c") - geo = open(geo, "r", newline='') + geo = open(geo, "r", newline="") Root = bpy.data.objects.new("Empty", None) Root.name = "Actor %s" % scene.ActImp.GeoLayout rt_col.objects.link(Root) @@ -1450,7 +1374,7 @@ def execute(self, context): models = construct_model_data_from_file(leveldat, scene, folder) # just a try, in case you are importing from not the base decomp repo try: - models.GetGenericTextures(path) + models.get_generic_textures(path) except: print("could not import genric textures, if this errors later from missing textures this may be why") meshes = {} # re use mesh data when the same DL is referenced (bbh is good example) From 1a53408c1b3feba3ab1dfc548985d11e6b13afab Mon Sep 17 00:00:00 2001 From: scut Date: Fri, 7 Jul 2023 10:52:21 -0400 Subject: [PATCH 05/11] made armature exporting matching for goomba as test, with working anim imports to imported rig, switch options also import correctly. Cleaned up code more and improved object geo layout transforms as well --- fast64_internal/f3d/f3d_import.py | 76 +- fast64_internal/sm64/sm64_level_importer.py | 958 +++++++++++++++----- fast64_internal/utility_importer.py | 33 +- 3 files changed, 801 insertions(+), 266 deletions(-) diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py index 509b0d762..631cbce4e 100644 --- a/fast64_internal/f3d/f3d_import.py +++ b/fast64_internal/f3d/f3d_import.py @@ -16,11 +16,7 @@ from numbers import Number from collections.abc import Sequence -from ..f3d.f3d_material import ( - F3DMaterialProperty, - RDPSettings, - TextureProperty -) +from ..f3d.f3d_material import F3DMaterialProperty, RDPSettings, TextureProperty from ..utility import hexOrDecInt from ..utility_importer import * @@ -29,6 +25,15 @@ # Classes # ------------------------------------------------------------------------ +# will format light struct data passed +class Lights1: + def __init__(self, name: str, data_str: str): + self.name = name + data = [eval(dat.strip()) for dat in data_str.split(",")] + self.ambient = [*data[0:3], 0xFF] + self.diffuse = [*data[3:6], 0xFF] + self.direction = data[9:12] + # this will hold tile properties class Tile: @@ -72,6 +77,9 @@ def __init__(self): self.tiles = [Tile() for a in range(8)] self.tex0 = None self.tex1 = None + self.num_lights = 1 + self.light_col = {} + self.ambient_light = tuple() # calc the hash for an f3d mat and see if its equal to this mats hash def mat_hash_f3d(self, f3d: F3DMaterialProperty): @@ -169,6 +177,7 @@ def apply_PBSDF_Mat(self, mat: bpy.types.Material, tex_path: Path, tex: Texture) return tex_node = nodes.new("ShaderNodeTexImage") links.new(pbsdf.inputs[0], tex_node.outputs[0]) # base color + links.new(pbsdf.inputs[21], tex_node.outputs[1]) # alpha color image = self.LoadTexture(0, tex_path, tex) if image: tex_node.image = image @@ -178,7 +187,7 @@ def apply_material_settings(self, mat: bpy.types.Material, tex_path: Path): self.set_register_settings(mat, f3d) self.set_textures(f3d) - + with bpy.context.temp_override(material=mat): bpy.ops.material.update_f3d_nodes() @@ -188,14 +197,14 @@ def set_register_settings(self, mat: bpy.types.Material, f3d: F3DMaterialPropert self.set_geo_mode(f3d.rdp_settings, mat) self.set_combiner(f3d) self.set_render_mode(f3d) - + def set_textures(self, f3d: F3DMaterialProperty, tex_path: Path): self.set_tex_scale(f3d) if self.tex0 and self.set_tex: self.set_tex_settings(f3d.tex0, self.load_texture(0, tex_path, self.tex0), self.tiles[0], self.tex0.Timg) if self.tex1 and self.set_tex: self.set_tex_settings(f3d.tex1, self.load_texture(0, tex_path, self.tex1), self.tiles[1], self.tex1.Timg) - + def set_fog(self, f3d: F3DMaterialProperty): if hasattr(self, "fog_position"): f3d.set_fog = True @@ -208,11 +217,14 @@ def set_fog(self, f3d: F3DMaterialProperty): f3d.fog_color = self.convert_color(self.fog_color) def set_color_registers(self, f3d: F3DMaterialProperty): - if hasattr(self, "light_col"): + if self.ambient_light: + f3d.set_ambient_from_light = False + f3d.ambient_light_color = self.convert_color(self.ambient_light) + if self.light_col: # this is a dict but I'll only use the first color for now f3d.set_lights = True if self.light_col.get(1): - f3d.default_light_color = self.convert_color(eval(self.light_col[1]).to_bytes(4, "big")) + f3d.default_light_color = self.convert_color(self.light_col[1]) if hasattr(self, "env_color"): f3d.set_env = True f3d.env_color = self.convert_color(self.env_color[-4:]) @@ -235,7 +247,9 @@ def set_tex_scale(self, f3d: F3DMaterialProperty): f3d.scale_autoprop = False f3d.tex_scale = self.tex_scale - def set_tex_settings(self, tex_prop: TextureProperty, image: bpy.types.Image, tile: Tile, tex_img: Union[Sequence, str]): + def set_tex_settings( + self, tex_prop: TextureProperty, image: bpy.types.Image, tile: Tile, tex_img: Union[Sequence, str] + ): tex_prop.tex_reference = str(tex_img) # setting prop for hash purposes tex_prop.tex_set = True tex_prop.tex = image @@ -366,6 +380,7 @@ def __init__(self, lastmat=None): self.Ambient_t = {} self.Lights1 = {} self.Textures = {} + self.NewMat = 1 if not lastmat: self.LastMat = Mat() self.LastMat.name = 0 @@ -384,6 +399,7 @@ def gsSPBranchList(self, macro: Macro): NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix ) ) + self.reset_parser(branched_dl) self.parse_stream(NewDL, branched_dl) return self.break_parse @@ -395,6 +411,7 @@ def gsSPDisplayList(self, macro: Macro): NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix ) ) + self.reset_parser(branched_dl) self.parse_stream(NewDL, branched_dl) return self.continue_parse @@ -455,16 +472,41 @@ def gsDPSetRenderMode(self, macro: Macro): self.LastMat.RenderMode = [a.strip() for a in macro.args] return self.continue_parse - # not finished yet + # The highest numbered light is always the ambient light def gsSPLight(self, macro: Macro): + self.NewMat = 1 + light = re.search("&.+\.", macro.args[0]).group()[1:-1] + light = Lights1(light, self.Lights1.get(light)[0]) + if ".a" in macro.args[0]: + self.LastMat.ambient_light = light.ambient + else: + num = re.search("_\d", macro.args[0]).group()[1] + if not num: + num = 1 + self.LastMat.light_col[num] = light.diffuse + return self.continue_parse + + # numlights0 still gives one ambient and diffuse light + def gsSPNumLights(self, macro: Macro): + self.NewMat = 1 + num = re.search("_\d", macro.args[0]).group()[1] + if not num: + num = 1 + self.LastMat.num_lights = num return self.continue_parse def gsSPLightColor(self, macro: Macro): + self.NewMat = 1 + num = re.search("_\d", macro.args[0]).group()[1] + if not num: + num = 1 + self.LastMat.light_col[num] = eval(macro.args[-1]).to_bytes(4, "big") return self.continue_parse def gsSPSetLights0(self, macro: Macro): return self.continue_parse + # not finished yet def gsSPSetLights1(self, macro: Macro): return self.continue_parse @@ -502,14 +544,6 @@ def gsSPFogPosition(self, macro: Macro): self.LastMat.fog_pos = macro.args return self.continue_parse - def gsSPLightColor(self, macro: Macro): - self.NewMat = 1 - if not hasattr(self.LastMat, "light_col"): - self.LastMat.light_col = {} - num = re.search("_\d", macro.args[0]).group()[1] - self.LastMat.light_col[num] = macro.args[-1] - return self.continue_parse - def gsDPSetPrimColor(self, macro: Macro): self.NewMat = 1 self.LastMat.prim_color = macro.args @@ -677,8 +711,6 @@ def make_new_material(self): self.NewMat = 0 self.Mats.append([len(self.Tris) - 1, self.LastMat]) self.LastMat = deepcopy(self.LastMat) # for safety - self.LastMat.name = self.num + 1 - self.num += 1 def parse_tri(self, Tri: Sequence[int]): return [self.VertBuff[a] for a in Tri] diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index 106ccee70..c5bc64f9c 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -33,12 +33,15 @@ from copy import deepcopy from dataclasses import dataclass from typing import TextIO +from numbers import Number +from collections.abc import Sequence # from SM64classes import * from ..f3d.f3d_import import * from ..utility_importer import * from ..utility import ( + transform_mtx_blender_to_n64, rotate_quat_n64_to_blender, rotate_object, parentObject, @@ -548,6 +551,7 @@ def apply_PBSDF_Mat(self, mat: bpy.types.Material, textures: dict, tex_path: Pat return tex_node = nodes.new("ShaderNodeTexImage") links.new(pbsdf.inputs[0], tex_node.outputs[0]) # base color + links.new(pbsdf.inputs[21], tex_node.outputs[1]) # alpha color image = self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, tex) if image: tex_node.image = image @@ -559,13 +563,13 @@ def apply_material_settings(self, mat: bpy.types.Material, textures: dict, tex_p return self.apply_PBSDF_Mat(mat, textures, tex_path, layer, self.tex0) f3d = mat.f3d_mat - + f3d.draw_layer.sm64 = layer self.set_register_settings(mat, f3d) self.set_textures(f3d, textures, tex_path) with bpy.context.temp_override(material=mat): bpy.ops.material.update_f3d_nodes() - + def set_textures(self, f3d: F3DMaterialProperty, textures: dict, tex_path: Path): self.set_tex_scale(f3d) if self.tex0 and self.set_tex: @@ -573,28 +577,21 @@ def set_textures(self, f3d: F3DMaterialProperty, textures: dict, tex_path: Path) f3d.tex0, self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, self.tex0), self.tiles[0], - self.tex0.Timg + self.tex0.Timg, ) if self.tex1 and self.set_tex: self.set_tex_settings( f3d.tex1, self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, self.tex1), self.tiles[1], - self.tex1.Timg + self.tex1.Timg, ) class SM64_F3D(DL): - def __init__(self, scene: bpy.types.Scene): - self.Vtx = {} - self.Gfx = {} - self.Light_t = {} - self.Ambient_t = {} - self.Lights1 = {} - self.Textures = {} + def __init__(self, scene): self.scene = scene - self.num = 0 - super().__init__() + super().__init__(lastmat=SM64_Material()) # Textures only contains the texture data found inside the model.inc.c file and the texture.inc.c file # this will add all the textures located in the /textures/ folder in decomp @@ -631,7 +628,7 @@ def get_generic_textures(self, root_path: Path): t.close() # recursively parse the display list in order to return a bunch of model data - def get_f3d_data_from_model(self, start: str): + def get_f3d_data_from_model(self, start: str, last_mat: SM64_Material = None): DL = self.Gfx.get(start) self.VertBuff = [0] * 32 # If you're doing some fucky shit with a larger vert buffer it sucks to suck I guess if not DL: @@ -641,7 +638,8 @@ def get_f3d_data_from_model(self, start: str): self.UVs = [] self.VCs = [] self.Mats = [] - self.LastMat = SM64_Material() + if last_mat: + self.LastMat = last_mat self.parse_stream(DL, start) self.NewMat = 0 self.StartName = start @@ -742,15 +740,75 @@ def create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): # holds model found by geo @dataclass class ModelDat: - translate: tuple - rotate: tuple + transform: Matrix layer: int - model: str - scale: float = 1.0 + model_name: str + vertex_group_name: str = None + switch_index: int = 0 + armature_obj: bpy.types.Object = None # base class for geo layouts and armatures class GraphNodes(DataParser): + def __init__( + self, + geo_layouts: dict, + scene: bpy.types.Scene, + name: str, + col: bpy.types.Collection, + parent_bone: bpy.types.Bone = None, + geo_parent: GeoArmature = None, + stream: Any = None, + ): + self.geo_layouts = geo_layouts + self.models = [] + self.children = [] + self.scene = scene + self.stream = stream + self.render_range = None + self.parent_transform = transform_mtx_blender_to_n64() + self.last_transform = transform_mtx_blender_to_n64() + self.name = name + self.col = col + super().__init__(parent=geo_parent) + + def parse_layer(self, layer: str): + if not layer.isdigit(): + layer = Layers.get(layer) + if not layer: + layer = 1 + return layer + + @property + def ordered_name(self): + return f"{self.get_parser(self.stream).head}_{self.name}" + + def get_translation(self, trans_vector: Sequence): + translation = [float(val) for val in trans_vector] + return [translation[0], -translation[2], translation[1]] + + def get_rotation(self, rot_vector: Sequence): + rotation = Euler((math.radians(float(val)) for val in rot_vector), "ZXY") + return rotate_quat_n64_to_blender(rotation.to_quaternion()).to_euler("XYZ") + + def set_transform(self, geo_obj, translation: Sequence): + raise Exception("you must call this function from a sublcass") + + def set_geo_type(self, geo_obj: bpy.types.Object, geo_type: str): + raise Exception("you must call this function from a sublcass") + + def set_draw_layer(self, geo_obj: bpy.types.Object, layer: int): + raise Exception("you must call this function from a sublcass") + + def make_root(self, name, *args): + raise Exception("you must call this function from a sublcass") + + def setup_geo_obj(self, *args): + raise Exception("you must call this function from a sublcass") + + def add_model(self, *args): + raise Exception("you must call this function from a sublcass") + def GEO_BRANCH_AND_LINK(self, macro: Macro, depth: int): new_geo_layout = self.geo_layouts.get(macro.args[0]) if new_geo_layout: @@ -774,201 +832,175 @@ def GEO_RETURN(self, macro: Macro, depth: int): return self.break_parse def GEO_CLOSE_NODE(self, macro: Macro, depth: int): - # if there is no more open nodes, then parent this to last node - if depth: - return self.break_parse - else: - return self.continue_parse + return self.break_parse - def GEO_OPEN_NODE(self, macro: Macro, depth: int): - if self.obj: - GeoChild = GeoLayout( - self.geo_layouts, self.obj, self.scene, self.name, self.area_root, col=self.col, geo_parent=self + def GEO_DISPLAY_LIST(self, macro: Macro, depth: int): + # translation, rotation, layer, model + geo_obj = self.add_model( + ModelDat(self.parent_transform, *macro.args), "display_list", self.display_list, macro.args[0] + ) + self.set_transform(geo_obj, self.last_transform) + return self.continue_parse + + def GEO_BILLBOARD_WITH_PARAMS_AND_DL(self, macro: Macro, depth: int): + transform = Matrix() + transform.translation = self.get_translation(macro.args[1:4]) + self.last_transform = self.parent_transform @ transform + + model = macro.args[-1] + if model != "NULL": + geo_obj = self.add_model( + ModelDat(self.last_transform, macro.args[0], model), "billboard", self.billboard, macro.args[0] ) else: - GeoChild = GeoLayout( - self.geo_layouts, self.root, self.scene, self.name, self.area_root, col=self.col, geo_parent=self - ) - GeoChild.parent_transform = self.last_transform - GeoChild.stream = self.stream - GeoChild.parse_stream(self.geo_layouts.get(self.stream), self.stream, depth + 1) - self.children.append(GeoChild) + geo_obj = self.setup_geo_obj("billboard", self.billboard, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) return self.continue_parse - # Append to models array. Only check this one for now - def GEO_DISPLAY_LIST(self, macro: Macro, depth: int): - # translation, rotation, layer, model - self.models.append(ModelDat(*self.parent_transform, *macro.args)) + def GEO_BILLBOARD_WITH_PARAMS(self, macro: Macro, depth: int): + transform = Matrix() + transform.translation = self.get_translation(macro.args[1:4]) + self.last_transform = self.parent_transform @ transform - # shadows aren't naturally supported but we can emulate them with custom geo cmds, note: possibly changed with fast64 updates, this is old code - def GEO_SHADOW(self, macro: Macro, depth: int): - obj = self.make_root_empty(self.name + "shadow empty", self.root) - obj.sm64_obj_type = "Custom Geo Command" - obj.customGeoCommand = "GEO_SHADOW" - obj.customGeoCommandArgs = ", ".join(macro.args) + geo_obj = self.setup_geo_obj("billboard", self.billboard, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) + return self.continue_parse + + def GEO_BILLBOARD(self, macro: Macro, depth: int): + self.setup_geo_obj("billboard", self.billboard, macro.args[0]) + return self.continue_parse def GEO_ANIMATED_PART(self, macro: Macro, depth: int): # layer, translation, DL - layer = macro.args[0] - Tlate = [float(arg) / bpy.context.scene.blenderToSM64Scale for arg in macro.args[1:4]] - Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - model = args[-1] - self.last_transform = [Tlate, self.last_transform[1]] - if model.strip() != "NULL": - self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) + transform = Matrix() + transform.translation = self.get_translation(macro.args[1:4]) + self.last_transform = self.parent_transform @ transform + model = macro.args[-1] + + if model != "NULL": + geo_obj = self.add_model( + ModelDat(self.last_transform, macro.args[0], model), "bone", self.animated_part, macro.args[0] + ) else: - obj = self.make_root_empty(self.name + "animated empty", self.root) - obj.location = Tlate + geo_obj = self.setup_geo_obj("bone", self.animated_part, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) + return self.continue_parse def GEO_ROTATION_NODE(self, macro: Macro, depth: int): - obj = self.GEO_ROTATE(macro) - if obj: - obj.sm64_obj_type = "Geo Rotation Node" + geo_obj = self.GEO_ROTATE(macro) + if geo_obj: + self.set_geo_type(geo_obj, self.rotate) + return self.continue_parse def GEO_ROTATE(self, macro: Macro, depth: int): - layer = macro.args[0] - Rotate = [math.radians(float(arg)) for arg in macro.args[1:4]] - Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.last_transform = [[0, 0, 0], Rotate] - self.last_transform = [[0, 0, 0], self.last_transform[1]] - obj = self.make_root_empty(self.name + "rotate", self.root) - obj.rotation_euler = Rotate - obj.sm64_obj_type = "Geo Translate/Rotate" - return obj + transform = Matrix.LocRotScale(Vector(), self.get_rotation(macro.args[1:4]), Vector()) + self.last_transform = self.parent_transform @ transform + return self.setup_geo_obj("rotate", self.translate_rotate, macro.args[0]) def GEO_ROTATION_NODE_WITH_DL(self, macro: Macro, depth: int): - obj = self.GEO_ROTATE_WITH_DL(macro) - if obj: - obj.sm64_obj_type = "Geo Translate/Rotate" + geo_obj = self.GEO_ROTATE_WITH_DL(macro) + return self.continue_parse def GEO_ROTATE_WITH_DL(self, macro: Macro, depth: int): - layer = macro.args[0] - Rotate = [math.radians(float(arg)) for arg in macro.args[1:4]] - Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.last_transform = [[0, 0, 0], Rotate] + transform = Matrix.LocRotScale(Vector(), self.get_rotation(macro.args[1:4]), Vector()) + self.last_transform = self.parent_transform @ transform + model = args[-1] - self.last_transform = [[0, 0, 0], self.last_transform[1]] - if model.strip() != "NULL": - self.models.append(ModelDat([0, 0, 0], Rotate, layer, model)) + if model != "NULL": + geo_obj = self.add_model( + ModelDat(self.last_transform, macro.args[0], model), "rotate", self.translate_rotate, macro.args[0] + ) else: - obj = self.make_root_empty(self.name + "rotate", self.root) - obj.rotation_euler = Rotate - obj.sm64_obj_type = "Geo Translate/Rotate" - return obj + geo_obj = self.setup_geo_obj("rotate", self.translate_rotate, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) + return geo_obj def GEO_TRANSLATE_ROTATE_WITH_DL(self, macro: Macro, depth: int): - layer = macro.args[0] - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in macro.args[1:4]] - Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - Rotate = [math.radians(float(arg)) for arg in macro.args[4:7]] - Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.last_transform = [Tlate, Rotate] + transform = Matrix.LocRotScale( + self.get_translation(macro.args[1:4]), self.get_rotation(macro.args[4:7]), Vector() + ) + self.last_transform = self.parent_transform @ transform + model = args[-1] - self.last_transform = [Tlate, self.last_transform[1]] - if model.strip() != "NULL": - self.models.append(ModelDat(Tlate, Rotate, layer, model)) + if model != "NULL": + geo_obj = self.add_model( + ModelDat(self.last_transform, macro.args[0], model), + "trans/rotate", + self.translate_rotate, + macro.args[0], + ) else: - obj = self.make_root_empty(self.name + "translate rotate", self.root) - obj.location = Tlate - obj.rotation_euler = Rotate - obj.sm64_obj_type = "Geo Translate/Rotate" + geo_obj = self.setup_geo_obj("trans/rotate", self.translate_rotate, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) + return self.continue_parse def GEO_TRANSLATE_ROTATE(self, macro: Macro, depth: int): - Tlate = [float(arg) / bpy.context.scene.blenderToSM64Scale for arg in macro.args[1:4]] - Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - Rotate = [math.radians(float(arg)) for arg in macro.args[4:7]] - Rotate = rotate_quat_n64_to_blender(Euler(Rotate, "ZXY").to_quaternion()).to_euler("XYZ") - self.last_transform = [Tlate, Rotate] - obj = self.make_root_empty(self.name + "translate", self.root) - obj.location = Tlate - obj.rotation_euler = Rotate - obj.sm64_obj_type = "Geo Translate/Rotate" + transform = Matrix.LocRotScale( + self.get_translation(macro.args[1:4]), self.get_rotation(macro.args[1:4]), Vector() + ) + self.last_transform = self.parent_transform @ transform + + geo_obj = self.setup_geo_obj("trans/rotate", self.translate_rotate, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) + return self.continue_parse def GEO_TRANSLATE_WITH_DL(self, macro: Macro, depth: int): - obj = self.GEO_TRANSLATE_NODE_WITH_DL(macro) - if obj: - obj.sm64_obj_type = "Geo Translate/Rotate" + geo_obj = self.GEO_TRANSLATE_NODE_WITH_DL(macro) + if geo_obj: + self.set_geo_type(geo_obj, self.translate_rotate) + return self.continue_parse def GEO_TRANSLATE_NODE_WITH_DL(self, macro: Macro, depth: int): - # translation, layer, model - layer = macro.args[0] - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in macro.args[1:4]] - Tlate = [Tlate[0], -Tlate[2], Tlate[1]] + transform = Matrix() + transform.translation = self.get_translation(macro.args[1:4]) + self.last_transform = self.parent_transform @ transform + model = macro.args[-1] - self.last_transform = [Tlate, (0, 0, 0)] - if model.strip() != "NULL": - self.models.append(ModelDat(Tlate, (0, 0, 0), layer, model)) + if model != "NULL": + geo_obj = self.add_model( + ModelDat(self.last_transform, macro.args[0], model), "translate", self.translate, macro.args[0] + ) else: - obj = self.make_root_empty(self.name + "translate", self.root) - obj.location = Tlate - obj.rotation_euler = Rotate - obj.sm64_obj_type = "Geo Translate Node" - return obj + geo_obj = self.setup_geo_obj("translate", self.translate, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) + return geo_obj def GEO_TRANSLATE(self, macro: Macro, depth: int): obj = self.GEO_TRANSLATE_NODE(macro) if obj: - obj.sm64_obj_type = "Geo Translate/Rotate" + self.set_geo_type(geo_obj, self.translate_rotate) + return self.continue_parse def GEO_TRANSLATE_NODE(self, macro: Macro, depth: int): - Tlate = [float(a) / bpy.context.scene.blenderToSM64Scale for a in macro.args[1:4]] - Tlate = [Tlate[0], -Tlate[2], Tlate[1]] - self.last_transform = [Tlate, self.last_transform[1]] - obj = self.make_root_empty(self.name + "translate", self.root) - obj.location = Tlate - obj.sm64_obj_type = "Geo Translate Node" - return obj + transform = Matrix() + transform.translation = self.get_translation(macro.args[1:4]) + self.last_transform = self.parent_transform @ transform + + geo_obj = self.setup_geo_obj("translate", self.translate, macro.args[0]) + self.set_transform(geo_obj, self.last_transform) + return geo_obj def GEO_SCALE_WITH_DL(self, macro: Macro, depth: int): scale = eval(macro.args[1]) / 0x10000 - model = macro.args[-1] - self.last_transform = [(0, 0, 0), self.last_transform[1]] - self.models.append(ModelDat((0, 0, 0), (0, 0, 0), layer, model, scale=scale)) + self.last_transform = scale * self.last_transform - def GEO_SCALE(self, macro: Macro, depth: int): - obj = self.make_root_empty(self.name + "scale", self.root) - scale = eval(macro.args[1]) / 0x10000 - obj.scale = (scale, scale, scale) - obj.sm64_obj_type = "Geo Scale" + model = macro.args[-1] + geo_obj = self.add_model(ModelDat(self.last_transform, macro.args[0], macro.args[-1])) + self.set_transform(geo_obj, self.last_transform) + return self.continue_parse def GEO_ASM(self, macro: Macro, depth: int): - obj = self.make_root_empty(self.name + "asm", self.root) - asm = self.obj.fast64.sm64.geo_asm - self.obj.sm64_obj_type = "Geo ASM" + geo_obj = self.setup_geo_obj("asm", self.asm) + # probably will need to be overridden by each subclass + asm = geo_obj.fast64.sm64.geo_asm asm.param = macro.args[0] asm.func = macro.args[1] - - def GEO_SWITCH_CASE(self, macro: Macro, depth: int): - obj = self.make_root_empty(self.name + "switch", self.root) - Switch = self.obj - Switch.sm64_obj_type = "Switch" - Switch.switchParam = eval(macro.args[0]) - Switch.switchFunc = macro.args[1] + return self.continue_parse # This has to be applied to meshes def GEO_RENDER_RANGE(self, macro: Macro, depth: int): self.render_range = macro.args - - # can only apply type to area root - def GEO_CAMERA(self, macro: Macro, depth: int): - self.area_root.camOption = "Custom" - self.area_root.camType = macro.args[0] - - # make better - def GEO_CAMERA_FRUSTUM_WITH_FUNC(self, macro: Macro, depth: int): - self.area_root.camOption = "Custom" - self.area_root.camType = macro.args[0] - - # Geo backgrounds is pointless because the only background possible is the one - # loaded in the level script. This is the only override - def GEO_BACKGROUND_COLOR(self, macro: Macro, depth: int): - self.area_root.areaOverrideBG = True - color = eval(macro.args[0]) - A = color & 1 - B = (color & 0x3E) > 1 - G = (color & (0x3E << 5)) >> 6 - R = (color & (0x3E << 10)) >> 11 - self.area_root.areaBGColor = (R / 0x1F, G / 0x1F, B / 0x1F, A) + return self.continue_parse # these have no affect on the bpy def GEO_BACKGROUND(self, macro: Macro, depth: int): @@ -986,44 +1018,91 @@ def GEO_NODE_ORTHO(self, macro: Macro, depth: int): def GEO_RENDER_OBJ(self, macro: Macro, depth: int): return self.continue_parse + # These need special bhv for each type + def GEO_SCALE(self, macro: Macro, depth: int): + raise Exception("you must call this function from a sublcass") + + def GEO_SWITCH_CASE(self, macro: Macro, depth: int): + raise Exception("you must call this function from a sublcass") + + def GEO_SHADOW(self, macro: Macro, depth: int): + raise Exception("you must call this function from a sublcass") + + def GEO_CAMERA(self, macro: Macro, depth: int): + raise Exception("you must call this function from a sublcass") + + def GEO_CAMERA_FRUSTUM_WITH_FUNC(self, macro: Macro, depth: int): + raise Exception("you must call this function from a sublcass") + + def GEO_BACKGROUND_COLOR(self, macro: Macro, depth: int): + raise Exception("you must call this function from a sublcass") + class GeoLayout(GraphNodes): + switch = "Switch" + translate_rotate = "Geo Translate/Rotat" + translate = "Geo Translate Node" + rotate = "Geo Rotation Node" + billboard = "Geo Billboard" + display_list = "Geo Displaylist" + shadow = "Custom Geo Command" + asm = "Geo ASM" + scale = "Geo Scale" + animated_part = "Geo Translate Node" + custom_animated = "Custom Geo Command" + custom = "Custom Geo Command" + def __init__( self, geo_layouts: dict, root: bpy.types.Object, scene: bpy.types.Scene, - name, + name: str, area_root: bpy.types.Object, col: bpy.types.Collection = None, geo_parent: GeoLayout = None, + stream: Any = None, ): - self.geo_layouts = geo_layouts self.parent = root - self.models = [] - self.children = [] - self.scene = scene - self.render_range = None self.area_root = area_root # for properties that can only be written to area self.root = root - self.parent_transform = [[0, 0, 0], [0, 0, 0]] - self.last_transform = [[0, 0, 0], [0, 0, 0]] - self.name = name self.obj = None # last object on this layer of the tree, will become parent of next child if not col: - self.col = area_root.users_collection[0] + col = area_root.users_collection[0] else: - self.col = col - super().__init__(parent=geo_parent) + col = col + super().__init__(geo_layouts, scene, name, col, geo_parent=geo_parent, stream=stream) + + def set_transform(self, geo_obj: bpy.types.Object, transform: Matrix): + if not geo_obj: + return + geo_obj.matrix_world = ( + geo_obj.matrix_world @ transform_matrix_to_bpy(transform) * (1 / self.scene.blenderToSM64Scale) + ) - def make_root_empty(self, name: str, root: bpy.types.Object): - # make an empty node to act as the root of this geo layout - # use this to hold a transform, or an actual cmd, otherwise rt is passed - E = bpy.data.objects.new(name, None) - self.obj = E - self.col.objects.link(E) - parentObject(root, E) - return E + def set_geo_type(self, geo_obj: bpy.types.Object, geo_cmd: str): + geo_obj.sm64_obj_type = geo_cmd + + def set_draw_layer(self, geo_obj: bpy.types.Object, layer: int): + geo_obj.draw_layer_static = self.geo_armature(layer) + + # make an empty node to act as the root of this geo layout + # use this to hold a transform, or an actual cmd, otherwise rt is passed + def make_root(self, name: str, parent_obj: bpy.types.Object): + self.obj = bpy.data.objects.new(name, None) + self.col.objects.link(self.obj) + parentObject(parent_obj, self.obj) + return self.obj + + def setup_geo_obj(self, obj_name: str, geo_cmd: str, layer: int = None): + geo_obj = self.make_root(f"{self.ordered_name} {obj_name}", self.root) + self.set_geo_type(geo_obj, "Geo Billboard") + if layer: + self.set_draw_layer(geo_obj, layer) + return geo_obj + + def add_model(self, model_data: ModelDat, *args): + self.models.append(model_data) def parse_level_geo(self, start: str, scene: bpy.types.Scene): geo_layout = self.geo_layouts.get(start) @@ -1038,6 +1117,278 @@ def parse_level_geo(self, start: str, scene: bpy.types.Scene): self.stream = start self.parse_stream(geo_layout, start, 0) + def GEO_SCALE(self, macro: Macro, depth: int): + scale = eval(macro.args[1]) / 0x10000 + geo_obj = self.setup_geo_obj("scale", self.scale, macro.args[0]) + geo_obj.scale = (scale, scale, scale) + return self.continue_parse + + # shadows aren't naturally supported but we can emulate them with custom geo cmds + # note: possibly changed with fast64 updates + def GEO_SHADOW(self, macro: Macro, depth: int): + geo_obj = self.setup_geo_obj("shadow empty", self.shadow) + # probably won't work in armatures?? + geo_obj.customGeoCommand = "GEO_SHADOW" + geo_obj.customGeoCommandArgs = ", ".join(macro.args) + return self.continue_parse + + def GEO_SWITCH_CASE(self, macro: Macro, depth: int): + geo_obj = self.setup_geo_obj("switch", self.switch) + # probably will need to be overridden by each subclass + geo_obj.switchParam = eval(macro.args[0]) + geo_obj.switchFunc = macro.args[1] + return self.continue_parse + + # This has to be applied to meshes + def GEO_RENDER_RANGE(self, macro: Macro, depth: int): + self.render_range = macro.args + return self.continue_parse + + # can only apply type to area root + def GEO_CAMERA(self, macro: Macro, depth: int): + self.area_root.camOption = "Custom" + self.area_root.camType = macro.args[0] + return self.continue_parse + + # make better + def GEO_CAMERA_FRUSTUM_WITH_FUNC(self, macro: Macro, depth: int): + self.area_root.camOption = "Custom" + self.area_root.camType = macro.args[0] + return self.continue_parse + + def GEO_OPEN_NODE(self, macro: Macro, depth: int): + if self.obj: + GeoChild = GeoLayout( + self.geo_layouts, + self.obj, + self.scene, + self.name, + self.area_root, + col=self.col, + geo_parent=self, + stream=self.stream, + ) + else: + GeoChild = GeoLayout( + self.geo_layouts, + self.root, + self.scene, + self.name, + self.area_root, + col=self.col, + geo_parent=self, + stream=self.stream, + ) + GeoChild.parent_transform = self.last_transform + GeoChild.parse_stream(self.geo_layouts.get(self.stream), self.stream, depth + 1) + self.children.append(GeoChild) + return self.continue_parse + + +class GeoArmature(GraphNodes): + switch = "Switch" + start = "Start" + translate_rotate = "TranslateRotate" + translate = "Translate" + rotate = "Rotate" + billboard = "Billboard" + display_list = "DisplayList" + shadow = "Shadow" + asm = "Function" + held_object = "HeldObject" + scale = "Scale" + render_area = "StartRenderArea" + animated_part = "DisplayListWithOffset" + custom_animated = "CustomAnimated" + custom = "CustomNonAnimated" + + def __init__( + self, + geo_layouts: dict, + armature_obj: bpy.types.Armature, + scene: bpy.types.Scene, + name: str, + col: bpy.types.Collection, + is_switch_child: bool = False, + parent_bone: bpy.types.Bone = None, + geo_parent: GeoArmature = None, + switch_armatures: dict[int, bpy.types.Object] = None, + stream: Any = None, + ): + self.armature = armature_obj + self.parent_bone = None if not parent_bone else parent_bone.name + self.bone = None + self.is_switch_child = is_switch_child + self.switch_index = 0 + if not switch_armatures: + self.switch_armatures = dict() + else: + self.switch_armatures = switch_armatures + super().__init__(geo_layouts, scene, name, col, geo_parent=geo_parent, stream=stream) + + def enter_edit_mode(self, geo_armature: bpy.types.Object): + geo_armature.select_set(True) + bpy.context.view_layer.objects.active = geo_armature + bpy.ops.object.mode_set(mode="EDIT", toggle=False) + + def get_or_init_geo_armature(self): + # if not the first child, make a new armature object and switch option root bone + if self.switch_index > 0 and not self.switch_armatures.get(self.switch_index, None): + name = f"{self.ordered_name} switch_option" + switch_armature = bpy.data.objects.new(name, bpy.data.armatures.new(name)) + self.col.objects.link(switch_armature) + self.switch_armatures[self.switch_index] = switch_armature + + self.enter_edit_mode(switch_armature) + edit_bone = switch_armature.data.edit_bones.new(name) + eb_name = edit_bone.name + # give it a non zero length + edit_bone.head = (0, 0, 0) + edit_bone.tail = (0, 0, 0.1) + bpy.ops.object.mode_set(mode="OBJECT", toggle=False) + self.parent_bone = name + switch_opt_bone = switch_armature.data.bones[name] + self.set_geo_type(switch_opt_bone, "SwitchOption") + # add switch option and set to mesh override + option = switch_opt_bone.switch_options.add() + option.switchType = "Mesh" + option.optionArmature = switch_armature + elif self.switch_armatures: + switch_armature = self.switch_armatures.get(self.switch_index, self.armature) + else: + switch_armature = self.armature + return switch_armature + + def set_transform(self, geo_bone: bpy.types.Bone, transform: Matrix): + # only the position of the head really matters, so the tail + # will take an ad hoc position of 1 above the head + name = geo_bone.name + self.enter_edit_mode(armature_obj := self.get_or_init_geo_armature()) + edit_bone = armature_obj.data.edit_bones.get(name, None) + location = transform_matrix_to_bpy(transform).to_translation() * (1 / self.scene.blenderToSM64Scale) + print(edit_bone, name, armature_obj) + edit_bone.head = location + edit_bone.tail = location + Vector((0, 0, 1)) + bpy.ops.object.mode_set(mode="OBJECT", toggle=False) + # due to blender ptr memes, swapping between edit and obj mode + # will mutate an attr, because the data struct self.bones is rebuilt + # or something idk, and now where the previous bone was is replaced by + # a new one, so I must retrieve it again + self.bone = armature_obj.data.bones[name] + # set the rotation mode + armature_obj.pose.bones[name].rotation_mode = "XYZ" + if self.is_switch_child: + self.switch_index += 1 + + def set_geo_type(self, geo_bone: bpy.types.Bone, geo_cmd: str): + geo_bone.geo_cmd = geo_cmd + + def set_draw_layer(self, geo_bone: bpy.types.Bone, layer: int): + geo_bone.draw_layer = str(self.parse_layer(layer)) + + def make_root(self, name: str): + self.enter_edit_mode(armature_obj := self.get_or_init_geo_armature()) + edit_bone = armature_obj.data.edit_bones.new(name) + eb_name = edit_bone.name + # give it a non zero length + edit_bone.head = (0, 0, 0) + edit_bone.tail = (0, 0, 1) + if self.parent_bone: + edit_bone.parent = armature_obj.data.edit_bones.get(self.parent_bone) + bpy.ops.object.mode_set(mode="OBJECT", toggle=False) + self.bone = armature_obj.data.bones[name] + return self.bone + + def setup_geo_obj(self, obj_name: str, geo_cmd: str, layer: int = None): + geo_bone = self.make_root(f"{self.ordered_name} {obj_name}") + self.set_geo_type(geo_bone, geo_cmd) + if layer: + self.set_draw_layer(geo_bone, layer) + return geo_bone + + def add_model(self, model_data: ModelDat, obj_name: str, geo_cmd: str, layer: int = None): + ind = self.get_parser(self.stream).head + self.models.append(model_data) + model_data.vertex_group_name = f"{self.ordered_name} {obj_name} {model_data.model_name}" + model_data.switch_index = self.switch_index + return self.setup_geo_obj(f"{obj_name} {model_data.model_name}", geo_cmd, layer) + + def parse_armature(self, start: str, scene: bpy.types.Scene): + geo_layout = self.geo_layouts.get(start) + if not geo_layout: + raise Exception( + "Could not find geo layout {} from levels/{}/{}geo.c".format( + start, scene.LevelImp.Level, scene.LevelImp.Prefix + ) + ) + bpy.context.view_layer.objects.active = self.get_or_init_geo_armature() + self.stream = start + self.parse_stream(geo_layout, start, 0) + + def GEO_SHADOW(self, macro: Macro, depth: int): + geo_bone = self.setup_geo_obj("shadow", self.shadow) + geo_bone.shadow_solidity = hexOrDecInt(macro.args[1]) / 255 + geo_bone.shadow_scale = hexOrDecInt(macro.args[2]) + return self.continue_parse + + def GEO_SWITCH_CASE(self, macro: Macro, depth: int): + geo_bone = self.setup_geo_obj("switch", self.switch) + # probably will need to be overridden by each subclass + geo_bone.func_param = eval(macro.args[0]) + geo_bone.geo_func = macro.args[1] + return self.continue_parse + + def GEO_SCALE_WITH_DL(self, macro: Macro, depth: int): + scale = eval(macro.args[1]) / 0x10000 + self.last_transform = [(0, 0, 0), self.last_transform[1]] + + model = macro.args[-1] + geo_obj = self.add_model( + ModelDat((0, 0, 0), (0, 0, 0), macro.args[0], macro.args[-1], scale=scale), + "scale", + self.scale, + macro.args[0], + ) + self.set_transform(geo_obj, self.last_transform) + return self.continue_parse + + def GEO_SCALE(self, macro: Macro, depth: int): + scale = eval(macro.args[1]) / 0x10000 + + geo_bone = self.setup_geo_obj("scale", self.scale, macro.args[0]) + geo_bone.geo_scale = scale + return self.continue_parse + + def GEO_OPEN_NODE(self, macro: Macro, depth: int): + if self.bone: + GeoChild = GeoArmature( + self.geo_layouts, + self.get_or_init_geo_armature(), + self.scene, + self.name, + self.col, + is_switch_child=(self.bone.geo_cmd == self.switch), + parent_bone=self.bone, + geo_parent=self, + stream=self.stream, + switch_armatures=self.switch_armatures, + ) + else: + GeoChild = GeoArmature( + self.geo_layouts, + self.get_or_init_geo_armature(), + self.scene, + self.name, + self.col, + geo_parent=self, + stream=self.stream, + switch_armatures=self.switch_armatures, + ) + GeoChild.parent_transform = self.last_transform + GeoChild.parse_stream(self.geo_layouts.get(self.stream), self.stream, depth + 1) + self.children.append(GeoChild) + return self.continue_parse + # ------------------------------------------------------------------------ # Functions @@ -1079,53 +1430,135 @@ def write_level_objects(lvl: Level, col_name: str = None): area.place_objects(col_name=col_name) +# from a geo layout, create all the mesh's +def write_armature_to_bpy( + geo_armature: GeoArmature, + scene: bpy.types.Scene, + f3d_dat: SM64_F3D, + root_path: Path, + parsed_model_data: dict, + cleanup: bool = True, +): + parsed_model_data = recurse_armature(geo_armature, scene, f3d_dat, root_path, parsed_model_data, cleanup=cleanup) + + objects_by_armature = dict() + for model_dat in parsed_model_data.values(): + if not objects_by_armature.get(model_dat.armature_obj, None): + objects_by_armature[model_dat.armature_obj] = [model_dat.object] + else: + objects_by_armature[model_dat.armature_obj].append(model_dat.object) + + for armature_obj, objects in objects_by_armature.items(): + # I don't really know the specific override needed for this to work + override = {**bpy.context.copy(), "selected_editable_objects": objects, "active_object": objects[0]} + with bpy.context.temp_override(**override): + bpy.ops.object.join() + + obj = objects[0] + parentObject(armature_obj, obj) + obj.scale *= 1 / scene.blenderToSM64Scale + rotate_object(-90, obj) + obj.ignore_collision = True + # armature deform + mod = obj.modifiers.new("deform", "ARMATURE") + mod.object = geo_armature.armature + + +def apply_mesh_data( + f3d_dat: SM64_F3D, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, root_path: Path, cleanup: bool = True +): + f3d_dat.apply_mesh_data(obj, mesh, layer, root_path) + if cleanup: + mesh = obj.data + # clean up after applying dat + mesh.validate() + mesh.update(calc_edges=True) + # final operators to clean stuff up + # shade smooth + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.shade_smooth() + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.remove_doubles() + bpy.ops.object.mode_set(mode="OBJECT") + + +def recurse_armature( + geo_armature: GeoArmature, + scene: bpy.types.Scene, + f3d_dat: SM64_F3D, + root_path: Path, + parsed_model_data: dict, + cleanup: bool = True, +): + if geo_armature.models: + # create a mesh for each one + for model_data in geo_armature.models: + name = f"{model_data.model_name} data" + if name in parsed_model_data.keys(): + mesh = parsed_model_data[name].mesh + name = 0 + else: + mesh = bpy.data.meshes.new(name) + model_data.mesh = mesh + parsed_model_data[name] = model_data + [verts, tris] = f3d_dat.get_f3d_data_from_model(model_data.model_name) + mesh.from_pydata(verts, [], tris) + + obj = bpy.data.objects.new(f"{model_data.model_name} obj", mesh) + + obj.matrix_world = transform_matrix_to_bpy(model_data.transform) * (1 / scene.blenderToSM64Scale) + + model_data.object = obj + geo_armature.col.objects.link(obj) + if model_data.vertex_group_name: + vertex_group = obj.vertex_groups.new(name=model_data.vertex_group_name) + vertex_group.add([vert.index for vert in obj.data.vertices], 1, "ADD") + if model_data.switch_index: + model_data.armature_obj = geo_armature.switch_armatures[model_data.switch_index] + else: + model_data.armature_obj = geo_armature.armature + + if name: + layer = geo_armature.parse_layer(model_data.layer) + apply_mesh_data(f3d_dat, obj, mesh, layer, root_path, cleanup) + + if not geo_armature.children: + return parsed_model_data + for arm in geo_armature.children: + parsed_model_data = recurse_armature(arm, scene, f3d_dat, root_path, parsed_model_data, cleanup=cleanup) + return parsed_model_data + + # from a geo layout, create all the mesh's def write_geo_to_bpy( geo: GeoLayout, scene: bpy.types.Scene, f3d_dat: SM64_F3D, root_path: Path, meshes: dict, cleanup: bool = True ): if geo.models: - rt = geo.root - col = geo.col # create a mesh for each one. - for m in geo.models: - name = m.model + " Data" + for model_data in geo.models: + name = f"{model_data.model_name} data" if name in meshes.keys(): mesh = meshes[name] name = 0 else: mesh = bpy.data.meshes.new(name) meshes[name] = mesh - [verts, tris] = f3d_dat.get_f3d_data_from_model(m.model.strip()) + [verts, tris] = f3d_dat.get_f3d_data_from_model(model_data.model_name) mesh.from_pydata(verts, [], tris) - obj = bpy.data.objects.new(m.model + " Obj", mesh) - layer = m.layer - if not layer.isdigit(): - layer = Layers.get(layer) - if not layer: - layer = 1 - obj.draw_layer_static = layer - col.objects.link(obj) - parentObject(rt, obj) - rotate_object(-90, obj) - scale = m.scale / scene.blenderToSM64Scale - obj.scale = [scale, scale, scale] - obj.location = m.translate + obj = bpy.data.objects.new(f"{model_data.model_name} obj", mesh) + geo.col.objects.link(obj) + parentObject(geo.root, obj, keep=1) + + obj.matrix_world = transform_matrix_to_bpy(model_data.transform) * (1 / scene.blenderToSM64Scale) + obj.ignore_collision = True + obj.draw_layer_static = geo_armature.parse_layer(model_data.layer) + if name: - f3d_dat.apply_mesh_data(obj, mesh, layer, root_path) - if cleanup: - # clean up after applying dat - mesh.validate() - mesh.update(calc_edges=True) - # final operators to clean stuff up - # shade smooth - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - bpy.ops.object.shade_smooth() - bpy.ops.object.mode_set(mode="EDIT") - bpy.ops.mesh.remove_doubles() - bpy.ops.object.mode_set(mode="OBJECT") + apply_mesh_data(f3d, obj, mesh, layer, root_path, cleanup) + if not geo.children: return for g in geo.children: @@ -1135,14 +1568,13 @@ def write_geo_to_bpy( # write the gfx for a level given the level data, and f3d data def write_level_to_bpy(lvl: Level, scene: bpy.types.Scene, root_path: Path, f3d_dat: SM64_F3D, cleanup: bool = True): for area in lvl.areas.values(): - print(area, area.geo) write_geo_to_bpy(area.geo, scene, f3d_dat, root_path, dict(), cleanup=cleanup) return lvl # given a geo.c file and a path, return cleaned up geo layouts in a dict -def construct_geo_layouts_from_file(geo: TextIO, root_path: Path): - geo_layout_files = get_all_aggregates(geo, "geo.inc.c", root_path) +def construct_geo_layouts_from_file(geo_path: Path, root_path: Path): + geo_layout_files = get_all_aggregates(geo_path, "geo.inc.c", root_path) if not geo_layout_files: return # because of fast64, these can be recursively defined (though I expect only a depth of one) @@ -1227,17 +1659,31 @@ def construct_model_data_from_file(aggregate: Path, scene: bpy.types.Scene, root # Parse an aggregate group file or level data file for geo layouts def find_actor_models_from_geo( - geo: TextIO, - Layout: str, + geo_path: Path, + layout_name: str, scene: bpy.types.Scene, - rt: bpy.types.Object, + root_obj: bpy.types.Object, root_path: Path, col: bpy.types.Collection = None, ): - GeoLayouts = construct_geo_layouts_from_file(geo, root_path) - Geo = GeoLayout(GeoLayouts, rt, scene, "{}".format(Layout), rt, col=col) - Geo.parse_level_geo(Layout, scene) - return Geo + geo_layout_dict = construct_geo_layouts_from_file(geo_path, root_path) + geo_layout = GeoLayout(geo_layout_dict, root_obj, scene, "{}".format(layout_name), root_obj, col=col) + geo_layout.parse_level_geo(layout_name, scene) + return geo_layout + + +def find_armature_models_from_geo( + geo_path: Path, + layout_name: str, + scene: bpy.types.Scene, + armature_obj: bpy.types.Armature, + root_path: Path, + col: bpy.types.Collection, +): + geo_layout_dict = construct_geo_layouts_from_file(geo_path, root_path) + geo_armature = GeoArmature(geo_layout_dict, armature_obj, scene, "{}".format(layout_name), col, stream=layout_name) + geo_armature.parse_armature(layout_name, scene) + return geo_armature # Find DL references given a level geo file and a path to a level folder @@ -1268,7 +1714,6 @@ def import_level_graphics( ): lvl = find_level_models_from_geo(geo, lvl, scene, root_path, col_name=col_name) models = construct_model_data_from_file(aggregate, scene, root_path) - print(lvl.areas, aggregate) # just a try, in case you are importing from something other than base decomp repo (like RM2C output folder) try: models.get_generic_textures(root_path) @@ -1354,22 +1799,60 @@ def execute(self, context): scene.gameEditorMode = "SM64" path = Path(bpy.path.abspath(scene.decompPath)) folder = path / scene.ActImp.FolderType - Layout = scene.ActImp.GeoLayout + layout_name = scene.ActImp.GeoLayout + prefix = scene.ActImp.Prefix + # different name schemes and I have no clean way to deal with it + if "actor" in scene.ActImp.FolderType: + geo_path = folder / (prefix + "_geo.c") + leveldat = folder / (prefix + ".c") + else: + geo_path = folder / (prefix + "geo.c") + leveldat = folder / (prefix + "leveldata.c") + root_obj = bpy.data.objects.new("Empty", None) + root_obj.name = f"Actor {scene.ActImp.GeoLayout}" + rt_col.objects.link(root_obj) + + geo_layout = find_actor_models_from_geo( + geo_path, layout_name, scene, root_obj, folder, col=rt_col + ) # return geo layout class and write the geo layout + models = construct_model_data_from_file(leveldat, scene, folder) + # just a try, in case you are importing from not the base decomp repo + try: + models.get_generic_textures(path) + except: + print("could not import genric textures, if this errors later from missing textures this may be why") + write_geo_to_bpy(geo_layout, scene, models, folder, {}, cleanup=self.cleanup) + return {"FINISHED"} + + +class SM64_OT_Armature_Import(Operator): + bl_label = "Import Armature" + bl_idname = "wm.sm64_import_armature" + bl_options = {"REGISTER", "UNDO"} + + cleanup: bpy.props.BoolProperty(name="Cleanup Mesh", default=1) + + def execute(self, context): + scene = context.scene + rt_col = context.collection + scene.gameEditorMode = "SM64" + path = Path(bpy.path.abspath(scene.decompPath)) + folder = path / scene.ActImp.FolderType + layout_name = scene.ActImp.GeoLayout prefix = scene.ActImp.Prefix # different name schemes and I have no clean way to deal with it if "actor" in scene.ActImp.FolderType: - geo = folder / (prefix + "_geo.c") + geo_path = folder / (prefix + "_geo.c") leveldat = folder / (prefix + ".c") else: - geo = folder / (prefix + "geo.c") + geo_path = folder / (prefix + "geo.c") leveldat = folder / (prefix + "leveldata.c") - geo = open(geo, "r", newline="") - Root = bpy.data.objects.new("Empty", None) - Root.name = "Actor %s" % scene.ActImp.GeoLayout - rt_col.objects.link(Root) + name = f"Actor {scene.ActImp.GeoLayout}" + armature_obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) + rt_col.objects.link(armature_obj) - Geo = find_actor_models_from_geo( - geo, Layout, scene, Root, folder, col=rt_col + geo_armature = find_armature_models_from_geo( + geo_path, layout_name, scene, armature_obj, folder, col=rt_col ) # return geo layout class and write the geo layout models = construct_model_data_from_file(leveldat, scene, folder) # just a try, in case you are importing from not the base decomp repo @@ -1377,8 +1860,7 @@ def execute(self, context): models.get_generic_textures(path) except: print("could not import genric textures, if this errors later from missing textures this may be why") - meshes = {} # re use mesh data when the same DL is referenced (bbh is good example) - write_geo_to_bpy(Geo, scene, models, folder, meshes, cleanup=self.cleanup) + write_armature_to_bpy(geo_armature, scene, models, folder, {}, cleanup=self.cleanup) return {"FINISHED"} @@ -1612,6 +2094,7 @@ def draw(self, context): layout.prop(ActImp, "Version") layout.prop(ActImp, "Target") layout.operator("wm.sm64_import_actor") + layout.operator("wm.sm64_import_armature") classes = ( @@ -1622,6 +2105,7 @@ def draw(self, context): SM64_OT_Lvl_Col_Import, SM64_OT_Obj_Import, SM64_OT_Act_Import, + SM64_OT_Armature_Import, Level_PT_Panel, Actor_PT_Panel, ) diff --git a/fast64_internal/utility_importer.py b/fast64_internal/utility_importer.py index dc0fef4a7..e40283a6d 100644 --- a/fast64_internal/utility_importer.py +++ b/fast64_internal/utility_importer.py @@ -5,7 +5,10 @@ from functools import partial from dataclasses import dataclass from pathlib import Path -from typing import TextIO, Any, Sequence, Union +from typing import TextIO, Any, Union +from numbers import Number +from collections.abc import Sequence +from .utility import transform_mtx_blender_to_n64 @dataclass @@ -18,6 +21,7 @@ def __post_init__(self): self.args = [arg.strip() for arg in self.args] self.cmd = self.cmd.strip() + @dataclass class Parser: cur_stream: Sequence[Any] @@ -28,6 +32,7 @@ def stream(self): self.head += 1 yield self.cur_stream[self.head] + # basic methods and utility to parse scripts or data streams of bytecode class DataParser: @@ -55,18 +60,24 @@ def parse_stream(self, dat_stream: Sequence[Any], entry_id: Any, *args, **kwargs flow_status = func(cur_macro, *args, **kwargs) if flow_status == self.break_parse: return - + + def reset_parser(self, entry_id: Any): + self.parsed_streams[entry_id] = None + def get_parser(self, entry_id: Any, relative_offset: int = 0): parser = self.parsed_streams[entry_id] parser.head += relative_offset - return parser.cur_stream - + return parser def c_macro_split(self, macro: str) -> list[str]: args_start = macro.find("(") return Macro(macro[:args_start], macro[args_start + 1 : macro.rfind(")")].split(",")) +def transform_matrix_to_bpy(transform: Matrix) -> Matrix: + return transform_mtx_blender_to_n64().inverted() @ transform @ transform_mtx_blender_to_n64() + + def evaluate_macro(line: str): scene = bpy.context.scene if scene.LevelImp.Version in line: @@ -85,7 +96,7 @@ def pre_parse_file(file: TextIO) -> list[str]: for line in file.splitlines(): # remove line comment if (comment := line.rfind("//")) > 0: - line = line[: comment] + line = line[:comment] # check for macro if "#ifdef" in line: skip_macro = evaluate_macro(line) @@ -128,20 +139,24 @@ def get_data_types_from_file(file: TextIO, type_dict, collated=False): # from a raw file, create a dict of types. Types should all be arrays file_lines = pre_parse_file(file) array_bounds_regx = "\[[0-9a-fx]*\]" # basically [] with any valid number in it + equality_regx = "\s*=" # finds the first char before the equals sign output_variables = {type_name: dict() for type_name in type_dict.keys()} type_found = None var_dat_buffer = [] for line in file_lines: if type_found: # Check for end of array - if "};" in line: + if ";" in line: output_variables[type_found[0]][type_found[1]] = "".join(var_dat_buffer) type_found = None var_dat_buffer = [] else: var_dat_buffer.append(line) continue + # name ends at the array bounds, or the equals sign match = re.search(array_bounds_regx, line, flags=re.IGNORECASE) + if not match: + match = re.search(equality_regx, line, flags=re.IGNORECASE) type_collisions = [type_name for type_name in type_dict.keys() if type_name in line] if match and type_collisions: # there should ideally only be one collision @@ -155,7 +170,11 @@ def get_data_types_from_file(file: TextIO, type_dict, collated=False): output_variables[data_type][variable] = format_data_arr(data, delimiters) # if collated, organize by data type, otherwise just take the various dicts raw - return output_variables if collated else {vd_key:vd_value for var_dict in output_variables.values() for vd_key, vd_value in var_dict.items() } + return ( + output_variables + if collated + else {vd_key: vd_value for var_dict in output_variables.values() for vd_key, vd_value in var_dict.items()} + ) # takes a raw string representing data and then formats it into an array From a7ce46f1e1fdd7fcb82fb4522d95498ee7aaaee5 Mon Sep 17 00:00:00 2001 From: scut Date: Sat, 9 Sep 2023 20:30:37 -0400 Subject: [PATCH 06/11] fixed file cleaning errors, fixed vtx buffer name parsing issue, fixed ifdef parsing bug, added additional cases for geo and material parsing and made script parsing more accurate when jumping. fixed initial transform of geo objects --- fast64_internal/f3d/f3d_import.py | 9 +++- fast64_internal/sm64/sm64_level_importer.py | 57 ++++++++++++--------- fast64_internal/utility_importer.py | 8 +++ 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py index 631cbce4e..acb1aba60 100644 --- a/fast64_internal/f3d/f3d_import.py +++ b/fast64_internal/f3d/f3d_import.py @@ -426,7 +426,7 @@ def gsSPVertex(self, macro: Macro): else: ref = macro.args[0] offset = 0 - VB = self.Vtx.get(ref) + VB = self.Vtx.get(ref.strip()) if not VB: raise Exception( "Could not find VB {} in levels/{}/{}leveldata.inc.c".format( @@ -528,6 +528,10 @@ def gsSPSetLights6(self, macro: Macro): def gsSPSetLights7(self, macro: Macro): return self.continue_parse + # upper/lower modes + def gsDPSetAlphaCompare(self, macro: Macro): + return self.continue_parse + def gsDPSetDepthSource(self, macro: Macro): return self.continue_parse @@ -691,6 +695,9 @@ def gsDPSetTile(self, macro: Macro): return self.continue_parse # syncs need no processing + def gsSPCullDisplayList(self, macro: Macro): + return self.continue_parse + def gsDPPipeSync(self, macro: Macro): return self.continue_parse diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index c5bc64f9c..6e1672c91 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -221,13 +221,13 @@ def parse_level_script(self, entry: str, col: bpy.types.Collection = None): scale = self.scene.blenderToSM64Scale if not col: col = self.scene.collection - self.parse_stream(script_stream, entry, col) + self.parse_stream_from_start(script_stream, entry, col) return self.areas def AREA(self, macro: Macro, col: bpy.types.Collection): area_root = bpy.data.objects.new("Empty", None) if self.scene.LevelImp.UseCol: - area_col = bpy.data.collections.new(f"{self.scene.LevelImp.Level} area {args[0]}") + area_col = bpy.data.collections.new(f"{self.scene.LevelImp.Level} area {macro.args[0]}") col.children.link(area_col) else: area_col = col @@ -766,8 +766,8 @@ def __init__( self.scene = scene self.stream = stream self.render_range = None - self.parent_transform = transform_mtx_blender_to_n64() - self.last_transform = transform_mtx_blender_to_n64() + self.parent_transform = transform_mtx_blender_to_n64().inverted() + self.last_transform = transform_mtx_blender_to_n64().inverted() self.name = name self.col = col super().__init__(parent=geo_parent) @@ -812,13 +812,13 @@ def add_model(self, *args): def GEO_BRANCH_AND_LINK(self, macro: Macro, depth: int): new_geo_layout = self.geo_layouts.get(macro.args[0]) if new_geo_layout: - self.parse_stream(new_geo_layout, depth) + self.parse_stream_from_start(new_geo_layout, macro.args[0], depth) return self.continue_parse def GEO_BRANCH(self, macro: Macro, depth: int): new_geo_layout = self.geo_layouts.get(macro.args[1]) if new_geo_layout: - self.parse_stream(new_geo_layout, depth) + self.parse_stream_from_start(new_geo_layout, macro.args[1], depth) # arg 0 determines if you return and continue or end after the branch if eval(macro.args[0]): return self.continue_parse @@ -887,25 +887,25 @@ def GEO_ANIMATED_PART(self, macro: Macro, depth: int): return self.continue_parse def GEO_ROTATION_NODE(self, macro: Macro, depth: int): - geo_obj = self.GEO_ROTATE(macro) + geo_obj = self.GEO_ROTATE(macro, depth) if geo_obj: self.set_geo_type(geo_obj, self.rotate) return self.continue_parse def GEO_ROTATE(self, macro: Macro, depth: int): - transform = Matrix.LocRotScale(Vector(), self.get_rotation(macro.args[1:4]), Vector()) + transform = Matrix.LocRotScale(Vector(), self.get_rotation(macro.args[1:4]), Vector((1, 1, 1))) self.last_transform = self.parent_transform @ transform return self.setup_geo_obj("rotate", self.translate_rotate, macro.args[0]) def GEO_ROTATION_NODE_WITH_DL(self, macro: Macro, depth: int): - geo_obj = self.GEO_ROTATE_WITH_DL(macro) + geo_obj = self.GEO_ROTATE_WITH_DL(macro, depth) return self.continue_parse def GEO_ROTATE_WITH_DL(self, macro: Macro, depth: int): - transform = Matrix.LocRotScale(Vector(), self.get_rotation(macro.args[1:4]), Vector()) + transform = Matrix.LocRotScale(Vector(), self.get_rotation(macro.args[1:4]), Vector((1, 1, 1))) self.last_transform = self.parent_transform @ transform - model = args[-1] + model = macro.args[-1] if model != "NULL": geo_obj = self.add_model( ModelDat(self.last_transform, macro.args[0], model), "rotate", self.translate_rotate, macro.args[0] @@ -917,11 +917,11 @@ def GEO_ROTATE_WITH_DL(self, macro: Macro, depth: int): def GEO_TRANSLATE_ROTATE_WITH_DL(self, macro: Macro, depth: int): transform = Matrix.LocRotScale( - self.get_translation(macro.args[1:4]), self.get_rotation(macro.args[4:7]), Vector() + self.get_translation(macro.args[1:4]), self.get_rotation(macro.args[4:7]), Vector((1, 1, 1)) ) self.last_transform = self.parent_transform @ transform - model = args[-1] + model = macro.args[-1] if model != "NULL": geo_obj = self.add_model( ModelDat(self.last_transform, macro.args[0], model), @@ -936,7 +936,7 @@ def GEO_TRANSLATE_ROTATE_WITH_DL(self, macro: Macro, depth: int): def GEO_TRANSLATE_ROTATE(self, macro: Macro, depth: int): transform = Matrix.LocRotScale( - self.get_translation(macro.args[1:4]), self.get_rotation(macro.args[1:4]), Vector() + self.get_translation(macro.args[1:4]), self.get_rotation(macro.args[1:4]), Vector((1, 1, 1)) ) self.last_transform = self.parent_transform @ transform @@ -945,7 +945,7 @@ def GEO_TRANSLATE_ROTATE(self, macro: Macro, depth: int): return self.continue_parse def GEO_TRANSLATE_WITH_DL(self, macro: Macro, depth: int): - geo_obj = self.GEO_TRANSLATE_NODE_WITH_DL(macro) + geo_obj = self.GEO_TRANSLATE_NODE_WITH_DL(macro, depth) if geo_obj: self.set_geo_type(geo_obj, self.translate_rotate) return self.continue_parse @@ -966,7 +966,7 @@ def GEO_TRANSLATE_NODE_WITH_DL(self, macro: Macro, depth: int): return geo_obj def GEO_TRANSLATE(self, macro: Macro, depth: int): - obj = self.GEO_TRANSLATE_NODE(macro) + obj = self.GEO_TRANSLATE_NODE(macro, depth) if obj: self.set_geo_type(geo_obj, self.translate_rotate) return self.continue_parse @@ -1003,6 +1003,9 @@ def GEO_RENDER_RANGE(self, macro: Macro, depth: int): return self.continue_parse # these have no affect on the bpy + def GEO_NODE_START(self, macro: Macro, depth: int): + return self.continue_parse + def GEO_BACKGROUND(self, macro: Macro, depth: int): return self.continue_parse @@ -1084,7 +1087,7 @@ def set_geo_type(self, geo_obj: bpy.types.Object, geo_cmd: str): geo_obj.sm64_obj_type = geo_cmd def set_draw_layer(self, geo_obj: bpy.types.Object, layer: int): - geo_obj.draw_layer_static = self.geo_armature(layer) + geo_obj.draw_layer_static = str(self.parse_layer(layer)) # make an empty node to act as the root of this geo layout # use this to hold a transform, or an actual cmd, otherwise rt is passed @@ -1112,11 +1115,9 @@ def parse_level_geo(self, start: str, scene: bpy.types.Scene): start, scene.LevelImp.Level, scene.LevelImp.Prefix ) ) - # This won't parse the geo layout perfectly. For now I'll just get models. This is mostly because fast64 - # isn't a bijection to geo layouts, the props are sort of handled all over the place self.stream = start - self.parse_stream(geo_layout, start, 0) - + self.parse_stream_from_start(geo_layout, start, 0) + def GEO_SCALE(self, macro: Macro, depth: int): scale = eval(macro.args[1]) / 0x10000 geo_obj = self.setup_geo_obj("scale", self.scale, macro.args[0]) @@ -1323,8 +1324,14 @@ def parse_armature(self, start: str, scene: bpy.types.Scene): ) bpy.context.view_layer.objects.active = self.get_or_init_geo_armature() self.stream = start - self.parse_stream(geo_layout, start, 0) + self.parse_stream_from_start(geo_layout, start, 0) + def GEO_ASM(self, macro: Macro, depth: int): + geo_obj = self.setup_geo_obj("asm", self.asm) + geo_obj.func_param = int(macro.args[0]) + geo_obj.geo_func = macro.args[1] + return self.continue_parse + def GEO_SHADOW(self, macro: Macro, depth: int): geo_bone = self.setup_geo_obj("shadow", self.shadow) geo_bone.shadow_solidity = hexOrDecInt(macro.args[1]) / 255 @@ -1554,10 +1561,11 @@ def write_geo_to_bpy( obj.matrix_world = transform_matrix_to_bpy(model_data.transform) * (1 / scene.blenderToSM64Scale) obj.ignore_collision = True - obj.draw_layer_static = geo_armature.parse_layer(model_data.layer) + layer = geo.parse_layer(model_data.layer) + obj.draw_layer_static = layer if name: - apply_mesh_data(f3d, obj, mesh, layer, root_path, cleanup) + apply_mesh_data(f3d_dat, obj, mesh, layer, root_path, cleanup) if not geo.children: return @@ -1603,7 +1611,6 @@ def construct_sm64_f3d_data_from_file(gfx: SM64_F3D, model_file: TextIO): for key, value in gfx_dat.items(): attr = getattr(gfx, key) attr.update(value) - # For textures, try u8, and s16 aswell gfx.Textures.update( get_data_types_from_file( model_file, diff --git a/fast64_internal/utility_importer.py b/fast64_internal/utility_importer.py index e40283a6d..8dd2791b2 100644 --- a/fast64_internal/utility_importer.py +++ b/fast64_internal/utility_importer.py @@ -47,6 +47,12 @@ def __init__(self, parent: DataParser = None): else: self.parsed_streams = dict() + # for if you're jumping, you start from the beginning, but if you're starting/stopping + # then you want to just pickup from the last spot + def parse_stream_from_start(self, dat_stream: Sequence[Any], entry_id: Any, *args, **kwargs): + self.reset_parser(entry_id) + self.parse_stream(dat_stream, entry_id, *args, **kwargs) + def parse_stream(self, dat_stream: Sequence[Any], entry_id: Any, *args, **kwargs): parser = self.parsed_streams.get(entry_id) if not parser: @@ -100,8 +106,10 @@ def pre_parse_file(file: TextIO) -> list[str]: # check for macro if "#ifdef" in line: skip_macro = evaluate_macro(line) + continue if "#elif" in line: skip_macro = evaluate_macro(line) + continue if "#else" in line or "#endif" in line: skip_macro = 0 continue From 17d7b7e61964151197be749ffed34544772d17c1 Mon Sep 17 00:00:00 2001 From: scut Date: Sun, 10 Sep 2023 16:28:19 -0400 Subject: [PATCH 07/11] added enums for selecting actor models to import --- fast64_internal/f3d/f3d_import.py | 6 +- fast64_internal/sm64/sm64_constants.py | 266 +++++++++++++++- fast64_internal/sm64/sm64_level_importer.py | 333 +++++++++++++++----- fast64_internal/sm64/sm64_objects.py | 8 +- fast64_internal/utility_importer.py | 4 +- 5 files changed, 518 insertions(+), 99 deletions(-) diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py index acb1aba60..8073058a0 100644 --- a/fast64_internal/f3d/f3d_import.py +++ b/fast64_internal/f3d/f3d_import.py @@ -396,7 +396,7 @@ def gsSPBranchList(self, macro: Macro): if not NewDL: raise Exception( "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( - NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + NewDL, self.scene.level_import.Level, self.scene.level_import.Prefix ) ) self.reset_parser(branched_dl) @@ -408,7 +408,7 @@ def gsSPDisplayList(self, macro: Macro): if not NewDL: raise Exception( "Could not find DL {} in levels/{}/{}leveldata.inc.c".format( - NewDL, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + NewDL, self.scene.level_import.Level, self.scene.level_import.Prefix ) ) self.reset_parser(branched_dl) @@ -430,7 +430,7 @@ def gsSPVertex(self, macro: Macro): if not VB: raise Exception( "Could not find VB {} in levels/{}/{}leveldata.inc.c".format( - ref, self.scene.LevelImp.Level, self.scene.LevelImp.Prefix + ref, self.scene.level_import.Level, self.scene.level_import.Prefix ) ) vertex_load_start = hexOrDecInt(macro.args[2]) diff --git a/fast64_internal/sm64/sm64_constants.py b/fast64_internal/sm64/sm64_constants.py index b3bd92b2b..9f37bdccf 100644 --- a/fast64_internal/sm64/sm64_constants.py +++ b/fast64_internal/sm64/sm64_constants.py @@ -1775,8 +1775,13 @@ def __init__(self, geoAddr, level, switchDict): ("VERSION_SH", "VERSION_SH", ""), ] -# groups you can select for levels -groupsSeg5 = [ +# groups and whats in them +# used across various enums +all_groups = [ + ("common0", "common0", "Common enemies (amps, cannons, flyguy, goomba etc.)"), + ("common1", "common1", "Common objects (coins, doors, trees, star, wooden sign)"), + ("group0", "group0", "Mario and particles"), + ("group0", "group0", "Mario and particles"), ("group1", "group1", "Ground objects (Thwomp, Heave-Ho, Hoot etc.)"), ("group2", "group2", "Bully/Blargg"), ("group3", "group3", "King Bob-Omb"), @@ -1788,22 +1793,273 @@ def __init__(self, geoAddr, level, switchDict): ("group9", "group9", "Haunted Objects (Boo, Mad Piano etc.)"), ("group10", "group10", "Peach/Yoshi"), ("group11", "group11", "THI Ojbects (Lakitu, Wiggler, Bubba)"), - ("Do Not Write", "Do Not Write", "Do Not Write"), + ("group12", "group12", "Bowser/Bowser Bomb"), + ("group13", "group13", "Water Objects (Skeeter, Treasure Chest etc.)"), + ("group14", "group14", "Ground Objects (Piranha Plant, Chain Chomp etc.)"), + ("group15", "group15", "Castle Objects (MIPS, Toad etc.)"), + ("group16", "group16", "Ice Objects (Chill Bully, Moneybags)"), + ("group17", "group17", "Cave Objects (Swoop, Scuttlebug, Dorrie etc.)"), ("Custom", "Custom", "Custom"), ] +groups_seg_5 = [ + ("group1", "group1", "Ground objects (Thwomp, Heave-Ho, Hoot etc.)"), + ("group2", "group2", "Bully/Blargg"), + ("group3", "group3", "King Bob-Omb"), + ("group4", "group4", "Water Objects (Unagi, Manta, Clam)"), + ("group5", "group5", "Sand Objects (Pokey, Eyerock, klepto)"), + ("group6", "group6", "TTM Objects (Monty Mole, uUkiki, Fwoosh)"), + ("group7", "group7", "Snow Objects (Mr Blizzard, Spindrift etc.)"), + ("group8", "group8", "Cap Switch"), + ("group9", "group9", "Haunted Objects (Boo, Mad Piano etc.)"), + ("group10", "group10", "Peach/Yoshi"), + ("group11", "group11", "THI Ojbects (Lakitu, Wiggler, Bubba)"), + ("Custom", "Custom", "Custom"), +] -groupsSeg6 = [ +groups_seg_6 = [ ("group12", "group12", "Bowser/Bowser Bomb"), ("group13", "group13", "Water Objects (Skeeter, Treasure Chest etc.)"), ("group14", "group14", "Ground Objects (Piranha Plant, Chain Chomp etc.)"), ("group15", "group15", "Castle Objects (MIPS, Toad etc.)"), ("group16", "group16", "Ice Objects (Chill Bully, Moneybags)"), ("group17", "group17", "Cave Objects (Swoop, Scuttlebug, Dorrie etc.)"), - ("Do Not Write", "Do Not Write", "Do Not Write"), ("Custom", "Custom", "Custom"), ] +# enums specifically for level loading +groups_seg_5_lvl_load = [ + *groups_seg_5, + ("Do Not Write", "Do Not Write", "Do Not Write"), +] + +groups_seg_6_lvl_load = [ + *groups_seg_6, + ("Do Not Write", "Do Not Write", "Do Not Write"), +] + +# what is in specific groups and the segmented addresses +group_0_geos = [ + ("bubble_geo", "bubble_geo", "0x17000000"), + ("purple_marble_geo", "purple_marble_geo", "0x1700001c"), + ("smoke_geo", "smoke_geo", "0x17000038"), + ("burn_smoke_geo", "burn_smoke_geo", "0x17000084"), + ("small_water_splash_geo", "small_water_splash_geo", "0x1700009c"), + ("idle_water_wave_geo", "idle_water_wave_geo", "0x17000124"), + ("wave_trail_geo", "wave_trail_geo", "0x17000168"), + ("sparkles_geo", "sparkles_geo", "0x170001bc"), + ("water_splash_geo", "water_splash_geo", "0x17000230"), + ("sparkles_animation_geo", "sparkles_animation_geo", "0x17000284"), + ("mario_geo", "mario_geo", "0x17002dd4"), +] + +group_1_geos = [ + ("yellow_sphere_geo", "yellow_sphere_geo", "0x0c000000"), + ("hoot_geo", "hoot_geo", "0x0c000018"), + ("yoshi_egg_geo", "yoshi_egg_geo", "0x0c0001e4"), + ("thwomp_geo", "thwomp_geo", "0x0c000248"), + ("bullet_bill_geo", "bullet_bill_geo", "0x0c000264"), + ("heave_ho_geo", "heave_ho_geo", "0x0c00028c"), +] + +group_2_geos = [ + ("bully_geo", "bully_geo", "0x0c000000"), + ("bully_boss_geo", "bully_boss_geo", "0x0c000120"), + ("blargg_geo", "blargg_geo", "0x0c000240"), +] + +group_3_geos = [ + ("king_bobomb_geo", "king_bobomb_geo", "0x0c000000"), + ("water_bomb_geo", "water_bomb_geo", "0x0c000308"), + ("water_bomb_shadow_geo", "water_bomb_shadow_geo", "0x0c000328"), +] + +group_4_geos = [ + ("clam_shell_geo", "clam_shell_geo", "0x0c000000"), + ("sushi_geo", "sushi_geo", "0x0c000068"), + ("unagi_geo", "unagi_geo", "0x0c00010c"), +] + +group_5_geos = [ + ("klepto_geo", "klepto_geo", "0x0c000000"), + ("eyerok_left_hand_geo", "eyerok_left_hand_geo", "0x0c0005a8"), + ("eyerok_right_hand_geo", "eyerok_right_hand_geo", "0x0c0005e4"), + ("pokey_head_geo", "pokey_head_geo", "0x0c000610"), + ("pokey_body_part_geo", "pokey_body_part_geo", "0x0c000644"), +] + +group_6_geos = [ + ("monty_mole_geo", "monty_mole_geo", "0x0c000000"), + ("ukiki_geo", "ukiki_geo", "0x0c000110"), + ("fwoosh_geo", "fwoosh_geo", "0x0c00036c"), +] + +group_7_geos = [ + ("spindrift_geo", "spindrift_geo", "0x0c000000"), + ("penguin_geo", "penguin_geo", "0x0c000104"), + ("mr_blizzard_hidden_geo", "mr_blizzard_hidden_geo", "0x0c00021c"), + ("mr_blizzard_geo", "mr_blizzard_geo", "0x0c000348"), +] + +group_8_geos = [ + ("springboard_top_geo", "springboard_top_geo", "0x0c000000"), + ("springboard_spring_geo", "springboard_spring_geo", "0x0c000018"), + ("springboard_bottom_geo", "springboard_bottom_geo", "0x0c000030"), + ("cap_switch_geo", "cap_switch_geo", "0x0c000048"), +] + +group_9_geos = [ + ("bookend_part_geo", "bookend_part_geo", "0x0c000000"), + ("bookend_geo", "bookend_geo", "0x0c0000c0"), + ("haunted_chair_geo", "haunted_chair_geo", "0x0c0000d8"), + ("small_key_geo", "small_key_geo", "0x0c000188"), + ("mad_piano_geo", "mad_piano_geo", "0x0c0001b4"), + ("boo_geo", "boo_geo", "0x0c000224"), + ("haunted_cage_geo", "haunted_cage_geo", "0x0c000274"), +] + +group_10_geos = [ + ("birds_geo", "birds_geo", "0x0c000000"), + ("peach_geo", "peach_geo", "0x0c000410"), + ("yoshi_geo", "yoshi_geo", "0x0c000468"), +] + +group_11_geos = [ + ("bubba_geo", "bubba_geo", "0x0c000000"), + ("wiggler_head_geo", "wiggler_head_geo", "0x0c000030"), + ("enemy_lakitu_geo", "enemy_lakitu_geo", "0x0c0001bc"), + ("spiny_ball_geo", "spiny_ball_geo", "0x0c000290"), + ("spiny_geo", "spiny_geo", "0x0c000328"), +] + +group_12_geos = [ + ("bowser_flames_geo", "bowser_flames_geo", "0x0d000000"), + ("invisible_bowser_accessory_geo", "invisible_bowser_accessory_geo", "0x0d000090"), + ("bowser_1_yellow_sphere_geo", "bowser_1_yellow_sphere_geo", "0x0d0000b0"), + ("bowser_shadow_geo", "bowser_shadow_geo", "0x0d000ab8"), + ("bowser_geo", "bowser_geo", "0x0d000ac4"), + ("bowser2_geo", "bowser2_geo", "0x0d000b40"), + ("bowser_bomb_geo", "bowser_bomb_geo", "0x0d000bbc"), + ("bowser_impact_smoke_geo", "bowser_impact_smoke_geo", "0x0d000bfc"), +] + +group_13_geos = [ + ("skeeter_geo", "skeeter_geo", "0x0d000000"), + ("seaweed_geo", "seaweed_geo", "0x0d000284"), + ("water_mine_geo", "water_mine_geo", "0x0d0002f4"), + ("cyan_fish_geo", "cyan_fish_geo", "0x0d000324"), + ("bub_geo", "bub_geo", "0x0d00038c"), + ("water_ring_geo", "water_ring_geo", "0x0d000414"), + ("treasure_chest_base_geo", "treasure_chest_base_geo", "0x0d000450"), + ("treasure_chest_lid_geo", "treasure_chest_lid_geo", "0x0d000468"), +] + +group_14_geos = [ + ("koopa_flag_geo", "koopa_flag_geo", "0x0d000000"), + ("wooden_post_geo", "wooden_post_geo", "0x0d0000b8"), + ("koopa_without_shell_geo", "koopa_without_shell_geo", "0x0d0000d0"), + ("koopa_with_shell_geo", "koopa_with_shell_geo", "0x0d000214"), + ("piranha_plant_geo", "piranha_plant_geo", "0x0d000358"), + ("whomp_geo", "whomp_geo", "0x0d000480"), + ("metallic_ball_geo", "metallic_ball_geo", "0x0d0005d0"), + ("chain_chomp_geo", "chain_chomp_geo", "0x0d0005ec"), +] + +group_15_geos = [ + ("lakitu_geo", "lakitu_geo", "0x0d000000"), + ("toad_geo", "toad_geo", "0x0d0003e4"), + ("mips_geo", "mips_geo", "0x0d000448"), + ("boo_castle_geo", "boo_castle_geo", "0x0d0005b0"), +] + +group_16_geos = [ + ("moneybag_geo", "moneybag_geo", "0x0d0000f0"), + ("mr_i_geo", "mr_i_geo", "0x0d000000"), + ("mr_i_iris_geo", "mr_i_iris_geo", "0x0d00001c"), +] + +group_17_geos = [ + ("swoop_geo", "swoop_geo", "0x0d0000dc"), + ("snufit_geo", "snufit_geo", "0x0d0001a0"), + ("dorrie_geo", "dorrie_geo", "0x0d000230"), + ("scuttlebug_geo", "scuttlebug_geo", "0x0d000394"), +] + +common_0_geos = [ + ("blue_coin_switch_geo", "blue_coin_switch_geo", "0x0f000000"), + ("test_platform_geo", "test_platform_geo", "0x0f000020"), + ("amp_geo", "amp_geo", "0x0f000028"), + ("cannon_base_geo", "cannon_base_geo", "0x0f0001a8"), + ("cannon_barrel_geo", "cannon_barrel_geo", "0x0f0001c0"), + ("chuckya_geo", "chuckya_geo", "0x0f0001d8"), + ("purple_switch_geo", "purple_switch_geo", "0x0f0004cc"), + ("checkerboard_platform_geo", "checkerboard_platform_geo", "0x0f0004e4"), + ("heart_geo", "heart_geo", "0x0f0004fc"), + ("flyguy_geo", "flyguy_geo", "0x0f000518"), + ("breakable_box_geo", "breakable_box_geo", "0x0f0005d0"), + ("breakable_box_small_geo", "breakable_box_small_geo", "0x0f000610"), + ("bowling_ball_geo", "bowling_ball_geo", "0x0f000640"), + ("bowling_ball_track_geo", "bowling_ball_track_geo", "0x0f00066c"), + ("exclamation_box_geo", "exclamation_box_geo", "0x0f000694"), + ("goomba_geo", "goomba_geo", "0x0f0006e4"), + ("black_bobomb_geo", "black_bobomb_geo", "0x0f0007b8"), + ("bobomb_buddy_geo", "bobomb_buddy_geo", "0x0f0008f4"), + ("metal_box_geo", "metal_box_geo", "0x0f000a30"), + ("exclamation_box_outline_geo", "exclamation_box_outline_geo", "0x0f000a58"), + ("koopa_shell_geo", "koopa_shell_geo", "0x0f000ab0"), + ("koopa_shell2_geo", "koopa_shell2_geo", "0x0f000adc"), + ("koopa_shell3_geo", "koopa_shell3_geo", "0x0f000b08"), +] + +common_1_geos = [ + ("mist_geo", "mist_geo", "0x16000000"), + ("white_puff_geo", "white_puff_geo", "0x16000020"), + ("explosion_geo", "explosion_geo", "0x16000040"), + ("butterfly_geo", "butterfly_geo", "0x160000a8"), + ("yellow_coin_geo", "yellow_coin_geo", "0x1600013c"), + ("yellow_coin_no_shadow_geo", "yellow_coin_no_shadow_geo", "0x160001a0"), + ("blue_coin_geo", "blue_coin_geo", "0x16000200"), + ("blue_coin_no_shadow_geo", "blue_coin_no_shadow_geo", "0x16000264"), + ("red_coin_geo", "red_coin_geo", "0x160002c4"), + ("red_coin_no_shadow_geo", "red_coin_no_shadow_geo", "0x16000328"), + ("warp_pipe_geo", "warp_pipe_geo", "0x16000388"), + ("castle_door_geo", "castle_door_geo", "0x160003a8"), + ("cabin_door_geo", "cabin_door_geo", "0x1600043c"), + ("wooden_door_geo", "wooden_door_geo", "0x160004d0"), + ("wooden_door2_geo", "wooden_door2_geo", "0x16000564"), + ("metal_door_geo", "metal_door_geo", "0x160005f8"), + ("hazy_maze_door_geo", "hazy_maze_door_geo", "0x1600068c"), + ("haunted_door_geo", "haunted_door_geo", "0x16000720"), + ("castle_door_0_star_geo", "castle_door_0_star_geo", "0x160007b4"), + ("castle_door_1_star_geo", "castle_door_1_star_geo", "0x16000868"), + ("castle_door_3_stars_geo", "castle_door_3_stars_geo", "0x1600091c"), + ("key_door_geo", "key_door_geo", "0x160009d0"), + ("bowser_key_geo", "bowser_key_geo", "0x16000a84"), + ("bowser_key_cutscene_geo", "bowser_key_cutscene_geo", "0x16000ab0"), + ("red_flame_shadow_geo", "red_flame_shadow_geo", "0x16000b10"), + ("red_flame_geo", "red_flame_geo", "0x16000b2c"), + ("blue_flame_geo", "blue_flame_geo", "0x16000b8c"), + ("fish_shadow_geo", "fish_shadow_geo", "0x16000bec"), + ("fish_geo", "fish_geo", "0x16000c44"), + ("leaves_geo", "leaves_geo", "0x16000c8c"), + ("marios_cap_geo", "marios_cap_geo", "0x16000ca4"), + ("marios_metal_cap_geo", "marios_metal_cap_geo", "0x16000cf0"), + ("marios_wing_cap_geo", "marios_wing_cap_geo", "0x16000d3c"), + ("marios_winged_metal_cap_geo", "marios_winged_metal_cap_geo", "0x16000da8"), + ("number_geo", "number_geo", "0x16000e14"), + ("mushroom_1up_geo", "mushroom_1up_geo", "0x16000e84"), + ("star_geo", "star_geo", "0x16000ea0"), + ("dirt_animation_geo", "dirt_animation_geo", "0x16000ed4"), + ("cartoon_star_geo", "cartoon_star_geo", "0x16000f24"), + ("transparent_star_geo", "transparent_star_geo", "0x16000f6c"), + ("white_particle_geo", "white_particle_geo", "0x16000f98"), + ("wooden_signpost_geo", "wooden_signpost_geo", "0x16000fb4"), + ("bubbly_tree_geo", "bubbly_tree_geo", "0x16000fe8"), + ("spiky_tree_geo", "spiky_tree_geo", "0x16001000"), + ("snow_tree_geo", "snow_tree_geo", "0x16001018"), + ("spiky_tree1_geo", "spiky_tree1_geo", "0x16001030"), + ("palm_tree_geo", "palm_tree_geo", "0x16001048"), +] marioAnimations = [ # ( Adress, "Animation name" ), diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index 6e1672c91..af3da0a18 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -51,6 +51,27 @@ from .sm64_constants import ( enumVersionDefs, enumLevelNames, + all_groups, + group_0_geos, + group_1_geos, + group_2_geos, + group_3_geos, + group_4_geos, + group_5_geos, + group_6_geos, + group_7_geos, + group_8_geos, + group_9_geos, + group_10_geos, + group_11_geos, + group_12_geos, + group_13_geos, + group_14_geos, + group_15_geos, + group_16_geos, + group_17_geos, + common_0_geos, + common_1_geos, ) # ------------------------------------------------------------------------ @@ -226,13 +247,13 @@ def parse_level_script(self, entry: str, col: bpy.types.Collection = None): def AREA(self, macro: Macro, col: bpy.types.Collection): area_root = bpy.data.objects.new("Empty", None) - if self.scene.LevelImp.UseCol: - area_col = bpy.data.collections.new(f"{self.scene.LevelImp.Level} area {macro.args[0]}") + if self.scene.level_import.use_collection: + area_col = bpy.data.collections.new(f"{self.scene.level_import.level} area {macro.args[0]}") col.children.link(area_col) else: area_col = col area_col.objects.link(area_root) - area_root.name = f"{self.scene.LevelImp.Level} Area Root {macro.args[0]}" + area_root.name = f"{self.scene.level_import.level} Area Root {macro.args[0]}" self.areas[macro.args[0]] = Area(area_root, macro.args[1], self.root, int(macro.args[0]), self.scene, area_col) self.cur_area = macro.args[0] return self.continue_parse @@ -520,13 +541,13 @@ def COL_END(self, macro: Macro): class SM64_Material(Mat): - def load_texture(self, ForceNewTex: bool, textures: dict, path: Path, tex: Texture): + def load_texture(self, force_new_tex: bool, textures: dict, path: Path, tex: Texture): if not tex: return None Timg = textures.get(tex.Timg)[0].split("/")[-1] Timg = Timg.replace("#include ", "").replace('"', "").replace("'", "").replace("inc.c", "png") image = bpy.data.images.get(Timg) - if not image or ForceNewTex: + if not image or force_new_tex: Timg = textures.get(tex.Timg)[0] Timg = Timg.replace("#include ", "").replace('"', "").replace("'", "").replace("inc.c", "png") # deal with duplicate pathing (such as /actors/actors etc.) @@ -552,14 +573,14 @@ def apply_PBSDF_Mat(self, mat: bpy.types.Material, textures: dict, tex_path: Pat tex_node = nodes.new("ShaderNodeTexImage") links.new(pbsdf.inputs[0], tex_node.outputs[0]) # base color links.new(pbsdf.inputs[21], tex_node.outputs[1]) # alpha color - image = self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, tex) + image = self.load_texture(bpy.context.scene.level_import.force_new_tex, textures, tex_path, tex) if image: tex_node.image = image if int(layer) > 4: mat.blend_method == "BLEND" def apply_material_settings(self, mat: bpy.types.Material, textures: dict, tex_path: Path, layer: int): - if bpy.context.scene.LevelImp.AsObj: + if bpy.context.scene.level_import.as_obj: return self.apply_PBSDF_Mat(mat, textures, tex_path, layer, self.tex0) f3d = mat.f3d_mat @@ -575,14 +596,14 @@ def set_textures(self, f3d: F3DMaterialProperty, textures: dict, tex_path: Path) if self.tex0 and self.set_tex: self.set_tex_settings( f3d.tex0, - self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, self.tex0), + self.load_texture(bpy.context.scene.level_import.force_new_tex, textures, tex_path, self.tex0), self.tiles[0], self.tex0.Timg, ) if self.tex1 and self.set_tex: self.set_tex_settings( f3d.tex1, - self.load_texture(bpy.context.scene.LevelImp.ForceNewTex, textures, tex_path, self.tex1), + self.load_texture(bpy.context.scene.level_import.force_new_tex, textures, tex_path, self.tex1), self.tiles[1], self.tex1.Timg, ) @@ -714,7 +735,7 @@ def apply_mesh_data(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: in # create a new f3d_mat given an SM64_Material class but don't create copies with same props def create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): - if not self.scene.LevelImp.ForceNewTex: + if not self.scene.level_import.force_new_tex: # check if this mat was used already in another mesh (or this mat if DL is garbage or something) # even looping n^2 is probably faster than duping 3 mats with blender speed for j, F3Dmat in enumerate(bpy.data.materials): @@ -728,7 +749,7 @@ def create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): # add a mat slot and add mat to it mesh.materials.append(new) else: - if self.scene.LevelImp.AsObj: + if self.scene.level_import.as_obj: NewMat = bpy.data.materials.new(f"sm64 {mesh.name.replace('Data', 'material')}") mesh.materials.append(NewMat) # the newest mat should be in slot[-1] for the mesh materials NewMat.use_nodes = True @@ -1112,7 +1133,7 @@ def parse_level_geo(self, start: str, scene: bpy.types.Scene): if not geo_layout: raise Exception( "Could not find geo layout {} from levels/{}/{}geo.c".format( - start, scene.LevelImp.Level, scene.LevelImp.Prefix + start, scene.level_import.level, scene.level_import.prefix ) ) self.stream = start @@ -1319,7 +1340,7 @@ def parse_armature(self, start: str, scene: bpy.types.Scene): if not geo_layout: raise Exception( "Could not find geo layout {} from levels/{}/{}geo.c".format( - start, scene.LevelImp.Level, scene.LevelImp.Prefix + start, scene.level_import.level, scene.level_import.prefix ) ) bpy.context.view_layer.objects.active = self.get_or_init_geo_armature() @@ -1420,13 +1441,13 @@ def parse_level_script(script_file: Path, scene: bpy.types.Scene, col: bpy.types scene.collection.objects.link(Root) else: col.objects.link(Root) - Root.name = f"Level Root {scene.LevelImp.Level}" + Root.name = f"Level Root {scene.level_import.level}" Root.sm64_obj_type = "Level Root" # Now parse the script and get data about the level # Store data in attribute of a level class then assign later and return class with open(script_file, "r", newline="") as script_file: lvl = Level(script_file, scene, Root) - entry = scene.LevelImp.Entry.format(scene.LevelImp.Level) + entry = scene.level_import.entry.format(scene.level_import.level) lvl.parse_level_script(entry, col=col) return lvl @@ -1702,7 +1723,7 @@ def find_level_models_from_geo(geo: TextIO, lvl: Level, scene: bpy.types.Scene, else: col = None Geo = GeoLayout( - GeoLayouts, area.root, scene, f"GeoRoot {scene.LevelImp.Level} {area_index}", area.root, col=col + GeoLayouts, area.root, scene, f"GeoRoot {scene.level_import.level} {area_index}", area.root, col=col ) Geo.parse_level_geo(area.geo, scene) area.geo = Geo @@ -1744,7 +1765,7 @@ def find_collision_data_from_path(aggregate: Path, lvl: Level, scene: bpy.types. area.ColFile = col_data.get(area.terrain, None) if not area.ColFile: raise Exception( - f"Collision {area.terrain} not found in levels/{scene.LevelImp.Level}/{scene.LevelImp.Prefix}leveldata.c" + f"Collision {area.terrain} not found in levels/{scene.level_import.level}/{scene.level_import.prefix}leveldata.c" ) return lvl @@ -1757,7 +1778,7 @@ def write_level_collision_to_bpy(lvl: Level, scene: bpy.types.Scene, cleanup: bo col = create_collection(area.root.users_collection[0], col_name) col_parser = Collision(area.ColFile, scene.blenderToSM64Scale) col_parser.parse_collision() - name = "SM64 {} Area {} Col".format(scene.LevelImp.Level, area_index) + name = "SM64 {} Area {} Col".format(scene.level_import.level, area_index) obj = col_parser.write_collision(scene, name, area.root, col=col) # final operators to clean stuff up if cleanup: @@ -1805,18 +1826,18 @@ def execute(self, context): rt_col = context.collection scene.gameEditorMode = "SM64" path = Path(bpy.path.abspath(scene.decompPath)) - folder = path / scene.ActImp.FolderType - layout_name = scene.ActImp.GeoLayout - prefix = scene.ActImp.Prefix + folder = path / scene.actor_import.folder_type + layout_name = scene.actor_import.geo_layout + prefix = scene.actor_import.prefix # different name schemes and I have no clean way to deal with it - if "actor" in scene.ActImp.FolderType: + if "actor" in scene.actor_import.folder_type: geo_path = folder / (prefix + "_geo.c") leveldat = folder / (prefix + ".c") else: geo_path = folder / (prefix + "geo.c") leveldat = folder / (prefix + "leveldata.c") root_obj = bpy.data.objects.new("Empty", None) - root_obj.name = f"Actor {scene.ActImp.GeoLayout}" + root_obj.name = f"Actor {scene.actor_import.geo_layout}" rt_col.objects.link(root_obj) geo_layout = find_actor_models_from_geo( @@ -1844,17 +1865,17 @@ def execute(self, context): rt_col = context.collection scene.gameEditorMode = "SM64" path = Path(bpy.path.abspath(scene.decompPath)) - folder = path / scene.ActImp.FolderType - layout_name = scene.ActImp.GeoLayout - prefix = scene.ActImp.Prefix + folder = path / scene.actor_import.folder_type + layout_name = scene.actor_import.geo_layout + prefix = scene.actor_import.prefix # different name schemes and I have no clean way to deal with it - if "actor" in scene.ActImp.FolderType: + if "actor" in scene.actor_import.folder_type: geo_path = folder / (prefix + "_geo.c") leveldat = folder / (prefix + ".c") else: geo_path = folder / (prefix + "geo.c") leveldat = folder / (prefix + "leveldata.c") - name = f"Actor {scene.ActImp.GeoLayout}" + name = f"Actor {scene.actor_import.geo_layout}" armature_obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) rt_col.objects.link(armature_obj) @@ -1881,17 +1902,17 @@ def execute(self, context): scene = context.scene col = context.collection - if scene.LevelImp.UseCol: - obj_col = f"{scene.LevelImp.Level} obj" - gfx_col = f"{scene.LevelImp.Level} gfx" - col_col = f"{scene.LevelImp.Level} col" + if scene.level_import.use_collection: + obj_col = f"{scene.level_import.level} obj" + gfx_col = f"{scene.level_import.level} gfx" + col_col = f"{scene.level_import.level} col" else: obj_col = gfx_col = col_col = None scene.gameEditorMode = "SM64" - prefix = scene.LevelImp.Prefix + prefix = scene.level_import.prefix path = Path(bpy.path.abspath(scene.decompPath)) - level = path / "levels" / scene.LevelImp.Level + level = path / "levels" / scene.level_import.level script = level / (prefix + "script.c") geo = level / (prefix + "geo.c") leveldat = level / (prefix + "leveldata.c") @@ -1912,15 +1933,15 @@ def execute(self, context): scene = context.scene col = context.collection - if scene.LevelImp.UseCol: - gfx_col = f"{scene.LevelImp.Level} gfx" + if scene.level_import.use_collection: + gfx_col = f"{scene.level_import.level} gfx" else: gfx_col = None scene.gameEditorMode = "SM64" - prefix = scene.LevelImp.Prefix + prefix = scene.level_import.prefix path = Path(bpy.path.abspath(scene.decompPath)) - level = path / "levels" / scene.LevelImp.Level + level = path / "levels" / scene.level_import.level script = level / (prefix + "script.c") geo = level / (prefix + "geo.c") model = level / (prefix + "leveldata.c") @@ -1939,15 +1960,15 @@ def execute(self, context): scene = context.scene col = context.collection - if scene.LevelImp.UseCol: - col_col = f"{scene.LevelImp.Level} collision" + if scene.level_import.use_collection: + col_col = f"{scene.level_import.level} collision" else: col_col = None scene.gameEditorMode = "SM64" - prefix = scene.LevelImp.Prefix + prefix = scene.level_import.prefix path = Path(bpy.path.abspath(scene.decompPath)) - level = path / "levels" / scene.LevelImp.Level + level = path / "levels" / scene.level_import.level script = level / (prefix + "script.c") model = level / (prefix + "leveldata.c") lvl = parse_level_script(script, scene, col=col) # returns level class @@ -1963,15 +1984,15 @@ def execute(self, context): scene = context.scene col = context.collection - if scene.LevelImp.UseCol: - obj_col = f"{scene.LevelImp.Level} objs" + if scene.level_import.use_collection: + obj_col = f"{scene.level_import.level} objs" else: obj_col = None scene.gameEditorMode = "SM64" - prefix = scene.LevelImp.Prefix + prefix = scene.level_import.prefix path = Path(bpy.path.abspath(scene.decompPath)) - level = path / "levels" / scene.LevelImp.Level + level = path / "levels" / scene.level_import.level script = level / (prefix + "script.c") lvl = parse_level_script(script, scene, col=col) # returns level class write_level_objects(lvl, col_name=obj_col) @@ -1984,8 +2005,11 @@ def execute(self, context): class ActorImport(PropertyGroup): - GeoLayout: StringProperty(name="GeoLayout", description="Name of GeoLayout") - FolderType: EnumProperty( + geo_layout_str: StringProperty( + name="geo_layout", + description="Name of GeoLayout" + ) + folder_type: EnumProperty( name="Source", description="Whether the actor is from a level or from a group", items=[ @@ -1993,55 +2017,206 @@ class ActorImport(PropertyGroup): ("levels", "levels", ""), ], ) - Prefix: StringProperty( + group_preset: EnumProperty( + name="group preset", + description="The group you want to load geo from", + items=all_groups + ) + group_0_geo_enum: EnumProperty( + name="group 0 geos", + description="preset geos from vanilla in group 0", + items=[*group_0_geos, ("Custom", "Custom", "Custom")] + ) + group_1_geo_enum: EnumProperty( + name="group 1 geos", + description="preset geos from vanilla in group 1", + items=[*group_1_geos, ("Custom", "Custom", "Custom")] + ) + group_2_geo_enum: EnumProperty( + name="group 2 geos", + description="preset geos from vanilla in group 2", + items=[*group_2_geos, ("Custom", "Custom", "Custom")] + ) + group_3_geo_enum: EnumProperty( + name="group 3 geos", + description="preset geos from vanilla in group 3", + items=[*group_3_geos, ("Custom", "Custom", "Custom")] + ) + group_4_geo_enum: EnumProperty( + name="group 4 geos", + description="preset geos from vanilla in group 4", + items=[*group_4_geos, ("Custom", "Custom", "Custom")] + ) + group_5_geo_enum: EnumProperty( + name="group 5 geos", + description="preset geos from vanilla in group 5", + items=[*group_5_geos, ("Custom", "Custom", "Custom")] + ) + group_6_geo_enum: EnumProperty( + name="group 6 geos", + description="preset geos from vanilla in group 6", + items=[*group_6_geos, ("Custom", "Custom", "Custom")] + ) + group_7_geo_enum: EnumProperty( + name="group 7 geos", + description="preset geos from vanilla in group 7", + items=[*group_7_geos, ("Custom", "Custom", "Custom")] + ) + group_8_geo_enum: EnumProperty( + name="group 8 geos", + description="preset geos from vanilla in group 8", + items=[*group_8_geos, ("Custom", "Custom", "Custom")] + ) + group_9_geo_enum: EnumProperty( + name="group 9 geos", + description="preset geos from vanilla in group 9", + items=[*group_9_geos, ("Custom", "Custom", "Custom")] + ) + group_10_geo_enum: EnumProperty( + name="group 10 geos", + description="preset geos from vanilla in group 10", + items=[*group_10_geos, ("Custom", "Custom", "Custom")] + ) + group_11_geo_enum: EnumProperty( + name="group 11 geos", + description="preset geos from vanilla in group 11", + items=[*group_11_geos, ("Custom", "Custom", "Custom")] + ) + group_12_geo_enum: EnumProperty( + name="group 12 geos", + description="preset geos from vanilla in group 12", + items=[*group_12_geos, ("Custom", "Custom", "Custom")] + ) + group_13_geo_enum: EnumProperty( + name="group 13 geos", + description="preset geos from vanilla in group 13", + items=[*group_13_geos, ("Custom", "Custom", "Custom")] + ) + group_14_geo_enum: EnumProperty( + name="group 14 geos", + description="preset geos from vanilla in group 14", + items=[*group_14_geos, ("Custom", "Custom", "Custom")] + ) + group_15_geo_enum: EnumProperty( + name="group 15 geos", + description="preset geos from vanilla in group 15", + items=[*group_15_geos, ("Custom", "Custom", "Custom")] + ) + group_16_geo_enum: EnumProperty( + name="group 16 geos", + description="preset geos from vanilla in group 16", + items=[*group_16_geos, ("Custom", "Custom", "Custom")] + ) + group_17_geo_enum: EnumProperty( + name="group 17 geos", + description="preset geos from vanilla in group 17", + items=[*group_17_geos, ("Custom", "Custom", "Custom")] + ) + common_0_geo_enum: EnumProperty( + name="common 0 geos", + description="preset geos from vanilla in common 0", + items=[*common_0_geos, ("Custom", "Custom", "Custom")] + ) + common_1_geo_enum: EnumProperty( + name="common 1 geos", + description="preset geos from vanilla in common 1", + items=[*common_1_geos, ("Custom", "Custom", "Custom")] + ) + prefix_custom: StringProperty( name="Prefix", - description="Prefix before expected aggregator files like script.c, leveldata.c and geo.c", + description="Prefix before expected aggregator files like script.c, leveldata.c and geo.c. Enter group name if not using dropdowns.", default="", ) - Version: EnumProperty( + version: EnumProperty( name="Version", description="Version of the game for any ifdef macros", items=enumVersionDefs, ) - Target: StringProperty( + target: StringProperty( name="Target", description="The platform target for any #ifdefs in code", default="TARGET_N64" ) + + @property + def geo_group_name(self): + if self.group_preset == "Custom": + return None + if self.group_preset == "common0": + return "common_0_geo_enum" + if self.group_preset == "common1": + return "common_1_geo_enum" + else: + return f"group_{self.group_preset.removeprefix('group')}_geo_enum" + + @property + def prefix(self): + if self.folder_type == "levels" or self.group_preset == "custom": + return self.prefix_custom + else: + return self.group_preset + + @property + def geo_layout(self): + if self.folder_type == "levels" or self.group_preset == "custom": + return self.geo_layout_str + else: + return getattr(self, self.geo_group_name) + + def draw(self, layout: bpy.types.UILayout): + layout.prop(self, "folder_type") + layout.prop(self, "group_preset") + if self.folder_type == "levels" or self.group_preset == "custom": + layout.prop(self, "prefix_custom") + layout.prop(self, "geo_layout_str") + else: + layout.prop(self, self.geo_group_name) + layout.prop(self, "version") + layout.prop(self, "target") class LevelImport(PropertyGroup): - Level: EnumProperty( + level: EnumProperty( name="Level", description="Choose a level", items=enumLevelNames, ) - Prefix: StringProperty( + prefix: StringProperty( name="Prefix", - description="Prefix before expected aggregator files like script.c, leveldata.c and geo.c", + description="Prefix before expected aggregator files like script.c, leveldata.c and geo.c. Leave blank unless using custom files", default="", ) - Entry: StringProperty( - name="Entrypoint", description="The name of the level script entry variable", default="level_{}_entry" + entry: StringProperty( + name="Entrypoint", description="The name of the level script entry variable. Levelname is put between braces.", default="level_{}_entry" ) - Version: EnumProperty( + version: EnumProperty( name="Version", description="Version of the game for any ifdef macros", items=enumVersionDefs, ) - Target: StringProperty( + target: StringProperty( name="Target", description="The platform target for any #ifdefs in code", default="TARGET_N64" ) - ForceNewTex: BoolProperty( - name="ForceNewTex", + force_new_tex: BoolProperty( + name="force_new_tex", description="Forcefully load new textures even if duplicate path/name is detected", default=False, ) - AsObj: BoolProperty( + as_obj: BoolProperty( name="As OBJ", description="Make new materials as PBSDF so they export to obj format", default=False ) - UseCol: BoolProperty( - name="Use Col", description="Make new collections to organzie content during imports", default=True + use_collection: BoolProperty( + name="use_collection", description="Make new collections to organzie content during imports", default=True ) - + + def draw(self, layout: bpy.types.UILayout): + layout.prop(self, "level") + layout.prop(self, "entry") + layout.prop(self, "prefix") + layout.prop(self, "version") + layout.prop(self, "target") + row = layout.row() + row.prop(self, "force_new_tex") + row.prop(self, "as_obj") + row.prop(self, "use_collection") # ------------------------------------------------------------------------ # Panels @@ -2063,16 +2238,8 @@ def poll(self, context): def draw(self, context): layout = self.layout scene = context.scene - LevelImp = scene.LevelImp - layout.prop(LevelImp, "Level") - layout.prop(LevelImp, "Entry") - layout.prop(LevelImp, "Prefix") - layout.prop(LevelImp, "Version") - layout.prop(LevelImp, "Target") - row = layout.row() - row.prop(LevelImp, "ForceNewTex") - row.prop(LevelImp, "AsObj") - row.prop(LevelImp, "UseCol") + level_import = scene.level_import + level_import.draw(layout) layout.operator("wm.sm64_import_level") layout.operator("wm.sm64_import_level_gfx") layout.operator("wm.sm64_import_level_col") @@ -2094,12 +2261,8 @@ def poll(self, context): def draw(self, context): layout = self.layout scene = context.scene - ActImp = scene.ActImp - layout.prop(ActImp, "FolderType") - layout.prop(ActImp, "GeoLayout") - layout.prop(ActImp, "Prefix") - layout.prop(ActImp, "Version") - layout.prop(ActImp, "Target") + actor_import = scene.actor_import + actor_import.draw(layout) layout.operator("wm.sm64_import_actor") layout.operator("wm.sm64_import_armature") @@ -2124,8 +2287,8 @@ def sm64_import_register(): for cls in classes: register_class(cls) - bpy.types.Scene.LevelImp = PointerProperty(type=LevelImport) - bpy.types.Scene.ActImp = PointerProperty(type=ActorImport) + bpy.types.Scene.level_import = PointerProperty(type=LevelImport) + bpy.types.Scene.actor_import = PointerProperty(type=ActorImport) def sm64_import_unregister(): @@ -2133,5 +2296,5 @@ def sm64_import_unregister(): for cls in reversed(classes): unregister_class(cls) - del bpy.types.Scene.LevelImp - del bpy.types.Scene.ActImp + del bpy.types.Scene.level_import + del bpy.types.Scene.actor_import diff --git a/fast64_internal/sm64/sm64_objects.py b/fast64_internal/sm64/sm64_objects.py index 3329fc408..744b1ae4e 100644 --- a/fast64_internal/sm64/sm64_objects.py +++ b/fast64_internal/sm64/sm64_objects.py @@ -23,8 +23,8 @@ enumMacrosNames, enumSpecialsNames, enumBehaviourPresets, - groupsSeg5, - groupsSeg6, + groups_seg_5_lvl_load, + groups_seg_6_lvl_load, ) from .sm64_spline import ( @@ -1836,8 +1836,8 @@ class SM64_SegmentProperties(bpy.types.PropertyGroup): seg5_group_custom: bpy.props.StringProperty(name="Segment 5 Group") seg6_load_custom: bpy.props.StringProperty(name="Segment 6 Seg") seg6_group_custom: bpy.props.StringProperty(name="Segment 6 Group") - seg5_enum: bpy.props.EnumProperty(name="Segment 5 Group", default="Do Not Write", items=groupsSeg5) - seg6_enum: bpy.props.EnumProperty(name="Segment 6 Group", default="Do Not Write", items=groupsSeg6) + seg5_enum: bpy.props.EnumProperty(name="Segment 5 Group", default="Do Not Write", items=groups_seg_5_lvl_load) + seg6_enum: bpy.props.EnumProperty(name="Segment 6 Group", default="Do Not Write", items=groups_seg_6_lvl_load) def draw(self, layout): col = layout.column() diff --git a/fast64_internal/utility_importer.py b/fast64_internal/utility_importer.py index 8dd2791b2..f86670520 100644 --- a/fast64_internal/utility_importer.py +++ b/fast64_internal/utility_importer.py @@ -86,9 +86,9 @@ def transform_matrix_to_bpy(transform: Matrix) -> Matrix: def evaluate_macro(line: str): scene = bpy.context.scene - if scene.LevelImp.Version in line: + if scene.level_import.version in line: return False - if scene.LevelImp.Target in line: + if scene.level_import.target in line: return False return True From 3ef1690b000fc427c52a0f846bd55007c0012a8c Mon Sep 17 00:00:00 2001 From: scut Date: Sun, 4 Aug 2024 15:00:40 -0400 Subject: [PATCH 08/11] fixed more bugs with importing and added macro parsing for all the defined enums in hacker64 --- fast64_internal/f3d/f3d_import.py | 130 ++++++-- fast64_internal/sm64/sm64_level_importer.py | 316 ++++++++++++++++---- 2 files changed, 359 insertions(+), 87 deletions(-) diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py index b67ac712e..3ce31dec6 100644 --- a/fast64_internal/f3d/f3d_import.py +++ b/fast64_internal/f3d/f3d_import.py @@ -80,7 +80,6 @@ def eval_texture_format(self): # used when importing for kirby class Mat: def __init__(self): - self.TwoCycle = False self.GeoSet = [] self.GeoClear = [] self.tiles = [Tile() for a in range(8)] @@ -89,6 +88,7 @@ def __init__(self): self.base_tile = 0 self.tex0 = None self.tex1 = None + self.other_mode = dict() self.num_lights = 1 self.light_col = {} self.ambient_light = tuple() @@ -209,7 +209,8 @@ def set_register_settings(self, mat: bpy.types.Material, f3d: F3DMaterialPropert self.set_color_registers(f3d) self.set_geo_mode(f3d.rdp_settings, mat) self.set_combiner(f3d) - self.set_render_mode(f3d) + self.set_rendermode(f3d) + self.set_othermode(f3d) # map tiles to locations in tmem # this ignores the application of LoDs for magnification @@ -222,7 +223,6 @@ def set_texture_tile_mapping(self): continue tex = self.tmem.get(tile.tmem, None) setattr(self, f"tex{tex_index}", tex) - print(f"tex{tex_index} attribute set to :{getattr(self, f'tex{tex_index}')}") def set_textures(self, f3d: F3DMaterialProperty, tex_path: Path): self.set_tex_scale(f3d) @@ -280,26 +280,22 @@ def set_tex_settings( tex_prop.tex_set = True tex_prop.tex = image tex_prop.tex_format = tile.eval_texture_format() - Sflags = self.eval_tile_flags(tile.Sflags) - for f in Sflags: - setattr(tex_prop.S, f, True) - Tflags = self.eval_tile_flags(tile.Tflags) - for f in Sflags: - setattr(tex_prop.T, f, True) + s_flags = self.eval_tile_flags(tile.Sflags) + tex_prop.S.mirror = "mirror" in s_flags + tex_prop.S.clamp = "clamp" in s_flags + t_flags = self.eval_tile_flags(tile.Tflags) + tex_prop.T.mirror = "mirror" in t_flags + tex_prop.T.clamp = "clamp" in t_flags tex_prop.S.low = tile.Slow tex_prop.T.low = tile.Tlow tex_prop.S.high = tile.Shigh tex_prop.T.high = tile.Thigh tex_prop.S.mask = tile.SMask tex_prop.T.mask = tile.TMask - - # rework with combinatoric render mode draft PR - def set_render_mode(self, f3d: F3DMaterialProperty): + + # rework with new render mode stuffs + def set_rendermode(self, f3d: F3DMaterialProperty): rdp = f3d.rdp_settings - if self.TwoCycle: - rdp.g_mdsft_cycletype = "G_CYC_2CYCLE" - else: - rdp.g_mdsft_cycletype = "G_CYC_1CYCLE" if hasattr(self, "RenderMode"): rdp.set_rendermode = True # if the enum isn't there, then just print an error for now @@ -309,6 +305,12 @@ def set_render_mode(self, f3d: F3DMaterialProperty): # print(f"set render modes with render mode {self.RenderMode}") except: print(f"could not set render modes with render mode {self.RenderMode}") + + def set_othermode(self, f3d: F3DMaterialProperty): + rdp = f3d.rdp_settings + for prop, val in self.other_mode.items(): + setattr(rdp, prop, val) + # add in exception handling here def set_geo_mode(self, rdp: RDPSettings, mat: bpy.types.Material): # texture gen has a different name than gbi @@ -448,6 +450,25 @@ def gsSPVertex(self, macro: Macro): self.LastLoad = vertex_load_length return self.continue_parse + def gsSPModifyVertex(self, macro: Macro): + vtx = self.VertBuff[hexOrDecInt(macro.args[0])] + where = self.eval_modify_vtx(macro.args[1]) + val = hexOrDecInt(macro.args[2]) + # if it is None, something weird, or screenspace I won't edit it + if where == "ST": + uv = (val >> 16)& 0xFFFF, val & 0xFFFF + self.Verts.append(self.Verts[vtx]) + self.UVs.append(uv) + self.VCs.append(self.VCs[vtx]) + self.VertBuff[hexOrDecInt(macro.args[0])] = len(self.Verts) + elif where == "RGBA": + vertex_col = [(val >> 8*i)&0xFF for i in range(4)].reverse() + self.Verts.append(self.Verts[vtx]) + self.UVs.append(self.UVs[vtx]) + self.VCs.append(vertex_col) + self.VertBuff[hexOrDecInt(macro.args[0])] = len(self.Verts) + return self.continue_parse + def gsSP2Triangles(self, macro: Macro): self.make_new_material() args = [hexOrDecInt(a) for a in macro.args] @@ -481,8 +502,7 @@ def gsSPLight(self, macro: Macro): self.LastMat.ambient_light = light.ambient else: num = re.search("_\d", macro.args[0]).group()[1] - if not num: - num = 1 + num = int(num) if num else 1 self.LastMat.light_col[num] = light.diffuse return self.continue_parse @@ -490,23 +510,21 @@ def gsSPLight(self, macro: Macro): def gsSPNumLights(self, macro: Macro): self.NewMat = 1 num = re.search("_\d", macro.args[0]).group()[1] - if not num: - num = 1 + num = int(num) if num else 1 self.LastMat.num_lights = num return self.continue_parse def gsSPLightColor(self, macro: Macro): self.NewMat = 1 num = re.search("_\d", macro.args[0]).group()[1] - if not num: - num = 1 + num = int(num) if num else 1 self.LastMat.light_col[num] = eval(macro.args[-1]).to_bytes(4, "big") return self.continue_parse - + + # not finished yet def gsSPSetLights0(self, macro: Macro): return self.continue_parse - # not finished yet def gsSPSetLights1(self, macro: Macro): return self.continue_parse @@ -528,11 +546,65 @@ def gsSPSetLights6(self, macro: Macro): def gsSPSetLights7(self, macro: Macro): return self.continue_parse - # upper/lower modes - def gsDPSetAlphaCompare(self, macro: Macro): + # some independent other mode settings + def gsDPSetTexturePersp(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_textpersp"] = macro.args[0] return self.continue_parse def gsDPSetDepthSource(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_zsrcsel"] = macro.args[0] + return self.continue_parse + + def gsDPSetColorDither(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_rgb_dither"] = macro.args[0] + return self.continue_parse + + def gsDPSetAlphaDither(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_alpha_dither"] = macro.args[0] + return self.continue_parse + + def gsDPSetCombineKey(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_combkey"] = macro.args[0] + return self.continue_parse + + def gsDPSetTextureConvert(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_textconv"] = macro.args[0] + return self.continue_parse + + def gsDPSetTextureFilter(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_text_filt"] = macro.args[0] + return self.continue_parse + + def gsDPSetTextureLOD(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_textlod"] = macro.args[0] + return self.continue_parse + + def gsDPSetTextureDetail(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_textdetail"] = macro.args[0] + return self.continue_parse + + def gsDPSetCycleType(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_cycletype"] = macro.args[0] + return self.continue_parse + + def gsDPPipelineMode(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_pipeline"] = macro.args[0] + return self.continue_parse + + def gsDPSetAlphaCompare(self, macro: Macro): + self.NewMat = 1 + self.LastMat.other_mode["g_mdsft_alpha_compare"] = macro.args[0] return self.continue_parse def gsSPFogFactor(self, macro: Macro): @@ -593,12 +665,6 @@ def gsSPGeometryMode(self, macro: Macro): self.LastMat.GeoSet.extend(argsS) return self.continue_parse - def gsDPSetCycleType(self, macro: Macro): - if "G_CYC_1CYCLE" in macro.args[0]: - self.LastMat.TwoCycle = False - if "G_CYC_2CYCLE" in macro.args[0]: - self.LastMat.TwoCycle = True - return self.continue_parse def gsDPSetCombineMode(self, macro: Macro): self.NewMat = 1 diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index 8125a1721..db95277b8 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -47,10 +47,12 @@ parentObject, GetEnums, create_collection, + read16bitRGBA, ) from .sm64_constants import ( enumVersionDefs, enumLevelNames, + enumSpecialsNames, groups_obj_export, group_0_geos, group_1_geos, @@ -155,7 +157,7 @@ def __init__( self.objects = [] self.col = col - def add_warp(self, args: list[str]): + def add_warp(self, args: list[str], type: str): # set context to the root bpy.context.view_layer.objects.active = self.root # call fast64s warp node creation operator @@ -170,6 +172,7 @@ def add_warp(self, args: list[str]): level = Num2Name.get(eval(level)) if not level: level = "bob" + warp.warpType = type warp.destLevelEnum = level warp.destArea = args[2] chkpoint = args[-1].strip() @@ -178,6 +181,17 @@ def add_warp(self, args: list[str]): warp.warpFlagEnum = "WARP_NO_CHECKPOINT" else: warp.warpFlagEnum = "WARP_CHECKPOINT" + + def add_instant_warp(self, args: list[str]): + # set context to the root + bpy.context.view_layer.objects.active = self.root + # call fast64s warp node creation operator + bpy.ops.bone.add_warp_node() + warp = self.root.warpNodes[0] + warp.type = "Instant" + warp.warpID = args[0] + warp.destArea = args[1] + warp.instantOffset = [hexOrDecInt(val) for val in args[2:5]] def add_object(self, args: list[str]): self.objects.append(args) @@ -190,6 +204,29 @@ def place_objects(self, col_name: str = None): for object_args in self.objects: self.place_object(object_args, col) + def write_special_objects(self, special_objs: list[str], col: bpy.types.Collection): + special_presets = {enum[0] for enum in enumSpecialsNames} + for special in special_objs: + obj = bpy.data.objects.new("Empty", None) + col.objects.link(obj) + parentObject(self.root, obj) + obj.name = f"Special Object {special[0]}" + obj.sm64_obj_type = "Special" + if special[0] in special_presets: + obj.sm64_special_enum = special[0] + else: + obj.sm64_special_enum = "Custom" + obj.sm64_obj_preset = special[0] + loc = [eval(a.strip()) / self.scene.fast64.sm64.blender_to_sm64_scale for a in special[1:4]] + # rotate to fit sm64s axis + obj.location = [loc[0], -loc[2], loc[1]] + obj.rotation_euler[2] = hexOrDecInt(special[4]) + obj.sm64_obj_set_yaw = True + if special[5]: + obj.sm64_obj_set_bparam = True + obj.fast64.sm64.game_object.use_individual_params = False + obj.fast64.sm64.game_object.bparams = str(special[5]) + def place_object(self, args: list[str], col: bpy.types.Collection): Obj = bpy.data.objects.new("Empty", None) col.objects.link(Obj) @@ -206,8 +243,7 @@ def place_object(self, args: list[str], col: bpy.types.Collection): Obj.sm64_obj_model = args[0] loc = [eval(a.strip()) / self.scene.fast64.sm64.blender_to_sm64_scale for a in args[1:4]] # rotate to fit sm64s axis - loc = [loc[0], -loc[2], loc[1]] - Obj.location = loc + Obj.location = [loc[0], -loc[2], loc[1]] # fast64 just rotations by 90 on x rot = Euler([math.radians(eval(a.strip())) for a in args[4:7]], "ZXY") rot = rotate_quat_n64_to_blender(rot).to_euler("XYZ") @@ -284,7 +320,15 @@ def RETURN(self, macro: Macro, col: bpy.types.Collection): # Now deal with data cmds rather than flow control ones def WARP_NODE(self, macro: Macro, col: bpy.types.Collection): - self.areas[self.cur_area].add_warp(macro.args) + self.areas[self.cur_area].add_warp(macro.args, "Warp") + return self.continue_parse + + def PAINTING_WARP_NODE(self, macro: Macro, col: bpy.types.Collection): + self.areas[self.cur_area].add_warp(macro.args, "Painting") + return self.continue_parse + + def INSTANT_WARP(self, macro: Macro, col: bpy.types.Collection): + self.areas[self.cur_area].add_instant_warp(macro.args) return self.continue_parse def OBJECT_WITH_ACTS(self, macro: Macro, col: bpy.types.Collection): @@ -342,6 +386,12 @@ def TERRAIN(self, macro: Macro, col: bpy.types.Collection): def SET_BACKGROUND_MUSIC(self, macro: Macro, col: bpy.types.Collection): return self.generic_music(macro, col) + + def SET_MENU_MUSIC_WITH_REVERB(self, macro: Macro, col: bpy.types.Collection): + return self.generic_music(macro, col) + + def SET_BACKGROUND_MUSIC_WITH_REVERB(self, macro: Macro, col: bpy.types.Collection): + return self.generic_music(macro, col) def SET_MENU_MUSIC(self, macro: Macro, col: bpy.types.Collection): return self.generic_music(macro, col) @@ -349,15 +399,70 @@ def SET_MENU_MUSIC(self, macro: Macro, col: bpy.types.Collection): def generic_music(self, macro: Macro, col: bpy.types.Collection): root = self.areas[self.cur_area].root root.musicSeqEnum = "Custom" - root.music_seq = macro.args[-1] + root.music_seq = macro.args[1] return self.continue_parse # Don't support these for now def MACRO_OBJECTS(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def WHIRLPOOL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def SET_ECHO(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def MARIO_POS(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + + def SET_REG(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def GET_OR_SET(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def CHANGE_AREA_SKYBOX(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + # Don't support for now but maybe later + def JUMP_LINK_PUSH_ARG(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def JUMP_N_TIMES(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def LOOP_BEGIN(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def LOOP_UNTIL(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def JUMP_IF(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def JUMP_LINK_IF(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def SKIP_IF(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def SKIP(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def SKIP_NOP(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def LOAD_AREA(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def UNLOAD_AREA(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def UNLOAD_MARIO_AREA(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") + + def UNLOAD_AREA(self, macro: Macro, col: bpy.types.Collection): + raise Exception("no support yet woops") # use group mapping to set groups eventually def LOAD_MIO0(self, macro: Macro, col: bpy.types.Collection): @@ -366,6 +471,36 @@ def LOAD_MIO0(self, macro: Macro, col: bpy.types.Collection): def LOAD_MIO0_TEXTURE(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + def LOAD_TITLE_SCREEN_BG(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_GODDARD(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_BEHAVIOR_DATA(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_COMMON0(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_GROUPB(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_GROUPA(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_EFFECTS(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_SKYBOX(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_TEXTURE_BIN(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_LEVEL_DATA(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def LOAD_YAY0(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse @@ -377,8 +512,60 @@ def LOAD_VANILLA_OBJECTS(self, macro: Macro, col: bpy.types.Collection): def LOAD_RAW(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse + + def LOAD_RAW_WITH_CODE(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def LOAD_MARIO_HEAD(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + # throw exception saying I cannot process + def EXECUTE(self, macro: Macro, col: bpy.types.Collection): + raise Exception("Processing of EXECUTE macro is not currently supported") + + def EXIT_AND_EXECUTE(self, macro: Macro, col: bpy.types.Collection): + raise Exception("Processing of EXIT_AND_EXECUTE macro is not currently supported") + + def EXECUTE_WITH_CODE(self, macro: Macro, col: bpy.types.Collection): + raise Exception("Processing of EXECUTE_WITH_CODE macro is not currently supported") + + def EXIT_AND_EXECUTE_WITH_CODE(self, macro: Macro, col: bpy.types.Collection): + raise Exception("Processing of EXIT_AND_EXECUTE_WITH_CODE macro is not currently supported") + # not useful for bpy, dummy these script cmds + def CMD3A(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def STOP_MUSIC(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def GAMMA(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def BLACKOUT(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def TRANSITION(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def NOP(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def CMD23(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def PUSH_POOL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def POP_POOL(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def SLEEP(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + + def ROOMS(self, macro: Macro, col: bpy.types.Collection): + return self.continue_parse + def MARIO(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse @@ -409,6 +596,11 @@ def CLEAR_LEVEL(self, macro: Macro, col: bpy.types.Collection): def SLEEP_BEFORE_EXIT(self, macro: Macro, col: bpy.types.Collection): return self.continue_parse +@dataclass +class ColTri(): + type: Any + verts: list[int] + special_param: Any = None class Collision(DataParser): def __init__(self, collision: list[str], scale: float): @@ -416,10 +608,9 @@ def __init__(self, collision: list[str], scale: float): self.scale = scale self.vertices = [] # key=type,value=tri data - self.tris = {} - self.type = None + self.tris: list[ColTri] = [] + self.type: str = None self.special_objects = [] - self.tri_types = [] self.water_boxes = [] super().__init__() @@ -450,53 +641,39 @@ def write_collision( if not col: col = scene.collection self.write_water_boxes(scene, parent, name, col) - mesh = bpy.data.meshes.new(name + " data") - tris = [] - for t in self.tris.values(): - # deal with special tris - if len(t[0]) > 3: - t = [a[0:3] for a in t] - tris.extend(t) - mesh.from_pydata(self.vertices, [], tris) - - obj = bpy.data.objects.new(name + " Mesh", mesh) + mesh = bpy.data.meshes.new(f"{name} data") + mesh.from_pydata(self.vertices, [], [tri.verts for tri in self.tris]) + obj = bpy.data.objects.new(f"{name} mesh", mesh) col.objects.link(obj) obj.ignore_render = True if parent: parentObject(parent, obj) rotate_object(-90, obj, world=1) - polys = obj.data.polygons - x = 0 bpy.context.view_layer.objects.active = obj - max = len(polys) - for i, p in enumerate(polys): - a = self.tri_types[x][0] - if i >= a: + max = len(obj.data.polygons) + col_materials: dict[str, "mat_index"] = dict() + for i, (bpy_tri, col_tri) in enumerate(zip(obj.data.polygons, self.tris)): + if col_tri.type not in col_materials: bpy.ops.object.create_f3d_mat() # the newest mat should be in slot[-1] - mat = obj.data.materials[x] + mat = obj.data.materials[-1] + col_materials[col_tri.type] = len(obj.data.materials) - 1 + # fix this mat.collision_type_simple = "Custom" - mat.collision_custom = self.tri_types[x][1] - mat.name = "Sm64_Col_Mat_{}".format(self.tri_types[x][1]) - color = ((max - a) / (max), (max + a) / (2 * max - a), a / max, 1) # Just to give some variety - mat.f3d_mat.default_light_color = color - # check for param - if len(self.tri_types[x][2]) > 3: + mat.collision_custom = col_tri.type + mat.name = "Sm64_Col_Mat_{}".format(col_tri.type) + # Just to give some variety + mat.f3d_mat.default_light_color = [a / 255 for a in (hash(id(int(i))) & 0xFFFFFFFF).to_bytes(4, "big")] + if col_tri.special_param is not None: mat.use_collision_param = True - mat.collision_param = str(self.tri_types[x][2][3]) - x += 1 - with bpy.context.temp_override(material=mat): - bpy.ops.material.update_f3d_nodes() - p.material_index = x - 1 + mat.collision_param = str(col_tri.special_param) + # I don't think I care about this. It makes program slow + # with bpy.context.temp_override(material=mat): + # bpy.ops.material.update_f3d_nodes() + bpy_tri.material_index = col_materials[col_tri.type] return obj def parse_collision(self): self.parse_stream(self.collision, 0) - # This will keep track of how to assign mats - a = 0 - for k, v in self.tris.items(): - self.tri_types.append([a, k, v[0]]) - a += len(v) - self.tri_types.append([a, 0]) def COL_VERTEX(self, macro: Macro): self.vertices.append([eval(v) / self.scale for v in macro.args]) @@ -504,12 +681,14 @@ def COL_VERTEX(self, macro: Macro): def COL_TRI_INIT(self, macro: Macro): self.type = macro.args[0] - if not self.tris.get(self.type): - self.tris[self.type] = [] return self.continue_parse def COL_TRI(self, macro: Macro): - self.tris[self.type].append([eval(a) for a in macro.args]) + self.tris.append(ColTri(self.type, [eval(a) for a in macro.args])) + return self.continue_parse + + def COL_TRI_SPECIAL(self, macro: Macro): + self.tris.append(ColTri(self.type, [eval(a) for a in macro.args[0:3]], special_param = eval(macro.args[3]))) return self.continue_parse def COL_WATER_BOX(self, macro: Macro): @@ -519,10 +698,14 @@ def COL_WATER_BOX(self, macro: Macro): # not written out currently def SPECIAL_OBJECT(self, macro: Macro): - self.special_objects.append(macro.args) + self.special_objects.append((*macro.args, 0, 0)) return self.continue_parse def SPECIAL_OBJECT_WITH_YAW(self, macro: Macro): + self.special_objects.append((*macro.args, 0)) + return self.continue_parse + + def SPECIAL_OBJECT_WITH_YAW_AND_PARAM(self, macro: Macro): self.special_objects.append(macro.args) return self.continue_parse @@ -1038,9 +1221,6 @@ def GEO_NOP_1F(self, macro: Macro, depth: int): def GEO_NODE_START(self, macro: Macro, depth: int): return self.continue_parse - - def GEO_BACKGROUND(self, macro: Macro, depth: int): - return self.continue_parse def GEO_NODE_SCREEN_AREA(self, macro: Macro, depth: int): return self.continue_parse @@ -1091,7 +1271,10 @@ def GEO_CAMERA_FRUSTRUM(self, macro: Macro, depth: int): def GEO_CAMERA_FRUSTUM_WITH_FUNC(self, macro: Macro, depth: int): raise Exception("you must call this function from a sublcass") - + + def GEO_BACKGROUND(self, macro: Macro, depth: int): + raise Exception("you must call this function from a sublcass") + def GEO_BACKGROUND_COLOR(self, macro: Macro, depth: int): raise Exception("you must call this function from a sublcass") @@ -1225,6 +1408,28 @@ def GEO_CAMERA(self, macro: Macro, depth: int): self.area_root.camOption = "Custom" self.area_root.camType = macro.args[0] return self.continue_parse + + def GEO_BACKGROUND(self, macro: Macro, depth: int): + level_root = self.area_root.parent + # check if in enum + skybox_name = macro.args[0].replace("BACKGROUND_","") + bg_enums = {enum.identifier for enum in level_root.bl_rna.properties["background"].enum_items} + if skybox_name in bg_enums: + level_root.background = skybox_name + else: + level_root.background = "CUSTOM" + # this is cringe and should be changed + scene.fast64.sm64.level.backgroundID = macro.args[0] + # I don't have access to the bg segment, that is in level obj + scene.fast64.sm64.level.backgroundSegment = "unavailable srry :(" + + return self.continue_parse + + def GEO_BACKGROUND_COLOR(self, macro: Macro, depth: int): + level_root = self.area_root.parent + level_root.useBackgroundColor = True + level_root.backgroundColor = read16bitRGBA(hexOrDecInt(macro.args[0])) + return self.continue_parse # can only apply to meshes def GEO_RENDER_RANGE(self, macro: Macro, depth: int): @@ -1859,7 +2064,8 @@ def write_level_collision_to_bpy(lvl: Level, scene: bpy.types.Scene, cleanup: bo col_parser = Collision(area.ColFile, scene.fast64.sm64.blender_to_sm64_scale) col_parser.parse_collision() name = "SM64 {} Area {} Col".format(scene.level_import.level, area_index) - obj = col_parser.write_collision(scene, name, area.root, col=col) + obj = col_parser.write_collision(scene, name, area.root, col) + area.write_special_objects(col_parser.special_objects, col) # final operators to clean stuff up if cleanup: obj.data.validate() @@ -1916,12 +2122,12 @@ def execute(self, context): else: geo_path = folder / (prefix + "geo.c") leveldat = folder / (prefix + "leveldata.c") - root_obj = bpy.data.objects.new("Empty", None) - root_obj.name = f"Actor {scene.actor_import.geo_layout}" - rt_col.objects.link(root_obj) + # root_obj = bpy.data.objects.new("Empty", None) + # root_obj.name = f"Actor {scene.actor_import.geo_layout}" + # rt_col.objects.link(root_obj) geo_layout = find_actor_models_from_geo( - geo_path, layout_name, scene, root_obj, folder, col=rt_col + geo_path, layout_name, scene, None, folder, col=rt_col ) # return geo layout class and write the geo layout models = construct_model_data_from_file(leveldat, scene, folder) # just a try, in case you are importing from not the base decomp repo From 933676e19e0448b2cd6164301f88807720fcc4de Mon Sep 17 00:00:00 2001 From: scut Date: Wed, 1 Jan 2025 13:01:43 -0500 Subject: [PATCH 09/11] fixed plugin UVs on newer blender versions --- fast64_internal/sm64/sm64_level_importer.py | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index db95277b8..10b5e880a 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -5,6 +5,7 @@ import bpy +import bmesh from bpy.props import ( StringProperty, @@ -872,7 +873,8 @@ def parse_tri(self, Tri: list[int]): return [a + L - self.LastLoad for a in Tri] def apply_mesh_data(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_path: Path): - tris = mesh.polygons + # bpy.app.version >= (3, 5, 0) + bpy.context.view_layer.objects.active = obj ind = -1 new = -1 @@ -890,6 +892,15 @@ def apply_mesh_data(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: in Valph = obj.data.color_attributes.get("Alpha") if not Valph: Valph = obj.data.color_attributes.new(name="Alpha", type=e, domain="CORNER") + + b_mesh = bmesh.new() + b_mesh.from_mesh(mesh) + tris = b_mesh.faces + tris.ensure_lookup_table() + uv_map = b_mesh.loops.layers.uv.active + v_color = b_mesh.loops.layers.float_color["Col"] + v_alpha = b_mesh.loops.layers.float_color["Alpha"] + self.Mats.append([len(tris), 0]) for i, t in enumerate(tris): if i > self.Mats[ind + 1][0]: @@ -915,14 +926,15 @@ def apply_mesh_data(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: in else: WH = i.size # Set UV data and Vertex Color Data - for v, l in zip(t.vertices, t.loop_indices): - uv = self.UVs[v] - vcol = self.VCs[v] + for v, l in zip(t.verts, t.loops): + uv = self.UVs[v.index] + vcol = self.VCs[v.index] # scale verts - UVmap.data[l].uv = [a * (1 / (32 * b)) if b > 0 else a * 0.001 * 32 for a, b in zip(uv, WH)] + l[uv_map].uv = [a * (1 / (32 * b)) if b > 0 else a * 0.001 * 32 for a, b in zip(uv, WH)] # idk why this is necessary. N64 thing or something? - UVmap.data[l].uv[1] = UVmap.data[l].uv[1] * -1 + 1 - Vcol.data[l].color = [a / 255 for a in vcol] + l[uv_map].uv[1] = l[uv_map].uv[1] * -1 + 1 + l[v_color] = [a / 255 for a in vcol] + b_mesh.to_mesh(mesh) # create a new f3d_mat given an SM64_Material class but don't create copies with same props def create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): From c456be79165e02b70280c7b574e6a88dbfe0928d Mon Sep 17 00:00:00 2001 From: scut Date: Mon, 17 Mar 2025 21:36:15 -0400 Subject: [PATCH 10/11] added support for linking objects for special objects, added passes for linked actors with missing geos --- fast64_internal/f3d/f3d_import.py | 27 ++++++ fast64_internal/sm64/sm64_level_importer.py | 101 +++++++++++++------- 2 files changed, 93 insertions(+), 35 deletions(-) diff --git a/fast64_internal/f3d/f3d_import.py b/fast64_internal/f3d/f3d_import.py index 89f359c1d..6926c600f 100644 --- a/fast64_internal/f3d/f3d_import.py +++ b/fast64_internal/f3d/f3d_import.py @@ -554,6 +554,25 @@ def gsSPSetLights6(self, macro: Macro): def gsSPSetLights7(self, macro: Macro): return self.continue_parse + def gsSPSetOtherMode(self, macro: Macro): + self.NewMat = 1 + if macro.args[0] == "G_SETOTHERMODE_H": + for i, othermode in enumerate(macro.args[3].split("|")): + # this may cause an issue if someone uses a wacky custom othermode H + mode_h_attr = RDPSettings.other_mode_h_attributes[i][1] + self.LastMat.other_mode[mode_h_attr] = othermode.strip() + else: + if int(macro.args[2]) > 3: + self.LastMat.RenderMode = [] + # top two bits are z src and alpha compare, rest is render mode + for i, othermode in enumerate(macro.args[3].split("|")): + if int(macro.args[2]) > 3 and i > 1: + self.LastMat.RenderMode.append(othermode) + continue + mode_l_attr = RDPSettings.other_mode_l_attributes[i][1] + self.LastMat.other_mode[mode_l_attr] = othermode.strip() + return self.continue_parse + # some independent other mode settings def gsDPSetTexturePersp(self, macro: Macro): self.NewMat = 1 @@ -683,6 +702,14 @@ def gsSPGeometryMode(self, macro: Macro): self.LastMat.GeoSet.extend(argsS) return self.continue_parse + def gsSPLoadGeometryMode(self, macro: Macro): + self.NewMat = 1 + geo_set = {a.strip().lower() for a in macro.args[0].split("|")} + all_geos = set(RDPSettings.geo_mode_attributes.values()) + self.LastMat.GeoSet = list(geo_set) + self.LastMat.GeoClear = list(all_geos.difference(geo_set)) + return self.continue_parse + def gsDPSetCombineMode(self, macro: Macro): self.NewMat = 1 self.LastMat.Combiner = self.eval_set_combine_macro(macro.args) diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index 3cf51bf63..f801c3430 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -137,6 +137,7 @@ def __init__( root.sm64_obj_type = "Area Root" root.areaIndex = num self.objects = [] + self.placed_special_objects = [] # for linking objects later self.col = col def add_warp(self, args: list[str], type: str): @@ -196,28 +197,18 @@ def place_objects(self, col_name: str = None, actor_models: dict[model_name, bpy model_obj = actor_models.get(object.model, None) if model_obj is None: continue - # duplicate, idk why temp override doesn't work - # with bpy.context.temp_override(active_object = model_obj, selected_objects = model_obj.children_recursive): - # bpy.ops.object.duplicate_move_linked() - bpy.ops.object.select_all(action="DESELECT") - for child in model_obj.children_recursive: - child.select_set(True) - model_obj.select_set(True) - bpy.context.view_layer.objects.active = model_obj - bpy.ops.object.duplicate() - new_obj = bpy.context.active_object - bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) - # unlink from col, add to area col - for obj in (new_obj, *new_obj.children_recursive): - obj.users_collection[0].objects.unlink(obj) - col.objects.link(obj) - new_obj.location = bpy_obj.location - new_obj.rotation_euler = bpy_obj.rotation_euler - # add constraints so obj follows along when you move empty - copy_loc = new_obj.constraints.new("COPY_LOCATION") - copy_loc.target = bpy_obj - copy_rot = new_obj.constraints.new("COPY_ROTATION") - copy_rot.target = bpy_obj + self.link_bpy_obj_to_empty(bpy_obj, model_obj, col) + if not actor_models: + return + for placed_obj in self.placed_special_objects: + if "level_geo" in placed_obj.sm64_special_enum: + level_geo_model_name = self.get_level_geo_from_special(placed_obj.sm64_special_enum) + model_obj = actor_models.get(level_geo_model_name, None) + if model_obj: + self.link_bpy_obj_to_empty(placed_obj, model_obj, col) + + def get_level_geo_from_special(self, special_name: str): + return special_name.replace("special", "MODEL").replace("geo", "GEOMETRY").upper() def write_special_objects(self, special_objs: list[str], col: bpy.types.Collection): special_presets = {enum[0] for enum in enumSpecialsNames} @@ -241,6 +232,7 @@ def write_special_objects(self, special_objs: list[str], col: bpy.types.Collecti bpy_obj.sm64_obj_set_bparam = True bpy_obj.fast64.sm64.game_object.use_individual_params = False bpy_obj.fast64.sm64.game_object.bparams = str(special[5]) + self.placed_special_objects.append(bpy_obj) def place_object(self, object: Object, col: bpy.types.Collection): bpy_obj = bpy.data.objects.new("Empty", None) @@ -274,6 +266,32 @@ def place_object(self, object: Object, col: bpy.types.Collection): setattr(bpy_obj, form.format(i), False) return bpy_obj + def link_bpy_obj_to_empty( + self, bpy_obj: bpy.Types.Object, model_obj: bpy.Types.Collection, col: bpy.Types.Collection + ): + # duplicate, idk why temp override doesn't work + # with bpy.context.temp_override(active_object = model_obj, selected_objects = model_obj.children_recursive): + # bpy.ops.object.duplicate_move_linked() + bpy.ops.object.select_all(action="DESELECT") + for child in model_obj.children_recursive: + child.select_set(True) + model_obj.select_set(True) + bpy.context.view_layer.objects.active = model_obj + bpy.ops.object.duplicate() + new_obj = bpy.context.active_object + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) + # unlink from col, add to area col + for obj in (new_obj, *new_obj.children_recursive): + obj.users_collection[0].objects.unlink(obj) + col.objects.link(obj) + new_obj.location = bpy_obj.location + new_obj.rotation_euler = bpy_obj.rotation_euler + # add constraints so obj follows along when you move empty + copy_loc = new_obj.constraints.new("COPY_LOCATION") + copy_loc.target = bpy_obj + copy_rot = new_obj.constraints.new("COPY_ROTATION") + copy_rot.target = bpy_obj + class Level(DataParser): def __init__(self, scripts: dict[str, list[str]], scene: bpy.types.Scene, root: bpy.types.Object): @@ -284,7 +302,7 @@ def __init__(self, scripts: dict[str, list[str]], scene: bpy.types.Scene, root: self.cur_area: int = None self.root = root self.loaded_geos: dict[model_name:str, geo_name:str] = dict() - self.loaded_dls: dict[dl_name:str, geo_name:str] = dict() + self.loaded_dls: dict[model_name:str, dl_name:str] = dict() super().__init__() def parse_level_script(self, entry: str, col: bpy.types.Collection = None): @@ -997,6 +1015,7 @@ class GraphNodes(DataParser): "geo_movtex_pause_control", "geo_movtex_draw_water_regions", "geo_cannon_circle_base", + "geo_envfx_main", } def __init__( @@ -1429,7 +1448,8 @@ def parse_level_geo(self, start: str): raise Exception( "Could not find geo layout {} from levels/{}/{}geo.c".format( start, self.props.level_name, self.props.level_prefix - ) + ), + "pass_linked_export", ) self.stream.append(start) self.parse_stream_from_start(geo_layout, start, 0) @@ -2100,8 +2120,14 @@ def find_actor_models_from_model_ids( print(f"could not find model {model}") continue geo_layout = GeoLayout(geo_layout_dict, root_obj, scene, layout_name, root_obj, col=col) - geo_layout.parse_level_geo(layout_name) - geo_layout_per_model[model] = geo_layout + try: + geo_layout.parse_level_geo(layout_name) + geo_layout_per_model[model] = geo_layout + except Exception as exc: + if exc.args[1] == "pass_linked_export": + print(exc) + else: + raise Exception(exc) return geo_layout_per_model @@ -2194,7 +2220,13 @@ def find_collision_data_from_path(aggregate: Path, lvl: Level, scene: bpy.types. return lvl -def write_level_collision_to_bpy(lvl: Level, scene: bpy.types.Scene, cleanup: bool, col_name: str = None): +def write_level_collision_to_bpy( + lvl: Level, + scene: bpy.types.Scene, + cleanup: bool, + col_name: str = None, + actor_models: dict[model_name, bpy.Types.Mesh] = None, +): for area_index, area in lvl.areas.items(): if not col_name: col = area.root.users_collection[0] @@ -2220,12 +2252,7 @@ def write_level_collision_to_bpy(lvl: Level, scene: bpy.types.Scene, cleanup: bo # import level collision given a level script def import_level_collision( - aggregate: Path, - lvl: Level, - scene: bpy.types.Scene, - root_path: Path, - cleanup: bool, - col_name: str = None, + aggregate: Path, lvl: Level, scene: bpy.types.Scene, root_path: Path, cleanup: bool, col_name: str = None ) -> Level: lvl = find_collision_data_from_path( aggregate, lvl, scene, root_path @@ -2355,7 +2382,10 @@ def execute(self, context): ) # returns level class if props.import_linked_actors: - unique_model_ids = {object.model for area in lvl.areas.values() for object in area.objects} + unique_model_ids = {model for model in lvl.loaded_geos.keys()} + unique_model_ids.update({model for model in lvl.loaded_dls.keys()}) + unique_model_ids.update({object.model for area in lvl.areas.values() for object in area.objects}) + geo_actor_paths = [ *( decomp_path / "actors" / (linked_group.group_prefix + "_geo.c") @@ -2383,12 +2413,13 @@ def execute(self, context): # update model to be root obj of geo actor_geo_layouts[model] = geo_layout.first_obj + lvl = import_level_collision(level_data_path, lvl, scene, decomp_path, self.cleanup, col_name=col_col) write_level_objects(lvl, col_name=obj_col, actor_models=actor_geo_layouts) # actor_col.hide_render = True # actor_col.hide_viewport = True else: write_level_objects(lvl, col_name=obj_col) - lvl = import_level_collision(level_data_path, lvl, scene, decomp_path, self.cleanup, col_name=col_col) + lvl = import_level_collision(level_data_path, lvl, scene, decomp_path, self.cleanup, col_name=col_col) lvl = import_level_graphics( [geo_path], lvl, scene, decomp_path, [level_data_path], cleanup=self.cleanup, col_name=gfx_col ) From 4da480943372b4a6020a3162567a0162f3b72849 Mon Sep 17 00:00:00 2001 From: scut Date: Fri, 21 Mar 2025 23:25:44 -0400 Subject: [PATCH 11/11] profiling, sped up import by approx half, still much more to go --- fast64_internal/sm64/sm64_level_importer.py | 80 +++++++++++++-------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/fast64_internal/sm64/sm64_level_importer.py b/fast64_internal/sm64/sm64_level_importer.py index f801c3430..9f67fef63 100644 --- a/fast64_internal/sm64/sm64_level_importer.py +++ b/fast64_internal/sm64/sm64_level_importer.py @@ -27,6 +27,9 @@ ) from bpy.utils import register_class, unregister_class +import cProfile, pstats, io +from pstats import SortKey + import os, sys, math, re, typing from array import array from struct import * @@ -43,6 +46,7 @@ # from SM64classes import * from ..f3d.f3d_import import * +from ..f3d.f3d_material import update_node_values_of_material from ..panels import SM64_Panel from ..utility_importer import * from ..utility import ( @@ -277,7 +281,7 @@ def link_bpy_obj_to_empty( child.select_set(True) model_obj.select_set(True) bpy.context.view_layer.objects.active = model_obj - bpy.ops.object.duplicate() + bpy.ops.object.duplicate_move() new_obj = bpy.context.active_object bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) # unlink from col, add to area col @@ -815,8 +819,12 @@ def apply_material_settings(self, mat: bpy.types.Material, textures: dict, tex_p f3d.draw_layer.sm64 = layer self.set_register_settings(mat, f3d) self.set_textures(f3d, textures, tex_path) - with bpy.context.temp_override(material=mat): - bpy.ops.material.update_f3d_nodes() + + # manually call node update for speed + mat.f3d_update_flag = True + update_node_values_of_material(mat, bpy.context) + mat.f3d_mat.presetName = "Custom" + mat.f3d_update_flag = False def set_textures(self, f3d: F3DMaterialProperty, textures: dict, tex_path: Path): self.set_tex_scale(f3d) @@ -909,8 +917,6 @@ def parse_tri(self, Tri: list[int]): return [a + L - self.LastLoad for a in Tri] def apply_mesh_data(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, tex_path: Path): - # bpy.app.version >= (3, 5, 0) - bpy.context.view_layer.objects.active = obj ind = -1 new = -1 @@ -954,24 +960,27 @@ def apply_mesh_data(self, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: in new = len(mesh.materials) - 1 # if somehow there is no material assigned to the triangle or something is lost if new != -1: - t.material_index = new - # Get texture size or assume 32, 32 otherwise - i = mesh.materials[new].f3d_mat.tex0.tex - if not i: - WH = (32, 32) - else: - WH = i.size - # Set UV data and Vertex Color Data - for v, l in zip(t.verts, t.loops): - uv = self.UVs[v.index] - vcol = self.VCs[v.index] - # scale verts - l[uv_map].uv = [a * (1 / (32 * b)) if b > 0 else a * 0.001 * 32 for a, b in zip(uv, WH)] - # idk why this is necessary. N64 thing or something? - l[uv_map].uv[1] = l[uv_map].uv[1] * -1 + 1 - l[v_color] = [a / 255 for a in vcol] + self.apply_loop_data(new, mesh, t, uv_map, v_color, v_alpha) b_mesh.to_mesh(mesh) + def apply_loop_data(self, mat: bpy.Types.Material, mesh: bpy.Types.Mesh, tri, uv_map, v_color, v_alpha): + tri.material_index = mat + # Get texture size or assume 32, 32 otherwise + i = mesh.materials[mat].f3d_mat.tex0.tex + if not i: + WH = (32, 32) + else: + WH = i.size + # Set UV data and Vertex Color Data + for v, l in zip(tri.verts, tri.loops): + uv = self.UVs[v.index] + vcol = self.VCs[v.index] + # scale verts + l[uv_map].uv = [a * (1 / (32 * b)) if b > 0 else a * 0.001 * 32 for a, b in zip(uv, WH)] + # idk why this is necessary. N64 thing or something? + l[uv_map].uv[1] = l[uv_map].uv[1] * -1 + 1 + l[v_color] = [a / 255 for a in vcol] + # create a new f3d_mat given an SM64_Material class but don't create copies with same props def create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): if not self.props.force_new_tex: @@ -982,11 +991,14 @@ def create_new_f3d_mat(self, mat: SM64_Material, mesh: bpy.types.Mesh): dupe = mat.mat_hash_f3d(F3Dmat.f3d_mat) if dupe: return F3Dmat - if mesh.materials: - mat = mesh.materials[-1] - new = mat.id_data.copy() # make a copy of the data block + f3d_mat = None + for mat in bpy.data.materials: + if mat.is_f3d: + f3d_mat = mat + if f3d_mat: + new_mat = f3d_mat.id_data.copy() # make a copy of the data block # add a mat slot and add mat to it - mesh.materials.append(new) + mesh.materials.append(new_mat) else: if self.props.as_obj: NewMat = bpy.data.materials.new(f"sm64 {mesh.name.replace('Data', 'material')}") @@ -1900,7 +1912,7 @@ def write_armature_to_bpy( def apply_mesh_data( - f3d_dat: SM64_F3D, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, root_path: Path, cleanup: bool = True + f3d_dat: SM64_F3D, obj: bpy.types.Object, mesh: bpy.types.Mesh, layer: int, root_path: Path, cleanup: bool = False ): f3d_dat.apply_mesh_data(obj, mesh, layer, root_path) if cleanup: @@ -2005,7 +2017,7 @@ def write_geo_to_bpy( # write the gfx for a level given the level data, and f3d data -def write_level_to_bpy(lvl: Level, scene: bpy.types.Scene, root_path: Path, f3d_dat: SM64_F3D, cleanup: bool = True): +def write_level_to_bpy(lvl: Level, scene: bpy.types.Scene, root_path: Path, f3d_dat: SM64_F3D, cleanup: bool = False): for area in lvl.areas.values(): write_geo_to_bpy(area.geo, scene, f3d_dat, root_path, dict(), cleanup=cleanup) return lvl @@ -2186,7 +2198,7 @@ def import_level_graphics( scene: bpy.types.Scene, root_path: Path, aggregates: list[Path], - cleanup: bool = True, + cleanup: bool = False, col_name: str = None, ) -> Level: lvl = find_level_models_from_geo(geo_paths, lvl, scene, root_path, col_name=col_name) @@ -2360,9 +2372,11 @@ class SM64_LvlImport(Operator): bl_label = "Import Level" bl_idname = "wm.sm64_import_level" - cleanup = True + cleanup = False def execute(self, context): + pr = cProfile.Profile() + pr.enable() scene = context.scene props = scene.fast64.sm64.importer @@ -2423,6 +2437,12 @@ def execute(self, context): lvl = import_level_graphics( [geo_path], lvl, scene, decomp_path, [level_data_path], cleanup=self.cleanup, col_name=gfx_col ) + pr.disable() + s = io.StringIO() + sortby = SortKey.CUMULATIVE + ps = pstats.Stats(pr, stream=s).sort_stats(sortby) + ps.print_stats(20) + print(s.getvalue()) return {"FINISHED"} @@ -2430,7 +2450,7 @@ class SM64_LvlGfxImport(Operator): bl_label = "Import Gfx" bl_idname = "wm.sm64_import_level_gfx" - cleanup = True + cleanup = False def execute(self, context): scene = context.scene