diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index 80573b23..3da5db96 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -310,9 +310,8 @@ def execute(self, context): else: self.report( {"ERROR"}, - "Nothing modified, be sure you selected objects with existing materials!" - ) - + "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 diff --git a/MCprep_addon/materials/skin.py b/MCprep_addon/materials/skin.py index 43b1c3ba..a949a90a 100644 --- a/MCprep_addon/materials/skin.py +++ b/MCprep_addon/materials/skin.py @@ -31,8 +31,17 @@ from . import generate from .. import tracking from .. import util +from ..spawner import spawn_util from ..conf import env +from .prep import McprepMaterialProps + + +swap_all_imgs_desc = ( + "Swap textures in all image nodes that exist on the selected \n" + "material; if off, will instead seek to only replace the images of \n" + "nodes (not image blocks) named MCPREP_SKIN_SWAP" +) swap_all_imgs_desc = ( @@ -335,7 +344,29 @@ def download_user(self, context: Context, username: str) -> Optional[Path]: self.track_param = "username" return saveloc +def check_entity_texture(texture_path: str) -> Optional[Path]: + context = bpy.context + addon_prefs = util.get_user_preferences(context) + active_pack = bpy.path.abspath(context.scene.mcprep_texturepack_path) + active_pack = os.path.join( + active_pack, "assets", "minecraft", "textures", "entity") + + base_pack = bpy.path.abspath(addon_prefs.custom_texturepack_path) + base_pack = os.path.join( + base_pack, "assets", "minecraft", "textures", "entity") + + # if not os.path.isdir(active_pack): + # env.log(f"No models found for active path {active_pack}") + # return None + base_has_textures = os.path.isdir(base_pack) + + if base_has_textures: + return generate.find_from_texturepack(texture_path) + else: + env.log(f"Base resource pack has no entity texture folder: {base_pack}") + return None + # ----------------------------------------------------------------------------- # Operators / UI classes # ----------------------------------------------------------------------------- @@ -395,7 +426,6 @@ def execute(self, context): return {'FINISHED'} - class MCPREP_OT_apply_skin(bpy.types.Operator): """Apply the active UIlist skin to select characters""" bl_idname = "mcprep.applyskin" @@ -703,6 +733,77 @@ def execute(self, context): return {'FINISHED'} +class MCPREP_OT_swap_skin_variant(bpy.types.Operator, spawn_util.VariationProp): + """Apply the active UIlist skin to select characters""" + bl_idname = "mcprep.swap_skin" + bl_label = "Swap mob skin" + bl_description = "Swap the mobs variant" + bl_options = {'REGISTER', 'UNDO'} + + # def invoke(self, context, event): + # return context.window_manager.invoke_props_dialog( + # self, width=300 * util.ui_scale()) + + # def draw(self, context: Context): + # layout = self.layout + # self.draw_variation_ui(context, layout) + + @tracking.report_error + def execute(self, context: Context): + obj = context.object + if obj.type != 'ARMATURE': + self.report({'ERROR'}, "Please select an armature") + return {'CANCELLED'} + mats, skipped = getMatsFromSelected(obj, False) + mob_type = obj.get("MCPREP_mob_type", "Custom") + + if mob_type == "Custom": + self.report({'ERROR'}, "You are not allowed to use on Custom rig") + return {'CANCELLED'} + + texture_paths = self.get_texture_paths(mob_type) + name = mob_type.lower().replace(" ", "_") + if mob_type == "Villager": + self.doVillager(mats, texture_paths) + elif mob_type == "Zombie": + check_entity_texture(context) + if self.zombie_variation in ["ZOMBIE", "HUSK"]: + # Using Player model so convert player skin to 1.8 format + # loadSkinFile() + pass + else: + # Assign textures materials for drown + pass + elif mob_type == "Skeleton": + pass + + elif mob_type in ("Allay", "Vex"): + if mob_type == "Allay": + name += "/" + name + else: + name = "illager" + + else: + pass + return {'FINISHED'} + + def doVillager(self, materials: List[Material], texture_paths: List[str]): + """ materials texture path order follows by profession, profession level, type """ + prof_mat, biome_mat, level_mat = None, None, None + if mat[0].get("MCPREP_VILLAGER_PROFESSION"): + image = util.loadTexture(texture_paths[0]) + proStat = generate.assert_textures_on_materials(image, [mat]) + if mat.get("MCPREP_VILLAGER_BIOME"): + image = util.loadTexture(texture_paths[1]) + biomeStat = generate.assert_textures_on_materials(image, [mat]) + if mat.get("MCPREP_VILLAGER_LEVEL"): + image = util.loadTexture(texture_paths[2]) + levelStat = generate.assert_textures_on_materials(image, [mat]) + if not all([proStat, biomeStat, levelStat]): + self.report({'ERROR'}, "Something wrong happen during swap variant texture") + else: + pass + class MCPREP_OT_download_username_list(bpy.types.Operator): """Apply the active UIlist skin to select characters""" bl_idname = "mcprep.download_username_list" @@ -788,6 +889,7 @@ def execute(self, context): ListColl, MCPREP_OT_swap_skin_from_file, MCPREP_OT_apply_skin, + MCPREP_OT_swap_skin_variant, MCPREP_OT_apply_username_skin, MCPREP_OT_download_username_list, # MCPREP_OT_skin_fix_eyes, diff --git a/MCprep_addon/mcprep_ui.py b/MCprep_addon/mcprep_ui.py index 7639768d..88ea440b 100644 --- a/MCprep_addon/mcprep_ui.py +++ b/MCprep_addon/mcprep_ui.py @@ -943,6 +943,8 @@ class MCPREP_PT_skins(bpy.types.Panel): def draw(self, context): layout = self.layout + wm_props = context.window_manager.mcprep + if addon_just_updated(): restart_layout(layout) return @@ -951,56 +953,63 @@ def draw(self, context): sind = context.scene.mcprep_skins_list_index mob_ind = context.scene.mcprep_props.mob_list_index skinname = None - row = layout.row() row.label(text=env._("Select skin")) row.operator( "mcprep.open_help", text="", icon="QUESTION", emboss=False ).url = "https://theduckcow.com/dev/blender/mcprep/skin-swapping/" - - # set size of UIlist row = layout.row() - col = row.column() - - is_sortable = len(env.skin_list) > 1 - rows = 1 - if (is_sortable): - rows = 4 - - # any other conditions for needing reloading? - if not env.skin_list: - col = layout.column() - col.label(text=env._("No skins found/loaded")) - p = col.operator( - "mcprep.reload_skins", text=env._("Press to reload"), icon="ERROR") - elif env.skin_list and len(env.skin_list) <= sind: - col = layout.column() - col.label(text=env._("Reload skins")) - p = col.operator( - "mcprep.reload_skins", text=env._("Press to reload"), icon="ERROR") - else: - col.template_list( - "MCPREP_UL_skins", "", - context.scene, "mcprep_skins_list", - context.scene, "mcprep_skins_list_index", - rows=rows) - - col = layout.column(align=True) - - row = col.row(align=True) - row.scale_y = 1.5 - if env.skin_list: - skinname = bpy.path.basename(env.skin_list[sind][0]) - p = row.operator("mcprep.applyskin", text=f"Apply {skinname}") - p.filepath = env.skin_list[sind][1] + row.prop(wm_props, "skin_modes",expand=True) + if wm_props.skin_modes == 'PLAYER': + + # set size of UIlist + row = layout.row() + col = row.column() + + is_sortable = len(env.skin_list) > 1 + rows = 1 + if (is_sortable): + rows = 4 + + # any other conditions for needing reloading? + if not env.skin_list: + col = layout.column() + col.label(text="No skins found/loaded") + p = col.operator( + "mcprep.reload_skins", text="Press to reload", icon="ERROR") + elif env.skin_list and len(env.skin_list) <= sind: + col = layout.column() + col.label(text="Reload skins") + p = col.operator( + "mcprep.reload_skins", text="Press to reload", icon="ERROR") else: - row.enabled = False - p = row.operator("mcprep.skin_swapper", text=env._("No skins found")) - row = col.row(align=True) - row.operator("mcprep.skin_swapper", text=env._("Skin from file")) - row = col.row(align=True) - row.operator("mcprep.applyusernameskin", text=env._("Skin from username")) + col.template_list( + "MCPREP_UL_skins", "", + context.scene, "mcprep_skins_list", + context.scene, "mcprep_skins_list_index", + rows=rows) + + col = layout.column(align=True) + + row = col.row(align=True) + row.scale_y = 1.5 + if env.skin_list: + skinname = bpy.path.basename(env.skin_list[sind][0]) + p = row.operator("mcprep.applyskin", text=f"Apply {skinname}") + p.filepath = env.skin_list[sind][1] + else: + row.enabled = False + p = row.operator("mcprep.skin_swapper", text="No skins found") + row = col.row(align=True) + row.operator("mcprep.skin_swapper", text="Skin from file") + row = col.row(align=True) + row.operator("mcprep.applyusernameskin", text="Skin from username") + else: + row = layout.row() + col = row.column() + wm_props.draw_variation_ui(context, col) + col.operator("mcprep.swap_skin") split = layout.split() col = split.column(align=True) row = col.row(align=True) @@ -1044,6 +1053,12 @@ def draw(self, context): # datapass = scn_props.mob_list[mob_ind].mcmob_type tx = f"Spawn {name} with {skinname}" row.operator("mcprep.spawn_with_skin", text=tx) + b_row.label(text="Resource pack") + subrow = b_row.row(align=True) + subrow.prop(context.scene, "mcprep_texturepack_path", text="") + subrow.operator( + "mcprep.reset_texture_path", icon=LOAD_FACTORY, text="") + class MCPREP_PT_materials(bpy.types.Panel): @@ -1969,6 +1984,25 @@ class McprepProps(bpy.types.PropertyGroup): effects_list_index: bpy.props.IntProperty(default=0) +class MCprepWindowManager(spawn_util.VariationProp, bpy.types.PropertyGroup): + skin_modes : bpy.props.EnumProperty( + name="Modes", + description="Skinswap modes", + items=( + ('PLAYER', "Player", ""), + ('MOB', "Mob/Entity", "") + ) + ) + + @classmethod + def register(cls): + bpy.types.WindowManager.mcprep = bpy.props.PointerProperty(type=cls) + + @classmethod + def unregister(cls): + del bpy.types.WindowManager.mcprep + + # ----------------------------------------------------------------------------- # Register functions # ----------------------------------------------------------------------------- @@ -1977,6 +2011,7 @@ class McprepProps(bpy.types.PropertyGroup): classes = ( McprepPreference, McprepProps, + MCprepWindowManager, MCPREP_MT_mob_spawner, MCPREP_MT_meshswap_place, MCPREP_MT_item_spawn, diff --git a/MCprep_addon/spawner/entities.py b/MCprep_addon/spawner/entities.py index 96b70711..f2e57a3d 100644 --- a/MCprep_addon/spawner/entities.py +++ b/MCprep_addon/spawner/entities.py @@ -21,7 +21,10 @@ import bpy -from bpy.types import Context +from bpy.types import ( + Context, + Event +) from ..conf import env, Entity from .. import util from .. import tracking diff --git a/MCprep_addon/spawner/spawn_util.py b/MCprep_addon/spawner/spawn_util.py index a8031347..4a370da5 100644 --- a/MCprep_addon/spawner/spawn_util.py +++ b/MCprep_addon/spawner/spawn_util.py @@ -18,11 +18,15 @@ import os import re -from typing import List, Optional +from typing import List, Optional, Tuple, Union from pathlib import Path import bpy -from bpy.types import Context, Collection, BlendDataLibraries +from bpy.types import ( + Context, Collection, + BlendDataLibraries, + UILayout +) from ..conf import env from .. import util @@ -41,11 +45,298 @@ COLL_ICON = 'OUTLINER_COLLECTION' if util.bv30() else 'COLLECTION_NEW' +class VariationProp: + def color_items(self, context: Context) -> List[Tuple[str,str,str]]: + """Color variation in ID order""" + items = [ + ('WHITE', "White", ""), + ('ORANGE', "Orange", ""), + ('MAGENTA', "Magenta", ""), + ('LIGHTBLUE', "Light Blue", ""), + ('YELLOW', "Yellow", ""), + ('LIME', "Lime", ""), + ('PINK', "Pink", ""), + ('GRAY', "Gray", ""), + ('LIGHTGRAY', "Light Gray", ""), + ('CYAN', "Cyan", ""), + ('PURPLE', "Purple", ""), + ('BLUE', "Blue", ""), + ('BROWN', "Brown", ""), + ('GREEN', "Green", ""), + ('RED', "Red", ""), + ('BLACK', "Black", ""), + ] + return items + + def profession_items(self, context: Context) -> List[Tuple[str, str, str]]: + """Villager Professional""" + items = [ + ('ARMORER', "Armorer", ""), + ('BUTCHER', "Butcher", ""), + ('CARTOGRAPHER', "Cartographer", ""), + ('CLERIC', "Cleric", ""), + ('FARMER', "Farmer", ""), + ('FISHERMAN', "Fisherman", ""), + ('FLETCHER', "Fletcher", ""), + ('LEATHERWORKER', "Leatherworker", ""), + ('LIBRARIAN', "Librarian", ""), + ('MASON', "Mason", ""), + ('NITWIT', "Nitwit", ""), + ('SHEPHERD', "Shepherd", ""), + ('TOOLSMITH', "Toolsmith", ""), + ('WEAPONSMITH', "Weaponsmith", ""), + ('WANDER', "Wandering", ""), # Wandering Trader is not a villager but leave it there, illagers could be in the list too (witch?) + ] + return items + + def level_items(self, context: Context) -> List[Tuple[str, str, str]]: + """Villager Level """ + items = [ + ('NOVICE', "Novice", ""), # Stone + ('APPRENTICE', "Apprentice", ""), # Iron + ('JOURNEYMAN', "Journeyman", ""), # Gold + ('EXPERT', "Expert", ""), # Emerald + ('MASTER', "Master", ""), # Diamond + ] + return items + + def biome_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('DESERT', "Desert", ""), + ('JUNGLE', "Jungle", ""), + ('PLAINS', "Plains", ""), + ('SAVANNA', "Savanna", ""), + ('SNOWY', "Snowy", ""), + ('SWAMP', "Swamp", ""), + ('TAIGA', "Taiga", ""), + ] + return items + + def zombie_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('DEFAULT', "Default", ""), + ('DROWN', "Drown", ""), + ('HUSK', "Husk", ""), + ] + return items + + def skeleton_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('DEFAULT', "Default", ""), + ('STRAY', "Stray", ""), + ('WITHER', "Wither", ""), + # ('BOGGED', "Bogged", "") # 1.21 Added new skeleton varia + ] + return items + + def axolotl_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('BLUE', "Blue", ""), + ('CYAN', "Cyan", ""), + ('GOLD', "Gold", ""), + ('LUCY', "Lucy", ""), + ('WILD', "Wild", ""), + ] + return items + + def rabbit_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('BLACK', "Black", ""), + ('BROWN', "Brown", ""), + ('CAERBANNOG', "Caerbannog", ""), + ('GOLD', "Gold", ""), + ('SALT', "Salt", ""), + ('TOAST',"Toast", ""), + ('WHITE', "White", ""), + ('WHITE_SPLOTCHED',"White splotched", ""), + ] + return items + + def frog_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('COLD', "Cold", ""), + ('TEMPERATE', "Temperate", ""), + ('WARM', "Warm", ""), + ] + return items + + def parrot_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('BLUE', "Blue", ""), + ('GREEN',"Green", ""), + ('GREY', "Grey", ""), + ('RED_BLUE', "Red blue", ""), + ('YELLOW_BLUE', "Yellow blue", ""), + ] + return items + + def llama_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('BROWN',"Brown", ""), + ('CREAMY',"Creamy",""), + ('GRAY', "Gray", ""), + ('WHITE', "White", ""), + ] + return items + + def horse_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('BLACK', "Black", ""), + ('BROWN', "BROWN", ""), + ('CHESTNUT', "CHESTNUT", ""), + ('CREAMY', "CREAMY", ""), + ('DARK_BROWN', "DARK_BROWN", ""), + ('GRAY', "GRAY", ""), + ('ZOMBIE', "ZOMBIE", ""), + ('SKELETON', "SKELETON", ""), + ] + return items + + def cat_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('ALL_BLACK', "All Black",""), + ('BLACK', "Black", ""), + ('BRITISH_SHORTHAIR', "British Shorthair", ""), + ('CALICO', "Calico", ""), + ('JELLIE', "Jellie", ""), + ('OCELOT', "Ocelot", ""), + ('PERSIAN', "Persian", ""), + ('RAGDOLL', "Ragdoll", ""), + ('RED', "Red", ""), + ('SIAMESE', "Siamese", ""), + ('TABBY', "Tabby", ""), + ('WHITE', "White", ""), + # ('CAT_COLLAR', "", "") + ] + return items + + def fox_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('NORMAL', "Normal", ""), + ('SNOW', "Snow", "") + ] + return items + + def squid_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('NORMAL', "Squid", ""), + ('GLOW', "Glow", "") + ] + return items + + def tropical_pattern_items(self, context: Context) -> List[Tuple[str, str, str]]: + items = [ + ('PATTERN_1', "Pattern 1", ""), + ('PATTERN_2', "Pattern 2", ""), + ('PATTERN_3', "Pattern 3", ""), + ('PATTERN_4', "Pattern 4", ""), + ('PATTERN_5', "Pattern 5", ""), + ("PATTERN_6", "Pattern 6", ""), + ] + return items + + def dog_items(self, context) -> List[Tuple[str, str, str]]: + items = [ + # ignore this just placeholder for 1.21 wolf variations + ('DEF', "def", ""), + ('DEA', "dea", "") + ] + return items + + def get_villager_names(self) -> Tuple[str, str, str]: + """Returns profession, profession level, type path in order""" + # (adds villager/{}.png to get the texture path, same for zombie_villager/) + return f"profession/{self.profession_variation}", f"profession_level/{self.level_variation}", f"type/{self.biome_variation}" + + def get_squid_name(self) -> str: + return self.squid_variation == 'GLOW' if 'glow_squid' else 'squid' + + def get_fox_name(self) -> Tuple[str, str]: + return ("snow_fox", "snow_fox_sleeping") if self.fox_variation == 'SNOW' else ("fox", "fox_sleeping") + + def get_tropical_name(self) -> str: + """Returns the tropical fish type and pattern""" + # (adds fish/{}.png to get the texture path) + type = self.tropical_type_variation.lower() + pattern = self.tropical_pattern_variation.replace("PATTERN_", "") + return f"tropical_{type}_pattern_{pattern}" + + def get_texture_path(self, mob_type: str, is_zombified: bool = False) -> List[Union[str, Path]]: + if mob_type == "Villager": + names = self.get_villager_names() + return [f"zombie_villager/{t}.png" for t in names] if is_zombified else[f"villager/{t}.png" for t in names] + elif mob_type == "Zombie": + return [f"zombie/{self.zombie_variation.lower()}.png"] + elif mob_type == "Skeleton": + return [f"zombie/{self.skeleton_variation.lower()}.png"] + elif mob_type == "Allay" or mob_type == "Vex": + return [] if self.is_zombiefied or mob_type == "Vex" else [] + # TODO: Add the rest of the mobs + + profession_variation: bpy.props.EnumProperty(name="Profession", items=profession_items) + level_variation: bpy.props.EnumProperty(name="Level", items=level_items) + biome_variation: bpy.props.EnumProperty(name="Biome", items=biome_items) + zombie_variation: bpy.props.EnumProperty(name="Zombie Variation", items=zombie_items) + skeleton_variation: bpy.props.EnumProperty(name="Skeleton Variation", items=skeleton_items) + is_zombiefied: bpy.props.BoolProperty(name="Is Zombiefied") # Use this for Allay-Vex + fox_variation: bpy.props.EnumProperty(items=fox_items) + dog_variation: bpy.props.EnumProperty(items=dog_items) + cat_variation: bpy.props.EnumProperty(items=cat_items) + + def draw_variation_ui(self, context: Context, layout: UILayout): + """ Sharable method for drawing""" + obj = context.object + mob_type = getmob_type(obj) + if mob_type in ["Villager", "Trader"]: + layout.prop(self, "profession_variation") + layout.prop(self, "level_variation") + layout.prop(self, "biome_variation") + + elif mob_type == "Zombie": + layout.prop(self, "zombie_variation") + elif mob_type == "Skeleton": + layout.prop(self, "skeleton_variation") + elif mob_type == "Axolotl": + layout.prop(self, "axolotl_variation") + elif mob_type == "Rabbit": + layout.prop(self, "rabbit_variation") + elif mob_type == "Frog": + layout.prop(self, "frog_variation") + elif mob_type == "Parrot": + layout.prop(self, "parrot_variation") + elif mob_type == "Llama": + layout.prop(self, "llama_variation") + elif mob_type == "Horse": + layout.prop(self, "horse_variation") + elif mob_type == "Cat" or mob_type == "Ocelot": + layout.prop(self, "cat_variation") + elif mob_type == "Dog" or mob_type == "Wolf": + layout.prop(self, "dog_variation") + + counter_variant = ["Villager", "Piglin", "Hoglin", "Allay", "Vex"] + if mob_type in counter_variant: + text = "Is Vex" if mob_type == "Allay" else "Is Zombified" + layout.prop(self, "is_zombiefied", text=text) + + has_variant = counter_variant + ["Zombie", "Skeleton", "Llama", "Axolotl", "Rabbit", "Llama", "Parrot", "Frog", "Horse", "Cat", "Ocelot"] + if mob_type == "Custom" or mob_type not in has_variant: + layout.label(text="This mob doesn't has any variant to swap skin yet") + elif mob_type == "Player": + layout.label(text="Please use Player for this") + # ----------------------------------------------------------------------------- # Reusable functions for spawners # ----------------------------------------------------------------------------- +def getmob_type(rig: bpy.types.Object): + """ Get mob type from rig + args + obj: Armature Object + """ + return rig.type == 'ARMATURE' and rig.get("MCPREP_MOBTYPE", "Custom") + + def filter_collections(data_from: BlendDataLibraries) -> List[str]: """ TODO 2.7 groups Generalized way to prefilter collections in a blend file. diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 85921ce5..2f3c48eb 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -17,7 +17,7 @@ # ##### END GPL LICENSE BLOCK ##### from subprocess import Popen, PIPE -from typing import List, Optional, Union, Tuple +from typing import List, Optional, Union, Tuple, Any, Dict import enum import json import operator @@ -35,7 +35,8 @@ Material, Image, Node, - UILayout + UILayout, + ID ) from mathutils import Vector, Matrix @@ -778,3 +779,16 @@ def move_assets_to_excluded_layer(context: Context, collections: List[Collection continue # not linked, likely a sub-group not added to scn spawner_exclude_vl.collection.children.link(grp) initial_view_coll.collection.children.unlink(grp) + + +def set_prop(id_block: ID, key: str, value: Any, **kwargs: Dict[str, Any]): + """Create or set the properties + 3.0 got more functionalities + """ + id_block[key] = value + if bv30(): + id_props = id_block.id_properties_ui(key) + id_props.update(**kwargs) + overrides = kwargs.get("overridable_library", True) + if overrides is not None: + id_block.property_overridable_library_set(f'["{key}"]', overrides)