diff --git a/.gitignore b/.gitignore index db71b38..cede366 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /build /run /.unused +*.pyc +addon/umap.py +BlenderUmap.log diff --git a/addon/__init__.py b/addon/__init__.py new file mode 100644 index 0000000..2469040 --- /dev/null +++ b/addon/__init__.py @@ -0,0 +1,35 @@ +# 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 3 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 +# MERCHANTIBILITY 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, see . + +bl_info = { + "name" : "BlenderUmap", + "author" : "Amrsatrio, MountainFlash (MinshuG)", + "description" : "", + "blender" : (2, 80, 0), + "version" : (0, 0, 1), + "location" : "View3D > Properties > Umap", + "warning" : "", + "doc_url": "https://github.com/Amrsatrio/BlenderUmap/blob/master/README.md", + "category" : "Add Mesh" +} + + +from . import auto_load + +auto_load.init() + +def register(): + auto_load.register() + +def unregister(): + auto_load.unregister() diff --git a/addon/auto_load.py b/addon/auto_load.py new file mode 100644 index 0000000..197a638 --- /dev/null +++ b/addon/auto_load.py @@ -0,0 +1,138 @@ +import os +import bpy +import sys +import typing +import inspect +import pkgutil +import importlib +from pathlib import Path + +__all__ = ( + "init", + "register", + "unregister", +) + +modules = None +ordered_classes = None + +def init(): + global modules + global ordered_classes + + modules = get_all_submodules(Path(__file__).parent) + ordered_classes = get_ordered_classes_to_register(modules) + +def register(): + for cls in ordered_classes: + bpy.utils.register_class(cls) + + for module in modules: + if module.__name__ == __name__: + continue + if hasattr(module, "register"): + module.register() + +def unregister(): + for cls in reversed(ordered_classes): + bpy.utils.unregister_class(cls) + + for module in modules: + if module.__name__ == __name__: + continue + if hasattr(module, "unregister"): + module.unregister() + + +# Import modules +################################################# + +def get_all_submodules(directory): + return list(iter_submodules(directory, directory.name)) + +def iter_submodules(path, package_name): + for name in sorted(iter_submodule_names(path)): + yield importlib.import_module("." + name, package_name) + +def iter_submodule_names(path, root=""): + for _, module_name, is_package in pkgutil.iter_modules([str(path)]): + if is_package: + sub_path = path / module_name + sub_root = root + module_name + "." + yield from iter_submodule_names(sub_path, sub_root) + else: + yield root + module_name + + +# Find classes to register +################################################# + +def get_ordered_classes_to_register(modules): + return toposort(get_register_deps_dict(modules)) + +def get_register_deps_dict(modules): + deps_dict = {} + classes_to_register = set(iter_classes_to_register(modules)) + for cls in classes_to_register: + deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register)) + return deps_dict + +def iter_own_register_deps(cls, own_classes): + yield from (dep for dep in iter_register_deps(cls) if dep in own_classes) + +def iter_register_deps(cls): + for value in typing.get_type_hints(cls, {}, {}).values(): + dependency = get_dependency_from_annotation(value) + if dependency is not None: + yield dependency + +def get_dependency_from_annotation(value): + if isinstance(value, tuple) and len(value) == 2: + if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty): + return value[1]["type"] + return None + +def iter_classes_to_register(modules): + base_types = get_register_base_types() + for cls in get_classes_in_modules(modules): + if any(base in base_types for base in cls.__bases__): + if not getattr(cls, "is_registered", False): + yield cls + +def get_classes_in_modules(modules): + classes = set() + for module in modules: + for cls in iter_classes_in_module(module): + classes.add(cls) + return classes + +def iter_classes_in_module(module): + for value in module.__dict__.values(): + if inspect.isclass(value): + yield value + +def get_register_base_types(): + return set(getattr(bpy.types, name) for name in [ + "Panel", "Operator", "PropertyGroup", + "AddonPreferences", "Header", "Menu", + "Node", "NodeSocket", "NodeTree", + "UIList", "RenderEngine" + ]) + + +# Find order to register to solve dependencies +################################################# + +def toposort(deps_dict): + sorted_list = [] + sorted_values = set() + while len(deps_dict) > 0: + unsorted = [] + for value, deps in deps_dict.items(): + if len(deps) == 0: + sorted_list.append(value) + sorted_values.add(value) + else: + unsorted.append(value) + deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} + return sorted_list \ No newline at end of file diff --git a/addon/config.py b/addon/config.py new file mode 100644 index 0000000..c4046ce --- /dev/null +++ b/addon/config.py @@ -0,0 +1,120 @@ +import bpy +from typing import List, Any, TypeVar +import os +import json + +T = TypeVar("T") + +def from_list(x: Any) -> List[T]: + l = [] + l.append({ + "Guid": "00000000000000000000000000000000", + "Key": bpy.context.scene.aeskey + }) + for a in x: + if a.pakname == "" and a.daeskey == "": + continue + if a.guid == "" and a.pakname == "": + continue + if a.daeskey == "": + continue + + d = {} + if a.guid != "": + d["Guid"] = a.guid + if a.pakname != "": + d["FileName"] = a.pakname + d["Key"] = a.daeskey + l.append(d) + return l + +class Config: + Documentation: str = "https://github.com/Amrsatrio/BlenderUmap/blob/master/README.md" + PaksDirectory: str + ExportPath: str + UEVersion: str + EncryptionKeys: List[Any] + bDumpAssets: bool + ObjectCacheSize: int + bReadMaterials: bool + bExportToDDSWhenPossible: bool + bExportBuildingFoundations: bool + bUseUModel: bool + UModelAdditionalArgs: str + ExportPackage: str + + def __init__(self) -> None: + sc = bpy.context.scene + self.PaksDirectory = sc.Game_Path[:-1] + self.ExportPath = sc.exportPath + self.UEVersion = sc.ue4_versions + self.EncryptionKeys = sc.dpklist + self.bDumpAssets = sc.bdumpassets + self.ObjectCacheSize = sc.ObjectCacheSize + self.bReadMaterials = sc.readmats + self.bExportToDDSWhenPossible = sc.bExportToDDSWhenPossible + self.bExportBuildingFoundations = sc.bExportBuildingFoundations + self.bUseUModel = sc.bUseUModel + self.UModelAdditionalArgs = sc.additionalargs + self.ExportPackage = sc.package + + def to_dict(self) -> dict: + result: dict = {} + result["_Documentation"] = self.Documentation + result["PaksDirectory"] = self.PaksDirectory + result["ExportPath"] = self.ExportPath + result["UEVersion"] = self.UEVersion + result["bDumpAssets"] = self.bDumpAssets + result["ObjectCacheSize"] = self.ObjectCacheSize + result["bReadMaterials"] = self.bReadMaterials + result["bExportToDDSWhenPossible"] = self.bExportToDDSWhenPossible + result["bExportBuildingFoundations"] = self.bExportBuildingFoundations + result["bUseUModel"] = self.bUseUModel + result["UModelAdditionalArgs"] = self.UModelAdditionalArgs + result["ExportPackage"] = self.ExportPackage + result["EncryptionKeys"] = from_list(self.EncryptionKeys) + return result + + def load(self, out = {}): + if not os.path.exists(os.path.join(self.ExportPath,"config.json")): + return + with open(os.path.join(self.ExportPath,"config.json"),"r") as f: + data = json.load(f) + out = data + + sc = bpy.context.scene + + sc.Game_Path = data["PaksDirectory"] + "/" + sc.exportPath = data["ExportPath"] + sc.ue4_versions = data["UEVersion"] + sc.bdumpassets = data["bDumpAssets"] + sc.ObjectCacheSize = data["ObjectCacheSize"] + sc.readmats = data["bReadMaterials"] + sc.bExportToDDSWhenPossible = data["bExportToDDSWhenPossible"] + sc.bExportBuildingFoundations = data["bExportBuildingFoundations"] + sc.bUseUModel = data["bUseUModel"] + sc.additionalargs = data["UModelAdditionalArgs"] + sc.package = data["ExportPackage"] + + for a in range(len(sc.dpklist)): + sc.dpklist.remove(a) + + sc.list_index = 0 + i = 0 + for x in data["EncryptionKeys"]: + if guid := x.get("Guid"): + if guid == "00000000000000000000000000000000": + sc.aeskey = x["Key"] + continue + + sc.list_index = i + sc.dpklist.add() + dpk = sc.dpklist[i] + dpk.guid = x.get("Guid") or "" + dpk.pakname = x.get("FileName") or "" + dpk.daeskey = x["Key"] + i += 1 + + def dump(self,path): + with open(os.path.join(path,"config.json"),"w") as f: + json.dump(self.to_dict(),f,indent=4) diff --git a/addon/main.py b/addon/main.py new file mode 100644 index 0000000..48f38a0 --- /dev/null +++ b/addon/main.py @@ -0,0 +1,536 @@ +import typing +import bpy +from bpy.props import StringProperty, IntProperty, CollectionProperty, BoolProperty +import json +import os +from urllib.request import urlopen, Request +import re + +from bpy.types import Context +from .config import Config + +try: + from .umap import import_umap, cleanup +except ImportError: + from ..umap import import_umap, cleanup + +def main(context): + sc = bpy.context.scene + reuse_maps = sc.reuse_maps + reuse_meshes = sc.reuse_mesh + use_cube_as_fallback = sc.use_cube_as_fallback + data_dir = sc.exportPath + addon_dir = os.path.dirname(os.path.splitext(__file__)[0]) + + Config().dump(sc.exportPath) + + exporter_result = os.system(f'START /WAIT /D "{data_dir}" cmd /K java -jar "{os.path.join(addon_dir,"BlenderUmap.jar")}"') + if exporter_result != 0: + raise Exception("Exporter returned non zero exit code") + + uvm = bpy.data.node_groups.get("UV Shader Mix") + tex_shader = bpy.data.node_groups.get("Texture Shader") + + if not uvm or not tex_shader: + with bpy.data.libraries.load(os.path.join(addon_dir, "deps.blend")) as (data_from, data_to): + data_to.node_groups = data_from.node_groups + + uvm = bpy.data.node_groups.get("UV Shader Mix") + tex_shader = bpy.data.node_groups.get("Texture Shader") + + # make sure we're on main scene to deal with the fallback objects + main_scene = bpy.data.scenes.get("Scene") or bpy.data.scenes.new("Scene") + bpy.context.window.scene = main_scene + + # prepare collection for imports + import_collection = bpy.data.collections.get("Imported") + + if import_collection: + bpy.ops.object.select_all(action='DESELECT') + + for obj in import_collection.objects: + obj.select_set(True) + + bpy.ops.object.delete() + else: + import_collection = bpy.data.collections.new("Imported") + main_scene.collection.children.link(import_collection) + + cleanup() + + # setup fallback cube mesh + bpy.ops.mesh.primitive_cube_add(size=2) + fallback_cube = bpy.context.active_object + fallback_cube_mesh = fallback_cube.data + fallback_cube_mesh.name = "__fallback" + bpy.data.objects.remove(fallback_cube) + + # 2. empty mesh + empty_mesh = bpy.data.meshes.get("__empty", bpy.data.meshes.new("__empty")) + + # do it! + with open(os.path.join(data_dir, "processed.json")) as file: + import_umap(json.loads(file.read()), import_collection, data_dir, reuse_maps, reuse_meshes, use_cube_as_fallback, tex_shader) + + # go back to main scene + bpy.context.window.scene = main_scene + cleanup() + +class UE4Version: # idk why + """Supported UE4 Versions""" + + Versions = ( + ("GAME_UE4_0", "GAME_UE4_0", ""), + ("GAME_UE4_1", "GAME_UE4_1", ""), + ("GAME_UE4_2", "GAME_UE4_2", ""), + ("GAME_UE4_3", "GAME_UE4_3", ""), + ("GAME_UE4_4", "GAME_UE4_4", ""), + ("GAME_UE4_5", "GAME_UE4_5", ""), + ("GAME_UE4_6", "GAME_UE4_6", ""), + ("GAME_UE4_7", "GAME_UE4_7", ""), + ("GAME_UE4_8", "GAME_UE4_8", ""), + ("GAME_UE4_9", "GAME_UE4_9", ""), + ("GAME_UE4_10", "GAME_UE4_10", ""), + ("GAME_UE4_11", "GAME_UE4_11", ""), + ("GAME_UE4_12", "GAME_UE4_12", ""), + ("GAME_UE4_13", "GAME_UE4_13", ""), + ("GAME_UE4_14", "GAME_UE4_14", ""), + ("GAME_UE4_15", "GAME_UE4_15", ""), + ("GAME_UE4_16", "GAME_UE4_16", ""), + ("GAME_UE4_17", "GAME_UE4_17", ""), + ("GAME_UE4_18", "GAME_UE4_18", ""), + ("GAME_UE4_19", "GAME_UE4_19", ""), + ("GAME_UE4_20", "GAME_UE4_20", ""), + ("GAME_UE4_21", "GAME_UE4_21", ""), + ("GAME_UE4_22", "GAME_UE4_22", ""), + ("GAME_UE4_23", "GAME_UE4_23", ""), + ("GAME_UE4_24", "GAME_UE4_24", ""), + ("GAME_UE4_25", "GAME_UE4_25", ""), + ("GAME_UE4_26", "GAME_UE4_26", ""), + ("GAME_VALORANT", "GAME_VALORANT", ""), + ("GAME_UE4_LATEST", "GAME_UE4_LATEST", ""), + ) + +class VIEW3D_MT_AdditionalOptions(bpy.types.Menu): + bl_label = "Additional Options" + + def draw(self, context): + layout = self.layout + col = layout.column() + + col.operator("umap.fillfortnitekeys", icon="FILE_FONT",text="Fill Fortnite AES Keys", depress=False) + +# UI +class VIEW3D_PT_Import(bpy.types.Panel): + """Creates a Panel in Properties(N)""" + + bl_label = "BlenderUmap" + bl_idname = "VIEW3D_PT_Umap" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Umap" + bl_context = "objectmode" + + bpy.types.Scene.ue4_versions = bpy.props.EnumProperty( + name="UE4 Version", items=UE4Version.Versions + ) + + def draw(self, context): + layout = self.layout + + col = layout.column(align=True, heading="Exporter Settings:") + + if os.path.isfile(os.path.join(bpy.context.scene.exportPath, "config.json")): + col.operator("umap.load_configs", icon="FILE_REFRESH", text="Reload Last Used Config") + + # col.label(text="Exporter Settings:") + col.prop(context.scene, "Game_Path") + col.prop(context.scene, "aeskey") + col.prop(context.scene, "exportPath") + col.label(text="Dynamic Keys:") + + row = col.row(align=True) + + col2 = row.column(align=True) + col2.template_list( + "VIEW3D_UL_DPKLIST", + "VIEW3D_UL_dpklist", + context.scene, + "dpklist", + context.scene, + "list_index", + rows=3, + ) + row.separator() + + col3 = row.column(align=True) + col3.operator("dpklist.new_item", icon='ADD', text="") + col3.operator("dpklist.delete_item", icon='REMOVE', text="") + col3.separator() + col3.menu("VIEW3D_MT_AdditionalOptions", icon='DOWNARROW_HLT', text="") + col.separator() + + if context.scene.list_index >= 0 and context.scene.dpklist: + item = context.scene.dpklist[context.scene.list_index] + col.prop(item, "pakname") + col.prop(item, "daeskey") + col.prop(item, "guid") + col.separator() + + col.prop(context.scene, "package") + + if re.search(r"/Plugins/GameFeatures/.*/Content/", context.scene.package): + col.label(text="Provided package path might not work.", icon="ERROR") + col.operator("umap.convert_path", icon="FILE_REFRESH", text="Convert Path") + col.separator() + + col.prop(context.scene, "ue4_versions") # TODO custom ue4 versions + + col.prop(context.scene, "readmats") + col.prop(context.scene, "bExportToDDSWhenPossible") + col.prop(context.scene,"bExportBuildingFoundations") + col.prop(context.scene, "bdumpassets") + col.prop(context.scene, "ObjectCacheSize") + col.prop(context.scene, "bUseUModel") + if context.scene.bUseUModel == True: + col.prop(context.scene, "additionalargs") + col.separator() + + col = col.column(align=True, heading="Importer Settings:") + # col.label(text="Importer Settings:") + col.prop(context.scene, "reuse_maps", text="Reuse Maps") + col.prop(context.scene, "reuse_mesh", text="Reuse Meshes") + col.prop(context.scene, "use_cube_as_fallback") + + col.operator("umap.import", text="Import", icon="IMPORT") + + +class VIEW_PT_UmapOperator(bpy.types.Operator): + """Import Umap""" + + bl_idname = "umap.import" + bl_label = "Umap Exporter" + + def execute(self, context): + if bpy.context.scene.ue4_versions in ["GAME_UE4_26","GAME_UE4_27","GAME_UE4_LATEST"]: + self.check_mappings() + + if context.scene.bUseUModel: + if not os.path.exists(os.path.join(bpy.context.scene.exportPath,"umodel.exe")): + self.report({'ERROR'}, 'umodel.exe not found in Export Directory(Export Path)') + return {"CANCELLED"} + + main(context) + return {"FINISHED"} + + def check_mappings(self): + path = bpy.context.scene.exportPath + mappings_path = os.path.join(path, "mappings") + if not os.path.exists(mappings_path): + os.makedirs(mappings_path) + self.dl_mappings(mappings_path) + return False + + try: + self.dl_mappings(mappings_path) + except: pass + return True + + def dl_mappings(self,path): + ENDPOINT = "https://benbot.app/api/v1/mappings" + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + } + + req = Request(url=ENDPOINT, headers=headers) + r = urlopen(req) + data = json.loads(r.read().decode(r.info().get_param("charset") or "utf-8")) + + if not os.path.exists(os.path.join(path,data[0]["fileName"])): + with open(os.path.join(path,data[0]["fileName"]),"wb") as f: + downfile = urlopen(Request(url=data[0]["url"], headers=headers)) + print("Downloading",data[0]["fileName"]) + f.write(downfile.read(downfile.length)) + return True + + +class ListItem(bpy.types.PropertyGroup): + pakname: StringProperty( + name="Pak Name", description="Name of the Pak file.", default="" + ) + daeskey: StringProperty( + name="AES Key", description="AES key for the Pak file.", default="" + ) + guid: StringProperty( + name="Encryption Guid", description= "Encryption Guid for the Pak file.", default="", maxlen=32) + + +class VIEW3D_UL_DPKLIST(bpy.types.UIList): + """Dynamic Pak AES key List""" + + def draw_item( + self, context, layout, data, item, icon, active_data, active_propname, index + ): + if self.layout_type in {"DEFAULT", "COMPACT"}: + if item.pakname == "": + layout.label(text=f"{item.guid}:{item.daeskey}") + else: + layout.label(text=f"{item.pakname}:{item.daeskey}") + + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + if item.pakname == "": + layout.label(text=f"{item.guid}") + else: + layout.label(text=item.pakname) + +class Fortnite(bpy.types.Operator): + bl_idname = "umap.fillfortnitekeys" + bl_label = "Fill Fortnite Keys" + bl_description = "Automatically fill AES Key/s for Latest Fortnite version" + bl_options = {"UNDO"} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.3" + } + + req = Request(url="https://fortnite-api.com/v2/aes", headers=headers) + r = urlopen(req) + + if r.status != 200: + self.report({"ERROR"}, "API returned {r.status} status code") + return {"CANCELLED"} + + raw_data = r.read().decode(r.info().get_param("charset") or "utf-8") + try: + data = json.loads(raw_data)["data"] + except Exception as e: + self.report({'ERROR'}, "Error Loading JSON \n{e}") + return {"CANCELLED"} + + main_key = data["mainKey"] + bpy.context.scene.aeskey = main_key if main_key.startswith("0x") else f"0x{main_key}" + + dpklist = context.scene.dpklist + context.scene.list_index = len(dpklist) + index = context.scene.list_index + + for _ in bpy.context.scene.dpklist: + dpklist.remove(index) + index = index - 1 + + context.scene.list_index = 0 + for x in data["dynamicKeys"]: + PakPath, Guid, AESKey = x.values() + Pakname = os.path.basename(PakPath) + context.scene.dpklist.add() + item = context.scene.dpklist[index] + item.pakname = Pakname + item.guid = Guid + item.daeskey = AESKey if AESKey.startswith("0x") else "0x" + AESKey + index = index + 1 + + return {"FINISHED"} + + +class DPKLIST_OT_NewItem(bpy.types.Operator): + """Add a new item to the list.""" + + bl_idname = "dpklist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + context.scene.dpklist.add() + context.scene.list_index + 1 + return {"FINISHED"} + + +class LIST_OT_DeleteItem(bpy.types.Operator): + """Delete the selected item from the list.""" + + bl_idname = "dpklist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(cls, context): + return len(context.scene.dpklist) > 0 + + def execute(self, context): + dpklist = context.scene.dpklist + index = context.scene.list_index + dpklist.remove(index) + context.scene.list_index = min(max(0, index - 1), len(dpklist) - 1) + return {"FINISHED"} + + +class LOAD_Configs(bpy.types.Operator): + bl_label = "Load Config from File" + bl_idname = "umap.load_configs" + bl_description = "Load Configs from File" + + def execute(self, context: 'Context') -> typing.Union[typing.Set[str], typing.Set[int]]: + try: + Config().load() + except Exception as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + return {"FINISHED"} + +# convert path +class CONVERT_Path(bpy.types.Operator): + bl_label = "Convert Path" + bl_idname = "umap.convert_path" + bl_description = "Convert Path to BlenderUmap Supported format." + + def execute(self, context: 'Context'): + """ + from GameName/Plugins/GameFeatures/GameFeatureName/Content/Maps/MapName.umap + to /GameFeatureName/Maps/MapName + """ + path = context.scene.package.split("/") + path = path[3:] + b_content = True + fixed_path = [""] + for x in path: + if x == "Content" and b_content: + b_content = False + continue + if x.endswith(".umap"): + x = x[:-5] + fixed_path.append(x) + + context.scene.package = "/".join(fixed_path) + + return {"FINISHED"} + + + +def register(): + # bpy.utils.register_class(VIEW_PT_UmapOperator) + # bpy.utils.register_class(VIEW_PT_Import) + + bpy.types.Scene.dpklist = CollectionProperty(type=ListItem) + bpy.types.Scene.list_index = IntProperty(name="", default=0) + + bpy.types.Scene.Game_Path = StringProperty( + name="Game Path", + description="Path to the Paks folder", + subtype="DIR_PATH", + ) + + bpy.types.Scene.aeskey = StringProperty( + name="Main AES Key", + description="AES key", + subtype="NONE", + ) + + bpy.types.Scene.package = StringProperty( + name="Package", + description="Umap to export", + subtype="NONE", + ) + + bpy.types.Scene.readmats = BoolProperty( + name="Read Materials", + description="Import Materials", + default=True, + subtype="NONE", + ) + + bpy.types.Scene.bExportToDDSWhenPossible = BoolProperty( + name="Export DDS When Possible", + description="Export textures to .dds format", + default=False, + subtype="NONE", + ) + + bpy.types.Scene.bExportBuildingFoundations = BoolProperty( + name="Export Building Foundations", + description="You can turn off exporting sub-buildings in large POIs\ +if you want to quickly port the base POI structures, by setting this to false", + default=True, + subtype="NONE", + ) + + bpy.types.Scene.bdumpassets = BoolProperty( + name="Dump Assets", + description="Save assets as JSON format", + default=False, + subtype="NONE", + ) + + bpy.types.Scene.ObjectCacheSize = IntProperty( + name="Object Cache Size", + description="Configure the object loader cache size to tune the performance, or set to 0 to disable", + default=100, + min=0, + ) + + bpy.types.Scene.bUseUModel = BoolProperty( + name="Use UModel", + description="Use UModel for the exporting process to export meshes, materials, and textures", + default=False, + subtype="NONE", + ) + + bpy.types.Scene.reuse_maps = BoolProperty( + name="Reuse Maps", + description="Reuse already imported map rather then importing them again", + default=True, + subtype="NONE", + ) + + bpy.types.Scene.reuse_mesh = BoolProperty( + name="Reuse Meshes", + description="Reuse already imported meshes rather then importing them again", + default=True, + subtype="NONE", + ) + + bpy.types.Scene.use_cube_as_fallback = BoolProperty( + name="Use Cube as Fallback Mesh", + description="Use cube if mesh is not found", + default=True, + subtype="NONE", + ) + + bpy.types.Scene.additionalargs = StringProperty( + name="UModel Additional Args", + description="Additional Args for UModel", + subtype="NONE", + ) + + bpy.types.Scene.exportPath = StringProperty( + name="Export Path", + description="Path to Export Folder", + subtype="DIR_PATH", + ) + +def unregister(): + # bpy.utils.unregister_class(VIEW_PT_UmapOperator) + # bpy.utils.unregister_class(VIEW_PT_Import) + + sc = bpy.types.Scene + del sc.Game_Path + del sc.aeskey + del sc.package + del sc.readmats + del sc.bExportToDDSWhenPossible + del sc.bExportBuildingFoundations + del sc.bdumpassets + del sc.ObjectCacheSize + del sc.bUseUModel + del sc.reuse_maps + del sc.reuse_mesh + del sc.use_cube_as_fallback + del sc.additionalargs + del sc.exportPath + +if __name__ == "__main__": + register() diff --git a/src/main/java/com/tb24/blenderumap/Main.java b/src/main/java/com/tb24/blenderumap/Main.java index 613c963..cd1ffe5 100644 --- a/src/main/java/com/tb24/blenderumap/Main.java +++ b/src/main/java/com/tb24/blenderumap/Main.java @@ -17,6 +17,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -299,7 +300,7 @@ private static Package exportAndProduceProcessed(String path) { if (pkgName.endsWith(".umap")) { pkgName = pkgName.substring(0, pkgName.lastIndexOf('.')); } - File file = new File(MyFileProvider.JSONS_FOLDER, pkgName + ".processed.json"); + File file = new File(config.ExportPath + "/" + MyFileProvider.JSONS_FOLDER, pkgName + ".processed.json"); file.getParentFile().mkdirs(); LOGGER.info("Writing to {}", file.getAbsolutePath()); @@ -360,7 +361,7 @@ public static File getExportDir(UObject exportObj) { pkgPath = pkgPath.substring(1); } - File outputDir = new File(pkgPath).getParentFile(); + File outputDir = new File(String.valueOf(Paths.get(config.ExportPath, pkgPath))).getParentFile(); String pkgName = StringsKt.substringAfterLast(pkgPath, '/', pkgPath); if (!exportObj.getName().equals(pkgName)) { @@ -392,11 +393,13 @@ private static void exportUmodel() throws InterruptedException, IOException { pw.println("-path=\"" + config.PaksDirectory + '\"'); pw.println("-game=ue4." + GameKt.GAME_UE4_GET_MINOR(config.UEVersion.getGame())); - if (config.EncryptionKeys.size() > 0) { // TODO run umodel multiple times if there's more than one encryption key - pw.println("-aes=0x" + DataTypeConverterKt.printHexBinary(config.EncryptionKeys.get(0).Key)); + if (config.EncryptionKeys.size() > 0) { + for (int i = 0; i < config.EncryptionKeys.size(); i++) { + pw.println("-aes=0x" + DataTypeConverterKt.printHexBinary(config.EncryptionKeys.get(i).Key)); + } } - pw.println("-out=\"" + new File("").getAbsolutePath() + '\"'); + pw.println("-out=\"" + config.ExportPath); if (!isEmpty(config.UModelAdditionalArgs)) { pw.println(config.UModelAdditionalArgs); @@ -566,6 +569,7 @@ public void addToObj(JsonObject obj) { public static class Config { public String PaksDirectory = "C:\\Program Files\\Epic Games\\Fortnite\\FortniteGame\\Content\\Paks"; public Ue4Version UEVersion = Ue4Version.GAME_UE4_LATEST; + public String ExportPath = ""; public List EncryptionKeys = Collections.emptyList(); public boolean bDumpAssets = false; public int ObjectCacheSize = 100; diff --git a/umap.py b/umap.py index 12a4ee6..0812908 100644 --- a/umap.py +++ b/umap.py @@ -19,7 +19,7 @@ # ---------- END INPUTS, DO NOT MODIFY ANYTHING BELOW UNLESS YOU NEED TO ---------- def import_umap(processed_map_path: str, - into_collection: bpy.types.Collection) -> bpy.types.Object: + into_collection: bpy.types.Collection, data_dir: str, reuse_maps: bool, reuse_meshes: bool, use_cube_as_fallback: bool, tex_shader) -> bpy.types.Object: map_name = processed_map_path[processed_map_path.rindex("/") + 1:] map_collection = bpy.data.collections.get(map_name) @@ -63,7 +63,7 @@ def new_object(data: bpy.types.Mesh = None): if child_comps and len(child_comps) > 0: for i, child_comp in enumerate(child_comps): apply_ob_props( - import_umap(child_comp, map_collection), + import_umap(child_comp, map_collection, data_dir, reuse_maps, reuse_meshes, use_cube_as_fallback, tex_shader), name if i is 0 else ("%s_%d" % (name, i))) continue @@ -108,7 +108,7 @@ def new_object(data: bpy.types.Mesh = None): for m_idx, (m_path, m_textures) in enumerate(mats.items()): if m_textures: - import_material(imported, m_idx, m_path, td_suffix, m_textures, texture_data) + import_material(imported, m_idx, m_path, td_suffix, m_textures, texture_data, tex_shader, data_dir) else: print("WARNING: Mesh not imported, defaulting to fallback mesh:", full_mesh_path) new_object() @@ -121,7 +121,7 @@ def import_material(ob: bpy.types.Object, path: str, suffix: str, base_textures: list, - tex_data: dict) -> bpy.types.Material: + tex_data: dict,tex_shader, data_dir) -> bpy.types.Material: # .mat is required to prevent conflicts with empty ones imported by PSK/PSA plugin m_name = os.path.basename(path + ".mat" + suffix) m = bpy.data.materials.get(m_name) @@ -147,7 +147,7 @@ def import_material(ob: bpy.types.Object, m.use_backface_culling = True m.blend_method = "OPAQUE" - def group(sub_tex_idx, location): + def group(sub_tex_idx, location,tex_shader): sh = tree.nodes.new("ShaderNodeGroup") sh.location = location sh.node_tree = tex_shader @@ -155,7 +155,7 @@ def group(sub_tex_idx, location): for tex_index, sub_tex in enumerate(sub_textures): if sub_tex: - img = get_or_load_img(sub_tex) if not sub_tex.endswith("/T_EmissiveColorChart") else None + img = get_or_load_img(sub_tex, data_dir) if not sub_tex.endswith("/T_EmissiveColorChart") else None if img: d_tex = tree.nodes.new("ShaderNodeTexImage") @@ -184,13 +184,13 @@ def group(sub_tex_idx, location): uv_map.location = [-100, 700] uv_map.uv_map = "EXTRAUVS0" tree.links.new(uv_map.outputs[0], uvm_ng.inputs[0]) - tree.links.new(group(0, [-100, 550]).outputs[0], uvm_ng.inputs[1]) - tree.links.new(group(1, [-100, 300]).outputs[0], uvm_ng.inputs[2]) - tree.links.new(group(2, [-100, 50]).outputs[0], uvm_ng.inputs[3]) - tree.links.new(group(3, [-100, -200]).outputs[0], uvm_ng.inputs[4]) + tree.links.new(group(0, [-100, 550],tex_shader).outputs[0], uvm_ng.inputs[1]) + tree.links.new(group(1, [-100, 300], tex_shader).outputs[0], uvm_ng.inputs[2], ) + tree.links.new(group(2, [-100, 50], tex_shader).outputs[0], uvm_ng.inputs[3]) + tree.links.new(group(3, [-100, -200], tex_shader).outputs[0], uvm_ng.inputs[4]) tree.links.new(uvm_ng.outputs[0], mat_out.inputs[0]) else: - tree.links.new(group(0, [100, 300]).outputs[0], mat_out.inputs[0]) + tree.links.new(group(0, [100, 300], tex_shader).outputs[0], mat_out.inputs[0]) print("Material imported") @@ -208,7 +208,7 @@ def place_map(collection: bpy.types.Collection, into_collection: bpy.types.Colle return c_inst -def get_or_load_img(img_path: str) -> bpy.types.Image: +def get_or_load_img(img_path: str, data_dir: str) -> bpy.types.Image: name = os.path.basename(img_path) existing = bpy.data.images.get(name) @@ -258,55 +258,55 @@ def string_hash_code(s: str) -> int: h = (31 * h + ord(c)) & 0xFFFFFFFF return ((h + 0x80000000) & 0xFFFFFFFF) - 0x80000000 - -start = int(time.time() * 1000.0) - -uvm = bpy.data.node_groups.get("UV Shader Mix") -tex_shader = bpy.data.node_groups.get("Texture Shader") - -if not uvm or not tex_shader: - with bpy.data.libraries.load(os.path.join(data_dir, "deps.blend")) as (data_from, data_to): - data_to.node_groups = data_from.node_groups +if __name__ == "__main__": + start = int(time.time() * 1000.0) uvm = bpy.data.node_groups.get("UV Shader Mix") tex_shader = bpy.data.node_groups.get("Texture Shader") -# make sure we're on main scene to deal with the fallback objects -main_scene = bpy.data.scenes.get("Scene") or bpy.data.scenes.new("Scene") -bpy.context.window.scene = main_scene + if not uvm or not tex_shader: + with bpy.data.libraries.load(os.path.join(data_dir, "deps.blend")) as (data_from, data_to): + data_to.node_groups = data_from.node_groups -# prepare collection for imports -import_collection = bpy.data.collections.get("Imported") + uvm = bpy.data.node_groups.get("UV Shader Mix") + tex_shader = bpy.data.node_groups.get("Texture Shader") -if import_collection: - bpy.ops.object.select_all(action='DESELECT') + # make sure we're on main scene to deal with the fallback objects + main_scene = bpy.data.scenes.get("Scene") or bpy.data.scenes.new("Scene") + bpy.context.window.scene = main_scene - for obj in import_collection.objects: - obj.select_set(True) + # prepare collection for imports + import_collection = bpy.data.collections.get("Imported") - bpy.ops.object.delete() -else: - import_collection = bpy.data.collections.new("Imported") - main_scene.collection.children.link(import_collection) + if import_collection: + bpy.ops.object.select_all(action='DESELECT') + + for obj in import_collection.objects: + obj.select_set(True) + + bpy.ops.object.delete() + else: + import_collection = bpy.data.collections.new("Imported") + main_scene.collection.children.link(import_collection) -cleanup() + cleanup() -# setup fallback cube mesh -bpy.ops.mesh.primitive_cube_add(size=2) -fallback_cube = bpy.context.active_object -fallback_cube_mesh = fallback_cube.data -fallback_cube_mesh.name = "__fallback" -bpy.data.objects.remove(fallback_cube) + # setup fallback cube mesh + bpy.ops.mesh.primitive_cube_add(size=2) + fallback_cube = bpy.context.active_object + fallback_cube_mesh = fallback_cube.data + fallback_cube_mesh.name = "__fallback" + bpy.data.objects.remove(fallback_cube) -# 2. empty mesh -empty_mesh = bpy.data.meshes.get("__empty", bpy.data.meshes.new("__empty")) + # 2. empty mesh + empty_mesh = bpy.data.meshes.get("__empty", bpy.data.meshes.new("__empty")) -# do it! -with open(os.path.join(data_dir, "processed.json")) as file: - import_umap(json.loads(file.read()), import_collection) + # do it! + with open(os.path.join(data_dir, "processed.json")) as file: + import_umap(json.loads(file.read()), import_collection, data_dir, reuse_maps, reuse_meshes, use_cube_as_fallback, tex_shader) -# go back to main scene -bpy.context.window.scene = main_scene -cleanup() + # go back to main scene + bpy.context.window.scene = main_scene + cleanup() -print("All done in " + str(int((time.time() * 1000.0) - start)) + "ms") + print("All done in " + str(int((time.time() * 1000.0) - start)) + "ms")