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