From fcafcfbb3387a1bb68161247028c6bef93117bb9 Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Wed, 10 Feb 2021 00:10:04 +0530 Subject: [PATCH 1/9] initial addon work --- .gitignore | 1 + addon/__init__.py | 35 +++++ addon/auto_load.py | 138 +++++++++++++++++ addon/config.py | 69 +++++++++ addon/main.py | 377 +++++++++++++++++++++++++++++++++++++++++++++ umap.py | 78 +++++----- 6 files changed, 659 insertions(+), 39 deletions(-) create mode 100644 addon/__init__.py create mode 100644 addon/auto_load.py create mode 100644 addon/config.py create mode 100644 addon/main.py diff --git a/.gitignore b/.gitignore index db71b38..beb5592 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /build /run /.unused +*.pyc \ No newline at end of file diff --git a/addon/__init__.py b/addon/__init__.py new file mode 100644 index 0000000..2b99844 --- /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", + "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..077f16e --- /dev/null +++ b/addon/config.py @@ -0,0 +1,69 @@ +import bpy +from typing import List, Any, TypeVar +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: + l.append({ + "Key" : a.pakname, + "FileName" : a.daeskey + }) + 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["EncryptionKeys"] = from_list(self.EncryptionKeys) + 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 + return result + + def dump(self): + with open("config.json","w") as f: + json.dump(self.to_dict(),f,indent=4) \ No newline at end of file diff --git a/addon/main.py b/addon/main.py new file mode 100644 index 0000000..045d6d2 --- /dev/null +++ b/addon/main.py @@ -0,0 +1,377 @@ +import bpy +from bpy.props import StringProperty, IntProperty, CollectionProperty, BoolProperty +import json +import os +from math import * +from urllib.request import urlopen, Request +from .config import Config +# from ..umap import import_umap, cleanup + + +def main(context): + pass + + +class UE4Version(bpy.types.Operator): # idk why + """Supported UE4 Versions""" + + bl_idname = "umap.ue4versions" + bl_label = "uhhh versions" + bl_description = "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", ""), + ) + + # ue4_versions: bpy.props.EnumProperty(name="UE4 Version:", items=Versions) + + +# Button +class VIEW_PT_Import(bpy.types.Panel): + """Creates a Panel in Properties(N)""" + + bl_label = "BlenderUmap" + bl_idname = "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) + + col.label(text="Exporter Settings:") + + col.prop(context.scene, "Game_Path", text="Game Path") + + col.prop(context.scene, "aeskey", text="Main AES Key") + + col.prop(context.scene, "exportPath", text="Export Path") + + + col.label(text="Dynamic Keys:") + col.template_list( + "DPKLIST", + "DPK_List", + context.scene, + "dpklist", + context.scene, + "list_index", + rows=2, + ) + + row = col.row() + row.operator("dpklist.new_item", text="+") + row.operator("dpklist.delete_item", text="-") + + 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(context.scene, "package", text="Package") + + col.prop(context.scene, "ue4_versions") + + col.prop(context.scene, "readmats", text="Read Materials") + + col.prop( + context.scene, "bExportToDDSWhenPossible", text="Export DDS When Possible" + ) + + col.prop( + context.scene, + "bExportBuildingFoundations", + text="Export Building Foundations", + ) + + col.prop(context.scene, "bdumpassets", text="Dump Assets") + + col.prop(context.scene, "ObjectCacheSize", text="Object Cache Size") + + col.prop(context.scene, "bUseUModel", text="Use UModel") + + if context.scene.bUseUModel == True: + col.prop(context.scene, "additionalargs", text="UModel Additional Args") + + 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", text="Use Cube as Fallback Mesh" + ) + + col.operator("umap.import", text="Import", icon="IMPORT") + + col.operator( + "umap.fillfortnitekeys", text="Fill Fortnite AES Keys", icon="FILE_FONT" + ) + + +class VIEW_PT_UmapOperator(bpy.types.Operator): + """Import Umap""" + + bl_idname = "umap.import" + bl_label = "Umap Exporter" + + def execute(self, context): + Config().dump() + main(context) + return {"FINISHED"} + + +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="" + ) + + +class 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"}: + layout.label(text=f"{item.pakname}:{item.daeskey}") + + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text=item.pakname) + + +class Fortnite(bpy.types.Operator): + bl_idname = "umap.fillfortnitekeys" + bl_label = "Fill Fortnite Keys" + bl_description = "Description that shows in blender tooltips" + 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://benbotfn.tk/api/v1/aes", headers=headers) + r = urlopen(req) + data = json.loads(r.read().decode(r.info().get_param("charset") or "utf-8")) + # bpy.types.Scene.aeskey = data["mainKey"] + + 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 + + for PakPath, AESKey in data["dynamicKeys"].items(): + Pakname = os.path.basename(PakPath) + context.scene.dpklist.add() + item = context.scene.dpklist[index] + item.pakname = Pakname + item.daeskey = 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 context.scene.dpklist + + 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"} + + +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="Index for dpklist", 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=True, + 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=True, + subtype="NONE", + ) + + bpy.types.Scene.reuse_maps = BoolProperty( + name="Reuse Maps", + description="Reuse already imported map rather then importing them again", + default=False, + subtype="NONE", + ) + + bpy.types.Scene.reuse_mesh = BoolProperty( + name="Reuse Meshes", + description="Reuse already imported meshes rather then importing them again", + default=False, + 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) + + # idk why we unregister + sc = bpy.context.scene + del sc.dpklist + del sc.list_index + 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/umap.py b/umap.py index 8ca3135..91f2ec1 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) -> bpy.types.Object: map_name = processed_map_path[processed_map_path.rindex("/") + 1:] map_collection = bpy.data.collections.get(map_name) @@ -257,55 +257,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) -# 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") From 022c48f30170c415950b86db21cfc7e59d05a414 Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Fri, 12 Feb 2021 00:10:39 +0530 Subject: [PATCH 2/9] some more addon work --- addon/config.py | 11 ++- addon/main.py | 79 +++++++++++++++++--- src/main/java/com/tb24/blenderumap/Main.java | 14 ++-- umap.py | 28 +++---- 4 files changed, 97 insertions(+), 35 deletions(-) diff --git a/addon/config.py b/addon/config.py index 077f16e..a6eb30a 100644 --- a/addon/config.py +++ b/addon/config.py @@ -1,5 +1,6 @@ import bpy from typing import List, Any, TypeVar +import os import json T = TypeVar("T") @@ -11,9 +12,11 @@ def from_list(x: Any) -> List[T]: "Key": bpy.context.scene.aeskey }) for a in x: + if a.pakname == "" and a.daeskey == "": + continue l.append({ - "Key" : a.pakname, - "FileName" : a.daeskey + "FileName" : a.pakname, + "Key" : a.daeskey }) return l @@ -64,6 +67,6 @@ def to_dict(self) -> dict: result["ExportPackage"] = self.ExportPackage return result - def dump(self): - with open("config.json","w") as f: + def dump(self,path): + with open(os.path.join(path,"config.json"),"w") as f: json.dump(self.to_dict(),f,indent=4) \ No newline at end of file diff --git a/addon/main.py b/addon/main.py index 045d6d2..eee7b7d 100644 --- a/addon/main.py +++ b/addon/main.py @@ -5,11 +5,69 @@ from math import * from urllib.request import urlopen, Request from .config import Config -# from ..umap import import_umap, cleanup - +from .umap import import_umap, cleanup def main(context): - pass + 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 result which means something went wrong while exporting") + + 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(bpy.types.Operator): # idk why @@ -152,7 +210,6 @@ class VIEW_PT_UmapOperator(bpy.types.Operator): bl_label = "Umap Exporter" def execute(self, context): - Config().dump() main(context) return {"FINISHED"} @@ -198,7 +255,7 @@ def execute(self, context): req = Request(url="https://benbotfn.tk/api/v1/aes", headers=headers) r = urlopen(req) data = json.loads(r.read().decode(r.info().get_param("charset") or "utf-8")) - # bpy.types.Scene.aeskey = data["mainKey"] + bpy.context.scene.aeskey = data["mainKey"] dpklist = context.scene.dpklist context.scene.list_index = len(dpklist) @@ -299,7 +356,7 @@ def register(): bpy.types.Scene.bdumpassets = BoolProperty( name="Dump Assets", description="Save assets as JSON format", - default=True, + default=False, subtype="NONE", ) @@ -313,21 +370,21 @@ def register(): bpy.types.Scene.bUseUModel = BoolProperty( name="Use UModel", description="Use UModel for the exporting process to export meshes, materials, and textures", - default=True, + default=False, subtype="NONE", ) bpy.types.Scene.reuse_maps = BoolProperty( name="Reuse Maps", description="Reuse already imported map rather then importing them again", - default=False, + default=True, subtype="NONE", ) bpy.types.Scene.reuse_mesh = BoolProperty( name="Reuse Meshes", description="Reuse already imported meshes rather then importing them again", - default=False, + default=True, subtype="NONE", ) @@ -355,9 +412,7 @@ def unregister(): # bpy.utils.unregister_class(VIEW_PT_Import) # idk why we unregister - sc = bpy.context.scene - del sc.dpklist - del sc.list_index + sc = bpy.types.Scene del sc.Game_Path del sc.aeskey del sc.package 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 91f2ec1..ea1d485 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, data_dir: str, reuse_maps: bool, reuse_meshes: bool, use_cube_as_fallback: bool) -> 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) @@ -146,7 +146,7 @@ def import_material(ob: bpy.types.Object, 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 @@ -154,7 +154,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") @@ -183,13 +183,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") @@ -207,7 +207,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) @@ -257,7 +257,7 @@ def string_hash_code(s: str) -> int: h = (31 * h + ord(c)) & 0xFFFFFFFF return ((h + 0x80000000) & 0xFFFFFFFF) - 0x80000000 -if __name__ == "__main": +if __name__ == "__main__": start = int(time.time() * 1000.0) uvm = bpy.data.node_groups.get("UV Shader Mix") @@ -302,7 +302,7 @@ def string_hash_code(s: str) -> int: # 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) + 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 From 7a1ab77b8ed4c759bf28a7ab700a2319bf2e50ee Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Mon, 15 Feb 2021 23:32:55 +0530 Subject: [PATCH 3/9] Auto download mappings --- addon/main.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/addon/main.py b/addon/main.py index eee7b7d..b10b8c7 100644 --- a/addon/main.py +++ b/addon/main.py @@ -2,7 +2,6 @@ from bpy.props import StringProperty, IntProperty, CollectionProperty, BoolProperty import json import os -from math import * from urllib.request import urlopen, Request from .config import Config from .umap import import_umap, cleanup @@ -210,9 +209,47 @@ class VIEW_PT_UmapOperator(bpy.types.Operator): 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 {"FINISHED"} + 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://benbotfn.tk/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( From 26f15dfb67cd4023fa9d68b04bc54df0bf530db4 Mon Sep 17 00:00:00 2001 From: MinshuG <65584814+MinshuG@users.noreply.github.com> Date: Mon, 8 Mar 2021 00:15:25 +0530 Subject: [PATCH 4/9] cleaned UE4Version --- addon/main.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/addon/main.py b/addon/main.py index b10b8c7..86b8a75 100644 --- a/addon/main.py +++ b/addon/main.py @@ -69,13 +69,9 @@ def main(context): cleanup() -class UE4Version(bpy.types.Operator): # idk why +class UE4Version: # idk why """Supported UE4 Versions""" - bl_idname = "umap.ue4versions" - bl_label = "uhhh versions" - bl_description = "Supported UE4 Versions" - Versions = ( ("GAME_UE4_0", "GAME_UE4_0", ""), ("GAME_UE4_1", "GAME_UE4_1", ""), @@ -108,8 +104,6 @@ class UE4Version(bpy.types.Operator): # idk why ("GAME_UE4_LATEST", "GAME_UE4_LATEST", ""), ) - # ue4_versions: bpy.props.EnumProperty(name="UE4 Version:", items=Versions) - # Button class VIEW_PT_Import(bpy.types.Panel): @@ -215,7 +209,7 @@ def execute(self, context): 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 {"FINISHED"} + return {"CANCELLED"} main(context) return {"FINISHED"} From 88944758ab857e0ab16221eacbdf0352b9819d76 Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Mon, 7 Jun 2021 10:30:27 +0530 Subject: [PATCH 5/9] benbotfn.tk -> benbot.app --- addon/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon/main.py b/addon/main.py index 86b8a75..047ef76 100644 --- a/addon/main.py +++ b/addon/main.py @@ -228,7 +228,7 @@ def check_mappings(self): return True def dl_mappings(self,path): - ENDPOINT = "https://benbotfn.tk/api/v1/mappings" + 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" @@ -283,7 +283,7 @@ def execute(self, context): "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://benbotfn.tk/api/v1/aes", headers=headers) + req = Request(url="https://benbot.app/api/v1/aes", headers=headers) r = urlopen(req) data = json.loads(r.read().decode(r.info().get_param("charset") or "utf-8")) bpy.context.scene.aeskey = data["mainKey"] From 5ca4968bb3db2567e1a24c35ef912be4971f7834 Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Sat, 31 Jul 2021 15:54:19 +0530 Subject: [PATCH 6/9] UI Improvements --- addon/__init__.py | 2 +- addon/config.py | 60 ++++++++++++++-- addon/main.py | 171 ++++++++++++++++++++++++++++------------------ 3 files changed, 159 insertions(+), 74 deletions(-) diff --git a/addon/__init__.py b/addon/__init__.py index 2b99844..2469040 100644 --- a/addon/__init__.py +++ b/addon/__init__.py @@ -13,7 +13,7 @@ bl_info = { "name" : "BlenderUmap", - "author" : "Amrsatrio, MountainFlash", + "author" : "Amrsatrio, MountainFlash (MinshuG)", "description" : "", "blender" : (2, 80, 0), "version" : (0, 0, 1), diff --git a/addon/config.py b/addon/config.py index a6eb30a..4b41f67 100644 --- a/addon/config.py +++ b/addon/config.py @@ -14,10 +14,18 @@ def from_list(x: Any) -> List[T]: for a in x: if a.pakname == "" and a.daeskey == "": continue - l.append({ - "FileName" : a.pakname, - "Key" : a.daeskey - }) + if a.guid == "" and a.pakname == "": + continue + if a.daeskey == "": + continue + + d = {} + if a.guid != "": + d["Guid"] = a.guid + else: + d["Pakname"] = a.pakname + d["Key"] = a.daeskey + l.append(d) return l class Config: @@ -56,7 +64,6 @@ def to_dict(self) -> dict: result["PaksDirectory"] = self.PaksDirectory result["ExportPath"] = self.ExportPath result["UEVersion"] = self.UEVersion - result["EncryptionKeys"] = from_list(self.EncryptionKeys) result["bDumpAssets"] = self.bDumpAssets result["ObjectCacheSize"] = self.ObjectCacheSize result["bReadMaterials"] = self.bReadMaterials @@ -65,8 +72,49 @@ def to_dict(self) -> dict: 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) \ No newline at end of file + json.dump(self.to_dict(),f,indent=4) diff --git a/addon/main.py b/addon/main.py index 86b8a75..e4077cf 100644 --- a/addon/main.py +++ b/addon/main.py @@ -1,10 +1,17 @@ +import typing import bpy from bpy.props import StringProperty, IntProperty, CollectionProperty, BoolProperty import json import os from urllib.request import urlopen, Request + +from bpy.types import Context from .config import Config -from .umap import import_umap, cleanup + +try: + from .umap import import_umap, cleanup +except ImportError: + from ..umap import import_umap, cleanup def main(context): sc = bpy.context.scene @@ -18,7 +25,7 @@ def main(context): 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 result which means something went wrong while exporting") + 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") @@ -68,7 +75,6 @@ def main(context): bpy.context.window.scene = main_scene cleanup() - class UE4Version: # idk why """Supported UE4 Versions""" @@ -104,13 +110,21 @@ class UE4Version: # idk why ("GAME_UE4_LATEST", "GAME_UE4_LATEST", ""), ) +class VIEW3D_MT_AdditionalOptions(bpy.types.Menu): + bl_label = "Additional Options" -# Button -class VIEW_PT_Import(bpy.types.Panel): + 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 = "Umap" + bl_idname = "VIEW3D_PT_Umap" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Umap" @@ -125,76 +139,63 @@ def draw(self, context): col = layout.column(align=True) - col.label(text="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.prop(context.scene, "Game_Path", text="Game Path") - - col.prop(context.scene, "aeskey", text="Main AES Key") - - col.prop(context.scene, "exportPath", text="Export Path") + 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) - col.label(text="Dynamic Keys:") - col.template_list( - "DPKLIST", - "DPK_List", + col2 = row.column(align=True) + col2.template_list( + "VIEW3D_UL_DPKLIST", + "VIEW3D_UL_dpklist", context.scene, "dpklist", context.scene, "list_index", - rows=2, + rows=3, ) + row.separator() - row = col.row() - row.operator("dpklist.new_item", text="+") - row.operator("dpklist.delete_item", text="-") + 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(context.scene, "package", text="Package") - - col.prop(context.scene, "ue4_versions") - - col.prop(context.scene, "readmats", text="Read Materials") - - col.prop( - context.scene, "bExportToDDSWhenPossible", text="Export DDS When Possible" - ) - - col.prop( - context.scene, - "bExportBuildingFoundations", - text="Export Building Foundations", - ) - - col.prop(context.scene, "bdumpassets", text="Dump Assets") - - col.prop(context.scene, "ObjectCacheSize", text="Object Cache Size") - - col.prop(context.scene, "bUseUModel", text="Use UModel") - + col.prop(item, "guid") + col.separator() + + col.prop(context.scene, "package") + 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", text="UModel Additional Args") + col.prop(context.scene, "additionalargs") + col.separator() 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", text="Use Cube as Fallback Mesh" - ) + col.prop(context.scene, "use_cube_as_fallback") col.operator("umap.import", text="Import", icon="IMPORT") - col.operator( - "umap.fillfortnitekeys", text="Fill Fortnite AES Keys", icon="FILE_FONT" - ) - class VIEW_PT_UmapOperator(bpy.types.Operator): """Import Umap""" @@ -245,6 +246,7 @@ def dl_mappings(self,path): 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="" @@ -252,26 +254,33 @@ class ListItem(bpy.types.PropertyGroup): 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 DPKLIST(bpy.types.UIList): +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"}: - layout.label(text=f"{item.pakname}:{item.daeskey}") + 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" - layout.label(text=item.pakname) - + 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 = "Description that shows in blender tooltips" + bl_description = "Automatically fill AES Key/s for Latest Fortnite version" bl_options = {"UNDO"} @classmethod @@ -283,10 +292,22 @@ def execute(self, context): "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://benbotfn.tk/api/v1/aes", headers=headers) + req = Request(url="https://fortnite-api.com/v2/aes", headers=headers) r = urlopen(req) - data = json.loads(r.read().decode(r.info().get_param("charset") or "utf-8")) - bpy.context.scene.aeskey = data["mainKey"] + + 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) @@ -296,12 +317,15 @@ def execute(self, context): dpklist.remove(index) index = index - 1 - for PakPath, AESKey in data["dynamicKeys"].items(): + 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.daeskey = AESKey + item.guid = Guid + item.daeskey = AESKey if AESKey.startswith("0x") else "0x" + AESKey index = index + 1 return {"FINISHED"} @@ -327,7 +351,7 @@ class LIST_OT_DeleteItem(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.dpklist + return len(context.scene.dpklist) > 0 def execute(self, context): dpklist = context.scene.dpklist @@ -337,12 +361,26 @@ def execute(self, context): 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"} + + 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="Index for dpklist", default=0) + bpy.types.Scene.list_index = IntProperty(name="", default=0) bpy.types.Scene.Game_Path = StringProperty( name="Game Path", @@ -442,7 +480,6 @@ def unregister(): # bpy.utils.unregister_class(VIEW_PT_UmapOperator) # bpy.utils.unregister_class(VIEW_PT_Import) - # idk why we unregister sc = bpy.types.Scene del sc.Game_Path del sc.aeskey From d8c123c0c7aaf74eb3db5cbe402e2bb7905c6903 Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Sat, 31 Jul 2021 15:56:39 +0530 Subject: [PATCH 7/9] Update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index beb5592..cede366 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ /build /run /.unused -*.pyc \ No newline at end of file +*.pyc +addon/umap.py +BlenderUmap.log From 75bce633f67c695b92f3cb596130b6a8ccec43ae Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Thu, 12 Aug 2021 11:52:47 +0530 Subject: [PATCH 8/9] warning and conversion for unsupported path GameName/Plugins/GameFeatures/ paths --- addon/main.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/addon/main.py b/addon/main.py index 56b936f..48f38a0 100644 --- a/addon/main.py +++ b/addon/main.py @@ -4,6 +4,7 @@ import json import os from urllib.request import urlopen, Request +import re from bpy.types import Context from .config import Config @@ -137,12 +138,12 @@ class VIEW3D_PT_Import(bpy.types.Panel): def draw(self, context): layout = self.layout - col = layout.column(align=True) + 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.label(text="Exporter Settings:") col.prop(context.scene, "Game_Path") col.prop(context.scene, "aeskey") col.prop(context.scene, "exportPath") @@ -177,6 +178,12 @@ def draw(self, context): 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") @@ -189,7 +196,8 @@ def draw(self, context): col.prop(context.scene, "additionalargs") col.separator() - col.label(text="Importer Settings:") + 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") @@ -374,6 +382,34 @@ def execute(self, context: 'Context') -> typing.Union[typing.Set[str], typing.Se 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) From b104adcd1b19854629405668f1dfc783206681c1 Mon Sep 17 00:00:00 2001 From: MountainFlash <65584814+MinshuG@users.noreply.github.com> Date: Thu, 12 Aug 2021 11:53:39 +0530 Subject: [PATCH 9/9] FileName not Pakname --- addon/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon/config.py b/addon/config.py index 4b41f67..c4046ce 100644 --- a/addon/config.py +++ b/addon/config.py @@ -22,8 +22,8 @@ def from_list(x: Any) -> List[T]: d = {} if a.guid != "": d["Guid"] = a.guid - else: - d["Pakname"] = a.pakname + if a.pakname != "": + d["FileName"] = a.pakname d["Key"] = a.daeskey l.append(d) return l