From 487bfcb8ed5297093bb2bd0f003313f1088da093 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 17:51:09 -0600 Subject: [PATCH 01/45] Added msg field to MCprepError Sometimes an exception may not be so clear cut, so let's allow an optional message to return --- MCprep_addon/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MCprep_addon/conf.py b/MCprep_addon/conf.py index 57ffa66d..766923ac 100644 --- a/MCprep_addon/conf.py +++ b/MCprep_addon/conf.py @@ -303,10 +303,16 @@ class MCprepError(object): Path of file the exception object was created in. The preferred way to get this is __file__ + + msg: Optional[str] + Optional message to display for an + exception. Use this if the exception + type may not be so clear cut """ err_type: BaseException line: int file: str + msg: Optional[str] = None env = MCprepEnv() From 936cb84532943cae5e57baef9e2f6ce5bb73766b Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 17:51:54 -0600 Subject: [PATCH 02/45] refactor: have convert_mtl use MCprepError --- MCprep_addon/world_tools.py | 39 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index fd598020..c89e146d 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -26,7 +26,7 @@ from bpy.types import Context, Camera from bpy_extras.io_utils import ExportHelper, ImportHelper -from .conf import env, VectorType +from .conf import MCprepError, env, VectorType from . import util from . import tracking from .materials import generate @@ -157,7 +157,7 @@ def detect_world_exporter(filepath: Path) -> None: obj_header.set_seperated() -def convert_mtl(filepath): +def convert_mtl(filepath) -> Optional[MCprepError]: """Convert the MTL file if we're not using one of Blender's built in colorspaces @@ -170,7 +170,8 @@ def convert_mtl(filepath): - Add a header at the end Returns: - True if success or skipped, False if failed, or None if skipped + - None if successful or skipped + - MCprepError if failed (may return with message) """ # Check if the MTL exists. If not, then check if it # uses underscores. If still not, then return False @@ -180,7 +181,8 @@ def convert_mtl(filepath): if mtl_underscores.exists(): mtl = mtl_underscores else: - return False + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file) lines = None copied_file = None @@ -190,8 +192,9 @@ def convert_mtl(filepath): lines = mtl_file.readlines() except Exception as e: print(e) - return False - + line, file = env.current_line_and_file() + return MCprepError(e, line, file, "Could not read file!") + # This checks to see if the user is using a built-in colorspace or if none of the lines have map_d. If so # then ignore this file and return None if bpy.context.scene.view_settings.view_transform in BUILTIN_SPACES or not any("map_d" in s for s in lines): @@ -215,10 +218,11 @@ def convert_mtl(filepath): print("Header " + str(header)) copied_file = shutil.copy2(mtl, original_mtl_path.absolute()) else: - return True + return None except Exception as e: print(e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file) # In this section, we go over each line # and check to see if it begins with map_d. If @@ -231,7 +235,8 @@ def convert_mtl(filepath): lines[index] = "# " + line except Exception as e: print(e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file, "Could not read file!") # This needs to be seperate since it involves writing try: @@ -243,9 +248,10 @@ def convert_mtl(filepath): except Exception as e: print(e) shutil.copy2(copied_file, mtl) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file) - return True + return None def enble_obj_importer() -> Optional[bool]: @@ -503,10 +509,13 @@ def execute(self, context): # First let's convert the MTL if needed conv_res = convert_mtl(self.filepath) try: - if conv_res is None: - pass # skipped, no issue anyways. - elif conv_res is False: - self.report({"WARNING"}, "MTL conversion failed!") + if isinstance(conv_res, MCprepError): + if isinstance(conv_res.err_type, FileNotFoundError): + self.report({"WARNING"}, "MTL not found!") + elif conv_res.msg is not None: + self.report({"WARNING"}, conv_res.msg) + else: + self.report({"WARNING"}, conv_res.err_type) res = None if util.min_bv((3, 5)): From f29d30837f2cddc26768e2ad1823cc715ca04051 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 20:46:27 -0600 Subject: [PATCH 03/45] refactor: General refactoring of world.py In this commit, we do the following: - Convert more functions to use MCprepError - Remove Blender Internal and 2.7X specific code --- MCprep_addon/world_tools.py | 132 ++++++++++-------------------------- 1 file changed, 35 insertions(+), 97 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index c89e146d..d0abc360 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -16,10 +16,11 @@ # # ##### END GPL LICENSE BLOCK ##### +import enum import os import math from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Union import shutil import bpy @@ -253,16 +254,29 @@ def convert_mtl(filepath) -> Optional[MCprepError]: return None +class OBJImportCode(enum.Enum): + """ + This represents the state of the + OBJ import addon in pre-4.0 versions + of Blender + """ + ALREADY_ENABLED = 0 + DISABLED = 1 -def enble_obj_importer() -> Optional[bool]: - """Checks if obj import is avail and tries to activate if not. +def enable_obj_importer() -> Union[OBJImportCode, MCprepError]: + """ + Checks if the obj import addon (pre-Blender 4.0) is enabled, + and enable it if it isn't enabled. - If we fail to enable obj importing, return false. True if enabled, and Non - if nothing changed. + Returns: + - OBJImportCode.ALREADY_ENABLED if either enabled already or + the user is using Blender 4.0. + - OBJImportCode.DISABLED if the addon had to be enabled. + - MCprepError with a message if the addon could not be enabled. """ enable_addon = None if util.min_bv((4, 0)): - return None # No longer an addon, native built in. + return OBJImportCode.ALREADY_ENABLED # No longer an addon, native built in. else: in_import_scn = "obj_import" not in dir(bpy.ops.wm) in_wm = "" @@ -270,13 +284,14 @@ def enble_obj_importer() -> Optional[bool]: enable_addon = "io_scene_obj" if enable_addon is None: - return None + return OBJImportCode.ALREADY_ENABLED try: bpy.ops.preferences.addon_enable(module=enable_addon) - return True + return OBJImportCode.DISABLED except RuntimeError: - return False + line, file = env.current_line_and_file() + return MCprepError(Exception(), line, file, "Could not enable the Built-in OBJ importer!") # ----------------------------------------------------------------------------- @@ -480,15 +495,15 @@ def execute(self, context): self.report({"ERROR"}, "You must select a .obj file to import") return {'CANCELLED'} - res = enble_obj_importer() - if res is None: + res = enable_obj_importer() + if res is OBJImportCode.ALREADY_ENABLED: pass - elif res is True: + elif res is OBJImportCode.DISABLED: self.report( {"INFO"}, "FYI: had to enable OBJ imports in user preferences") - elif res is False: - self.report({"ERROR"}, "Built-in OBJ importer could not be enabled") + elif isinstance(res, MCprepError): + self.report({"ERROR"}, res.msg) return {'CANCELLED'} # There are a number of bug reports that come from the generic call @@ -671,10 +686,8 @@ def execute(self, context): self.prep_world_cycles(context) elif engine == 'BLENDER_EEVEE': self.prep_world_eevee(context) - elif engine == 'BLENDER_RENDER' or engine == 'BLENDER_GAME': - self.prep_world_internal(context) else: - self.report({'ERROR'}, "Must be cycles, eevee, or blender internal") + self.report({'ERROR'}, "Must be Cycles or EEVEE") return {'FINISHED'} def prep_world_cycles(self, context: Context) -> None: @@ -765,41 +778,7 @@ def prep_world_eevee(self, context: Context) -> None: # Renders faster at a (minor?) cost of the image output # TODO: given the output change, consider make a bool toggle for this - bpy.context.scene.render.use_simplify = True - - def prep_world_internal(self, context): - # check for any suns with the sky setting on; - if not context.scene.world: - return - context.scene.world.use_nodes = False - context.scene.world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - context.scene.world.light_settings.use_ambient_occlusion = True - context.scene.world.light_settings.ao_blend_type = 'MULTIPLY' - context.scene.world.light_settings.ao_factor = 0.1 - context.scene.world.light_settings.use_environment_light = True - context.scene.world.light_settings.environment_energy = 0.05 - context.scene.render.use_shadows = True - context.scene.render.use_raytrace = True - context.scene.render.use_textures = True - - # check for any sunlamps with sky setting - sky_used = False - for lamp in context.scene.objects: - if lamp.type not in ("LAMP", "LIGHT") or lamp.data.type != "SUN": - continue - if lamp.data.sky.use_sky: - sky_used = True - break - if sky_used: - env.log("MCprep sky being used with atmosphere") - context.scene.world.use_sky_blend = False - context.scene.world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - else: - env.log("No MCprep sky with atmosphere") - context.scene.world.use_sky_blend = True - context.scene.world.horizon_color = (0.647705, 0.859927, 0.940392) - context.scene.world.zenith_color = (0.0954261, 0.546859, 1) - + bpy.context.scene.render.use_simplify = True class MCPREP_OT_add_mc_sky(bpy.types.Operator): """Add sun lamp and time of day (dynamic) driver, setup sky with sun and moon""" @@ -811,7 +790,7 @@ def enum_options(self, context: Context) -> List[tuple]: """Dynamic set of enums to show based on engine""" engine = bpy.context.scene.render.engine enums = [] - if bpy.app.version >= (2, 77) and engine in ("CYCLES", "BLENDER_EEVEE"): + if engine in ("CYCLES", "BLENDER_EEVEE"): enums.append(( "world_shader", "Dynamic sky + shader sun/moon", @@ -905,17 +884,7 @@ def execute(self, context): if self.world_type in ("world_static_mesh", "world_static_only"): # Create world dynamically (previous, simpler implementation) new_sun = self.create_sunlamp(context) - new_objs.append(new_sun) - - if engine in ('BLENDER_RENDER', 'BLENDER_GAME'): - world = context.scene.world - if not world: - world = bpy.data.worlds.new("MCprep World") - context.scene.world = world - new_sun.data.shadow_method = 'RAY_SHADOW' - new_sun.data.shadow_soft_size = 0.5 - world.use_sky_blend = False - world.horizon_color = (0.00938029, 0.0125943, 0.0140572) + new_objs.append(new_sun) bpy.ops.mcprep.world(skipUsage=True) # do rest of sky setup elif engine == 'CYCLES' or engine == 'BLENDER_EEVEE': @@ -929,35 +898,7 @@ def execute(self, context): if wname in bpy.data.worlds: prev_world = bpy.data.worlds[wname] prev_world.name = "-old" - new_objs += self.create_dynamic_world(context, blendfile, wname) - - elif engine == 'BLENDER_RENDER' or engine == 'BLENDER_GAME': - # dynamic world using built-in sun sky and atmosphere - new_sun = self.create_sunlamp(context) - new_objs.append(new_sun) - new_sun.data.shadow_method = 'RAY_SHADOW' - new_sun.data.shadow_soft_size = 0.5 - - world = context.scene.world - if not world: - world = bpy.data.worlds.new("MCprep World") - context.scene.world = world - world.use_sky_blend = False - world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - - # be sure to turn off all other sun lamps with atmosphere set - new_sun.data.sky.use_sky = True # use sun orientation settings if BI - for lamp in context.scene.objects: - if lamp.type not in ("LAMP", "LIGHT") or lamp.data.type != "SUN": - continue - if lamp == new_sun: - continue - lamp.data.sky.use_sky = False - - time_obj = get_time_object() - if not time_obj: - env.log( - "TODO: implement create time_obj, parent sun to it & driver setup") + new_objs += self.create_dynamic_world(context, blendfile, wname) if self.world_type in ("world_static_mesh", "world_mesh"): if not os.path.isfile(blendfile): @@ -1026,10 +967,7 @@ def execute(self, context): def create_sunlamp(self, context: Context) -> bpy.types.Object: """Create new sun lamp from primitives""" - if hasattr(bpy.data, "lamps"): # 2.7 - newlamp = bpy.data.lamps.new("Sun", "SUN") - else: # 2.8 - newlamp = bpy.data.lights.new("Sun", "SUN") + newlamp = bpy.data.lights.new("Sun", "SUN") obj = bpy.data.objects.new("Sunlamp", newlamp) obj.location = (0, 0, 20) obj.rotation_euler[0] = 0.481711 From d57a48e20afd897d3132da6c71350a8f04af8a53 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 21:21:24 -0600 Subject: [PATCH 04/45] refactor: util.py refactoring for new error type This refactors the following functions to use the new error type: - bAppendLink - open_program In addition, the function bAppendLink has had all 2.7X related code removed, although an argument related to 2.7X layers is kept to avoid widescale breakage --- MCprep_addon/spawner/spawn_util.py | 7 +-- MCprep_addon/util.py | 71 ++++++++++++++++++------------ MCprep_addon/world_tools.py | 33 +++++++------- 3 files changed, 66 insertions(+), 45 deletions(-) diff --git a/MCprep_addon/spawner/spawn_util.py b/MCprep_addon/spawner/spawn_util.py index a8031347..ae1ce8d0 100644 --- a/MCprep_addon/spawner/spawn_util.py +++ b/MCprep_addon/spawner/spawn_util.py @@ -24,7 +24,7 @@ import bpy from bpy.types import Context, Collection, BlendDataLibraries -from ..conf import env +from ..conf import MCprepError, env from .. import util from .. import tracking from . import mobs @@ -372,6 +372,7 @@ def load_linked(self, context: Context, path: str, name: str) -> None: path = bpy.path.abspath(path) act = None + res = None if hasattr(bpy.data, "groups"): res = util.bAppendLink(f"{path}/Group", name, True) act = context.object # assumption of object after linking, 2.7 only @@ -382,10 +383,10 @@ def load_linked(self, context: Context, path: str, name: str) -> None: else: print("Error: Should have had at least one object selected.") - if res is False: + if isinstance(res, MCprepError): # Most likely scenario, path was wrong and raised "not a library". # end and automatically reload assets. - self.report({'WARNING'}, "Failed to load asset file") + self.report({'WARNING'}, res.msg) bpy.ops.mcprep.prompt_reset_spawners('INVOKE_DEFAULT') return diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 85921ce5..0d74f132 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -134,19 +134,27 @@ def materialsFromObj(obj_list: List[bpy.types.Object]) -> List[Material]: return mat_list -def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True) -> bool: - """For multiple version compatibility, this function generalized - appending/linking blender post 2.71 changed to new append/link methods +def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True) -> Optional[MCprepError]: + """ + This function calls the append and link methods in an + easy and safe manner. Note that for 2.8 compatibility, the directory passed in should already be correctly identified (eg Group or Collection) Arguments: - directory: xyz.blend/Type, where Type is: Collection, Group, Material... - name: asset name + directory: str + xyz.blend/Type, where Type is: Collection, Group, Material... + name: str + Asset name toLink: bool + If true, link instead of append + active_layer: bool=True + Deprecated in MCprep 3.6 as it relates to pre-2.8 layers - Returns: true if successful, false if not. + Returns: + - None if successful + - MCprepError with message if the asset could not be appended or linked """ env.log(f"Appending {directory} : {name}", vv_only=True) @@ -155,17 +163,7 @@ def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True if directory[-1] != "/" and directory[-1] != os.path.sep: directory += os.path.sep - if "link_append" in dir(bpy.ops.wm): - # OLD method of importing, e.g. in blender 2.70 - env.log("Using old method of append/link, 2.72 <=", vv_only=True) - try: - bpy.ops.wm.link_append(directory=directory, filename=name, link=toLink) - return True - except RuntimeError as e: - print("bAppendLink", e) - return False - elif "link" in dir(bpy.ops.wm) and "append" in dir(bpy.ops.wm): - env.log("Using post-2.72 method of append/link", vv_only=True) + if "link" in dir(bpy.ops.wm) and "append" in dir(bpy.ops.wm): if toLink: bpy.ops.wm.link(directory=directory, filename=name) else: @@ -173,10 +171,11 @@ def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True bpy.ops.wm.append( directory=directory, filename=name) - return True + return None except RuntimeError as e: print("bAppendLink", e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file, f"Could not append {name}!") def obj_copy( @@ -228,7 +227,11 @@ def min_bv(version: Tuple, *, inclusive: bool = True) -> bool: def bv28() -> bool: - """Check if blender 2.8, for layouts, UI, and properties. """ + """ + Check if blender 2.8, for layouts, UI, and properties. + + Deprecated in MCprep 3.5, but kept to avoid breakage for now... + """ env.deprecation_warning() return min_bv((2, 80)) @@ -339,19 +342,33 @@ def link_selected_objects_to_scene() -> None: if ob not in list(bpy.context.scene.objects): obj_link_scene(ob) +def open_program(executable: str) -> Optional[MCprepError]: + """ + Runs an executable such as Mineways or jmc2OBJ, taking into account the + user's operating system (using Wine if Mineways is to be launched on + a non-Windows OS such as macOS or Linux) and automatically checks if + the program exists or has the right permissions to be executed. -def open_program(executable: str) -> Union[int, str]: + Returns: + - None if the program is found and ran successfully + - MCprepError in all error cases (may have error message) + """ # Open an external program from filepath/executbale executable = bpy.path.abspath(executable) env.log(f"Open program request: {executable}") + # Doesn't matter where the exact error occurs + # in this function, since they're all going to + # be crazy hard to decipher + line, file = env.current_line_and_file() + # input could be .app file, which appears as if a folder if not os.path.isfile(executable): env.log("File not executable") if not os.path.isdir(executable): - return -1 + return MCprepError(FileNotFoundError(), line, file) elif not executable.lower().endswith(".app"): - return -1 + return MCprepError(FileNotFoundError(), line, file) # try to open with wine, if available osx_or_linux = platform.system() == "Darwin" @@ -373,13 +390,13 @@ def open_program(executable: str) -> Union[int, str]: # for line in iter(p.stdout.readline, ''): # # will print lines as they come, instead of just at end # print(stdout) - return 0 + return None try: # attempt to use blender's built-in method res = bpy.ops.wm.path_open(filepath=executable) if res == {"FINISHED"}: env.log("Opened using built in path opener") - return 0 + return None else: env.log("Did not get finished response: ", str(res)) except: @@ -393,8 +410,8 @@ def open_program(executable: str) -> Union[int, str]: p = Popen(['open', executable], stdin=PIPE, stdout=PIPE, stderr=PIPE) stdout, err = p.communicate(b"") if err != b"": - return f"Error occured while trying to open executable: {err}" - return "Failed to open executable" + return MCprepError(RuntimeError(), line, file, f"Error occured while trying to open executable: {err!r}") + return MCprepError(RuntimeError(), line, file, "Failed to open executable") def open_folder_crossplatform(folder: str) -> bool: diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index d0abc360..cf125d74 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -316,13 +316,14 @@ class MCPREP_OT_open_jmc2obj(bpy.types.Operator): def execute(self, context): addon_prefs = util.get_user_preferences(context) res = util.open_program(addon_prefs.open_jmc2obj_path) - - if res == -1: - bpy.ops.mcprep.install_jmc2obj('INVOKE_DEFAULT') - return {'CANCELLED'} - elif res != 0: - self.report({'ERROR'}, str(res)) - return {'CANCELLED'} + + if isinstance(res, MCprepError): + if isinstance(res.err_type, FileNotFoundError): + bpy.ops.mcprep.install_jmc2obj('INVOKE_DEFAULT') + return {'CANCELLED'} + else: + self.report({'ERROR'}, res.msg) + return {'CANCELLED'} else: self.report({'INFO'}, "jmc2obj should open soon") return {'FINISHED'} @@ -396,14 +397,16 @@ def execute(self, context): if os.path.isfile(addon_prefs.open_mineways_path): res = util.open_program(addon_prefs.open_mineways_path) else: - res = -1 - - if res == -1: - bpy.ops.mcprep.install_mineways('INVOKE_DEFAULT') - return {'CANCELLED'} - elif res != 0: - self.report({'ERROR'}, str(res)) - return {'CANCELLED'} + # Doesn't matter here, it's a dummy value + res = MCprepError(FileNotFoundError(), -1, "") + + if isinstance(res, MCprepError): + if isinstance(res.err_type, FileNotFoundError): + bpy.ops.mcprep.install_mineways('INVOKE_DEFAULT') + return {'CANCELLED'} + else: + self.report({'ERROR'}, res.msg) + return {'CANCELLED'} else: self.report({'INFO'}, "Mineways should open soon") return {'FINISHED'} From 49e5bbb7029fb3d83ae3599b03e724de5ca5c91e Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Fri, 1 Mar 2024 17:42:18 -0600 Subject: [PATCH 05/45] Removed default_materials.py This file is not used at all, and is poorly written. If we need to revisit this idea, we can use Vivy as reference --- MCprep_addon/materials/default_materials.py | 219 -------------------- MCprep_addon/mcprep_ui.py | 1 - 2 files changed, 220 deletions(-) delete mode 100644 MCprep_addon/materials/default_materials.py diff --git a/MCprep_addon/materials/default_materials.py b/MCprep_addon/materials/default_materials.py deleted file mode 100644 index 7b2a6909..00000000 --- a/MCprep_addon/materials/default_materials.py +++ /dev/null @@ -1,219 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -import os -from typing import Union, Optional - -import bpy -from bpy.types import Context, Material - -from .. import tracking -from .. import util -from . import sync - -from ..conf import env, Engine - - -def default_material_in_sync_library(default_material: str, context: Context) -> bool: - """Returns true if the material is in the sync mat library blend file.""" - if env.material_sync_cache is None: - sync.reload_material_sync_library(context) - if util.nameGeneralize(default_material) in env.material_sync_cache: - return True - elif default_material in env.material_sync_cache: - return True - return False - - -def sync_default_material(context: Context, material: Material, default_material: str, engine: Engine) -> Optional[Union[Material, str]]: - """Normal sync material method but with duplication and name change.""" - if default_material in env.material_sync_cache: - import_name = default_material - elif util.nameGeneralize(default_material) in env.material_sync_cache: - import_name = util.nameGeneralize(default_material) - - # If link is true, check library material not already linked. - sync_file = sync.get_sync_blend(context) - - init_mats = list(bpy.data.materials) - path = os.path.join(sync_file, "Material") - util.bAppendLink(path, import_name, False) # No linking. - - imported = set(list(bpy.data.materials)) - set(init_mats) - if not imported: - return f"Could not import {material.name}" - new_default_material = list(imported)[0] - - # Checking if there's a node with the label Texture. - new_material_nodes = new_default_material.node_tree.nodes - if not new_material_nodes.get("MCPREP_diffuse"): - return "Material has no MCPREP_diffuse node" - - if not material.node_tree.nodes: - return "Material has no nodes" - - # Change the texture. - new_default_material_nodes = new_default_material.node_tree.nodes - material_nodes = material.node_tree.nodes - - if not material_nodes.get("Image Texture"): - return "Material has no Image Texture node" - - default_texture_node = new_default_material_nodes.get("MCPREP_diffuse") - image_texture = material_nodes.get("Image Texture").image.name - texture_file = bpy.data.images.get(image_texture) - default_texture_node.image = texture_file - - if engine == "CYCLES" or engine == "BLENDER_EEVEE": - default_texture_node.interpolation = 'Closest' - - material.user_remap(new_default_material) - - # remove the old material since we're changing the default and we don't - # want to overwhelm users - bpy.data.materials.remove(material) - new_default_material.name = material.name - return None - - -class MCPREP_OT_default_material(bpy.types.Operator): - bl_idname = "mcprep.sync_default_materials" - bl_label = "Sync Default Materials" - bl_options = {'REGISTER', 'UNDO'} - - use_pbr: bpy.props.BoolProperty( - name="Use PBR", - description="Use PBR or not", - default=False) - - engine: bpy.props.StringProperty( - name="engine To Use", - description="Defines the engine to use", - default="CYCLES") - - SIMPLE = "simple" - PBR = "pbr" - - track_function = "sync_default_materials" - track_param = None - @tracking.report_error - def execute(self, context): - # Sync file stuff. - sync_file = sync.get_sync_blend(context) - if not os.path.isfile(sync_file): - self.report({'ERROR'}, f"Sync file not found: {sync_file}") - return {'CANCELLED'} - - if sync_file == bpy.data.filepath: - return {'CANCELLED'} - - # Find the default material. - workflow = self.SIMPLE if not self.use_pbr else self.PBR - material_name = material_name = f"default_{workflow}_{self.engine.lower()}" - if not default_material_in_sync_library(material_name, context): - self.report({'ERROR'}, "No default material found") - return {'CANCELLED'} - - # Sync materials. - mat_list = list(bpy.data.materials) - for mat in mat_list: - try: - err = sync_default_material(context, mat, material_name, self.engine.upper()) # no linking - if err: - env.log(err) - except Exception as e: - print(e) - - return {'FINISHED'} - - -class MCPREP_OT_create_default_material(bpy.types.Operator): - bl_idname = "mcprep.create_default_material" - bl_label = "Create Default Material" - bl_options = {'REGISTER', 'UNDO'} - - SIMPLE = "simple" - PBR = "pbr" - - def execute(self, context): - engine = context.scene.render.engine - self.create_default_material(context, engine, "simple") - return {'FINISHED'} - - def create_default_material(self, context, engine, type): - """ - create_default_material: takes 3 arguments and returns nothing - context: Blender Context - engine: the render engine - type: the type of texture that's being dealt with - """ - if not len(bpy.context.selected_objects): - # If there's no selected objects. - self.report({'ERROR'}, "Select an object to create the material") - return - - material_name = f"default_{type}_{engine.lower()}" - default_material = bpy.data.materials.new(name=material_name) - default_material.use_nodes = True - nodes = default_material.node_tree.nodes - links = default_material.node_tree.links - nodes.clear() - - default_texture_node = nodes.new(type="ShaderNodeTexImage") - principled = nodes.new("ShaderNodeBsdfPrincipled") - nodeOut = nodes.new("ShaderNodeOutputMaterial") - - default_texture_node.name = "MCPREP_diffuse" - default_texture_node.label = "Diffuse Texture" - default_texture_node.location = (120, 0) - - principled.inputs["Specular"].default_value = 0 - principled.location = (600, 0) - - nodeOut.location = (820, 0) - - links.new(default_texture_node.outputs[0], principled.inputs[0]) - links.new(principled.outputs["BSDF"], nodeOut.inputs[0]) - - if engine == "EEVEE": - if hasattr(default_material, "blend_method"): - default_material.blend_method = 'HASHED' - if hasattr(default_material, "shadow_method"): - default_material.shadow_method = 'HASHED' - - -classes = ( - MCPREP_OT_default_material, - MCPREP_OT_create_default_material, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.app.handlers.load_post.append(sync.clear_sync_cache) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) - try: - bpy.app.handlers.load_post.remove(sync.clear_sync_cache) - except: - pass diff --git a/MCprep_addon/mcprep_ui.py b/MCprep_addon/mcprep_ui.py index 7639768d..5a492f37 100644 --- a/MCprep_addon/mcprep_ui.py +++ b/MCprep_addon/mcprep_ui.py @@ -1063,7 +1063,6 @@ def draw(self, context): return row = layout.row() - # row.operator("mcprep.create_default_material") split = layout.split() col = split.column(align=True) From 4a8cc63aff1f628650afd4a1bf4e099aa4a83912 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 7 Mar 2024 18:21:48 -0600 Subject: [PATCH 06/45] refactor: Refactored find_from_texture_pack --- MCprep_addon/conf.py | 2 +- MCprep_addon/materials/generate.py | 68 ++++++++++++++++++------------ 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/MCprep_addon/conf.py b/MCprep_addon/conf.py index 766923ac..3d071984 100644 --- a/MCprep_addon/conf.py +++ b/MCprep_addon/conf.py @@ -133,7 +133,7 @@ def __init__(self): # list of material names, each is a string. None by default to indicate # that no reading has occurred. If lib not found, will update to []. # If ever changing the resource pack, should also reset to None. - self.material_sync_cache = [] + self.material_sync_cache: Optional[List] = [] # Whether we use PO files directly or use the converted form self.use_direct_i18n = False diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index c89274d6..8f5f5538 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -17,7 +17,7 @@ # ##### END GPL LICENSE BLOCK ##### import os -from typing import Dict, Optional, List, Any, Tuple, Union +from typing import Dict, Optional, List, Any, Tuple, Union, cast from pathlib import Path from dataclasses import dataclass from enum import Enum @@ -26,7 +26,7 @@ from bpy.types import Context, Material, Image, Texture, Nodes, NodeLinks, Node from .. import util -from ..conf import env, Form +from ..conf import MCprepError, env, Form AnimatedTex = Dict[str, int] @@ -125,43 +125,51 @@ def get_mc_canonical_name(name: str) -> Tuple[str, Optional[Form]]: return canon, form -def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) -> Path: +def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) -> Union[Path, MCprepError]: """Given a blockname (and resource folder), find image filepath. Finds textures following any pack which should have this structure, and the input folder or default resource folder could target at any of the following sublevels above the level. //pack_name/assets/minecraft/textures// + + Returns: + - Path if successful + - MCprepError if error occurs (may return with a message) """ - if not resource_folder: + if resource_folder is None: # default to internal pack - resource_folder = bpy.path.abspath(bpy.context.scene.mcprep_texturepack_path) + resource_folder = Path(cast( + str, + bpy.path.abspath(bpy.context.scene.mcprep_texturepack_path) + )) - if not os.path.isdir(resource_folder): + if not resource_folder.exists() or not resource_folder.is_dir(): env.log("Error, resource folder does not exist") - return + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file, f"Resource pack folder at {resource_folder} does not exist!") # Check multiple paths, picking the first match (order is important), # goal of picking out the /textures folder. check_dirs = [ - os.path.join(resource_folder, "textures"), - os.path.join(resource_folder, "minecraft", "textures"), - os.path.join(resource_folder, "assets", "minecraft", "textures")] + Path(resource_folder, "textures"), + Path(resource_folder, "minecraft", "textures"), + Path(resource_folder, "assets", "minecraft", "textures")] for path in check_dirs: - if os.path.isdir(path): + if path.exists(): resource_folder = path break search_paths = [ resource_folder, # Both singular and plural shown below as it has varied historically. - os.path.join(resource_folder, "blocks"), - os.path.join(resource_folder, "block"), - os.path.join(resource_folder, "items"), - os.path.join(resource_folder, "item"), - os.path.join(resource_folder, "entity"), - os.path.join(resource_folder, "models"), - os.path.join(resource_folder, "model"), + Path(resource_folder, "blocks"), + Path(resource_folder, "block"), + Path(resource_folder, "items"), + Path(resource_folder, "item"), + Path(resource_folder, "entity"), + Path(resource_folder, "models"), + Path(resource_folder, "model"), ] res = None @@ -170,32 +178,36 @@ def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) if "/" in blockname: newpath = blockname.replace("/", os.path.sep) for ext in extensions: - if os.path.isfile(os.path.join(resource_folder, newpath + ext)): - res = os.path.join(resource_folder, newpath + ext) + if Path(resource_folder, newpath + ext).exists(): + res = Path(resource_folder, newpath + ext) return res newpath = os.path.basename(blockname) # case where goes into other subpaths for ext in extensions: - if os.path.isfile(os.path.join(resource_folder, newpath + ext)): - res = os.path.join(resource_folder, newpath + ext) + if Path(resource_folder, newpath + ext).exists(): + res = Path(resource_folder, newpath + ext) return res # fallback (more common case), wide-search for for path in search_paths: - if not os.path.isdir(path): + if not path.is_dir(): continue for ext in extensions: - check_path = os.path.join(path, blockname + ext) - if os.path.isfile(check_path): - res = os.path.join(path, blockname + ext) + check_path = Path(path, blockname + ext) + if check_path.exists() and check_path.is_file(): + res = Path(path, blockname + ext) return res + # Mineways fallback for suffix in ["-Alpha", "-RGB", "-RGBA"]: if blockname.endswith(suffix): - res = os.path.join( + res = Path( resource_folder, "mineways_assets", f"mineways{suffix}.png") - if os.path.isfile(res): + if res.exists() and res.is_file(): return res + if res is None: + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file) return res From fddbe9c5eaa6239bbf451561cc488e856b9a2917 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 15:46:21 -0500 Subject: [PATCH 07/45] feat: Implemented CommonMCOBJ handling for imports --- MCprep_addon/commonmcobj_parser.py | 181 +++++++++++++++++++++++++++++ MCprep_addon/world_tools.py | 181 ++++++++++++++++++++++------- 2 files changed, 320 insertions(+), 42 deletions(-) create mode 100644 MCprep_addon/commonmcobj_parser.py diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py new file mode 100644 index 00000000..6ae57b35 --- /dev/null +++ b/MCprep_addon/commonmcobj_parser.py @@ -0,0 +1,181 @@ +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional, Tuple, TextIO + +class CommonMCOBJTextureType(Enum): + ATLAS = "ATLAS" + INDIVIDUAL_TILES = "INDIVIDUAL_TILES" + +@dataclass +class CommonMCOBJ: + """ + Python representation of the CommonMCOBJ header + """ + # Version of the CommonMCOBJ spec + version: int + + # Exporter name in all lowercase + exporter: str + + # Name of source world + world_name: str + + # Path of source world* + world_path: str + + # Min values of the selection bounding box + exported_bounds_min: Tuple[int, int, int] + + # Max values of the selection bounding box + exported_bounds_max: Tuple[int, int, int] + + # Offset from (0, 0, 0) + export_offset: Tuple[float, float, float] + + # Scale of each block in meters; by default, this should be 1 meter + block_scale: float + + # Coordinate offset for blocks + block_origin_offset: Tuple[float, float, float] + + # Is the Z axis of the OBJ considered up? + z_up: bool + + # Are the textures using large texture atlases or + # individual textures? + texture_type: CommonMCOBJTextureType + + # Are blocks split by type? + has_split_blocks: bool + +def parse_common_header(header_lines: list[str]) -> CommonMCOBJ: + """ + Parses the CommonMCOBJ header information from a list of strings. + + header_lines list[str]: + list of strings representing each line of the header. + + returns: + CommonMCOBJ object + """ + + # Split at the colon and clean up formatting + def clean_and_extract(line: str) -> Tuple[str, str]: + split = line.split(':', 1) + pos = 0 + for i,x in enumerate(split[0]): + if x.isalpha(): + pos = i + break + return (split[0][pos:], split[1].strip()) + + # Basic values + header = CommonMCOBJ( + version=0, + exporter="NULL", + world_name="NULL", + world_path="NULL", + exported_bounds_min=(0, 0, 0), + exported_bounds_max=(0, 0, 0), + export_offset=(0, 0, 0), + block_scale=0, + block_origin_offset=(0, 0, 0), + z_up=False, + texture_type=CommonMCOBJTextureType.ATLAS, + has_split_blocks=False + ) + + # Keys whose values do not need extra processing + NO_VALUE_PARSE = [ + "exporter", + "world_name", + "world_path", + ] + + # Keys whose values are tuples + TUPLE_PARSE_INT = [ + "exported_bounds_min", + "exported_bounds_max", + ] + + TUPLE_PARSE_FLOAT = [ + "export_offset", + "block_origin_offset" + ] + + # Keys whose values are booleans + BOOLEAN_PARSE = [ + "z_up", + "has_split_blocks" + ] + + # Although CommonMCOBJ states that + # order does matter in the header, + # future versions may change the order + # of some values, so it's best to + # use a non-order specific parser + for line in header_lines: + if ":" not in line: + continue + key, value = clean_and_extract(line) + + if key == "version": + header.version = int(value) + + elif key == "block_scale": + header.block_scale = float(value) + + elif key == "texture_type": + header.texture_type = CommonMCOBJTextureType[value] + + # All of these are parsed the same, with + # no parsing need to value + elif key in NO_VALUE_PARSE: + setattr(header, key, value) + + # All of these are parsed the same, with + # parsing the value to a tuple + elif key in TUPLE_PARSE_INT: + setattr(header, key, tuple(map(int, value[1:-1].split(', ')))) + + elif key in TUPLE_PARSE_FLOAT: + setattr(header, key, tuple(map(float, value[1:-1].split(', ')))) + + elif key in BOOLEAN_PARSE: + setattr(header, key, value == "true") + + return header + +def parse_header(f: TextIO) -> Optional[CommonMCOBJ]: + """ + Parses a file and returns a CommonMCOBJ object if + the header exists. + + f: TextIO + File object + + Returns: + - CommonMCOBJ object if header exists + - None otherwise + """ + lines = f.readlines() + + header: List[str] = [] + found_header = False + + # Read in the header + for l in lines: + tl = " ".join(l.split()) + if tl == "# COMMON_MC_OBJ_START": + header.append(tl) + found_header = True + continue + elif tl == "# COMMON_MC_OBJ_END": + header.append(tl) + break + if not found_header or tl == "#": + continue + header.append(tl) + if not len(header): + return None + return parse_common_header(header) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index fd598020..694ccccd 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -16,11 +16,14 @@ # # ##### END GPL LICENSE BLOCK ##### +from dataclasses import fields +from enum import Enum, auto import os import math from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Union import shutil +from MCprep_addon.commonmcobj_parser import CommonMCOBJ, CommonMCOBJTextureType, parse_header import bpy from bpy.types import Context, Camera @@ -79,8 +82,11 @@ class ObjHeaderOptions: """Wrapper functions to avoid typos causing issues.""" def __init__(self): - self._exporter: Optional[str] = None - self._file_type: Optional[str] = None + # This assumes all OBJs that aren't from Mineways + # and don't have a CommonMCOBJ header are from + # jmc2obj, and use individual tiles for textures + self._exporter: Optional[str] = "jmc2obj" + self._file_type: Optional[str] = "INDIVIDUAL_TILES" """ Wrapper functions to avoid typos causing issues @@ -110,51 +116,124 @@ def texture_type(self): return self._file_type if self._file_type is not None else "NONE" -obj_header = ObjHeaderOptions() +class WorldExporter(Enum): + """ + Defines all supported exporters + with a fallback + """ + + # Mineways with CommonMCOBJ + Mineways = auto() + + # Jmc2OBJ with CommonMCOBJ + Jmc2OBJ = auto() + + # Cmc2OBJ, the reference + # implementation of CommonMCOBJ + # + # For the most part, this + # will be treated as + # Unknown as it's not meant + # for regular use. The distinct + # option exists for testing purposes + Cmc2OBJ = auto() + # Any untested exporter + Unknown = auto() + + # Mineways before the CommonMCOBJ standard + ClassicMW = auto() + + # jmc2OBJ before the CommonMCOBJ standard + ClassicJmc = auto() + +EXPORTER_MAPPING = { + "mineways" : WorldExporter.Mineways, + "jmc2obj" : WorldExporter.Jmc2OBJ, + "cmc2obj" : WorldExporter.Cmc2OBJ, + "mineways-c" : WorldExporter.ClassicMW, + "jmc2obj-c" : WorldExporter.ClassicJmc +} + +def get_exporter(context: Context) -> Optional[WorldExporter]: + """ + Return the exporter on the active object if it has + an exporter attribute. -def detect_world_exporter(filepath: Path) -> None: + For maximum backwards compatibility, it'll convert the + explicit options we have in MCprep for world exporters to + WorldExporter enum objects, if the object does not have either + the CommonMCOBJ exporter attribute, or if it does not have the + MCPREP_OBJ_EXPORTER attribute added in MCprep 3.6. This backwards + compatibility will be removed by default in MCprep 4.0 + + Returns: + - WorldExporter if the world exporter can be detected + - None otherwise + """ + obj = context.active_object + if "COMMONMCOBJ_HEADER" in obj: + if obj["exporter"] in EXPORTER_MAPPING: + return EXPORTER_MAPPING[obj["exporter"]] + else: + return WorldExporter.Unknown + elif "MCPREP_OBJ_HEADER" in obj: + if "MCPREP_OBJ_EXPORTER" in obj: + return EXPORTER_MAPPING[obj["MCPREP_OBJ_EXPORTER"]] + + # This section will be placed behind a legacy + # option in MCprep 4.0, once CommonMCOBJ becomes + # more adopted in exporters + prefs = util.get_user_preferences(context) + if prefs.MCprep_exporter_type == "Mineways": + return WorldExporter.ClassicMW + elif prefs.MCprep_exporter_type == "jmc2obj": + return WorldExporter.ClassicJmc + return None + +def detect_world_exporter(filepath: Path) -> Union[CommonMCOBJ, ObjHeaderOptions]: """Detect whether Mineways or jmc2obj was used, based on prefix info. Primary heruistic: if detect Mineways header, assert Mineways, else assume jmc2obj. All Mineways exports for a long time have prefix info set in the obj file as comments. """ + obj_header = ObjHeaderOptions() with open(filepath, 'r') as obj_fd: try: - header = obj_fd.readline() - if 'mineways' in header.lower(): - obj_header.set_mineways() - # form of: # Wavefront OBJ file made by Mineways version 5.10... - for line in obj_fd: - if line.startswith("# File type:"): - header = line.rstrip() # Remove trailing newline - - # The issue here is that Mineways has changed how the header is generated. - # As such, we're limited with only a couple of OBJs, some from - # 2020 and some from 2023, so we'll assume people are using - # an up to date version. - atlas = ( - "# File type: Export all textures to three large images", - "# File type: Export full color texture patterns" - ) - tiles = ( - "# File type: Export tiles for textures to directory textures", - "# File type: Export individual textures to directory tex" - ) - print('"{}"'.format(header)) - if header in atlas: # If a texture atlas is used - obj_header.set_atlas() - elif header in tiles: # If the OBJ uses individual textures - obj_header.set_seperated() - return + cmc_header = parse_header(obj_fd) + if cmc_header is not None: + return cmc_header + else: + header = obj_fd.readline() + if 'mineways' in header.lower(): + obj_header.set_mineways() + # form of: # Wavefront OBJ file made by Mineways version 5.10... + for line in obj_fd: + if line.startswith("# File type:"): + header = line.rstrip() # Remove trailing newline + + # The issue here is that Mineways has changed how the header is generated. + # As such, we're limited with only a couple of OBJs, some from + # 2020 and some from 2023, so we'll assume people are using + # an up to date version. + atlas = ( + "# File type: Export all textures to three large images", + "# File type: Export full color texture patterns" + ) + tiles = ( + "# File type: Export tiles for textures to directory textures", + "# File type: Export individual textures to directory tex" + ) + print('"{}"'.format(header)) + if header in atlas: # If a texture atlas is used + obj_header.set_atlas() + elif header in tiles: # If the OBJ uses individual textures + obj_header.set_seperated() + return obj_header except UnicodeDecodeError: print(f"Failed to read first line of obj: {filepath}") - return - obj_header.set_jmc2obj() - # Since this is the default for Jmc2Obj, - # we'll assume this is what the OBJ is using - obj_header.set_seperated() + return obj_header def convert_mtl(filepath): @@ -591,17 +670,35 @@ def execute(self, context): return {'CANCELLED'} prefs = util.get_user_preferences(context) - detect_world_exporter(self.filepath) - prefs.MCprep_exporter_type = obj_header.exporter() + header = detect_world_exporter(Path(self.filepath)) + + if isinstance(header, ObjHeaderOptions): + prefs.MCprep_exporter_type = header.exporter() for obj in context.selected_objects: - obj["MCPREP_OBJ_HEADER"] = True - obj["MCPREP_OBJ_FILE_TYPE"] = obj_header.texture_type() + if isinstance(header, CommonMCOBJ): + obj["COMMONMCOBJ_HEADER"] = True + for field in fields(header): + if field.type == CommonMCOBJTextureType: + obj[field.name] = getattr(header, field.name).value + else: + obj[field.name] = getattr(header, field.name) + elif isinstance(header, ObjHeaderOptions): + obj["MCPREP_OBJ_HEADER"] = True + obj["MCPREP_OBJ_FILE_TYPE"] = header.texture_type() + + # Future-proofing for MCprep 4.0 when we + # put global exporter options behind a legacy + # option and by default use the object for + # getting the exporter + obj["MCPREP_OBJ_EXPORTER"] = "mineways-c" if header.exporter() == "Mineways" else "jmc2obj-c" self.split_world_by_material(context) - addon_prefs = util.get_user_preferences(context) - self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. + # TODO: figure out how we should + # track exporters from here on out + # addon_prefs = util.get_user_preferences(context) + # self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. return {'FINISHED'} def obj_name_to_material(self, obj): From 61eff5776ecc123ba9075cd78492d5ffbf73d396 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 15:47:40 -0500 Subject: [PATCH 08/45] refactor: updated UI to use new exporter handling --- MCprep_addon/mcprep_ui.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/MCprep_addon/mcprep_ui.py b/MCprep_addon/mcprep_ui.py index 7639768d..d038c645 100644 --- a/MCprep_addon/mcprep_ui.py +++ b/MCprep_addon/mcprep_ui.py @@ -753,13 +753,14 @@ def draw(self, context): row = col.row(align=True) row.prop(addon_prefs, "MCprep_exporter_type", expand=True) row = col.row(align=True) - if addon_prefs.MCprep_exporter_type == "(choose)": + exporter = world_tools.get_exporter(context) + if exporter is None: row.operator( "mcprep.open_jmc2obj", text=env._("Select exporter!"), icon='ERROR') row.enabled = False - elif addon_prefs.MCprep_exporter_type == "Mineways": + elif exporter is world_tools.WorldExporter.Mineways or exporter is world_tools.WorldExporter.ClassicMW: row.operator("mcprep.open_mineways") - else: + elif exporter is world_tools.WorldExporter.Jmc2OBJ or exporter is world_tools.WorldExporter.ClassicJmc: row.operator("mcprep.open_jmc2obj") wpath = addon_prefs.world_obj_path @@ -782,7 +783,8 @@ def draw(self, context): p.filepath = context.scene.mcprep_texturepack_path if context.mode == "OBJECT": col.operator("mcprep.meshswap", text=env._("Mesh Swap")) - if addon_prefs.MCprep_exporter_type == "(choose)": + exporter = world_tools.get_exporter(context) + if exporter is None or exporter is world_tools.WorldExporter.Unknown: col.label(text=env._("Select exporter!"), icon='ERROR') if context.mode == 'EDIT_MESH': col.operator("mcprep.scale_uv") @@ -1499,9 +1501,10 @@ def model_spawner(self, context: Context) -> None: ops = row.operator("mcprep.spawn_model", text=f"Place: {model.name}") ops.location = util.get_cursor_location(context) ops.filepath = model.filepath - if addon_prefs.MCprep_exporter_type == "Mineways": + exporter = world_tools.get_exporter(context) + if exporter is world_tools.WorldExporter.Mineways or exporter is world_tools.WorldExporter.ClassicMW: ops.snapping = "offset" - elif addon_prefs.MCprep_exporter_type == "jmc2obj": + elif exporter is world_tools.WorldExporter.Jmc2OBJ or exporter is world_tools.WorldExporter.ClassicJmc: ops.snapping = "center" else: box = col.box() @@ -1521,9 +1524,10 @@ def model_spawner(self, context: Context) -> None: ops = col.operator("mcprep.import_model_file") ops.location = util.get_cursor_location(context) - if addon_prefs.MCprep_exporter_type == "Mineways": + exporter = world_tools.get_exporter(context) + if exporter is world_tools.WorldExporter.Mineways or exporter is world_tools.WorldExporter.ClassicMW: ops.snapping = "center" - elif addon_prefs.MCprep_exporter_type == "jmc2obj": + elif exporter is world_tools.WorldExporter.Jmc2OBJ or exporter is world_tools.WorldExporter.ClassicJmc: ops.snapping = "offset" split = layout.split() From 8e98cfb36d4c78a02193b68200317547ecbe08f2 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 15:52:18 -0500 Subject: [PATCH 09/45] Fixed typo in export_bounds_* keys --- MCprep_addon/commonmcobj_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index 6ae57b35..9fd8048d 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -94,8 +94,8 @@ def clean_and_extract(line: str) -> Tuple[str, str]: # Keys whose values are tuples TUPLE_PARSE_INT = [ - "exported_bounds_min", - "exported_bounds_max", + "export_bounds_min", + "export_bounds_max", ] TUPLE_PARSE_FLOAT = [ From 373ae95fa8a0e128399659ccbd33a368c724d829 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 15:52:18 -0500 Subject: [PATCH 10/45] Fixed typo in export_bounds_* keys --- MCprep_addon/commonmcobj_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index 9fd8048d..4b9d21dc 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -24,10 +24,10 @@ class CommonMCOBJ: world_path: str # Min values of the selection bounding box - exported_bounds_min: Tuple[int, int, int] + export_bounds_min: Tuple[int, int, int] # Max values of the selection bounding box - exported_bounds_max: Tuple[int, int, int] + export_bounds_max: Tuple[int, int, int] # Offset from (0, 0, 0) export_offset: Tuple[float, float, float] From a419189bba801ceb5661100b66fa6477809b5897 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 15:53:55 -0500 Subject: [PATCH 11/45] Added guard statement in get_exporter --- MCprep_addon/world_tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index 694ccccd..8c1a5d2d 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -172,6 +172,9 @@ def get_exporter(context: Context) -> Optional[WorldExporter]: - None otherwise """ obj = context.active_object + if not obj: + return None + if "COMMONMCOBJ_HEADER" in obj: if obj["exporter"] in EXPORTER_MAPPING: return EXPORTER_MAPPING[obj["exporter"]] From 04542185df69eaf56d5360c1e661d84eb4f0046d Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 16:11:35 -0500 Subject: [PATCH 12/45] refactor: Migrated remaining pure string checks --- MCprep_addon/materials/material_manager.py | 5 +++-- MCprep_addon/materials/prep.py | 12 +++++++----- MCprep_addon/spawner/meshswap.py | 9 +++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/MCprep_addon/materials/material_manager.py b/MCprep_addon/materials/material_manager.py index 6a47f794..2f72e3a6 100644 --- a/MCprep_addon/materials/material_manager.py +++ b/MCprep_addon/materials/material_manager.py @@ -431,8 +431,9 @@ def execute(self, context): self.report({'INFO'}, f"Updated {count} materials") self.track_param = context.scene.render.engine - addon_prefs = util.get_user_preferences(context) - self.track_exporter = addon_prefs.MCprep_exporter_type + # TODO: Rework exporter tracking + # addon_prefs = util.get_user_preferences(context) + # self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} def load_from_texturepack(self, mat): diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index 194416a7..3b5b489e 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -18,6 +18,7 @@ import os +from MCprep_addon import world_tools import bpy from bpy_extras.io_utils import ImportHelper @@ -305,9 +306,10 @@ def execute(self, context): "Nothing modified, be sure you selected objects with existing materials!" ) - addon_prefs = util.get_user_preferences(context) self.track_param = context.scene.render.engine - self.track_exporter = addon_prefs.MCprep_exporter_type + # TODO: Rework exporter tracking + # addon_prefs = util.get_user_preferences(context) + # self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} @@ -411,8 +413,7 @@ class MCPREP_OT_swap_texture_pack( @classmethod def poll(cls, context): - addon_prefs = util.get_user_preferences(context) - if addon_prefs.MCprep_exporter_type != "(choose)": + if world_tools.get_exporter(context) != world_tools.WorldExporter.Unknown: return util.is_atlas_export(context) return False @@ -470,7 +471,8 @@ def execute(self, context): _ = generate.detect_form(mat_list) invalid_uv, affected_objs = uv_tools.detect_invalid_uvs_from_objs(obj_list) - self.track_exporter = addon_prefs.MCprep_exporter_type + # TODO: Rework exporter tracking + # self.track_exporter = addon_prefs.MCprep_exporter_type # set the scene's folder for the texturepack being swapped context.scene.mcprep_texturepack_path = folder diff --git a/MCprep_addon/spawner/meshswap.py b/MCprep_addon/spawner/meshswap.py index ea865051..0338b43f 100644 --- a/MCprep_addon/spawner/meshswap.py +++ b/MCprep_addon/spawner/meshswap.py @@ -20,6 +20,7 @@ from dataclasses import dataclass from typing import Dict, List, Union, Tuple import math +from MCprep_addon import world_tools import mathutils import os import random @@ -465,8 +466,7 @@ class MCPREP_OT_meshswap(bpy.types.Operator): @classmethod def poll(cls, context): - addon_prefs = util.get_user_preferences(context) - return addon_prefs.MCprep_exporter_type != "(choose)" and context.mode == 'OBJECT' + return world_tools.get_exporter(context) != world_tools.WorldExporter.Unknown and context.mode == 'OBJECT' def invoke(self, context, event): return context.window_manager.invoke_props_dialog( @@ -517,8 +517,9 @@ def draw(self, context): @tracking.report_error def execute(self, context): tprep = time.time() - addon_prefs = util.get_user_preferences(context) - self.track_exporter = addon_prefs.MCprep_exporter_type + # TODO: Rework exporter tracking + # addon_prefs = util.get_user_preferences(context) + # self.track_exporter = addon_prefs.MCprep_exporter_type direc = context.scene.meshswap_path if not direc.lower().endswith('.blend'): From d5760d1809a29a57f6d66378e31a7b8bdb3e9511 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 16:13:50 -0500 Subject: [PATCH 13/45] Updated is_atlas_export --- MCprep_addon/util.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 85921ce5..e179bca2 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -26,6 +26,7 @@ import random import re import subprocess +from MCprep_addon.commonmcobj_parser import CommonMCOBJTextureType import bpy from bpy.types import ( @@ -262,6 +263,12 @@ def is_atlas_export(context: Context) -> bool: file_types["ATLAS"] += 1 else: file_types["INDIVIDUAL"] += 1 + elif "COMMONMCOBJ_HEADER" in obj: + tex = CommonMCOBJTextureType[obj["texture_type"]] + if tex is CommonMCOBJTextureType.ATLAS: + file_types["ATLAS"] += 1 + elif tex is CommonMCOBJTextureType.INDIVIDUAL_TILES: + file_types["INDIVIDUAL"] += 1 else: continue From 12123ef0e390b27e283627237af86b4bd3a5965d Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sun, 24 Mar 2024 16:15:07 -0500 Subject: [PATCH 14/45] Fixed incorrect keyword args in parser --- MCprep_addon/commonmcobj_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index 4b9d21dc..718a5a79 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -75,8 +75,8 @@ def clean_and_extract(line: str) -> Tuple[str, str]: exporter="NULL", world_name="NULL", world_path="NULL", - exported_bounds_min=(0, 0, 0), - exported_bounds_max=(0, 0, 0), + export_bounds_min=(0, 0, 0), + export_bounds_max=(0, 0, 0), export_offset=(0, 0, 0), block_scale=0, block_origin_offset=(0, 0, 0), From ab2e1635fb5d5752e98971dd833ccdb00ff54014 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Tue, 26 Mar 2024 10:27:55 -0500 Subject: [PATCH 15/45] fix: Reduced memory usage of header parser --- MCprep_addon/commonmcobj_parser.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index 718a5a79..d67946fa 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -158,14 +158,13 @@ def parse_header(f: TextIO) -> Optional[CommonMCOBJ]: - CommonMCOBJ object if header exists - None otherwise """ - lines = f.readlines() header: List[str] = [] found_header = False # Read in the header - for l in lines: - tl = " ".join(l.split()) + for l in f: + tl = " ".join(l.rstrip().split()) if tl == "# COMMON_MC_OBJ_START": header.append(tl) found_header = True From 2da614e2a4ef7cb674df98a1990eec41c6ca11a1 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 30 Mar 2024 14:42:38 -0500 Subject: [PATCH 16/45] Added BSD license to commonmcobj_parser.py (copied from commonmcobj_parser.py) Normally, I wouldn't do dual licensing, but in this case, it makes sense as it would allow developers to reuse this parser for their own uses under more permissive terms. This doesn't change anything related to MCprep, which is GPL, as BSD 3-Clause is compatible with GPL. The only part that might conflict is Clause 3, but it could be argued that one can't do that under GPL anyway, or any license for that matter, and that Clause 3 is just a reminder to developers. --- MCprep_addon/commonmcobj_parser.py | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index d67946fa..0e12a57d 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -1,3 +1,43 @@ +# BSD 3-Clause License +# +# Copyright (c) 2024, Mahid Sheikh +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# The parser is under a more permissive BSD 3-Clause license to make it easier +# for developers to use in non-GPL code. Normally, I wouldn't do dual licensing, +# but in this case, it makes sense as it would allow developers to reuse this +# parser for their own uses under more permissive terms. This doesn't change anything +# related to MCprep, which is GPL, as BSD 3-Clause is compatible with GPL. The +# only part that might conflict is Clause 3, but it could be argued that one +# can't do that under GPL anyway, or any license for that matter, and that +# Clause 3 is just a reminder to developers. +# +# - Mahid Sheikh + from dataclasses import dataclass from enum import Enum from typing import List, Optional, Tuple, TextIO From 8d4efc06c33ba26b325b6256b93b38d6425e3667 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 30 Mar 2024 15:31:29 -0500 Subject: [PATCH 17/45] Added email to copyright in commonmcobj_parser.py --- MCprep_addon/commonmcobj_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index 0e12a57d..21c09696 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -1,6 +1,6 @@ # BSD 3-Clause License # -# Copyright (c) 2024, Mahid Sheikh +# Copyright (c) 2024, Mahid Sheikh # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: From 5d8ff420c48ccc244f92ff6ca9b72d7237740a10 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 30 Mar 2024 15:39:42 -0500 Subject: [PATCH 18/45] Added licensing information for other components --- LICENSE-3RD-PARTY.txt | 30 ++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 LICENSE-3RD-PARTY.txt diff --git a/LICENSE-3RD-PARTY.txt b/LICENSE-3RD-PARTY.txt new file mode 100644 index 00000000..4d5aad3b --- /dev/null +++ b/LICENSE-3RD-PARTY.txt @@ -0,0 +1,30 @@ +----------------------------------------------------------------------------- + The BSD 3-Clause License + applies to: + - commonmcobj_parser.py Copyright (c) 2024, Mahid Sheikh +----------------------------------------------------------------------------- + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 02cb3399..08597213 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Feature list CREDIT ====== -While this addon is released as open source software, the assets are being released as [Creative Commons Attributions, CC-BY](https://creativecommons.org/licenses/by/3.0/us/). If you use MeshSwap, **please credit the creators** by linking to this page wherever your project may appear: [http://github.com/TheDuckCow/MCprep](https://github.com/TheDuckCow/MCprep) +While this addon is released as open source software under the GNU GPL license, the assets are being released as [Creative Commons Attributions, CC-BY](https://creativecommons.org/licenses/by/3.0/us/). If you use MeshSwap, **please credit the creators** by linking to this page wherever your project may appear: [http://github.com/TheDuckCow/MCprep](https://github.com/TheDuckCow/MCprep). In addition, different parts of MCprep are under different, GPL compatible licenses (see `LICENSE-3RD-PARTY.txt.txt`). Meshswap Block models developed by [Patrick W. Crawford](https://twitter.com/TheDuckCow), [SilverC16](http://youtube.com/user/silverC16), and [Nils Söderman (rymdnisse)](http://youtube.com/rymdnisse). From c7b82e28e3f1681283f8ab63b761617cd3cdac1e Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 30 Mar 2024 15:44:24 -0500 Subject: [PATCH 19/45] Cleaned up LICENSE-3RD-PARTY.txt a little --- LICENSE-3RD-PARTY.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/LICENSE-3RD-PARTY.txt b/LICENSE-3RD-PARTY.txt index 4d5aad3b..8234520d 100644 --- a/LICENSE-3RD-PARTY.txt +++ b/LICENSE-3RD-PARTY.txt @@ -1,8 +1,8 @@ ----------------------------------------------------------------------------- - The BSD 3-Clause License - applies to: - - commonmcobj_parser.py Copyright (c) 2024, Mahid Sheikh ------------------------------------------------------------------------------ +The BSD 3-Clause License + +Applies to: + commonmcobj_parser.py Copyright (c) 2024, Mahid Sheikh Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -28,3 +28,5 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------------------------- From 383f71e9499dea4eb7ddd87014470ccccb456482 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 30 Mar 2024 15:46:28 -0500 Subject: [PATCH 20/45] Removed extra .txt in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08597213..f3d88e79 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Feature list CREDIT ====== -While this addon is released as open source software under the GNU GPL license, the assets are being released as [Creative Commons Attributions, CC-BY](https://creativecommons.org/licenses/by/3.0/us/). If you use MeshSwap, **please credit the creators** by linking to this page wherever your project may appear: [http://github.com/TheDuckCow/MCprep](https://github.com/TheDuckCow/MCprep). In addition, different parts of MCprep are under different, GPL compatible licenses (see `LICENSE-3RD-PARTY.txt.txt`). +While this addon is released as open source software under the GNU GPL license, the assets are being released as [Creative Commons Attributions, CC-BY](https://creativecommons.org/licenses/by/3.0/us/). If you use MeshSwap, **please credit the creators** by linking to this page wherever your project may appear: [http://github.com/TheDuckCow/MCprep](https://github.com/TheDuckCow/MCprep). In addition, different parts of MCprep are under different GPL compatible licenses (see `LICENSE-3RD-PARTY.txt`). Meshswap Block models developed by [Patrick W. Crawford](https://twitter.com/TheDuckCow), [SilverC16](http://youtube.com/user/silverC16), and [Nils Söderman (rymdnisse)](http://youtube.com/rymdnisse). From e3b75a7a0661d0100eacdebca9d92c8501a8c08d Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 6 Apr 2024 19:13:27 -0500 Subject: [PATCH 21/45] reft: updated uses of find_from_texturepack --- MCprep_addon/materials/generate.py | 8 ++++++-- MCprep_addon/materials/material_manager.py | 9 ++++++--- MCprep_addon/materials/sequences.py | 10 +++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index 8f5f5538..e86f28cf 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -356,7 +356,9 @@ def set_texture_pack( """ mc_name, _ = get_mc_canonical_name(material.name) image = find_from_texturepack(mc_name, folder) - if image is None: + if isinstance(image, MCprepError): + if image.msg: + env.log(image.msg) return 0 image_data = util.loadTexture(image) @@ -649,7 +651,9 @@ def replace_missing_texture(image: Image) -> bool: canon, _ = get_mc_canonical_name(name) # TODO: detect for pass structure like normal and still look for right pass image_path = find_from_texturepack(canon) - if not image_path: + if isinstance(image_path, MCprepError): + if image_path.msg: + env.log(image_path.msg) return False image.filepath = image_path # image.reload() # not needed? diff --git a/MCprep_addon/materials/material_manager.py b/MCprep_addon/materials/material_manager.py index 6a47f794..23fddc0d 100644 --- a/MCprep_addon/materials/material_manager.py +++ b/MCprep_addon/materials/material_manager.py @@ -26,7 +26,7 @@ from .. import tracking from .. import util -from ..conf import env +from ..conf import MCprepError, env # ----------------------------------------------------------------------------- @@ -440,8 +440,11 @@ def load_from_texturepack(self, mat): env.log(f"Loading from texpack for {mat.name}", vv_only=True) canon, _ = generate.get_mc_canonical_name(mat.name) image_path = generate.find_from_texturepack(canon) - if not image_path or not os.path.isfile(image_path): - env.log(f"Find missing images: No source file found for {mat.name}") + if isinstance(image_path, MCprepError): + if image_path.msg: + env.log(image_path.msg) + else: + env.log(f"Find missing images: No source file found for {mat.name}") return False # even if images of same name already exist, load new block diff --git a/MCprep_addon/materials/sequences.py b/MCprep_addon/materials/sequences.py index ba112f57..8dada4b9 100644 --- a/MCprep_addon/materials/sequences.py +++ b/MCprep_addon/materials/sequences.py @@ -32,7 +32,7 @@ from . import uv_tools from .. import tracking from .. import util -from ..conf import env, Engine, Form +from ..conf import MCprepError, env, Engine, Form class ExportLocation(enum.Enum): @@ -72,8 +72,12 @@ def animate_single_material( # get the base image from the texturepack (cycles/BI general) image_path_canon = generate.find_from_texturepack(canon) - if not image_path_canon: - env.log(f"Canon path not found for {mat_gen}:{canon}, form {form}, path: {image_path_canon}", vv_only=True) + + if isinstance(image_path_canon, MCprepError): + if image_path_canon.msg: + env.log(image_path_canon.msg) + else: + env.log(f"Error occured during texturepack search for {mat_gen}:{canon}, form {form}") return affectable, False, None if not os.path.isfile(f"{image_path_canon}.mcmeta"): From 0b25c43b3ed593a9fc8cfb4f9fa2e44883559b44 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 6 Apr 2024 19:17:25 -0500 Subject: [PATCH 22/45] doc: updated docstring for detect_form --- MCprep_addon/materials/generate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index e86f28cf..1a2d8127 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -216,6 +216,13 @@ def detect_form(materials: List[Material]) -> Optional[Form]: Useful for pre-determining elibibility of a function and also for tracking reporting to give sense of how common which exporter is used. + + materials: List[Material]: + List of materials to check from + + Returns: + - Form if detected + - None if not detected """ jmc2obj = 0 mc = 0 From 5ba5556f24f220ab5c269dd8e4f3630e3313d6e4 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Mon, 15 Apr 2024 13:07:20 -0500 Subject: [PATCH 23/45] feat: added basic forward compat for headers --- MCprep_addon/commonmcobj_parser.py | 42 +++++++++++++++++++++++++----- MCprep_addon/world_tools.py | 3 +++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index 21c09696..94ff5d4d 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -42,6 +42,8 @@ from enum import Enum from typing import List, Optional, Tuple, TextIO +MAX_SUPPORTED_VERSION = 1 + class CommonMCOBJTextureType(Enum): ATLAS = "ATLAS" INDIVIDUAL_TILES = "INDIVIDUAL_TILES" @@ -87,6 +89,9 @@ class CommonMCOBJ: # Are blocks split by type? has_split_blocks: bool + + # Original header + original_header: Optional[str] def parse_common_header(header_lines: list[str]) -> CommonMCOBJ: """ @@ -122,7 +127,8 @@ def clean_and_extract(line: str) -> Tuple[str, str]: block_origin_offset=(0, 0, 0), z_up=False, texture_type=CommonMCOBJTextureType.ATLAS, - has_split_blocks=False + has_split_blocks=False, + original_header=None ) # Keys whose values do not need extra processing @@ -160,29 +166,51 @@ def clean_and_extract(line: str) -> Tuple[str, str]: key, value = clean_and_extract(line) if key == "version": - header.version = int(value) + try: + header.version = int(value) + if header.version > MAX_SUPPORTED_VERSION: + header.original_header = "\n".join(header_lines) + except Exception: + pass elif key == "block_scale": - header.block_scale = float(value) + try: + header.block_scale = float(value) + except Exception: + pass elif key == "texture_type": - header.texture_type = CommonMCOBJTextureType[value] + try: + header.texture_type = CommonMCOBJTextureType[value] + except Exception: + pass # All of these are parsed the same, with # no parsing need to value + # + # No keys here will be classed as failed elif key in NO_VALUE_PARSE: setattr(header, key, value) # All of these are parsed the same, with # parsing the value to a tuple elif key in TUPLE_PARSE_INT: - setattr(header, key, tuple(map(int, value[1:-1].split(', ')))) + try: + setattr(header, key, tuple(map(int, value[1:-1].split(', ')))) + except Exception: + pass elif key in TUPLE_PARSE_FLOAT: - setattr(header, key, tuple(map(float, value[1:-1].split(', ')))) + try: + setattr(header, key, tuple(map(float, value[1:-1].split(', ')))) + except Exception: + pass elif key in BOOLEAN_PARSE: - setattr(header, key, value == "true") + try: + setattr(header, key, value == "true") + except Exception: + pass return header diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index 8c1a5d2d..1243c5cb 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -682,10 +682,13 @@ def execute(self, context): if isinstance(header, CommonMCOBJ): obj["COMMONMCOBJ_HEADER"] = True for field in fields(header): + if getattr(header, field.name) is None: + continue if field.type == CommonMCOBJTextureType: obj[field.name] = getattr(header, field.name).value else: obj[field.name] = getattr(header, field.name) + elif isinstance(header, ObjHeaderOptions): obj["MCPREP_OBJ_HEADER"] = True obj["MCPREP_OBJ_FILE_TYPE"] = header.texture_type() From 74a5367a557011df9ed6372b0806aaff6eb09890 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Fri, 3 May 2024 09:35:52 -0500 Subject: [PATCH 24/45] build: Added Blender 4.1 and 4.2 to build config Signed-off-by: Mahid Sheikh --- bpy-build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bpy-build.yaml b/bpy-build.yaml index b322b7c0..b5b0ccb3 100644 --- a/bpy-build.yaml +++ b/bpy-build.yaml @@ -2,6 +2,8 @@ addon_folder: MCprep_addon build_name: MCprep_addon install_versions: + - 4.2 + - 4.1 - 4.0 - 3.6 - 3.5 From 62ceb23aac0b5943a6de1182ed8f3fd9f0a441e2 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Thu, 9 May 2024 10:23:19 -0500 Subject: [PATCH 25/45] cmcobj: Header info is now stored inside an empty Signed-off-by: Mahid Sheikh --- MCprep_addon/util.py | 4 +- MCprep_addon/world_tools.py | 75 +++++++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 7ab733a0..82414d72 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -266,8 +266,8 @@ def is_atlas_export(context: Context) -> bool: file_types["ATLAS"] += 1 else: file_types["INDIVIDUAL"] += 1 - elif "COMMONMCOBJ_HEADER" in obj: - tex = CommonMCOBJTextureType[obj["texture_type"]] + elif "COMMONMCOBJ_HEADER" in obj and obj["PARENTED_EMPTY"] is not None: + tex = CommonMCOBJTextureType[obj["PARENTED_EMPTY"]["texture_type"]] if tex is CommonMCOBJTextureType.ATLAS: file_types["ATLAS"] += 1 elif tex is CommonMCOBJTextureType.INDIVIDUAL_TILES: diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index c5ec3331..ea1dbea2 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -177,8 +177,8 @@ def get_exporter(context: Context) -> Optional[WorldExporter]: return None if "COMMONMCOBJ_HEADER" in obj: - if obj["exporter"] in EXPORTER_MAPPING: - return EXPORTER_MAPPING[obj["exporter"]] + if obj["PARENTED_EMPTY"] is not None and obj["PARENTED_EMPTY"]["exporter"] in EXPORTER_MAPPING: + return EXPORTER_MAPPING[obj["PARENTED_EMPTY"]["exporter"]] else: return WorldExporter.Unknown elif "MCPREP_OBJ_HEADER" in obj: @@ -704,18 +704,61 @@ def execute(self, context): if isinstance(header, ObjHeaderOptions): prefs.MCprep_exporter_type = header.exporter() + - for obj in context.selected_objects: - if isinstance(header, CommonMCOBJ): - obj["COMMONMCOBJ_HEADER"] = True - for field in fields(header): + # Create empty at the center of the OBJ + if isinstance(header, CommonMCOBJ): + # Get actual 3D space coordinates of the full bounding box + # + # These are in Minecraft coordinates, so they translate + # from (X, Y, Z) to (X, -Z, Y) + max_pair = (header.export_bounds_max[0] + header.export_offset[0], + (-header.export_bounds_max[2]) + (-header.export_offset[2]), + header.export_bounds_max[1] + header.export_offset[1]) + + min_pair = (header.export_bounds_min[0] + header.export_offset[0], + (-header.export_bounds_min[2]) + (-header.export_offset[2]), + header.export_bounds_min[1] + header.export_offset[1]) + + # Calculate the center of the bounding box + # + # We do this by taking the average of the given + # points, so: + # (x1 + x2) / 2 + # (y1 + y2) / 2 + # (z1 + z2) / 2 + # + # This will give us the midpoints of these + # coordinates, which in turn will correspond + # to the center of the bounding box + location = ((max_pair[0] + min_pair[0]) / 2, + (max_pair[1] + min_pair[1]) / 2, + (max_pair[2] + min_pair[2]) / 2) + empty = bpy.data.objects.new(name=header.world_name + "_mcprep_empty", object_data=None) + bpy.context.collection.objects.link(empty) + empty.empty_display_size = 2 + empty.empty_display_type = 'PLAIN_AXES' + empty.location = location + self.update_matrices(empty) + for field in fields(header): if getattr(header, field.name) is None: continue if field.type == CommonMCOBJTextureType: - obj[field.name] = getattr(header, field.name).value + empty[field.name] = getattr(header, field.name).value else: - obj[field.name] = getattr(header, field.name) + empty[field.name] = getattr(header, field.name) + + for obj in context.selected_objects: + if isinstance(header, CommonMCOBJ): + obj["COMMONMCOBJ_HEADER"] = True + objects = bpy.data.objects + empty = objects[header.world_name + "_mcprep_empty"] + obj["PARENTED_EMPTY"] = empty + obj.parent = empty + obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object + self.track_exporter = header.exporter + elif isinstance(header, ObjHeaderOptions): obj["MCPREP_OBJ_HEADER"] = True obj["MCPREP_OBJ_FILE_TYPE"] = header.texture_type() @@ -725,15 +768,23 @@ def execute(self, context): # option and by default use the object for # getting the exporter obj["MCPREP_OBJ_EXPORTER"] = "mineways-c" if header.exporter() == "Mineways" else "jmc2obj-c" + self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. self.split_world_by_material(context) - # TODO: figure out how we should - # track exporters from here on out - # addon_prefs = util.get_user_preferences(context) - # self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. return {'FINISHED'} + def update_matrices(self, obj): + """Update mattrices of object so that we can accurately parent, + because for some reason, Blender doesn't do this by default""" + if obj.parent is None: + obj.matrix_world = obj.matrix_basis + + else: + obj.matrix_world = obj.parent.matrix_world * \ + obj.matrix_parent_inverse * \ + obj.matrix_basis + def obj_name_to_material(self, obj): """Update an objects name based on its first material""" if not obj: From 6b9257f6526ae4508df1f1610ac4279efabb5ced Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Thu, 9 May 2024 10:24:21 -0500 Subject: [PATCH 26/45] tracking: Re-enabled exporter tracking Signed-off-by: Mahid Sheikh --- MCprep_addon/materials/material_manager.py | 6 +++--- MCprep_addon/materials/prep.py | 10 +++++----- MCprep_addon/spawner/meshswap.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/MCprep_addon/materials/material_manager.py b/MCprep_addon/materials/material_manager.py index 1d5a08ea..e398b34d 100644 --- a/MCprep_addon/materials/material_manager.py +++ b/MCprep_addon/materials/material_manager.py @@ -431,9 +431,9 @@ def execute(self, context): self.report({'INFO'}, f"Updated {count} materials") self.track_param = context.scene.render.engine - # TODO: Rework exporter tracking - # addon_prefs = util.get_user_preferences(context) - # self.track_exporter = addon_prefs.MCprep_exporter_type + + # NOTE: This is temporary + self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} def load_from_texturepack(self, mat): diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index 3b5b489e..ef400a9e 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -307,9 +307,9 @@ def execute(self, context): ) self.track_param = context.scene.render.engine - # TODO: Rework exporter tracking - # addon_prefs = util.get_user_preferences(context) - # self.track_exporter = addon_prefs.MCprep_exporter_type + + # NOTE: This is temporary + self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} @@ -471,8 +471,8 @@ def execute(self, context): _ = generate.detect_form(mat_list) invalid_uv, affected_objs = uv_tools.detect_invalid_uvs_from_objs(obj_list) - # TODO: Rework exporter tracking - # self.track_exporter = addon_prefs.MCprep_exporter_type + # NOTE: This is temporary + self.track_exporter = addon_prefs.MCprep_exporter_type # set the scene's folder for the texturepack being swapped context.scene.mcprep_texturepack_path = folder diff --git a/MCprep_addon/spawner/meshswap.py b/MCprep_addon/spawner/meshswap.py index 0338b43f..4c32956a 100644 --- a/MCprep_addon/spawner/meshswap.py +++ b/MCprep_addon/spawner/meshswap.py @@ -517,9 +517,9 @@ def draw(self, context): @tracking.report_error def execute(self, context): tprep = time.time() - # TODO: Rework exporter tracking - # addon_prefs = util.get_user_preferences(context) - # self.track_exporter = addon_prefs.MCprep_exporter_type + + # NOTE: This is temporary + self.track_exporter = addon_prefs.MCprep_exporter_type direc = context.scene.meshswap_path if not direc.lower().endswith('.blend'): From 07a5b923612d97c526d4bbd899baaf0c013ad3e5 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Fri, 10 May 2024 13:22:40 -0500 Subject: [PATCH 27/45] style: Moved update_matrices to util.py Signed-off-by: Mahid Sheikh --- MCprep_addon/util.py | 12 ++++++++++++ MCprep_addon/world_tools.py | 15 ++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 82414d72..89fdc8d8 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -49,6 +49,18 @@ # GENERAL SUPPORTING FUNCTIONS (no registration required) # ----------------------------------------------------------------------------- +def update_matrices(obj): + """Update mattrices of object so that we can accurately parent, + because for some stupid reason, Blender doesn't do this by default""" + if obj.parent is None: + obj.matrix_world = obj.matrix_basis + + else: + obj.matrix_world = obj.parent.matrix_world * \ + obj.matrix_parent_inverse * \ + obj.matrix_basis + + def apply_noncolor_data(node: Node) -> Optional[MCprepError]: """ Apply the Non-Color/Generic Data option to the passed diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index ea1dbea2..eed4b68d 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -739,7 +739,7 @@ def execute(self, context): empty.empty_display_size = 2 empty.empty_display_type = 'PLAIN_AXES' empty.location = location - self.update_matrices(empty) + util.update_matrices(empty) for field in fields(header): if getattr(header, field.name) is None: continue @@ -773,18 +773,7 @@ def execute(self, context): self.split_world_by_material(context) return {'FINISHED'} - - def update_matrices(self, obj): - """Update mattrices of object so that we can accurately parent, - because for some reason, Blender doesn't do this by default""" - if obj.parent is None: - obj.matrix_world = obj.matrix_basis - - else: - obj.matrix_world = obj.parent.matrix_world * \ - obj.matrix_parent_inverse * \ - obj.matrix_basis - + def obj_name_to_material(self, obj): """Update an objects name based on its first material""" if not obj: From 841db9e65a7513d1ae4988e6be61ba0a07ac31e5 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Fri, 10 May 2024 13:27:16 -0500 Subject: [PATCH 28/45] feat(CommonMCOBJ): Hide OBJ empty in viewport Signed-off-by: Mahid Sheikh --- MCprep_addon/world_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index eed4b68d..58e17b17 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -739,6 +739,7 @@ def execute(self, context): empty.empty_display_size = 2 empty.empty_display_type = 'PLAIN_AXES' empty.location = location + empty.hide_viewport = True # Hide empty globally util.update_matrices(empty) for field in fields(header): if getattr(header, field.name) is None: From a4328a28e8e312df50c6a4bcb709ed39807f64df Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Sun, 12 May 2024 23:06:49 -0500 Subject: [PATCH 29/45] fix: Fixed missing variables for exporter tracking Signed-off-by: Mahid Sheikh --- MCprep_addon/materials/material_manager.py | 1 + MCprep_addon/materials/prep.py | 2 ++ MCprep_addon/spawner/meshswap.py | 1 + MCprep_addon/world_tools.py | 1 + 4 files changed, 5 insertions(+) diff --git a/MCprep_addon/materials/material_manager.py b/MCprep_addon/materials/material_manager.py index e398b34d..8866e206 100644 --- a/MCprep_addon/materials/material_manager.py +++ b/MCprep_addon/materials/material_manager.py @@ -433,6 +433,7 @@ def execute(self, context): self.track_param = context.scene.render.engine # NOTE: This is temporary + addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index 8c97fa55..01da1d8b 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -317,6 +317,7 @@ def execute(self, context): self.track_param = context.scene.render.engine # NOTE: This is temporary + addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} @@ -480,6 +481,7 @@ def execute(self, context): invalid_uv, affected_objs = uv_tools.detect_invalid_uvs_from_objs(obj_list) # NOTE: This is temporary + addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type # set the scene's folder for the texturepack being swapped diff --git a/MCprep_addon/spawner/meshswap.py b/MCprep_addon/spawner/meshswap.py index 4c32956a..8a245fb5 100644 --- a/MCprep_addon/spawner/meshswap.py +++ b/MCprep_addon/spawner/meshswap.py @@ -519,6 +519,7 @@ def execute(self, context): tprep = time.time() # NOTE: This is temporary + addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type direc = context.scene.meshswap_path diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index 58e17b17..7e4501bf 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -769,6 +769,7 @@ def execute(self, context): # option and by default use the object for # getting the exporter obj["MCPREP_OBJ_EXPORTER"] = "mineways-c" if header.exporter() == "Mineways" else "jmc2obj-c" + addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. self.split_world_by_material(context) From ddb92f741384d29038f56b15c3a59d1f63cf6fa7 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Sat, 8 Jun 2024 20:55:31 -0500 Subject: [PATCH 30/45] fix: Parent empty to all OBJs Signed-off-by: Mahid Sheikh --- MCprep_addon/world_tools.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index baa0eb8f..acf5f87c 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -707,6 +707,7 @@ def execute(self, context): # Create empty at the center of the OBJ + empty = None if isinstance(header, CommonMCOBJ): # Get actual 3D space coordinates of the full bounding box # @@ -749,12 +750,15 @@ def execute(self, context): else: empty[field.name] = getattr(header, field.name) - + else: + empty = bpy.data.objects.new("mcprep_obj_empty", object_data=None) + bpy.context.collection.objects.link(empty) + empty.empty_display_size = 2 + empty.empty_display_type = 'PLAIN_AXES' + empty.hide_viewport = True # Hide empty globally for obj in context.selected_objects: if isinstance(header, CommonMCOBJ): obj["COMMONMCOBJ_HEADER"] = True - objects = bpy.data.objects - empty = objects[header.world_name + "_mcprep_empty"] obj["PARENTED_EMPTY"] = empty obj.parent = empty obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object @@ -764,6 +768,9 @@ def execute(self, context): obj["MCPREP_OBJ_HEADER"] = True obj["MCPREP_OBJ_FILE_TYPE"] = header.texture_type() + obj.parent = empty + obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object + # Future-proofing for MCprep 4.0 when we # put global exporter options behind a legacy # option and by default use the object for From b5ea9e830beeff2a9ca0c0d242818e881f02e2df Mon Sep 17 00:00:00 2001 From: "Patrick W. Crawford" Date: Sat, 8 Jun 2024 20:54:40 -0700 Subject: [PATCH 31/45] Updated several partial migrations to use pathlib, fixes some tests Still have 6 failures locally. --- MCprep_addon/materials/generate.py | 9 +++++---- MCprep_addon/materials/material_manager.py | 2 +- MCprep_addon/materials/prep.py | 5 +++-- MCprep_addon/materials/sequences.py | 2 +- test_files/materials_test.py | 18 +++++++++--------- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index 4412d28e..4b3a2972 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -368,7 +368,7 @@ def set_texture_pack( env.log(image.msg) return 0 - image_data = util.loadTexture(image) + image_data = util.loadTexture(str(image)) _ = set_cycles_texture( image_data, material, extra_passes=use_extra_passes) return 1 @@ -413,7 +413,7 @@ def set_cycles_texture( # check if there is more data to see pass types img_sets = {} if extra_passes: - img_sets = find_additional_passes(image.filepath) + img_sets = find_additional_passes(Path(image.filepath)) changed = False is_grayscale = False @@ -583,7 +583,8 @@ def get_textures(material: Material) -> Dict[str, Image]: def find_additional_passes(image_file: Path) -> Dict[str, Image]: """Find relevant passes like normal and spec in same folder as image.""" - abs_img_file = bpy.path.abspath(image_file) + print("What is this?", image_file) + abs_img_file = bpy.path.abspath(str(image_file)) # needs to be blend file relative env.log(f"\tFind additional passes for: {image_file}", vv_only=True) if not os.path.isfile(abs_img_file): return {} @@ -662,7 +663,7 @@ def replace_missing_texture(image: Image) -> bool: if image_path.msg: env.log(image_path.msg) return False - image.filepath = image_path + image.filepath = str(image_path) # image.reload() # not needed? # pack? diff --git a/MCprep_addon/materials/material_manager.py b/MCprep_addon/materials/material_manager.py index 228dcb1c..0b96ee69 100644 --- a/MCprep_addon/materials/material_manager.py +++ b/MCprep_addon/materials/material_manager.py @@ -452,7 +452,7 @@ def load_from_texturepack(self, mat): # even if images of same name already exist, load new block env.log(f"Find missing images: Creating new image datablock for {mat.name}") # do not use 'check_existing=False' to keep compatibility pre 2.79 - image = bpy.data.images.load(image_path, check_existing=True) + image = bpy.data.images.load(str(image_path), check_existing=True) engine = bpy.context.scene.render.engine if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index eb608240..260942d1 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -18,7 +18,7 @@ import os -from MCprep_addon import world_tools +from pathlib import Path import bpy from bpy_extras.io_utils import ImportHelper @@ -29,6 +29,7 @@ from . import uv_tools from .. import tracking from .. import util +from .. import world_tools from ..conf import env # ----------------------------------------------------------------------------- @@ -492,7 +493,7 @@ def execute(self, context): res = 0 for mat in mat_list: self.preprocess_material(mat) - res += generate.set_texture_pack(mat, folder, self.useExtraMaps) + res += generate.set_texture_pack(mat, Path(folder), self.useExtraMaps) if self.animateTextures: sequences.animate_single_material( mat, diff --git a/MCprep_addon/materials/sequences.py b/MCprep_addon/materials/sequences.py index c1b9bbe1..4a0f9203 100644 --- a/MCprep_addon/materials/sequences.py +++ b/MCprep_addon/materials/sequences.py @@ -219,7 +219,7 @@ def generate_material_sequence(source_path: Path, image_path: Path, form: Option "try running blender as admin") for img_pass in img_pass_dict: - passfile = img_pass_dict[img_pass] + passfile = str(img_pass_dict[img_pass]) # Convert from Path env.log("Running on file:") env.log(bpy.path.abspath(passfile)) diff --git a/test_files/materials_test.py b/test_files/materials_test.py index de154cea..ed2af8f8 100644 --- a/test_files/materials_test.py +++ b/test_files/materials_test.py @@ -695,17 +695,17 @@ def cleanup(): # the test cases; input is diffuse, output is the whole dict cases = [ { - "diffuse": os.path.join(tmp_dir, "oak_log_top.png"), - "specular": os.path.join(tmp_dir, "oak_log_top-s.png"), - "normal": os.path.join(tmp_dir, "oak_log_top_n.png"), + "diffuse": Path(tmp_dir) / "oak_log_top.png", + "specular": Path(tmp_dir) / "oak_log_top-s.png", + "normal": Path(tmp_dir) / "oak_log_top_n.png", }, { - "diffuse": os.path.join(tmp_dir, "oak_log.jpg"), - "specular": os.path.join(tmp_dir, "oak_log_s.jpg"), - "normal": os.path.join(tmp_dir, "oak_log_n.jpeg"), - "displace": os.path.join(tmp_dir, "oak_log_disp.jpeg"), + "diffuse": Path(tmp_dir) / "oak_log.jpg", + "specular": Path(tmp_dir) / "oak_log_s.jpg", + "normal": Path(tmp_dir) / "oak_log_n.jpeg", + "displace": Path(tmp_dir) / "oak_log_disp.jpeg", }, { - "diffuse": os.path.join(tmp_dir, "stonecutter_saw.tiff"), - "normal": os.path.join(tmp_dir, "stonecutter_saw n.tiff"), + "diffuse": Path(tmp_dir) / "stonecutter_saw.tiff", + "normal": Path(tmp_dir) / "stonecutter_saw n.tiff", } ] From 0d65ffd0d7778d220a2b359a96f7b94f6962397c Mon Sep 17 00:00:00 2001 From: "Patrick W. Crawford" Date: Sat, 8 Jun 2024 21:39:29 -0700 Subject: [PATCH 32/45] Moving the added empty to the same collection as the rest of the world --- MCprep_addon/world_tools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index acf5f87c..d4e14cd8 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -736,7 +736,6 @@ def execute(self, context): (max_pair[1] + min_pair[1]) / 2, (max_pair[2] + min_pair[2]) / 2) empty = bpy.data.objects.new(name=header.world_name + "_mcprep_empty", object_data=None) - bpy.context.collection.objects.link(empty) empty.empty_display_size = 2 empty.empty_display_type = 'PLAIN_AXES' empty.location = location @@ -752,7 +751,6 @@ def execute(self, context): else: empty = bpy.data.objects.new("mcprep_obj_empty", object_data=None) - bpy.context.collection.objects.link(empty) empty.empty_display_size = 2 empty.empty_display_type = 'PLAIN_AXES' empty.hide_viewport = True # Hide empty globally @@ -779,7 +777,8 @@ def execute(self, context): addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. - self.split_world_by_material(context) + new_col = self.split_world_by_material(context) + new_col.objects.link(empty) # parent empty return {'FINISHED'} @@ -796,7 +795,7 @@ def obj_name_to_material(self, obj): return obj.name = util.nameGeneralize(mat.name) - def split_world_by_material(self, context: Context) -> None: + def split_world_by_material(self, context: Context) -> bpy.types.Collection: """2.8-only function, split combined object into parts by material""" world_name = os.path.basename(self.filepath) world_name = os.path.splitext(world_name)[0] @@ -816,6 +815,7 @@ def split_world_by_material(self, context: Context) -> None: # Force renames based on material, as default names are not useful. for obj in worldg.objects: self.obj_name_to_material(obj) + return worldg class MCPREP_OT_prep_world(bpy.types.Operator): From 0afc1c9e0f8656124a1beae033bc8ff5fa87ef65 Mon Sep 17 00:00:00 2001 From: "Patrick W. Crawford" Date: Mon, 24 Jun 2024 21:57:14 -0700 Subject: [PATCH 33/45] Added new test world save and exports from jmc2obj and mineways --- .gitattributes | 1 + MCprep_addon/commonmcobj_parser.py | 24 +++++++++++-------- test_files/test_data/jmc2obj_test_1_21.mtl | 3 +++ test_files/test_data/jmc2obj_test_1_21.obj | 3 +++ .../test_data/mineways_test_combined_1_21.mtl | 3 +++ .../test_data/mineways_test_combined_1_21.obj | 3 +++ .../mineways_test_separated_1_21.mtl | 3 +++ .../mineways_test_separated_1_21.obj | 3 +++ test_files/world_saves/Test MCprep 1.14.4.zip | 3 +++ test_files/world_saves/Test MCprep 1.15.2.zip | 3 +++ test_files/world_saves/Test MCprep 1.21.zip | 3 +++ 11 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 test_files/test_data/jmc2obj_test_1_21.mtl create mode 100644 test_files/test_data/jmc2obj_test_1_21.obj create mode 100644 test_files/test_data/mineways_test_combined_1_21.mtl create mode 100644 test_files/test_data/mineways_test_combined_1_21.obj create mode 100644 test_files/test_data/mineways_test_separated_1_21.mtl create mode 100644 test_files/test_data/mineways_test_separated_1_21.obj create mode 100644 test_files/world_saves/Test MCprep 1.14.4.zip create mode 100644 test_files/world_saves/Test MCprep 1.15.2.zip create mode 100644 test_files/world_saves/Test MCprep 1.21.zip diff --git a/.gitattributes b/.gitattributes index 63ea2676..1d8939fd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ *.mtl filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.blend filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py index 94ff5d4d..43346e24 100644 --- a/MCprep_addon/commonmcobj_parser.py +++ b/MCprep_addon/commonmcobj_parser.py @@ -214,14 +214,14 @@ def clean_and_extract(line: str) -> Tuple[str, str]: return header + def parse_header(f: TextIO) -> Optional[CommonMCOBJ]: """ - Parses a file and returns a CommonMCOBJ object if - the header exists. - + Parses a file and returns a CommonMCOBJ object if the header exists. + f: TextIO File object - + Returns: - CommonMCOBJ object if header exists - None otherwise @@ -229,19 +229,23 @@ def parse_header(f: TextIO) -> Optional[CommonMCOBJ]: header: List[str] = [] found_header = False - + # Read in the header - for l in f: - tl = " ".join(l.rstrip().split()) - if tl == "# COMMON_MC_OBJ_START": + lines_read = 0 + for _l in f: + tl = " ".join(_l.rstrip().split()) + lines_read += 1 + if lines_read > 100 and tl and not tl.startswith("#"): + break # no need to parse further than the true header area + elif tl == "# COMMON_MC_OBJ_START": header.append(tl) - found_header = True + found_header = True continue elif tl == "# COMMON_MC_OBJ_END": header.append(tl) break if not found_header or tl == "#": - continue + continue header.append(tl) if not len(header): return None diff --git a/test_files/test_data/jmc2obj_test_1_21.mtl b/test_files/test_data/jmc2obj_test_1_21.mtl new file mode 100644 index 00000000..8c6ebc7d --- /dev/null +++ b/test_files/test_data/jmc2obj_test_1_21.mtl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7155824abda1552da03233e2320dc89b3f1776a62470ab2a515f489269f9ff53 +size 82370 diff --git a/test_files/test_data/jmc2obj_test_1_21.obj b/test_files/test_data/jmc2obj_test_1_21.obj new file mode 100644 index 00000000..d7961ecf --- /dev/null +++ b/test_files/test_data/jmc2obj_test_1_21.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5032b39a22db60ecd3346a40c93b2b1f5871566e49496f04cdd846a934adc681 +size 409323 diff --git a/test_files/test_data/mineways_test_combined_1_21.mtl b/test_files/test_data/mineways_test_combined_1_21.mtl new file mode 100644 index 00000000..c81ef376 --- /dev/null +++ b/test_files/test_data/mineways_test_combined_1_21.mtl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06ef918eb7f9a84e3f33e2fe5970d58ea19e3376462161e62c0c1a05f6c1ab42 +size 133438 diff --git a/test_files/test_data/mineways_test_combined_1_21.obj b/test_files/test_data/mineways_test_combined_1_21.obj new file mode 100644 index 00000000..33504499 --- /dev/null +++ b/test_files/test_data/mineways_test_combined_1_21.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3f0b89937501e34913009f4e9c656674cb33fadded977589253f813b1c910d8 +size 583757 diff --git a/test_files/test_data/mineways_test_separated_1_21.mtl b/test_files/test_data/mineways_test_separated_1_21.mtl new file mode 100644 index 00000000..44029d93 --- /dev/null +++ b/test_files/test_data/mineways_test_separated_1_21.mtl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbe8a7093f0c7799b7919509316eb18146ad423c252852fcb0687dbfe56c08ac +size 147361 diff --git a/test_files/test_data/mineways_test_separated_1_21.obj b/test_files/test_data/mineways_test_separated_1_21.obj new file mode 100644 index 00000000..4c2e3b1e --- /dev/null +++ b/test_files/test_data/mineways_test_separated_1_21.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0f4b8e1742ef87a28b105330990628c6a6bc30d095a0cb87dcf0d3bdc90a250 +size 445809 diff --git a/test_files/world_saves/Test MCprep 1.14.4.zip b/test_files/world_saves/Test MCprep 1.14.4.zip new file mode 100644 index 00000000..4ab192fa --- /dev/null +++ b/test_files/world_saves/Test MCprep 1.14.4.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7aaa259299eefd6f97640f842a40a22c7655cb2207b49cae16686fcb15f90a6 +size 933854 diff --git a/test_files/world_saves/Test MCprep 1.15.2.zip b/test_files/world_saves/Test MCprep 1.15.2.zip new file mode 100644 index 00000000..6ef09189 --- /dev/null +++ b/test_files/world_saves/Test MCprep 1.15.2.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09a96a83293e40d581d6b3fb28496b5f3f1d59487e7fdfe0128b285355bec1cb +size 1057610 diff --git a/test_files/world_saves/Test MCprep 1.21.zip b/test_files/world_saves/Test MCprep 1.21.zip new file mode 100644 index 00000000..44c71bd8 --- /dev/null +++ b/test_files/world_saves/Test MCprep 1.21.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:117c5856616b825e2f5b6c34d4f6cf79efc1014cc2e2e4395ca243f3132449f0 +size 2647829 From 23d4fa4253cbb87cc44bf1848c433a7b30310fea Mon Sep 17 00:00:00 2001 From: "Patrick W. Crawford" Date: Mon, 24 Jun 2024 22:02:41 -0700 Subject: [PATCH 34/45] Fix obj file header parsing from being cleared and some pep8 cleanup --- MCprep_addon/world_tools.py | 151 ++++++++++++++++++--------------- test_files/world_tools_test.py | 88 ++++++++++++++++++- 2 files changed, 166 insertions(+), 73 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index d4e14cd8..06d68474 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -84,11 +84,11 @@ class ObjHeaderOptions: def __init__(self): # This assumes all OBJs that aren't from Mineways - # and don't have a CommonMCOBJ header are from + # and don't have a CommonMCOBJ header are from # jmc2obj, and use individual tiles for textures self._exporter: Optional[str] = "jmc2obj" self._file_type: Optional[str] = "INDIVIDUAL_TILES" - + """ Wrapper functions to avoid typos causing issues """ @@ -124,30 +124,31 @@ class WorldExporter(Enum): """ # Mineways with CommonMCOBJ - Mineways = auto() + Mineways = auto() # Jmc2OBJ with CommonMCOBJ - Jmc2OBJ = auto() + Jmc2OBJ = auto() - # Cmc2OBJ, the reference + # Cmc2OBJ, the reference # implementation of CommonMCOBJ # - # For the most part, this - # will be treated as + # For the most part, this + # will be treated as # Unknown as it's not meant # for regular use. The distinct # option exists for testing purposes - Cmc2OBJ = auto() + Cmc2OBJ = auto() # Any untested exporter - Unknown = auto() - + Unknown = auto() + # Mineways before the CommonMCOBJ standard - ClassicMW = auto() + ClassicMW = auto() # jmc2OBJ before the CommonMCOBJ standard ClassicJmc = auto() + EXPORTER_MAPPING = { "mineways" : WorldExporter.Mineways, "jmc2obj" : WorldExporter.Jmc2OBJ, @@ -156,15 +157,16 @@ class WorldExporter(Enum): "jmc2obj-c" : WorldExporter.ClassicJmc } + def get_exporter(context: Context) -> Optional[WorldExporter]: """ - Return the exporter on the active object if it has + Return the exporter on the active object if it has an exporter attribute. - For maximum backwards compatibility, it'll convert the - explicit options we have in MCprep for world exporters to + For maximum backwards compatibility, it'll convert the + explicit options we have in MCprep for world exporters to WorldExporter enum objects, if the object does not have either - the CommonMCOBJ exporter attribute, or if it does not have the + the CommonMCOBJ exporter attribute, or if it does not have the MCPREP_OBJ_EXPORTER attribute added in MCprep 3.6. This backwards compatibility will be removed by default in MCprep 4.0 @@ -184,8 +186,8 @@ def get_exporter(context: Context) -> Optional[WorldExporter]: elif "MCPREP_OBJ_HEADER" in obj: if "MCPREP_OBJ_EXPORTER" in obj: return EXPORTER_MAPPING[obj["MCPREP_OBJ_EXPORTER"]] - - # This section will be placed behind a legacy + + # This section will be placed behind a legacy # option in MCprep 4.0, once CommonMCOBJ becomes # more adopted in exporters prefs = util.get_user_preferences(context) @@ -195,6 +197,7 @@ def get_exporter(context: Context) -> Optional[WorldExporter]: return WorldExporter.ClassicJmc return None + def detect_world_exporter(filepath: Path) -> Union[CommonMCOBJ, ObjHeaderOptions]: """Detect whether Mineways or jmc2obj was used, based on prefix info. @@ -203,38 +206,41 @@ def detect_world_exporter(filepath: Path) -> Union[CommonMCOBJ, ObjHeaderOptions set in the obj file as comments. """ obj_header = ObjHeaderOptions() + + # First parse header for commonmcobj + with open(filepath, 'r') as obj_fd: + cmc_header = parse_header(obj_fd) + if cmc_header is not None: + return cmc_header + + # If not found, fall back to recognizing the mineway legacy convention with open(filepath, 'r') as obj_fd: try: - cmc_header = parse_header(obj_fd) - if cmc_header is not None: - return cmc_header - else: - header = obj_fd.readline() - if 'mineways' in header.lower(): - obj_header.set_mineways() - # form of: # Wavefront OBJ file made by Mineways version 5.10... - for line in obj_fd: - if line.startswith("# File type:"): - header = line.rstrip() # Remove trailing newline - - # The issue here is that Mineways has changed how the header is generated. - # As such, we're limited with only a couple of OBJs, some from - # 2020 and some from 2023, so we'll assume people are using - # an up to date version. - atlas = ( - "# File type: Export all textures to three large images", - "# File type: Export full color texture patterns" - ) - tiles = ( - "# File type: Export tiles for textures to directory textures", - "# File type: Export individual textures to directory tex" - ) - print('"{}"'.format(header)) - if header in atlas: # If a texture atlas is used - obj_header.set_atlas() - elif header in tiles: # If the OBJ uses individual textures - obj_header.set_seperated() - return obj_header + header = obj_fd.readline() + if 'mineways' in header.lower(): + obj_header.set_mineways() + # form of: # Wavefront OBJ file made by Mineways version 5.10... + for line in obj_fd: + if line.startswith("# File type:"): + header = line.rstrip() # Remove trailing newline + + # The issue here is that Mineways has changed how the header is generated. + # As such, we're limited with only a couple of OBJs, some from + # 2020 and some from 2023, so we'll assume people are using + # an up to date version. + atlas = ( + "# File type: Export all textures to three large images", + "# File type: Export full color texture patterns" + ) + tiles = ( + "# File type: Export tiles for textures to directory textures", + "# File type: Export individual textures to directory tex" + ) + if header in atlas: # If a texture atlas is used + obj_header.set_atlas() + elif header in tiles: # If the OBJ uses individual textures + obj_header.set_seperated() + return obj_header except UnicodeDecodeError: print(f"Failed to read first line of obj: {filepath}") return obj_header @@ -704,7 +710,6 @@ def execute(self, context): if isinstance(header, ObjHeaderOptions): prefs.MCprep_exporter_type = header.exporter() - # Create empty at the center of the OBJ empty = None @@ -720,7 +725,7 @@ def execute(self, context): min_pair = (header.export_bounds_min[0] + header.export_offset[0], (-header.export_bounds_min[2]) + (-header.export_offset[2]), header.export_bounds_min[1] + header.export_offset[1]) - + # Calculate the center of the bounding box # # We do this by taking the average of the given @@ -732,56 +737,64 @@ def execute(self, context): # This will give us the midpoints of these # coordinates, which in turn will correspond # to the center of the bounding box - location = ((max_pair[0] + min_pair[0]) / 2, - (max_pair[1] + min_pair[1]) / 2, - (max_pair[2] + min_pair[2]) / 2) - empty = bpy.data.objects.new(name=header.world_name + "_mcprep_empty", object_data=None) + location = ( + (max_pair[0] + min_pair[0]) / 2, + (max_pair[1] + min_pair[1]) / 2, + (max_pair[2] + min_pair[2]) / 2) + empty = bpy.data.objects.new( + name=header.world_name + "_mcprep_empty", object_data=None) empty.empty_display_size = 2 empty.empty_display_type = 'PLAIN_AXES' empty.location = location - empty.hide_viewport = True # Hide empty globally + empty.hide_viewport = True # Hide empty globally util.update_matrices(empty) for field in fields(header): - if getattr(header, field.name) is None: - continue - if field.type == CommonMCOBJTextureType: - empty[field.name] = getattr(header, field.name).value - else: - empty[field.name] = getattr(header, field.name) + if getattr(header, field.name) is None: + continue + if field.type == CommonMCOBJTextureType: + empty[field.name] = getattr(header, field.name).value + else: + empty[field.name] = getattr(header, field.name) else: empty = bpy.data.objects.new("mcprep_obj_empty", object_data=None) empty.empty_display_size = 2 empty.empty_display_type = 'PLAIN_AXES' - empty.hide_viewport = True # Hide empty globally + empty.hide_viewport = True # Hide empty globally + + addon_prefs = util.get_user_preferences(context) + for obj in context.selected_objects: if isinstance(header, CommonMCOBJ): obj["COMMONMCOBJ_HEADER"] = True obj["PARENTED_EMPTY"] = empty obj.parent = empty - obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object + obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object self.track_exporter = header.exporter - + elif isinstance(header, ObjHeaderOptions): obj["MCPREP_OBJ_HEADER"] = True obj["MCPREP_OBJ_FILE_TYPE"] = header.texture_type() obj.parent = empty - obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object + obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object - # Future-proofing for MCprep 4.0 when we + # Future-proofing for MCprep 4.0 when we # put global exporter options behind a legacy - # option and by default use the object for + # option and by default use the object for # getting the exporter obj["MCPREP_OBJ_EXPORTER"] = "mineways-c" if header.exporter() == "Mineways" else "jmc2obj-c" - addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. + # One final assignment of the preferences, to avoid doing each loop + val = header.exporter if isinstance(header, CommonMCOBJ) else header.exporter() + addon_prefs.MCprep_exporter_type = "Mineways" if val.lower().startswith("mineways") else "jmc2obj" + new_col = self.split_world_by_material(context) - new_col.objects.link(empty) # parent empty + new_col.objects.link(empty) # parent empty return {'FINISHED'} - + def obj_name_to_material(self, obj): """Update an objects name based on its first material""" if not obj: diff --git a/test_files/world_tools_test.py b/test_files/world_tools_test.py index ff56cadc..af961e61 100644 --- a/test_files/world_tools_test.py +++ b/test_files/world_tools_test.py @@ -168,8 +168,7 @@ def test_enable_obj_importer(self): in_import_scn = "obj_import" in dir(bpy.ops.wm) self.assertTrue(in_import_scn, "obj_import operator not found") - - def test_world_import_jmc_full(self): + def test_world_import_legacy_jmc_full(self): test_subpath = os.path.join( "test_data", "jmc2obj_test_1_15_2.obj") self._import_world_with_settings(file=test_subpath) @@ -190,7 +189,29 @@ def test_world_import_jmc_full(self): with self.subTest("test_mappings"): self._import_materials_util("block_mapping_jmc") - def test_world_import_mineways_separated(self): + def test_world_import_cmcobj_jmc_full(self): + test_subpath = os.path.join( + "test_data", "jmc2obj_test_1_21.obj") + self._import_world_with_settings(file=test_subpath) + # TODO: Check that affirms it picks up the mcobj format. + self.assertEqual(self.addon_prefs.MCprep_exporter_type, "jmc2obj") + + # UV tool test. Would be in its own test, but then we would be doing + # multiple unnecessary imports of the same world. So make it a subtest. + with self.subTest("test_uv_transform_no_alert_jmc2obj"): + invalid, invalid_objs = detect_invalid_uvs_from_objs( + bpy.context.selected_objects) + prt = ",".join([obj.name.split("_")[-1] for obj in invalid_objs]) + self.assertFalse( + invalid, f"jmc2obj export should not alert: {prt}") + + with self.subTest("canon_name_validation"): + self._canonical_name_no_none() + + with self.subTest("test_mappings"): + self._import_materials_util("block_mapping_jmc") + + def test_world_import_legacy_mineways_separated(self): test_subpath = os.path.join( "test_data", "mineways_test_separated_1_15_2.obj") self._import_world_with_settings(file=test_subpath) @@ -212,7 +233,29 @@ def test_world_import_mineways_separated(self): with self.subTest("test_mappings"): self._import_materials_util("block_mapping_mineways") - def test_world_import_mineways_combined(self): + def test_world_import_cmcobj_mineways_separated(self): + test_subpath = os.path.join( + "test_data", "mineways_test_separated_1_21.obj") + self._import_world_with_settings(file=test_subpath) + self.assertEqual(self.addon_prefs.MCprep_exporter_type, "Mineways") + + # UV tool test. Would be in its own test, but then we would be doing + # multiple unnecessary imports of the same world. So make it a subtest. + with self.subTest("test_uv_transform_no_alert_mineways"): + invalid, invalid_objs = detect_invalid_uvs_from_objs( + bpy.context.selected_objects) + prt = ",".join([obj.name for obj in invalid_objs]) + self.assertFalse( + invalid, + f"Mineways separated tiles export should not alert: {prt}") + + with self.subTest("canon_name_validation"): + self._canonical_name_no_none() + + with self.subTest("test_mappings"): + self._import_materials_util("block_mapping_mineways") + + def test_world_import_legacy_mineways_combined(self): test_subpath = os.path.join( "test_data", "mineways_test_combined_1_15_2.obj") self._import_world_with_settings(file=test_subpath) @@ -247,6 +290,41 @@ def test_world_import_mineways_combined(self): with self.subTest("test_mappings"): self._import_materials_util("block_mapping_mineways") + def test_world_import_cmcobj_mineways_combined(self): + test_subpath = os.path.join( + "test_data", "mineways_test_combined_1_21.obj") + self._import_world_with_settings(file=test_subpath) + self.assertEqual(self.addon_prefs.MCprep_exporter_type, "Mineways") + + with self.subTest("test_uv_transform_combined_alert"): + invalid, invalid_objs = detect_invalid_uvs_from_objs( + bpy.context.selected_objects) + self.assertTrue(invalid, "Combined image export should alert") + if not invalid_objs: + self.fail( + "Correctly alerted combined image, but no obj's returned") + + # Do specific checks for water and lava, could be combined and + # cover more than one uv position (and falsely pass the test) in + # combined, water is called "Stationary_Wat" and "Stationary_Lav" + # (yes, appears cutoff; and yes includes the flowing too) + # NOTE! in 2.7x, will be named "Stationary_Water", but in 2.9 it is + # "Test_MCprep_1.16.4__-145_4_1271_to_-118_255_1311_Stationary_Wat" + water_obj = [obj for obj in bpy.data.objects + if "Stationary_Wat" in obj.name][0] + lava_obj = [obj for obj in bpy.data.objects + if "Stationary_Lav" in obj.name][0] + + invalid, invalid_objs = detect_invalid_uvs_from_objs( + [lava_obj, water_obj]) + self.assertTrue(invalid, "Combined lava/water should still alert") + + with self.subTest("canon_name_validation"): + self._canonical_name_no_none() + + with self.subTest("test_mappings"): + self._import_materials_util("block_mapping_mineways") + def test_world_import_fails_expected(self): testdir = os.path.dirname(__file__) obj_path = os.path.join(testdir, "fake_world.obj") @@ -314,6 +392,8 @@ def test_convert_mtl_simple(self): # Resultant file res = world_tools.convert_mtl(tmp_mtl) + print("Need to fix this, it's giving none (meaning a success, when it shouldn't?)") + print(res, " for: ", tmp_mtl) # Restore the property we unset. world_tools.BUILTIN_SPACES = save_init From 1fec0da624d75f615590469ef1ca34b30ad432c5 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Tue, 25 Jun 2024 00:15:02 -0500 Subject: [PATCH 35/45] refactor: Remove bv28 `bv28` was deprecated back in MCprep 3.5. Although it was kept for a little bit longer then most deprecated functions, we should be good to go with removing this function as we no longer use it. Signed-off-by: Mahid Sheikh --- MCprep_addon/util.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 89fdc8d8..207bbf00 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -239,16 +239,6 @@ def min_bv(version: Tuple, *, inclusive: bool = True) -> bool: return bpy.app.version >= version -def bv28() -> bool: - """ - Check if blender 2.8, for layouts, UI, and properties. - - Deprecated in MCprep 3.5, but kept to avoid breakage for now... - """ - env.deprecation_warning() - return min_bv((2, 80)) - - def bv30() -> bool: """Check if we're dealing with Blender 3.0""" return min_bv((3, 00)) From f678b9f92ce73e1edb18e35233425a1509d82d58 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Tue, 25 Jun 2024 00:18:14 -0500 Subject: [PATCH 36/45] style: set MCprepEnv.material_sync_cache to typing.List Signed-off-by: Mahid Sheikh --- MCprep_addon/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCprep_addon/conf.py b/MCprep_addon/conf.py index 4aabb954..2038d430 100644 --- a/MCprep_addon/conf.py +++ b/MCprep_addon/conf.py @@ -135,7 +135,7 @@ def __init__(self): # list of material names, each is a string. None by default to indicate # that no reading has occurred. If lib not found, will update to []. # If ever changing the resource pack, should also reset to None. - self.material_sync_cache: Optional[List] = [] + self.material_sync_cache: List = [] # Whether we use PO files directly or use the converted form self.use_direct_i18n = False From c2269b0613f16b86af3d41654e0eb8f53e70bd0c Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Fri, 28 Jun 2024 15:28:51 -0500 Subject: [PATCH 37/45] fix: Add filepath to "File not found" error Signed-off-by: Mahid Sheikh --- MCprep_addon/world_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index 06d68474..fe458f8b 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -577,10 +577,10 @@ def execute(self, context): # Auto change from MTL to OBJ, latet if's will check if existing. self.filepath = str(new_filename) if not self.filepath: - self.report({"ERROR"}, "File not found, could not import obj") + self.report({"ERROR"}, f"File not found, could not import obj \'{self.filepath}\'") return {'CANCELLED'} if not os.path.isfile(self.filepath): - self.report({"ERROR"}, "File not found, could not import obj") + self.report({"ERROR"}, f"File not found, could not import obj \'{self.filepath}\'") return {'CANCELLED'} if not self.filepath.lower().endswith(".obj"): self.report({"ERROR"}, "You must select a .obj file to import") From 4e664ee7c07f587b6417b3a31d5ed0d9c71ce10b Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Sat, 29 Jun 2024 03:31:03 -0500 Subject: [PATCH 38/45] test: Fix test_convert_mtl_simple with convert_mtl return types Signed-off-by: Mahid Sheikh --- test_files/world_tools_test.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test_files/world_tools_test.py b/test_files/world_tools_test.py index af961e61..a41796bf 100644 --- a/test_files/world_tools_test.py +++ b/test_files/world_tools_test.py @@ -399,11 +399,9 @@ def test_convert_mtl_simple(self): world_tools.BUILTIN_SPACES = save_init print("TEST: post", world_tools.BUILTIN_SPACES) - self.assertIsNotNone( + self.assertIsNone( res, - "Failed to mock color space and thus could not test convert_mtl") - - self.assertTrue(res, "Convert mtl failed with false response") + f"Failed to mock color space and thus could not test convert_mtl") # Now check that the data is the same. res = filecmp.cmp(tmp_mtl, modified_mtl, shallow=False) From 2b4418f8cbd6f187b41dc1ec1a57c828ea1a4dad Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Sat, 29 Jun 2024 13:34:34 -0500 Subject: [PATCH 39/45] fix: Don't disable textureswap with WorldExporter.Unknown Signed-off-by: Mahid Sheikh --- MCprep_addon/materials/prep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index 260942d1..f2f9e6dd 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -423,7 +423,7 @@ class MCPREP_OT_swap_texture_pack( @classmethod def poll(cls, context): - if world_tools.get_exporter(context) != world_tools.WorldExporter.Unknown: + if world_tools.get_exporter(context) is not None: return util.is_atlas_export(context) return False From 1b38d08e27f464ae3236ad1e946e0e4c66ec2e4b Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Mon, 1 Jul 2024 21:44:25 -0500 Subject: [PATCH 40/45] style: Return bool for convert_mtl instead of None Signed-off-by: Mahid Sheikh --- MCprep_addon/world_tools.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index fe458f8b..004fba4e 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -246,7 +246,7 @@ def detect_world_exporter(filepath: Path) -> Union[CommonMCOBJ, ObjHeaderOptions return obj_header -def convert_mtl(filepath) -> Optional[MCprepError]: +def convert_mtl(filepath) -> Union[bool, MCprepError]: """Convert the MTL file if we're not using one of Blender's built in colorspaces @@ -259,9 +259,14 @@ def convert_mtl(filepath) -> Optional[MCprepError]: - Add a header at the end Returns: - - None if successful or skipped + - True if performed, False if not skipped - MCprepError if failed (may return with message) """ + + # Perform this early to get it out of the way + if bpy.context.scene.view_settings.view_transform in BUILTIN_SPACES: + return False + # Check if the MTL exists. If not, then check if it # uses underscores. If still not, then return False mtl = Path(filepath.rsplit(".", 1)[0] + '.mtl') @@ -284,10 +289,9 @@ def convert_mtl(filepath) -> Optional[MCprepError]: line, file = env.current_line_and_file() return MCprepError(e, line, file, "Could not read file!") - # This checks to see if the user is using a built-in colorspace or if none of the lines have map_d. If so - # then ignore this file and return None - if bpy.context.scene.view_settings.view_transform in BUILTIN_SPACES or not any("map_d" in s for s in lines): - return None + # This checks to see if none of the lines have map_d. If so then skip + if not any("map_d" in s for s in lines): + return False # This represents a new folder that'll backup the MTL filepath original_mtl_path = Path(filepath).parent.absolute() / "ORIGINAL_MTLS" @@ -307,7 +311,7 @@ def convert_mtl(filepath) -> Optional[MCprepError]: print("Header " + str(header)) copied_file = shutil.copy2(mtl, original_mtl_path.absolute()) else: - return None + return False except Exception as e: print(e) line, file = env.current_line_and_file() @@ -340,7 +344,7 @@ def convert_mtl(filepath) -> Optional[MCprepError]: line, file = env.current_line_and_file() return MCprepError(e, line, file) - return None + return True class OBJImportCode(enum.Enum): """ From f141844e4ce003f01305b46853236e754e80c8aa Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Mon, 1 Jul 2024 21:53:13 -0500 Subject: [PATCH 41/45] fix: Add Khronos PBR Neutral and AgX to natively supported spaces Signed-off-by: Mahid Sheikh --- MCprep_addon/world_tools.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index 004fba4e..40b4cdb9 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -40,18 +40,10 @@ # supporting functions # ----------------------------------------------------------------------------- -BUILTIN_SPACES = ( - "Standard", - "Filmic", - "Filmic Log", - "Raw", - "False Color" -) - +BUILTIN_SPACES = ('Standard', 'Khronos PBR Neutral', 'AgX', 'Filmic', 'Filmic Log', 'False Color', 'Raw') time_obj_cache = None - def get_time_object() -> None: """Returns the time object if present in the file""" global time_obj_cache # to avoid re parsing every time From 6f2c2261bc70b164ceaa76d326187b1de8d1d3de Mon Sep 17 00:00:00 2001 From: "Patrick W. Crawford" Date: Sat, 6 Jul 2024 16:25:38 -0700 Subject: [PATCH 42/45] Resolve 2x tests with more consistent use of world WorldExporter --- MCprep_addon/materials/prep.py | 6 +++++- MCprep_addon/world_tools.py | 14 ++++++++------ test_files/materials_test.py | 2 +- test_files/world_tools_test.py | 20 ++++---------------- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index f2f9e6dd..f9c83d92 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -423,7 +423,11 @@ class MCPREP_OT_swap_texture_pack( @classmethod def poll(cls, context): - if world_tools.get_exporter(context) is not None: + if world_tools.get_exporter(context) != world_tools.WorldExporter.Unknown: + return util.is_atlas_export(context) + # Fallback to legacy + addon_prefs = util.get_user_preferences(context) + if addon_prefs.MCprep_exporter_type != "(choose)": return util.is_atlas_export(context) return False diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index 40b4cdb9..53fd598e 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -150,7 +150,7 @@ class WorldExporter(Enum): } -def get_exporter(context: Context) -> Optional[WorldExporter]: +def get_exporter(context: Context) -> WorldExporter: """ Return the exporter on the active object if it has an exporter attribute. @@ -168,7 +168,7 @@ def get_exporter(context: Context) -> Optional[WorldExporter]: """ obj = context.active_object if not obj: - return None + return WorldExporter.Unknown if "COMMONMCOBJ_HEADER" in obj: if obj["PARENTED_EMPTY"] is not None and obj["PARENTED_EMPTY"]["exporter"] in EXPORTER_MAPPING: @@ -187,7 +187,7 @@ def get_exporter(context: Context) -> Optional[WorldExporter]: return WorldExporter.ClassicMW elif prefs.MCprep_exporter_type == "jmc2obj": return WorldExporter.ClassicJmc - return None + return WorldExporter.Unknown def detect_world_exporter(filepath: Path) -> Union[CommonMCOBJ, ObjHeaderOptions]: @@ -251,7 +251,8 @@ def convert_mtl(filepath) -> Union[bool, MCprepError]: - Add a header at the end Returns: - - True if performed, False if not skipped + - True if the file was converted + - False if conversion was skipped or it was already converted before - MCprepError if failed (may return with message) """ @@ -347,20 +348,21 @@ class OBJImportCode(enum.Enum): ALREADY_ENABLED = 0 DISABLED = 1 + def enable_obj_importer() -> Union[OBJImportCode, MCprepError]: """ Checks if the obj import addon (pre-Blender 4.0) is enabled, and enable it if it isn't enabled. Returns: - - OBJImportCode.ALREADY_ENABLED if either enabled already or + - OBJImportCode.ALREADY_ENABLED if either enabled already or the user is using Blender 4.0. - OBJImportCode.DISABLED if the addon had to be enabled. - MCprepError with a message if the addon could not be enabled. """ enable_addon = None if util.min_bv((4, 0)): - return OBJImportCode.ALREADY_ENABLED # No longer an addon, native built in. + return OBJImportCode.ALREADY_ENABLED # No longer an addon, native built in. else: in_import_scn = "obj_import" not in dir(bpy.ops.wm) in_wm = "" diff --git a/test_files/materials_test.py b/test_files/materials_test.py index ed2af8f8..8af37b25 100644 --- a/test_files/materials_test.py +++ b/test_files/materials_test.py @@ -886,7 +886,7 @@ def test_swap_texture_pack(self): obj.active_material = new_mat self.assertIsNotNone(obj.active_material, "Material should be applied") - # Ensure if no texture pack selected, it fails. + # Ensure if no exporter type selected, it fails. addon_prefs = util.get_user_preferences(bpy.context) addon_prefs.MCprep_exporter_type = "(choose)" with self.assertRaises(RuntimeError): diff --git a/test_files/world_tools_test.py b/test_files/world_tools_test.py index a41796bf..304aed10 100644 --- a/test_files/world_tools_test.py +++ b/test_files/world_tools_test.py @@ -388,27 +388,16 @@ def test_convert_mtl_simple(self): # framework, hence we'll just clear the world_tool's vars. save_init = list(world_tools.BUILTIN_SPACES) world_tools.BUILTIN_SPACES = ["NotRealSpace"] - print("TEST: pre", world_tools.BUILTIN_SPACES) - - # Resultant file res = world_tools.convert_mtl(tmp_mtl) - print("Need to fix this, it's giving none (meaning a success, when it shouldn't?)") - print(res, " for: ", tmp_mtl) - - # Restore the property we unset. world_tools.BUILTIN_SPACES = save_init - print("TEST: post", world_tools.BUILTIN_SPACES) - self.assertIsNone( + self.assertTrue( res, - f"Failed to mock color space and thus could not test convert_mtl") - - # Now check that the data is the same. + "Should return false ie skipped conversion") res = filecmp.cmp(tmp_mtl, modified_mtl, shallow=False) + os.remove(tmp_mtl) # Cleanup first, in case assert fails self.assertTrue( res, f"Generated MTL is different: {tmp_mtl} vs {modified_mtl}") - # Not removing file, since we likely want to inspect it. - os.remove(tmp_mtl) def test_convert_mtl_skip(self): """Ensures that we properly skip if a built in space active.""" @@ -440,11 +429,10 @@ def test_convert_mtl_skip(self): # Restore the property we unset. world_tools.BUILTIN_SPACES = save_init - # print("TEST: post", world_tools.BUILTIN_SPACES) if res is not None: os.remove(tmp_mtl) - self.assertIsNone(res, "Should not have converter MTL for valid space") + self.assertFalse(res, "Should not have converter MTL for valid space") if __name__ == '__main__': From 3a30cfe12af6107be2641d84a4080de525ecd1bb Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Sat, 6 Jul 2024 18:41:01 -0500 Subject: [PATCH 43/45] revert: Add nuance back to get_exporter return value Signed-off-by: Mahid Sheikh --- MCprep_addon/materials/prep.py | 2 +- MCprep_addon/world_tools.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index f9c83d92..7fb48522 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -423,7 +423,7 @@ class MCPREP_OT_swap_texture_pack( @classmethod def poll(cls, context): - if world_tools.get_exporter(context) != world_tools.WorldExporter.Unknown: + if world_tools.get_exporter(context) != None: return util.is_atlas_export(context) # Fallback to legacy addon_prefs = util.get_user_preferences(context) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index 53fd598e..83c871f3 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -149,8 +149,9 @@ class WorldExporter(Enum): "jmc2obj-c" : WorldExporter.ClassicJmc } +UNSUPPORTED_OR_NONE = (WorldExporter.Unknown, None) -def get_exporter(context: Context) -> WorldExporter: +def get_exporter(context: Context) -> Optional[WorldExporter]: """ Return the exporter on the active object if it has an exporter attribute. @@ -168,7 +169,7 @@ def get_exporter(context: Context) -> WorldExporter: """ obj = context.active_object if not obj: - return WorldExporter.Unknown + return None if "COMMONMCOBJ_HEADER" in obj: if obj["PARENTED_EMPTY"] is not None and obj["PARENTED_EMPTY"]["exporter"] in EXPORTER_MAPPING: @@ -187,7 +188,7 @@ def get_exporter(context: Context) -> WorldExporter: return WorldExporter.ClassicMW elif prefs.MCprep_exporter_type == "jmc2obj": return WorldExporter.ClassicJmc - return WorldExporter.Unknown + return None def detect_world_exporter(filepath: Path) -> Union[CommonMCOBJ, ObjHeaderOptions]: From acec9ea4d4c782180ac47da156f093e027067682 Mon Sep 17 00:00:00 2001 From: "Patrick W. Crawford" Date: Sat, 6 Jul 2024 18:32:20 -0700 Subject: [PATCH 44/45] Fixing the mapping test code to use the generalize functions. --- test_files/world_tools_test.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test_files/world_tools_test.py b/test_files/world_tools_test.py index 304aed10..10145af1 100644 --- a/test_files/world_tools_test.py +++ b/test_files/world_tools_test.py @@ -85,14 +85,16 @@ def _import_materials_util(self, mapping_set): # can't import conf separately. mcprep_data = util.env.json_data["blocks"][mapping_set] + generalized = [get_mc_canonical_name(mat.name)[0] for mat in bpy.data.materials] + # first detect alignment to the raw underlining mappings, nothing to # do with canonical yet mapped = [ - mat.name for mat in bpy.data.materials - if mat.name in mcprep_data] # ok! + name for name in generalized + if name in mcprep_data] # ok! unmapped = [ - mat.name for mat in bpy.data.materials - if mat.name not in mcprep_data] # not ok + name for name in generalized + if name not in mcprep_data] # not ok fullset = mapped + unmapped # ie all materials unleveraged = [ mat for mat in mcprep_data @@ -132,8 +134,9 @@ def _import_materials_util(self, mapping_set): if mats_not_canon and mapping_set != "block_mapping_mineways": # print("Non-canon material names found: ({})".format(len(mats_not_canon))) # print(mats_not_canon) - if len(mats_not_canon) > 30: # arbitrary threshold - self.fail("Too many materials found without canonical name") + if len(mats_not_canon) > 40: # arbitrary threshold + self.fail(("Too many materials found without canonical name: " + f"{len(mats_not_canon)}")) # affirm the correct mappings mats_no_packimage = [ From a0a2f293e5ab1d11b4a3d6c3cd0808e04f588361 Mon Sep 17 00:00:00 2001 From: Mahid Sheikh Date: Sat, 6 Jul 2024 20:49:07 -0500 Subject: [PATCH 45/45] rc-files: Update patches for MCprep 3.6 RC-3 Signed-off-by: Mahid Sheikh --- rc-files/patches/rc-bl_info.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rc-files/patches/rc-bl_info.patch b/rc-files/patches/rc-bl_info.patch index a231c202..7ff9c318 100644 --- a/rc-files/patches/rc-bl_info.patch +++ b/rc-files/patches/rc-bl_info.patch @@ -7,10 +7,10 @@ index e727f3f..39c9fa7 100755 bl_info = { - "name": "MCprep", -+ "name": "MCprep 3.6 RC-2", ++ "name": "MCprep 3.6 RC-3", "category": "Object", - "version": (3, 5, 3), -+ "version": (3, 5, 3, 2), ++ "version": (3, 5, 3, 3), "blender": (2, 80, 0), "location": "3D window toolshelf > MCprep tab", "description": "Minecraft workflow addon for rendering and animation",