diff --git a/fast64_internal/f3d/f3d_texture_writer.py b/fast64_internal/f3d/f3d_texture_writer.py index 0f912619b..e40a6768a 100644 --- a/fast64_internal/f3d/f3d_texture_writer.py +++ b/fast64_internal/f3d/f3d_texture_writer.py @@ -487,7 +487,7 @@ def moreSetupFromModel( self.palLen = len(self.pal) if self.palLen > (16 if self.texFormat == "CI4" else 256): raise PluginError( - f"Error in {material.name}: texture {self.indexInMat}" + f"Texture {self.indexInMat}" + (" (all flipbook textures)" if self.flipbook is not None else "") + f" uses too many unique colors to fit in format {self.texFormat}." ) @@ -566,9 +566,9 @@ def __init__( f3dMat = material.f3d_mat self.ti0, self.ti1 = TexInfo(), TexInfo() if not self.ti0.fromMat(0, f3dMat): - raise PluginError(f"In {material.name} tex0: {self.ti0.errorMsg}") + raise PluginError(f"Tex0: {self.ti0.errorMsg}") if not self.ti1.fromMat(1, f3dMat): - raise PluginError(f"In {material.name} tex1: {self.ti1.errorMsg}") + raise PluginError(f"Tex1: {self.ti1.errorMsg}") self.ti0.moreSetupFromModel(material, fMaterial, fModel) self.ti1.moreSetupFromModel(material, fMaterial, fModel) @@ -576,39 +576,24 @@ def __init__( if self.ti0.useTex and self.ti1.useTex: if self.ti0.isTexCI != self.ti1.isTexCI: - raise PluginError( - "In material " - + material.name - + ": N64 does not support CI + non-CI texture. " - + "Must be both CI or neither CI." - ) + raise PluginError("N64 does not support CI + non-CI texture. Must be both CI or neither CI.") if ( self.ti0.isTexRef and self.ti1.isTexRef and self.ti0.texProp.tex_reference == self.ti1.texProp.tex_reference and self.ti0.texProp.tex_reference_size != self.ti1.texProp.tex_reference_size ): - raise PluginError( - "In material " + material.name + ": Two textures with the same reference must have the same size." - ) + raise PluginError("Two textures with the same reference must have the same size.") if self.isCI: if self.ti0.palFormat != self.ti1.palFormat: - raise PluginError( - "In material " - + material.name - + ": Both CI textures must use the same palette format (usually RGBA16)." - ) + raise PluginError("Both CI textures must use the same palette format (usually RGBA16).") if ( self.ti0.isTexRef and self.ti1.isTexRef and self.ti0.texProp.pal_reference == self.ti1.texProp.pal_reference and self.ti0.texProp.pal_reference_size != self.ti1.texProp.pal_reference_size ): - raise PluginError( - "In material " - + material.name - + ": Two textures with the same palette reference must have the same palette size." - ) + raise PluginError("Two textures with the same palette reference must have the same palette size.") self.palFormat = self.ti0.palFormat if self.ti0.useTex else self.ti1.palFormat @@ -626,9 +611,7 @@ def writeAll( elif not convertTextureData: if self.ti0.texFormat == "CI8" or self.ti1.texFormat == "CI8": raise PluginError( - "In material " - + material.name - + ": When using export as PNGs mode, can't have multitexture with one or more CI8 textures." + "When using export as PNGs mode, can't have multitexture with one or more CI8 textures." + " Only single CI texture or two CI4 textures." ) self.ti0.loadPal = self.ti1.loadPal = True @@ -638,27 +621,19 @@ def writeAll( if self.ti0.texFormat == "CI8" and self.ti1.texFormat == "CI8": if (self.ti0.pal is None) != (self.ti1.pal is None): raise PluginError( - "In material " - + material.name - + ": can't have two CI8 textures where only one is a non-flipbook reference; " + "Can't have two CI8 textures where only one is a non-flipbook reference; " + "no way to assign the palette." ) self.ti0.loadPal = True if self.ti0.pal is None: if self.ti0.texProp.pal_reference != self.ti1.texProp.pal_reference: - raise PluginError( - "In material " - + material.name - + ": can't have two CI8 textures with different palette references." - ) + raise PluginError("Can't have two CI8 textures with different palette references.") else: self.ti0.pal = mergePalettes(self.ti0.pal, self.ti1.pal) self.ti0.palLen = len(self.ti0.pal) if self.ti0.palLen > 256: raise PluginError( - "In material " - + material.name - + ": the two CI textures together contain a total of " + "The two CI textures together contain a total of " + str(self.ti0.palLen) + " colors, which can't fit in a CI8 palette (256)." ) @@ -679,9 +654,7 @@ def writeAll( if self.ti0.pal is None or self.ti1.pal is None: if ci8PalLen > 256 - 16: raise PluginError( - "In material " - + material.name - + ": the CI8 texture has over 240 colors, which can't fit together with the CI4 palette." + "The CI8 texture has over 240 colors, which can't fit together with the CI4 palette." ) self.ti0.loadPal = self.ti1.loadPal = True if self.ti0.texFormat == "CI8": @@ -697,9 +670,7 @@ def writeAll( self.ti0.palLen = len(self.ti0.pal) if self.ti0.palLen > 256: raise PluginError( - "In material " - + material.name - + ": the two CI textures together contain a total of " + "The two CI textures together contain a total of " + str(self.ti0.palLen) + " colors, which can't fit in a CI8 palette (256)." + " The CI8 texture must contain up to 240 unique colors," @@ -792,7 +763,7 @@ def writeAll( if sameTextures: assert ( self.ti0.tmemSize == self.ti1.tmemSize - ), f"Unreachable code path in material {material.name}, same textures (same image or reference) somehow not the same size" + ), f"Unreachable code path, same textures (same image or reference) somehow not the same size" tmemOccupied = self.ti0.tmemSize self.ti1.doTexLoad = False self.ti1.texAddr = 0 @@ -835,11 +806,7 @@ def writeAll( self.ti1.texAddr = tmemSize - self.ti1.tmemSize else: # Both textures large - raise PluginError( - 'Error in "' - + material.name - + '": Multitexture with two large textures is not currently supported.' - ) + raise PluginError("Multitexture with two large textures is not currently supported.") # Limited cases of 2x large textures could be supported in the # future. However, these cases are either of questionable # utility or have substantial restrictions. Most cases could be @@ -877,16 +844,10 @@ def writeAll( tmemOccupied = tmemSize if tmemOccupied > tmemSize: if sameTextures and useLargeTextures: - raise PluginError( - 'Error in "' - + material.name - + '": Using the same texture for Tex0 and Tex1 is not compatible with large textures.' - ) + raise PluginError("Using the same texture for Tex0 and Tex1 is not compatible with large textures.") elif not bpy.context.scene.ignoreTextureRestrictions: raise PluginError( - 'Error in "' - + material.name - + '": Textures are too big. Max TMEM size is 4k ' + "Textures are too big. Max TMEM size is 4k " + "bytes, ex. 2 32x32 RGBA 16 bit textures.\nNote that texture width will be internally padded to 64 bit boundaries." ) diff --git a/fast64_internal/f3d/f3d_writer.py b/fast64_internal/f3d/f3d_writer.py index 4dfd45f34..6d776a0b3 100644 --- a/fast64_internal/f3d/f3d_writer.py +++ b/fast64_internal/f3d/f3d_writer.py @@ -1287,6 +1287,7 @@ def getTexDimensions(material): return texDimensions +@wrap_func_with_error_message(lambda args: (f"In material '{args['material'].name}': ")) def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): print(f"Writing material {material.name}") if material.mat_ver > 3: @@ -1317,7 +1318,7 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData): ) if not material.is_f3d: - raise PluginError("Material named " + material.name + " is not an F3D material.") + raise PluginError("Not an F3D material.") fMaterial = fModel.addMaterial(materialName) useDict = all_combiner_uses(f3dMat) @@ -1550,20 +1551,13 @@ def getLightDefinitions(fModel, material, lightsName=""): else: lights.a = Ambient(exportColor(material.ambient_light_color)) - if material.f3d_light1 is not None: - addLightDefinition(material, material.f3d_light1, lights) - if material.f3d_light2 is not None: - addLightDefinition(material, material.f3d_light2, lights) - if material.f3d_light3 is not None: - addLightDefinition(material, material.f3d_light3, lights) - if material.f3d_light4 is not None: - addLightDefinition(material, material.f3d_light4, lights) - if material.f3d_light5 is not None: - addLightDefinition(material, material.f3d_light5, lights) - if material.f3d_light6 is not None: - addLightDefinition(material, material.f3d_light6, lights) - if material.f3d_light7 is not None: - addLightDefinition(material, material.f3d_light7, lights) + for i in range(1, 8): + try: + light_prop = getattr(material, f"f3d_light{i}") + if light_prop is not None: + addLightDefinition(light_prop, lights) + except Exception as exc: + raise PluginError(f"Failed to get custom light {i}: {exc}") from exc return lights @@ -1581,7 +1575,7 @@ def saveLightsDefinition(fModel, fMaterial, material, lightsName): return lights -def addLightDefinition(mat, f3d_light, fLights): +def addLightDefinition(f3d_light, fLights): lightObj = lightDataToObj(f3d_light) fLights.l.append( Light( diff --git a/fast64_internal/oot/exporter/scene/__init__.py b/fast64_internal/oot/exporter/scene/__init__.py index f8b83cdd2..2f1f2f98b 100644 --- a/fast64_internal/oot/exporter/scene/__init__.py +++ b/fast64_internal/oot/exporter/scene/__init__.py @@ -41,23 +41,36 @@ def new(name: str, sceneObj: Object, transform: Matrix, useMacros: bool, saveTex True, ) - mainHeader = SceneHeader.new(f"{name}_header{i:02}", sceneObj.ootSceneHeader, sceneObj, transform, i, useMacros) + try: + mainHeader = SceneHeader.new( + f"{name}_header{i:02}", sceneObj.ootSceneHeader, sceneObj, transform, i, useMacros + ) + except Exception as exc: + raise PluginError(f"In main scene header: {exc}") from exc hasAlternateHeaders = False altHeader = SceneAlternateHeader(f"{name}_alternateHeaders") altProp = sceneObj.ootAlternateSceneHeaders for i, header in enumerate(altHeaderList, 1): altP: OOTSceneHeaderProperty = getattr(altProp, f"{header}Header") - if not altP.usePreviousHeader: + if altP.usePreviousHeader: + continue + try: setattr( altHeader, header, SceneHeader.new(f"{name}_header{i:02}", altP, sceneObj, transform, i, useMacros) ) hasAlternateHeaders = True - - altHeader.cutscenes = [ - SceneHeader.new(f"{name}_header{i:02}", csHeader, sceneObj, transform, i, useMacros) - for i, csHeader in enumerate(altProp.cutsceneHeaders, 4) - ] + except Exception as exc: + raise PluginError(f"In alternate scene header {header}: {exc}") from exc + + altHeader.cutscenes = [] + for i, csHeader in enumerate(altProp.cutsceneHeaders, 4): + try: + altHeader.cutscenes.append( + SceneHeader.new(f"{name}_header{i:02}", csHeader, sceneObj, transform, i, useMacros) + ) + except Exception as exc: + raise PluginError(f"In alternate, cutscene header {i}: {exc}") from exc hasAlternateHeaders = True if len(altHeader.cutscenes) > 0 else hasAlternateHeaders altHeader = altHeader if hasAlternateHeaders else None diff --git a/fast64_internal/oot/exporter/scene/general.py b/fast64_internal/oot/exporter/scene/general.py index 2a1638dea..987348390 100644 --- a/fast64_internal/oot/exporter/scene/general.py +++ b/fast64_internal/oot/exporter/scene/general.py @@ -91,32 +91,35 @@ class SceneLighting: @staticmethod def new(name: str, props: OOTSceneHeaderProperty): envLightMode = Utility.getPropValue(props, "skyboxLighting") - lightList: list[OOTLightProperty] = [] + lightList: dict[str, OOTLightProperty] = {} settings: list[EnvLightSettings] = [] if envLightMode == "LIGHT_MODE_TIME": todLights = props.timeOfDayLights - lightList = [todLights.dawn, todLights.day, todLights.dusk, todLights.night] + lightList = {"Dawn": todLights.dawn, "Day": todLights.day, "Dusk": todLights.dusk, "Night": todLights.night} else: - lightList = props.lightList - - for lightProp in lightList: - light1 = ootGetBaseOrCustomLight(lightProp, 0, True, True) - light2 = ootGetBaseOrCustomLight(lightProp, 1, True, True) - settings.append( - EnvLightSettings( - envLightMode, - exportColor(lightProp.ambient), - light1[0], - light1[1], - light2[0], - light2[1], - exportColor(lightProp.fogColor), - lightProp.fogNear, - lightProp.z_far, - lightProp.transitionSpeed, + lightList = {str(i): light for i, light in enumerate(props.lightList)} + + for name, lightProp in lightList.items(): + try: + light1 = ootGetBaseOrCustomLight(lightProp, 0, True, True) + light2 = ootGetBaseOrCustomLight(lightProp, 1, True, True) + settings.append( + EnvLightSettings( + envLightMode, + exportColor(lightProp.ambient), + light1[0], + light1[1], + light2[0], + light2[1], + exportColor(lightProp.fogColor), + lightProp.fogNear, + lightProp.z_far, + lightProp.transitionSpeed, + ) ) - ) + except Exception as exc: + raise PluginError(f"In light settings {name}: {exc}") from exc return SceneLighting(name, envLightMode, settings) def getCmd(self): diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index fceff5450..4c19b0b10 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -1,5 +1,5 @@ from pathlib import Path -import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator +import bpy, random, string, os, math, traceback, re, os, mathutils, ast, operator, inspect from math import pi, ceil, degrees, radians, copysign from mathutils import * from .utility_anim import * @@ -1662,7 +1662,9 @@ def lightDataToObj(lightData): for obj in bpy.context.scene.objects: if obj.data == lightData: return obj - raise PluginError("A material is referencing a light that is no longer in the scene (i.e. has been deleted).") + raise PluginError( + f'Referencing a light ("{lightData.name}") that is no longer in the scene (i.e. has been deleted).' + ) def ootGetSceneOrRoomHeader(parent, idx, isRoom): @@ -1699,14 +1701,17 @@ def ootGetBaseOrCustomLight(prop, idx, toExport: bool, errIfMissing: bool): col = getattr(prop, "diffuse" + str(idx)) dir = (mathutils.Vector((1.0, -1.0, 1.0)) * (1.0 if idx == 0 else -1.0)).normalized() if getattr(prop, "useCustomDiffuse" + str(idx)): - light = getattr(prop, "diffuse" + str(idx) + "Custom") - if light is None: - if errIfMissing: - raise PluginError("Error: Diffuse " + str(idx) + " light object not set in a scene lighting property.") - else: - col = tuple(c for c in light.color) + (1.0,) - lightObj = lightDataToObj(light) - dir = getObjDirectionVec(lightObj, toExport) + try: + light = getattr(prop, "diffuse" + str(idx) + "Custom") + if light is None: + if errIfMissing: + raise PluginError("Light object not set in a scene lighting property.") + else: + col = tuple(c for c in light.color) + (1.0,) + lightObj = lightDataToObj(light) + dir = getObjDirectionVec(lightObj, toExport) + except Exception as exc: + raise PluginError(f"In custom diffuse {idx}: {exc}") from exc col = mathutils.Vector(tuple(c for c in col)) if toExport: col, dir = exportColor(col), normToSigned8Vector(dir) @@ -1909,3 +1914,22 @@ def set_if_different(owner: object, prop: str, value): def set_prop_if_in_data(owner: object, prop_name: str, data: dict, data_name: str): if data_name in data: set_if_different(owner, prop_name, data[data_name]) + + +def wrap_func_with_error_message(error_message: Callable): + """Decorator for big, reused functions that need generic info in errors, such as material exports.""" + + def decorator(func): + def wrapper(*args, **kwargs): + # Get the argument names and values (positional and keyword) + sig = inspect.signature(func) + bound_args = sig.bind(*args, **kwargs) + bound_args.apply_defaults() + try: + return func(*args, **kwargs) + except Exception as exc: + raise PluginError(f"{error_message(bound_args.arguments)} {exc}") from exc + + return wrapper + + return decorator