diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 51ed10e..e3b20a2 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -17,35 +17,43 @@ jobs: outputs: nmsdk_version: ${{ steps.get_nmsdk_version.outputs.version }} steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.9 - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - name: Set up Python 3.11 + uses: actions/setup-python@v6 with: - python-version: "3.9" + python-version: "3.11" - name: Install dependencies run: python -m pip install flake8 mkdocs - # - name: Lint code - # run: flake8 . - name: Build docs run: mkdocs build - - name: Build release + - name: Install blender run: | - python ./tools/build.py - Expand-Archive build/nmsdk.zip -DestinationPath ./nmsdk + BLENDER_EXACT_VERSION="5.1.2" + BLENDER_GENERAL_VERSION="5.1" + BLENDER_ARCHIVE="blender-${BLENDER_EXACT_VERSION}-windows-x64.zip" + BLENDER_URL="https://download.blender.org/release/Blender${BLENDER_GENERAL_VERSION}/${BLENDER_ARCHIVE}" + curl -L ${BLENDER_URL} -o ${BLENDER_ARCHIVE} + unzip -qq ${BLENDER_ARCHIVE} + mv blender-${BLENDER_EXACT_VERSION}-windows-x64 blender + ls blender + shell: bash + - name: Build release + run: ./blender/blender.exe --command extension build --source-dir "src\addon\nmsdk" - name: Get NMSDK tag version id: get_nmsdk_version - run: echo "version=$(Tools/read_nmsdk_version.sh)" >> $GITHUB_OUTPUT + run: echo "version=$(python get_version.py)" >> $GITHUB_OUTPUT shell: bash - name: Upload plugin zip for release - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: - name: NMSDK - path: nmsdk - - name: Upload website for release - uses: actions/upload-artifact@v4 - with: - name: NMSDK_site - path: html_docs/* + name: NMSDK_plugin + path: nmsdk-${{ steps.get_nmsdk_version.outputs.version }}.zip + archive: false + # - name: Upload website for release + # uses: actions/upload-artifact@v7 + # with: + # name: NMSDK_site + # path: html_docs/* release: name: Release NMSDK zip and publish docs # Only run this job if the commit was tagged. @@ -56,28 +64,21 @@ jobs: VERSION: ${{ needs.build.outputs.nmsdk_version }} steps: - name: Download files for release - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: - name: NMSDK + name: NMSDK_plugin + skip-decompress: true - name: Get tagged version run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV shell: bash - name: Upload resources if version matches if: env.VERSION == env.TAG - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: name: "${{ env.TAG }}" tag_name: ${{ env.TAG }} - prerelease: false + prerelease: ${{ contains(env.VERSION, 'alpha') }} files: nmsdk.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # TODO: Also upload site. - - name: Check if tag doesn't match version - if: env.VERSION != env.TAG - run: | - echo "There is a version mismatch between the tag and NMSDK version!" - echo "NMSDK version: ${{ env.VERSION }}" - echo "Tag version: ${{ env.TAG }}" - exit 1 - shell: bash diff --git a/.gitignore b/.gitignore index 701ed28..a50ddba 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ classes/__pycache__/* classes/*.pyc __pycache__/ textures/* +*.egg-info/* # blender file backups *.blend1 diff --git a/ModelImporter/animation_handler.py b/ModelImporter/animation_handler.py deleted file mode 100644 index 42e21d0..0000000 --- a/ModelImporter/animation_handler.py +++ /dev/null @@ -1,328 +0,0 @@ -# stdlib imports -from collections import OrderedDict as odict -from collections import namedtuple - -# Blender imports -import bpy -from bpy.props import StringProperty -from mathutils import Vector, Quaternion - -# Internal imports -from ModelImporter.readers import read_anim -from serialization.NMS_Structures import TkAnimMetadata - - -DATA_PATH_MAP = {'Rotation': 'rotation_quaternion', - 'Translation': 'location', - 'Scale': 'scale'} - - -class AnimationHandler(bpy.types.Operator): - """ Animation handler class - - Parameters - ---------- - context : ImportScene object - The class that is used to load a particular scene. - Because this object contains so much information regarding the loaded - scene it is easiest to just store a reference to this class and - retrieve the properties required when needed. - """ - - bl_idname = "nmsdk.animation_handler" - bl_label = "Main operator to handle loading of animations for NMSDK" - - anim_name: StringProperty(default="") - anim_path: StringProperty(default="") - - def execute(self, context): - print(f'Adding animation: {self.anim_name}') - self.scn = bpy.context.scene - anim_data = read_anim(self.anim_path) - self._add_animation_to_scene(self.anim_name, anim_data) - self.scn.nmsdk_anim_data.loaded_anims.append(self.anim_name) - return {'FINISHED'} - - def invoke(self, context, event): - return self.execute(context) - - # TODO: Add the ability to add the 'None' animation. This will back in an - # action which is the rest post so that it can actually be set correctly - # from the animation selection menu. - - def _add_animation_to_scene(self, anim_name: str, anim_data: TkAnimMetadata): - # First, let's find out what animation data each object has - # We do this by looking at the indexes of the rotation, translation and - # scale data and see whether that lies within the AnimNodeData or the - # StillFrameData - node_data_map = odict() - rot_anim_len = len(anim_data.AnimFrameData[0].Rotation) - trans_anim_len = len(anim_data.AnimFrameData[0].Translation) - scale_anim_len = len(anim_data.AnimFrameData[0].Scale) - for node_data in anim_data.NodeData: - data = {'anim': dict(), 'still': dict()} - # For each node, check to see if the data is in the animation data - # or in the still frame data - rotIndex = node_data.RotIndex - if rotIndex >= rot_anim_len: - rotIndex -= rot_anim_len - data['still']['Rotation'] = rotIndex - else: - data['anim']['Rotation'] = rotIndex - transIndex = node_data.TransIndex - if transIndex >= trans_anim_len: - transIndex -= trans_anim_len - data['still']['Translation'] = transIndex - else: - data['anim']['Translation'] = transIndex - scaleIndex = node_data.ScaleIndex - if scaleIndex >= scale_anim_len: - scaleIndex -= scale_anim_len - data['still']['Scale'] = scaleIndex - else: - data['anim']['Scale'] = scaleIndex - node_data_map[node_data['Node']] = data - - # Now that we have all the indexes sorted out, for each node, we create - # a new action and give it all the information it requires. - for name, data in node_data_map.items(): - try: - obj = self.scn.objects[name] - except KeyError: - continue - - obj.animation_data_create() - action_name = "{0}.{1}".format(anim_name, name) - obj.animation_data.action = bpy.data.actions.new( - name=action_name) - # set the action to have a fake user - obj.animation_data.action.use_fake_user = True - fcurves = self._create_anim_channels(obj, action_name) - self._apply_animdata_to_fcurves(fcurves, data, anim_data, False) - - # If we have a mesh with joint bindings, also animate the armature - if self.scn.nmsdk_anim_data.has_bound_mesh: - armature = bpy.data.objects['Armature'] - armature.animation_data_create() - action_name = "{0}_Armature".format(anim_name) - armature.animation_data.action = bpy.data.actions.new( - name=action_name) - # set the action to have a fake user - armature.animation_data.action.use_fake_user = True - num_frames = anim_data['FrameCount'] - for name, node_data in node_data_map.items(): - # we only care about animating the joints - if name not in self.scn.nmsdk_anim_data.joints: - continue - print('-- adding {0} --'.format(name)) - - bone = armature.pose.bones[name] - - still_data = node_data['still'] - animated_data = node_data['anim'] - - location = None - rotation = None - scale = None - - # Apply the transforms as required - for key, value in still_data.items(): - data = anim_data['StillFrameData'][key][value] - if key == 'Translation': - location = Vector(data[:3]) - elif key == 'Rotation': - # move the w value to the start to initialize the - # quaternion - rotation = Quaternion([data[3], data[0], data[1], - data[2]]) - elif key == 'Scale': - scale = Vector(data[:3]) - - # Apply the proper animated data - # bone_ref_mat = bone.matrix.copy() - for i, frame in enumerate(anim_data['AnimFrameData']): - # First apply the required transforms - for key, value in animated_data.items(): - data = frame[key][value] - if key == 'Translation': - location = Vector(data[:3]) - elif key == 'Rotation': - # move the w value to the start to initialize the - # quaternion - rotation = Quaternion([data[3], data[0], data[1], - data[2]]) - elif key == 'Scale': - scale = Vector(data[:3]) - - bind_data = self.scn.objects[name]['bind_data'] - delta_loc = location - Vector(bind_data[0].to_list()) - delta_rot = rotation.rotation_difference( - Quaternion(bind_data[1].to_list())) - ref_scale = Vector(bind_data[2].to_list()) - delta_sca = Vector((scale[0] / ref_scale[0], - scale[1] / ref_scale[1], - scale[2] / ref_scale[2])) - - bone.location = delta_loc - bone.rotation_quaternion = delta_rot - bone.scale = delta_sca - # For each transform applied, add a keyframe - for key in ['Translation', 'Rotation', 'Scale']: - if key in still_data: - if i == 0 or i == num_frames - 1: - self._apply_pose_data(bone, DATA_PATH_MAP[key], - i, action_name) - elif key in animated_data: - self._apply_pose_data(bone, DATA_PATH_MAP[key], - i, action_name) - - def _apply_animdata_to_fcurves(self, fcurves, mapping: dict, anim_data: TkAnimMetadata, - use_null_transform: bool): - """ Apply the supplied animation data to the fcurves. - - Parameters - ---------- - fcurves : tuple of namedtuples. - A Tuple containing the location, rotation and scaling nameduples. - mapping : dict - Information describing what components are still frame and which - are animated. - anim_data - The actual animation data - use_null_transform : bool - If true, then the joints shouldn't be animated as there are bones - which will provide the animation data. - """ - loc, rot, sca = fcurves - num_frames = anim_data.FrameCount - # If we are using the null transforms, just make all animations still - # frame. - if use_null_transform: - self._apply_stillframe_data(loc.x, 0, num_frames) - self._apply_stillframe_data(loc.y, 0, num_frames) - self._apply_stillframe_data(loc.z, 0, num_frames) - self._apply_stillframe_data(rot.x, 0, num_frames) - self._apply_stillframe_data(rot.y, 0, num_frames) - self._apply_stillframe_data(rot.z, 0, num_frames) - self._apply_stillframe_data(rot.w, 1, num_frames) - self._apply_stillframe_data(sca.x, 1, num_frames) - self._apply_stillframe_data(sca.y, 1, num_frames) - self._apply_stillframe_data(sca.z, 1, num_frames) - return - # Apply still frame data first. - still_data = mapping['still'] - for key, value in still_data.items(): - data = getattr(anim_data.StillFrameData, key)[value] - if key == 'Translation': - self._apply_stillframe_data(loc.x, data[0], num_frames) - self._apply_stillframe_data(loc.y, data[1], num_frames) - self._apply_stillframe_data(loc.z, data[2], num_frames) - elif key == 'Rotation': - self._apply_stillframe_data(rot.x, data[0], num_frames) - self._apply_stillframe_data(rot.y, data[1], num_frames) - self._apply_stillframe_data(rot.z, data[2], num_frames) - self._apply_stillframe_data(rot.w, data[3], num_frames) - elif key == 'Scale': - self._apply_stillframe_data(sca.x, data[0], num_frames) - self._apply_stillframe_data(sca.y, data[1], num_frames) - self._apply_stillframe_data(sca.z, data[2], num_frames) - animated_data = mapping['anim'] - for key, value in animated_data.items(): - if key == 'Translation': - loc.x.keyframe_points.add(num_frames) - loc.y.keyframe_points.add(num_frames) - loc.z.keyframe_points.add(num_frames) - elif key == 'Rotation': - rot.x.keyframe_points.add(num_frames) - rot.y.keyframe_points.add(num_frames) - rot.z.keyframe_points.add(num_frames) - rot.w.keyframe_points.add(num_frames) - elif key == 'Scale': - sca.x.keyframe_points.add(num_frames) - sca.y.keyframe_points.add(num_frames) - sca.z.keyframe_points.add(num_frames) - for i, frame in enumerate(anim_data.AnimFrameData): - data = getattr(frame, key)[value] - if key == 'Translation': - self._apply_animframe_data(loc.x, data[0], i) - self._apply_animframe_data(loc.y, data[1], i) - self._apply_animframe_data(loc.z, data[2], i) - elif key == 'Rotation': - self._apply_animframe_data(rot.x, data[0], i) - self._apply_animframe_data(rot.y, data[1], i) - self._apply_animframe_data(rot.z, data[2], i) - self._apply_animframe_data(rot.w, data[3], i) - elif key == 'Scale': - self._apply_animframe_data(sca.x, data[0], i) - self._apply_animframe_data(sca.y, data[1], i) - self._apply_animframe_data(sca.z, data[2], i) - - def _apply_animframe_data(self, fcurve, data, frame): - fcurve.keyframe_points[int(frame)].co = float(frame), float(data) - - def _apply_stillframe_data(self, fcurve, data, num_frame): - fcurve.keyframe_points.add(2) - fcurve.keyframe_points[0].co = 0.0, float(data) - fcurve.keyframe_points[0].interpolation = 'CONSTANT' - fcurve.keyframe_points[1].co = float(num_frame - 1), float(data) - fcurve.keyframe_points[1].interpolation = 'CONSTANT' - - def _apply_pose_data(self, bone, _type, frame, name): - bone.keyframe_insert(data_path=_type, frame=frame, group=name) - - def _create_anim_channels(self, obj, anim_name: str): - """ Generate all the channels required for the animation. - - Parameters - ---------- - obj : Blender object - The object to create the anim channels on. - anim_name : str - Name of the animation so that all fcurves are in the same group. - - Returns - ------- - Tuple of collections.namedtuple's: - (location, rotation, scale) - """ - location = namedtuple('location', ['X', 'Y', 'Z']) - rotation = namedtuple('rotation', ['X', 'Y', 'Z', 'W']) - scale = namedtuple('scale', ['X', 'Y', 'Z']) - loc_x = obj.animation_data.action.fcurves.new(data_path='location', - index=0, - action_group=anim_name) - loc_y = obj.animation_data.action.fcurves.new(data_path='location', - index=1, - action_group=anim_name) - loc_z = obj.animation_data.action.fcurves.new(data_path='location', - index=2, - action_group=anim_name) - loc = location(loc_x, loc_y, loc_z) - rot_w = obj.animation_data.action.fcurves.new( - data_path='rotation_quaternion', - index=0, - action_group=anim_name) - rot_x = obj.animation_data.action.fcurves.new( - data_path='rotation_quaternion', - index=1, - action_group=anim_name) - rot_y = obj.animation_data.action.fcurves.new( - data_path='rotation_quaternion', - index=2, - action_group=anim_name) - rot_z = obj.animation_data.action.fcurves.new( - data_path='rotation_quaternion', - index=3, - action_group=anim_name) - rot = rotation(rot_x, rot_y, rot_z, rot_w) - sca_x = obj.animation_data.action.fcurves.new(data_path='scale', - index=0, - action_group=anim_name) - sca_y = obj.animation_data.action.fcurves.new(data_path='scale', - index=1, - action_group=anim_name) - sca_z = obj.animation_data.action.fcurves.new(data_path='scale', - index=2, - action_group=anim_name) - sca = scale(sca_x, sca_y, sca_z) - return (loc, rot, sca) diff --git a/__init__.py b/__init__.py deleted file mode 100644 index b143674..0000000 --- a/__init__.py +++ /dev/null @@ -1,123 +0,0 @@ -bl_info = { - "name": "No Man's Sky Development Kit", - "author": "gregkwaste, monkeyman192", - "version": (0, 9, 28), - "blender": (4, 2, 0), - "location": "File > Export/Import", - "description": "Create NMS scene structures and export to NMS File format", - "warning": "", - "wiki_url": "https://monkeyman192.github.io/NMSDK/", - "tracker_url": "https://github.com/monkeyman192/NMSDK/issues", - "category": "Import-Export"} - - -import bpy -from bpy.utils import register_class, unregister_class -from bpy.props import PointerProperty - -# Inject the directory this file is in into the sys.path so that the imports -# become significantly nicer... -import sys -import os.path as op -sys.path = [op.dirname(__file__)] + sys.path - -# External API operators -from .NMSDK import ImportSceneOperator, ImportMeshOperator, ExportSceneOperator -# Main IO operators -from .NMSDK import NMS_Export_Operator, NMS_Import_Operator -# NMSDK object node handling operators -from .NMSDK import CreateNMSDKScene -# Internal operators -from .NMSDK import (_FixOldFormat, _ToggleCollisionVisibility, - _SaveDefaultSettings, _FixActionNames, _GetPCBANKSFolder, - _RemovePCBANKSFolder, _GetMBINCompilerLocation, - _RemoveMBINCompilerLocation, _ImportReferencedScene) -# Settings -from .NMSDK import NMSDKSettings, NMSDKDefaultSettings -# Animation classes -from .NMSDK import (_ChangeAnimation, _PlayAnimation, _PauseAnimation, - _StopAnimation, _LoadAnimation, AnimProperties, - _RefreshAnimations) -from .ModelImporter.animation_handler import AnimationHandler -# extensions to blender UI -from .BlenderExtensions import (NMSNodes, NMSEntities, NMSPanels, - SettingsPanels, ContextMenus) # , NMSShaderNode) -# Note: The NMSShaderNode is broken for 2.8. This needs a lot of work anyway -# and isn't being used so we'll just not load it for now... - -customNodes = NMSNodes() - - -# Only needed if you want to add into a dynamic menu -def menu_func_export(self, context): - self.layout.operator(NMS_Export_Operator.bl_idname, - text="Export to NMS XML Format ") - - -def menu_func_import(self, context): - self.layout.operator(NMS_Import_Operator.bl_idname, - text="Import NMS SCENE") - - -classes = (NMS_Export_Operator, - NMS_Import_Operator, - NMSDKSettings, - NMSDKDefaultSettings, - ImportSceneOperator, - ImportMeshOperator, - ExportSceneOperator, - CreateNMSDKScene, - _FixOldFormat, - _FixActionNames, - _ImportReferencedScene, - _GetPCBANKSFolder, - _RemovePCBANKSFolder, - _GetMBINCompilerLocation, - _RemoveMBINCompilerLocation, - _ToggleCollisionVisibility, - _SaveDefaultSettings, - _ChangeAnimation, - _RefreshAnimations, - _LoadAnimation, - _PlayAnimation, - _PauseAnimation, - _StopAnimation, - AnimationHandler, - AnimProperties) - - -def register(): - for cls in classes: - register_class(cls) - bpy.types.Scene.nmsdk_settings = PointerProperty(type=NMSDKSettings) - bpy.types.Scene.nmsdk_default_settings = PointerProperty( - type=NMSDKDefaultSettings) - bpy.types.Scene.nmsdk_anim_data = PointerProperty(type=AnimProperties) - bpy.types.TOPBAR_MT_file_export.append(menu_func_export) - bpy.types.TOPBAR_MT_file_import.append(menu_func_import) - NMSPanels.register() - # NMSShaderNode.register() - customNodes.register() - NMSEntities.register() - SettingsPanels.register() - ContextMenus.register() - - -def unregister(): - for cls in reversed(classes): - unregister_class(cls) - del bpy.types.Scene.nmsdk_settings - del bpy.types.Scene.nmsdk_default_settings - del bpy.types.Scene.nmsdk_anim_data - bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) - bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) - NMSPanels.unregister() - # NMSShaderNode.unregister() - customNodes.unregister() - NMSEntities.unregister() - SettingsPanels.unregister() - ContextMenus.unregister() - - -if __name__ == '__main__': - register() diff --git a/build_cmf b/build_cmf new file mode 100644 index 0000000..f476f01 --- /dev/null +++ b/build_cmf @@ -0,0 +1 @@ +blender --command extension build --source-dir "src\addon\nmsdk" \ No newline at end of file diff --git a/get_version.py b/get_version.py new file mode 100644 index 0000000..9ba3a50 --- /dev/null +++ b/get_version.py @@ -0,0 +1,4 @@ +import tomllib + +with open("src/addon/nmsdk/blender_manifest.toml", "rb") as f: + print(tomllib.load(f)["version"]) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..655e5dd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "NMSDK" +description = "Blender plugin to import and export NMS scenes" +readme = "README.md" +requires-python = ">=3.11" +keywords = ["modelling", "games", "modding"] +authors = [ + {name = "monkeyman192"} +] +maintainers = [ + {name = "monkeyman192"} +] +dependencies = [ + "fake-bpy-module", + "hgpaktool", + "numpy", +] +dynamic = ["version"] + +[tool.uv] +python-preference = "only-system" + +[tool.setuptools.package-dir] +NMSDK = "src/addon/nmsdk" + +[tool.setuptools_scm] +local_scheme = "no-local-version" + +[tool.ruff] +line-length = 110 + +[tool.ruff.lint] +select = ["E", "F", "I"] +preview = true + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.ruff.lint.extend-per-file-ignores] +"cpptypes.py" = ["E501"] + +[project.urls] +homepage = "https://github.com/monkeyman192/NMSDK" +repository = "https://github.com/monkeyman192/NMSDK.git" + +[build-system] +requires = ["setuptools>=64", "wheel", "setuptools-scm>=8", "setuptools_scm_git_semver"] +build-backend = "setuptools.build_meta" diff --git a/BlenderExtensions/ContextMenu.py b/src/addon/nmsdk/BlenderExtensions/ContextMenu.py similarity index 99% rename from BlenderExtensions/ContextMenu.py rename to src/addon/nmsdk/BlenderExtensions/ContextMenu.py index cea9809..7e8f503 100644 --- a/BlenderExtensions/ContextMenu.py +++ b/src/addon/nmsdk/BlenderExtensions/ContextMenu.py @@ -5,8 +5,8 @@ import math # Local imports -from utils.misc import get_root_node, clone_node -from ModelExporter.utils import get_children +from ..utils.misc import get_root_node, clone_node +from ..ModelExporter.utils import get_children # Blender imports import bmesh diff --git a/BlenderExtensions/CustomNodes.py b/src/addon/nmsdk/BlenderExtensions/CustomNodes.py similarity index 100% rename from BlenderExtensions/CustomNodes.py rename to src/addon/nmsdk/BlenderExtensions/CustomNodes.py diff --git a/BlenderExtensions/EntityPanels.py b/src/addon/nmsdk/BlenderExtensions/EntityPanels.py similarity index 100% rename from BlenderExtensions/EntityPanels.py rename to src/addon/nmsdk/BlenderExtensions/EntityPanels.py diff --git a/BlenderExtensions/NMSObjectsPanels.py b/src/addon/nmsdk/BlenderExtensions/NMSObjectsPanels.py similarity index 99% rename from BlenderExtensions/NMSObjectsPanels.py rename to src/addon/nmsdk/BlenderExtensions/NMSObjectsPanels.py index a0fde26..bba78a0 100644 --- a/BlenderExtensions/NMSObjectsPanels.py +++ b/src/addon/nmsdk/BlenderExtensions/NMSObjectsPanels.py @@ -5,7 +5,7 @@ from bpy.props import (StringProperty, BoolProperty, EnumProperty, FloatProperty, IntVectorProperty, FloatVectorProperty, IntProperty) -from utils.misc import getParentRefScene +from ..utils.misc import getParentRefScene """ Various properties for each of the different node types """ diff --git a/BlenderExtensions/NMSShaderNode.py b/src/addon/nmsdk/BlenderExtensions/NMSShaderNode.py similarity index 100% rename from BlenderExtensions/NMSShaderNode.py rename to src/addon/nmsdk/BlenderExtensions/NMSShaderNode.py diff --git a/src/addon/nmsdk/BlenderExtensions/SceneExplorer-maybe.py b/src/addon/nmsdk/BlenderExtensions/SceneExplorer-maybe.py new file mode 100644 index 0000000..dcd570f --- /dev/null +++ b/src/addon/nmsdk/BlenderExtensions/SceneExplorer-maybe.py @@ -0,0 +1,167 @@ +import bpy +from bpy.props import StringProperty, IntProperty, CollectionProperty, BoolProperty +from bpy.types import PropertyGroup, UIList, Operator, Panel + +class ListItem(PropertyGroup): + """Group of properties representing an item in the list.""" + name: StringProperty( name="Name", description="A name for this item", default="Untitled") + random_prop: StringProperty( name="Any other property you want", description="", default="") + + +class MY_UL_List(UIList): + """Demo UIList.""" + # Filter by the value of random_prop + filter_by_random_prop: StringProperty(default='') + # Invert the random property filter + invert_filter_by_random: BoolProperty(default=False) + # Order by random prop + order_by_random_prop: BoolProperty(default=False) + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.label(text=item.name, icon = custom_icon) + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text='', icon = custom_icon) + + def draw_filter(self, context, layout): + """UI code for the filtering/sorting/search area.""" + layout.separator() + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, 'filter_by_random_prop', text='', icon='VIEWZOOM') + row.prop(self, 'invert_filter_by_random', text='', icon='ARROW_LEFTRIGHT') + + def filter_items(self, context, data, propname): + """Filter and order items in the list.""" + # We initialize filtered and ordered as empty lists. Notice that + # if all sorting and filtering is disabled, we will return + # these empty. + filtered = [] + ordered = [] + items = getattr(data, propname) + # Filter + if self.filter_by_random_prop: + # Initialize with all items visible + filtered = [self.bitflag_filter_item] * len(items) + for i, item in enumerate(items): + if item.random_prop != self.filter_by_random_prop: + filtered[i] &= ~self.bitflag_filter_item + # Invert the filter + if filtered and self.invert_filter_by_random: + show_flag = self.bitflag_filter_item & ~self.bitflag_filter_item + for i, bitflag in enumerate(filtered): + if bitflag == show_flag: + filtered[i] = self.bitflag_filter_item + else: + filtered[i] &= ~self.bitflag_filter_item + # Order by the length of random_prop + if self.order_by_random_prop: + sort_items = bpy.types.UI_UL_list.helper_funcs.sort_items_helper + ordered = sort_items(items, lambda i: len(i.random_prop), True) + return filtered, ordered + +class LIST_OT_NewItem(Operator): + """Add a new item to the list.""" + bl_idname = "my_list.new_item" + bl_label = "Add a new item" + + def execute(self, context): + context.scene.my_list.add() + return{'FINISHED'} + + +class LIST_OT_DeleteItem(Operator): + """Delete the selected item from the list.""" + bl_idname = "my_list.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(cls, context): + return context.scene.my_list + + def execute(self, context): + my_list = context.scene.my_list + index = context.scene.list_index + my_list.remove(index) + context.scene.list_index = min(max(0, index - 1), len(my_list) - 1) + return{'FINISHED'} + +class LIST_OT_MoveItem(Operator): + """Move an item in the list.""" + bl_idname = "my_list.move_item" + bl_label = "Move an item in the list" + direction: bpy.props.EnumProperty( + items=(('UP', 'Up', ""), ('DOWN', 'Down', ""),) + ) + + @classmethod + def poll(cls, context): + return context.scene.my_list + + def move_index(self): + """ Move index of an item render queue while clamping it. """ + index = bpy.context.scene.list_index + list_length = len(bpy.context.scene.my_list) - 1 # (index starts at 0) + new_index = index + (-1 if self.direction == 'UP' else 1) + bpy.context.scene.list_index = max(0, min(new_index, list_length)) + + def execute(self, context): + my_list = context.scene.my_list + index = context.scene.list_index + neighbor = index + (-1 if self.direction == 'UP' else 1) + my_list.move(neighbor, index) + self.move_index() + return{'FINISHED'} + + +class PT_ListExample(Panel): + """Demo panel for UI list Tutorial.""" + bl_label = "UI_List Demo" + bl_idname = "SCENE_PT_LIST_DEMO" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "scene" + + def draw(self, context): + layout = self.layout + scene = context.scene + row = layout.row() + row.template_list("MY_UL_List", "The_List", scene, "my_list", scene, "list_index") + row = layout.row() + row.operator('my_list.new_item', text='NEW') + row.operator('my_list.delete_item', text='REMOVE') + row.operator('my_list.move_item', text='UP').direction = 'UP' + row.operator('my_list.move_item', text='DOWN').direction = 'DOWN' + if scene.list_index >= 0 and scene.my_list: + item = scene.my_list[scene.list_index] + layout.row().prop(item, 'name') + layout.row().prop(item, 'random_prop') + + +def register(): + bpy.utils.register_class(ListItem) + bpy.utils.register_class(MY_UL_List) + bpy.utils.register_class(LIST_OT_NewItem) + bpy.utils.register_class(LIST_OT_DeleteItem) + bpy.utils.register_class(LIST_OT_MoveItem) + bpy.utils.register_class(PT_ListExample) + bpy.types.Scene.my_list = CollectionProperty(type = ListItem) + bpy.types.Scene.list_index = IntProperty(name = "Index for my_list", default = 0) + + +def unregister(): + del bpy.types.Scene.my_list + del bpy.types.Scene.list_index + bpy.utils.unregister_class(ListItem) + bpy.utils.unregister_class(MY_UL_List) + bpy.utils.unregister_class(LIST_OT_NewItem) + bpy.utils.unregister_class(LIST_OT_DeleteItem) + bpy.utils.unregister_class(LIST_OT_MoveItem) + bpy.utils.unregister_class(PT_ListExample) + +if __name__ == "__main__": + register() diff --git a/src/addon/nmsdk/BlenderExtensions/SceneExplorer.py b/src/addon/nmsdk/BlenderExtensions/SceneExplorer.py new file mode 100644 index 0000000..b925081 --- /dev/null +++ b/src/addon/nmsdk/BlenderExtensions/SceneExplorer.py @@ -0,0 +1,44 @@ +import bpy +from bpy.props import (StringProperty, BoolProperty, EnumProperty, IntProperty, + FloatProperty) +from bpy.types import NodeTree, Node, NodeSocket, UIList, Panel +import nodeitems_utils +from nodeitems_utils import NodeCategory, NodeItem + + +def retBool(x): + return bool(x) + + +# custom button in the node editor to change the mode to the custom NMS mode +# class SceneExplorer(UIList): +# '''NMS Scene explorer''' +# bl_idname = 'NMSSceneExplorer' +# bl_label = 'NMS Scene Explorer' +# bl_icon = 'DESKTOP' + + +class SceneExplorerPanel(Panel): + bl_idname = 'SceneExplorerPanel' + bl_label = 'NMS Scene Explorer' + bl_icon = 'DESKTOP' + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'WINDOW' + + def draw(self, context): + layout = self.layout + + obj = context.object + + layout.template_list("UI_UL_list", "", obj, "material_slots", obj, "active_material_index") + # layout.template_list("MATERIAL_UL_matslots_example", "", obj, "material_slots", obj, "active_material_index") + + + +class NMSSceneExplorer(): + def register(self): + # register base classes + bpy.utils.register_class(SceneExplorerPanel) + + def unregister(self): + bpy.utils.unregister_class(SceneExplorerPanel) diff --git a/BlenderExtensions/SettingsPanel.py b/src/addon/nmsdk/BlenderExtensions/SettingsPanel.py similarity index 83% rename from BlenderExtensions/SettingsPanel.py rename to src/addon/nmsdk/BlenderExtensions/SettingsPanel.py index a661639..a13e872 100644 --- a/BlenderExtensions/SettingsPanel.py +++ b/src/addon/nmsdk/BlenderExtensions/SettingsPanel.py @@ -63,26 +63,6 @@ def draw(self, context): layout = self.layout layout.prop(default_settings, 'export_directory') layout.prop(default_settings, 'group_name') - row = layout.split(factor=0.85, align=True) - row.alignment = 'LEFT' - row.operator("nmsdk._find_pcbanks", icon='ZOOM_ALL', - text='PCBANKS location') - row.separator() - row.operator('nmsdk._remove_pcbanks', - icon='X', emboss=False, text="Remove PCBANKS directory") - _dir = context.scene.nmsdk_default_settings.PCBANKS_directory - if _dir != "": - layout.label(text=_dir) - row = layout.split(factor=0.85, align=True) - row.alignment = 'LEFT' - row.operator("nmsdk._find_mbincompiler", icon='ZOOM_ALL', - text='MBINCompiler location') - row.separator() - row.operator('nmsdk._remove_mbincompiler', - icon='X', emboss=False, text="Remove MBINCompiler path") - _dir = context.scene.nmsdk_default_settings.MBINCompiler_path - if _dir != "": - layout.label(text=_dir) layout.operator("nmsdk._save_default_settings", icon='FILE_TICK', text='Save settings') diff --git a/BlenderExtensions/UIWidgets.py b/src/addon/nmsdk/BlenderExtensions/UIWidgets.py similarity index 100% rename from BlenderExtensions/UIWidgets.py rename to src/addon/nmsdk/BlenderExtensions/UIWidgets.py diff --git a/BlenderExtensions/__init__.py b/src/addon/nmsdk/BlenderExtensions/__init__.py similarity index 100% rename from BlenderExtensions/__init__.py rename to src/addon/nmsdk/BlenderExtensions/__init__.py diff --git a/ModelExporter/ActionTriggerParser.py b/src/addon/nmsdk/ModelExporter/ActionTriggerParser.py similarity index 97% rename from ModelExporter/ActionTriggerParser.py rename to src/addon/nmsdk/ModelExporter/ActionTriggerParser.py index d9387f7..fc79119 100644 --- a/ModelExporter/ActionTriggerParser.py +++ b/src/addon/nmsdk/ModelExporter/ActionTriggerParser.py @@ -1,6 +1,6 @@ import bpy -from NMS.classes import (GcTriggerActionComponentData, GcActionTrigger, List, GcActionTriggerState) +from ..NMS.classes import GcTriggerActionComponentData, GcActionTrigger, List, GcActionTriggerState def ParseNodes(): diff --git a/ModelExporter/Descriptor.py b/src/addon/nmsdk/ModelExporter/Descriptor.py similarity index 95% rename from ModelExporter/Descriptor.py rename to src/addon/nmsdk/ModelExporter/Descriptor.py index 49b183c..7ce6c9b 100644 --- a/ModelExporter/Descriptor.py +++ b/src/addon/nmsdk/ModelExporter/Descriptor.py @@ -4,10 +4,15 @@ __author__ = "monkeyman192" -from utils.misc import get_obj_name -from NMS.classes import (List, TkModelDescriptorList, NMSString0x80, - TkResourceDescriptorList, TkResourceDescriptorData) -from ModelExporter.utils import get_children +from ..NMS.classes import ( + List, + NMSString0x80, + TkModelDescriptorList, + TkResourceDescriptorData, + TkResourceDescriptorList, +) +from ..utils.misc import get_obj_name +from .utils import get_children # main external container. This is only slightly different to the Node_Data diff --git a/ModelExporter/__init__.py b/src/addon/nmsdk/ModelExporter/__init__.py similarity index 100% rename from ModelExporter/__init__.py rename to src/addon/nmsdk/ModelExporter/__init__.py diff --git a/ModelExporter/addon_script.py b/src/addon/nmsdk/ModelExporter/addon_script.py similarity index 98% rename from ModelExporter/addon_script.py rename to src/addon/nmsdk/ModelExporter/addon_script.py index 1d36bf4..882ff7d 100644 --- a/ModelExporter/addon_script.py +++ b/src/addon/nmsdk/ModelExporter/addon_script.py @@ -1,35 +1,42 @@ -# stdlib imports -from math import radians, degrees import os import os.path as op import shutil -# blender imports +from math import degrees, radians + +import bmesh import bpy -from bpy.types import Mesh as BlenderMesh +import numpy as np from bpy.types import Light as BlenderLight -import bmesh +from bpy.types import Mesh as BlenderMesh from idprop.types import IDPropertyGroup from mathutils import Matrix, Vector -# Internal imports -from ModelExporter.utils import calc_tangents -from utils.misc import CompareMatrices, get_obj_name -from utils.image_convert import convert_image -from ModelExporter.animations import process_anims -from ModelExporter.export import Export -from ModelExporter.Descriptor import Descriptor -from NMS.classes import (TkMaterialData, TkMaterialFlags, TkVolumeTriggerType, - TkMaterialSampler, TkMaterialUniform_Float, TkMaterialUniform_UInt, - TkRotationComponentData, TkPhysicsComponentData) -from NMS.classes import TkAnimationComponentData, TkAnimationData -from NMS.classes import List, Vector4f, Vector4i -from NMS.classes import TkAttachmentData -from NMS.classes.Object import Object, Model, Mesh, Locator, Reference, Collision, Light, Joint -from NMS.LOOKUPS import MATERIALFLAGS -from ModelExporter.ActionTriggerParser import ParseNodes -from serialization.NMS_Structures.Structures import TkTransformData - -import numpy as np +from ..NMS.classes import ( + List, + TkAnimationComponentData, + TkAnimationData, + TkAttachmentData, + TkMaterialData, + TkMaterialFlags, + TkMaterialSampler, + TkMaterialUniform_Float, + TkMaterialUniform_UInt, + TkPhysicsComponentData, + TkRotationComponentData, + TkVolumeTriggerType, + Vector4f, + Vector4i, +) +from ..NMS.classes.Object import Collision, Joint, Light, Locator, Mesh, Model, Object, Reference +from ..NMS.LOOKUPS import MATERIALFLAGS +from ..serialization.NMS_Structures.Structures import TkTransformData +from ..utils.image_convert import convert_image +from ..utils.misc import CompareMatrices, get_obj_name +from .ActionTriggerParser import ParseNodes +from .animations import process_anims +from .Descriptor import Descriptor +from .export import Export +from .utils import calc_tangents ROT_X_MAT = Matrix.Rotation(radians(-90), 4, 'X') diff --git a/ModelExporter/animations.py b/src/addon/nmsdk/ModelExporter/animations.py similarity index 97% rename from ModelExporter/animations.py rename to src/addon/nmsdk/ModelExporter/animations.py index a671168..c017de0 100644 --- a/ModelExporter/animations.py +++ b/src/addon/nmsdk/ModelExporter/animations.py @@ -1,9 +1,9 @@ import bpy # Internal imports -from ModelExporter.utils import transform_to_NMS_coords, get_actions_with_name -from NMS.classes import (TkAnimMetadata, TkAnimNodeData, TkAnimNodeFrameData) -from NMS.classes import List, Vector4f, Quaternion +from .utils import transform_to_NMS_coords, get_actions_with_name +from ..NMS.classes import (TkAnimMetadata, TkAnimNodeData, TkAnimNodeFrameData) +from ..NMS.classes import List, Vector4f, Quaternion def process_anims(anim_node_data): diff --git a/ModelExporter/cmd_export.py b/src/addon/nmsdk/ModelExporter/cmd_export.py similarity index 100% rename from ModelExporter/cmd_export.py rename to src/addon/nmsdk/ModelExporter/cmd_export.py diff --git a/ModelExporter/export.py b/src/addon/nmsdk/ModelExporter/export.py similarity index 90% rename from ModelExporter/export.py rename to src/addon/nmsdk/ModelExporter/export.py index cdde485..9ebad90 100644 --- a/ModelExporter/export.py +++ b/src/addon/nmsdk/ModelExporter/export.py @@ -9,32 +9,38 @@ __author__ = "monkeyman192" __credits__ = ["monkeyman192", "gregkwaste"] -# Blender imports -import bpy - -import numpy as np - -# stdlib imports import os +import struct import subprocess from collections import OrderedDict as odict -from array import array -import struct from itertools import accumulate -# Internal imports -from NMS.classes import TkAttachmentData -from NMS.LOOKUPS import SEMANTICS, REV_SEMANTICS, STRIDES, VERTS -from NMS.classes.Object import Model -from serialization.NMS_Structures import MBINHeader -from serialization.NMS_Structures.Structures import ( - TkMeshData, TkGeometryStreamData, TkVertexLayout, TkVertexElement, TkMeshMetaData -) -from serialization.NMS_Structures.Structures import ( +from typing import TYPE_CHECKING + +import bpy +import numpy as np + +if TYPE_CHECKING: + from .. import NMSDKPreferences +from ..NMS.classes import TkAttachmentData +from ..NMS.classes.Object import Model, jenkins_one_at_a_time +from ..NMS.LOOKUPS import SEMANTICS, STRIDES, UVS, VERTS +from ..serialization.NMS_Structures import MBINHeader +from ..serialization.NMS_Structures.Structures import ( TkGeometryData as TkGeometryData_new, ) -from serialization.StreamCompiler import StreamData -from serialization.serializers import serialize_vertex_stream -from ModelExporter.utils import nmsHash, traverse +from ..serialization.NMS_Structures.Structures import ( + TkGeometryStreamData, + TkMeshData, + TkMeshMetaData, + TkVertexElement, + TkVertexLayout, +) +from ..serialization.serializers import serialize_vertex_stream +from ..serialization.StreamCompiler import StreamData +from .utils import traverse + +# Get the parent package name. +_package = __package__.rpartition(".")[0] class Export(): @@ -133,7 +139,7 @@ def __init__(self, export_directory, scene_directory, scene_name, model: Model, else: self.c_stream[mesh.Name] = None self.chvertex_stream[mesh.Name] = mesh.CHVerts - self.mesh_metadata[mesh.Name] = {'hash': nmsHash(mesh.Vertices)} + self.mesh_metadata[mesh.Name] = {'hash': jenkins_one_at_a_time(mesh.Name)} # also add in the material data to the list if mesh.Material is not None: self.materials.add(mesh.Material) @@ -229,21 +235,18 @@ def preprocess_streams(self): # for this to be raised?) diff = streams.difference(mesh.provided_streams) if diff != set(): - print('ERROR! Object {0} is missing the streams: {1}'.format( - mesh.Name, diff)) + print(f"ERROR! Object {mesh.Name} is missing the streams: {diff}") if 'Vertices' in diff or 'Indexes' in diff: - print('CRITICAL ERROR! No vertex and/or index data ' - 'provided for {} Object'.format(mesh.Name)) + print(f"CRITICAL ERROR! No vertex and/or index data provided for {mesh.Name} Object") - self.stream_list = list( - SEMANTICS[x] for x in streams.difference({'Indexes', 'Vertices'})) + self.stream_list = list(SEMANTICS[x] for x in streams.difference({"Indexes", "Vertices", "UVs"})) self.stream_list.sort() self.element_count = len(self.stream_list) # Create a list to store the offset sizes for each data type offsets = list() for sid in self.stream_list: - if sid != VERTS: + if sid not in (VERTS, UVS): offsets.append(STRIDES[sid]) # Now create an ordered dictionary. Each kvp is the sid and the actual # offset as calculated by the sum of all the entries before it. @@ -278,15 +281,15 @@ def serialize_data(self): v_data = serialize_vertex_stream( requires=self.stream_list, count=count, - UVs=self.uv_stream[name], Normals=self.n_stream[name], Tangents=self.t_stream[name], Colours=self.c_stream[name] ) v_pos_data = serialize_vertex_stream( - requires={SEMANTICS["Vertices"]}, + requires=[VERTS, UVS], count=count, Vertices=self.vertex_stream[name], + UVs=self.uv_stream[name], ) v_len = len(v_data) vertex_sizes.append(v_len) @@ -348,7 +351,7 @@ def serialize_data(self): # start address of the vertex data since it's serialized in the same data. f.seek(entry_start + 0x3C, 0) vert_size = struct.unpack(" Vector: """ Calculate the tangents of 3 consecutive points in a polygon. This is a bit different to normal tangent calculation as we are not doing @@ -218,6 +172,23 @@ def calc_tangents(verts: Tuple[Vector], return t +def transform_to_matrix(loc: Vector4f, rot: tuple[float, float, float, float], sca: Vector4f) -> Matrix: + # Translation matrix + mat_loc = Matrix.Translation(loc) + + # Rotation matrix + mat_rot = Quaternion([rot[3], *rot[0: 3]]) + mat_rot = mat_rot.to_matrix().to_4x4() + + # Scale Matrix + mat_scax = Matrix.Scale(sca[0], 4, (1, 0, 0)) + mat_scay = Matrix.Scale(sca[1], 4, (0, 1, 0)) + mat_scaz = Matrix.Scale(sca[2], 4, (0, 0, 1)) + mat_sca = mat_scax @ mat_scay @ mat_scaz + + return mat_loc @ mat_rot @ mat_sca + + def transform_to_NMS_coords(ob): # this will return the local transform, rotation and scale of the object in # the NMS coordinate system diff --git a/ModelImporter/SceneNodeData.py b/src/addon/nmsdk/ModelImporter/SceneNodeData.py similarity index 93% rename from ModelImporter/SceneNodeData.py rename to src/addon/nmsdk/ModelImporter/SceneNodeData.py index d44d0f4..75a9d4d 100644 --- a/ModelImporter/SceneNodeData.py +++ b/src/addon/nmsdk/ModelImporter/SceneNodeData.py @@ -1,16 +1,18 @@ import math +from typing import Optional, Type -from mathutils import Matrix, Euler import numpy as np +from mathutils import Euler, Matrix + +from ..serialization.NMS_Structures import TkSceneNodeData -from serialization.NMS_Structures import TkSceneNodeData class SceneNodeData(): """ Our own internal representation of the TkSceneNodeData class. This makes no attempt to map fields directly to the fields in that class, but will instead be a version-independent representation of it. """ - def __init__(self, info: TkSceneNodeData, parent: 'SceneNodeData' = None): + def __init__(self, info: TkSceneNodeData, parent: "Optional[SceneNodeData]" = None): self.info = info self.parent = parent self.verts: dict[str, list[tuple]] = dict() @@ -24,6 +26,7 @@ def __init__(self, info: TkSceneNodeData, parent: 'SceneNodeData' = None): self.np_idxs: np.array = None self.np_blendIndex: np.array = None self.np_blendWeight: np.array = None + self.np_colours: np.array = None self.bounded_hull = list() # The metadata will be read from the geometry file later. @@ -36,7 +39,7 @@ def __init__(self, info: TkSceneNodeData, parent: 'SceneNodeData' = None): # region public methods - def Attribute(self, name, astype=str): + def Attribute(self, name, astype: Type = str): # Doesn't support AltID's if (attrib := self.attributes.get(name)) is not None: return astype(attrib) diff --git a/src/addon/nmsdk/ModelImporter/animation_handler.py b/src/addon/nmsdk/ModelImporter/animation_handler.py new file mode 100644 index 0000000..c8add30 --- /dev/null +++ b/src/addon/nmsdk/ModelImporter/animation_handler.py @@ -0,0 +1,308 @@ +from collections import namedtuple + +import bpy +from mathutils import Quaternion, Vector + +from ..ModelExporter.utils import transform_to_matrix +from ..serialization.NMS_Structures import TkAnimMetadata + +DATA_PATH_MAP = {'Rotation': 'rotation_quaternion', + 'Translation': 'location', + 'Scale': 'scale'} + + +def handle_stillframe_data(scene, anim_data: TkAnimMetadata): + # Loop over the nodes, and then based on their names, apply the still frame transforms. + for node in anim_data.NodeData: + name = node.Node + if (obj := scene.objects.get(name)) is None: + continue + location = anim_data.StillFrameData.Translations[node.TransIndex] + rotation = anim_data.StillFrameData.Rotations[node.RotIndex] + scale = anim_data.StillFrameData.Scales[node.ScaleIndex] + matrix = transform_to_matrix(location, rotation, scale) + obj.matrix_local = matrix + + +def add_animation_to_scene(scene, anim_name: str, anim_data: TkAnimMetadata, stillframe_only: bool = False): + # First, let's find out what animation data each object has + # We do this by looking at the indexes of the rotation, translation and + # scale data and see whether that lies within the AnimNodeData or the + # StillFrameData + if stillframe_only: + handle_stillframe_data(scene, anim_data) + return + node_data_map = dict() + rot_anim_len = len(anim_data.AnimFrameData[0].Rotations) + trans_anim_len = len(anim_data.AnimFrameData[0].Translations) + scale_anim_len = len(anim_data.AnimFrameData[0].Scales) + for node_data in anim_data.NodeData: + data = {'anim': dict(), 'still': dict()} + # For each node, check to see if the data is in the animation data + # or in the still frame data + rotIndex = node_data.RotIndex + if rotIndex >= rot_anim_len: + rotIndex -= rot_anim_len + data['still']['Rotation'] = rotIndex + else: + data['anim']['Rotation'] = rotIndex + transIndex = node_data.TransIndex + if transIndex >= trans_anim_len: + transIndex -= trans_anim_len + data['still']['Translation'] = transIndex + else: + data['anim']['Translation'] = transIndex + scaleIndex = node_data.ScaleIndex + if scaleIndex >= scale_anim_len: + scaleIndex -= scale_anim_len + data['still']['Scale'] = scaleIndex + else: + data['anim']['Scale'] = scaleIndex + node_data_map[node_data.Node] = data + + # Now that we have all the indexes sorted out, for each node, we create + # a new action and give it all the information it requires. + for name, data in node_data_map.items(): + try: + obj = scene.objects[name] + except KeyError: + continue + + obj.animation_data_create() + action_name = "{0}.{1}".format(anim_name, name) + obj.animation_data.action = bpy.data.actions.new( + name=action_name) + # set the action to have a fake user + obj.animation_data.action.use_fake_user = True + fcurves = _create_anim_channels(obj, action_name) + _apply_animdata_to_fcurves(fcurves, data, anim_data, False) + + # If we have a mesh with joint bindings, also animate the armature + if scene.nmsdk_anim_data.has_bound_mesh: + armature = bpy.data.objects['Armature'] + armature.animation_data_create() + action_name = "{0}_Armature".format(anim_name) + armature.animation_data.action = bpy.data.actions.new( + name=action_name) + # set the action to have a fake user + armature.animation_data.action.use_fake_user = True + num_frames = anim_data.FrameCount + for name, node_data in node_data_map.items(): + # we only care about animating the joints + if name not in scene.nmsdk_anim_data.joints: + continue + print('-- adding {0} --'.format(name)) + + bone = armature.pose.bones[name] + + still_data = node_data['still'] + animated_data = node_data['anim'] + + location = None + rotation = None + scale = None + + # Apply the transforms as required + location = Vector(data[:3]) + for key, value in still_data.items(): + data = anim_data.StillFrameData[key][value] + if key == 'Translation': + location = Vector(data[:3]) + elif key == 'Rotation': + # move the w value to the start to initialize the + # quaternion + rotation = Quaternion([data[3], data[0], data[1], + data[2]]) + elif key == 'Scale': + scale = Vector(data[:3]) + + # Apply the proper animated data + # bone_ref_mat = bone.matrix.copy() + for i, frame in enumerate(anim_data['AnimFrameData']): + # First apply the required transforms + for key, value in animated_data.items(): + data = frame[key][value] + if key == 'Translation': + location = Vector(data[:3]) + elif key == 'Rotation': + # move the w value to the start to initialize the + # quaternion + rotation = Quaternion([data[3], data[0], data[1], data[2]]) + elif key == 'Scale': + scale = Vector(data[:3]) + + bind_data = scene.objects[name]['bind_data'] + delta_loc = location - Vector(bind_data[0].to_list()) + delta_rot = rotation.rotation_difference(Quaternion(bind_data[1].to_list())) + ref_scale = Vector(bind_data[2].to_list()) + delta_sca = Vector((scale[0] / ref_scale[0], + scale[1] / ref_scale[1], + scale[2] / ref_scale[2])) + + bone.location = delta_loc + bone.rotation_quaternion = delta_rot + bone.scale = delta_sca + # For each transform applied, add a keyframe + for key in ['Translation', 'Rotation', 'Scale']: + if key in still_data: + if i == 0 or i == num_frames - 1: + _apply_pose_data(bone, DATA_PATH_MAP[key], i, action_name) + elif key in animated_data: + _apply_pose_data(bone, DATA_PATH_MAP[key], i, action_name) + scene.nmsdk_anim_data.loaded_anims.append(anim_name) + + +def _apply_animdata_to_fcurves(fcurves, mapping: dict, anim_data: TkAnimMetadata, + use_null_transform: bool): + """ Apply the supplied animation data to the fcurves. + + Parameters + ---------- + fcurves : tuple of namedtuples. + A Tuple containing the location, rotation and scaling nameduples. + mapping : dict + Information describing what components are still frame and which + are animated. + anim_data + The actual animation data + use_null_transform : bool + If true, then the joints shouldn't be animated as there are bones + which will provide the animation data. + """ + loc, rot, sca = fcurves + num_frames = anim_data.FrameCount + # If we are using the null transforms, just make all animations still + # frame. + if use_null_transform: + _apply_stillframe_data(loc.x, 0, num_frames) + _apply_stillframe_data(loc.y, 0, num_frames) + _apply_stillframe_data(loc.z, 0, num_frames) + _apply_stillframe_data(rot.x, 0, num_frames) + _apply_stillframe_data(rot.y, 0, num_frames) + _apply_stillframe_data(rot.z, 0, num_frames) + _apply_stillframe_data(rot.w, 1, num_frames) + _apply_stillframe_data(sca.x, 1, num_frames) + _apply_stillframe_data(sca.y, 1, num_frames) + _apply_stillframe_data(sca.z, 1, num_frames) + return + # Apply still frame data first. + still_data = mapping['still'] + for key, value in still_data.items(): + data = getattr(anim_data.StillFrameData, key)[value] + if key == 'Translation': + _apply_stillframe_data(loc.x, data[0], num_frames) + _apply_stillframe_data(loc.y, data[1], num_frames) + _apply_stillframe_data(loc.z, data[2], num_frames) + elif key == 'Rotation': + _apply_stillframe_data(rot.x, data[0], num_frames) + _apply_stillframe_data(rot.y, data[1], num_frames) + _apply_stillframe_data(rot.z, data[2], num_frames) + _apply_stillframe_data(rot.w, data[3], num_frames) + elif key == 'Scale': + _apply_stillframe_data(sca.x, data[0], num_frames) + _apply_stillframe_data(sca.y, data[1], num_frames) + _apply_stillframe_data(sca.z, data[2], num_frames) + animated_data = mapping['anim'] + for key, value in animated_data.items(): + if key == 'Translation': + loc.x.keyframe_points.add(num_frames) + loc.y.keyframe_points.add(num_frames) + loc.z.keyframe_points.add(num_frames) + elif key == 'Rotation': + rot.x.keyframe_points.add(num_frames) + rot.y.keyframe_points.add(num_frames) + rot.z.keyframe_points.add(num_frames) + rot.w.keyframe_points.add(num_frames) + elif key == 'Scale': + sca.x.keyframe_points.add(num_frames) + sca.y.keyframe_points.add(num_frames) + sca.z.keyframe_points.add(num_frames) + for i, frame in enumerate(anim_data.AnimFrameData): + data = getattr(frame, key)[value] + if key == 'Translation': + _apply_animframe_data(loc.x, data[0], i) + _apply_animframe_data(loc.y, data[1], i) + _apply_animframe_data(loc.z, data[2], i) + elif key == 'Rotation': + _apply_animframe_data(rot.x, data[0], i) + _apply_animframe_data(rot.y, data[1], i) + _apply_animframe_data(rot.z, data[2], i) + _apply_animframe_data(rot.w, data[3], i) + elif key == 'Scale': + _apply_animframe_data(sca.x, data[0], i) + _apply_animframe_data(sca.y, data[1], i) + _apply_animframe_data(sca.z, data[2], i) + + +def _apply_animframe_data(fcurve, data, frame): + fcurve.keyframe_points[int(frame)].co = float(frame), float(data) + + +def _apply_stillframe_data(fcurve, data, num_frame): + fcurve.keyframe_points.add(2) + fcurve.keyframe_points[0].co = 0.0, float(data) + fcurve.keyframe_points[0].interpolation = 'CONSTANT' + fcurve.keyframe_points[1].co = float(num_frame - 1), float(data) + fcurve.keyframe_points[1].interpolation = 'CONSTANT' + + +def _apply_pose_data(bone, _type, frame, name): + bone.keyframe_insert(data_path=_type, frame=frame, group=name) + + +def _create_anim_channels(obj, anim_name: str): + """ Generate all the channels required for the animation. + + Parameters + ---------- + obj : Blender object + The object to create the anim channels on. + anim_name : str + Name of the animation so that all fcurves are in the same group. + + Returns + ------- + Tuple of collections.namedtuple's: + (location, rotation, scale) + """ + location = namedtuple('location', ['X', 'Y', 'Z']) + rotation = namedtuple('rotation', ['X', 'Y', 'Z', 'W']) + scale = namedtuple('scale', ['X', 'Y', 'Z']) + loc_x = obj.animation_data.action.fcurves.new(data_path='location', + index=0, + action_group=anim_name) + loc_y = obj.animation_data.action.fcurves.new(data_path='location', + index=1, + action_group=anim_name) + loc_z = obj.animation_data.action.fcurves.new(data_path='location', + index=2, + action_group=anim_name) + loc = location(loc_x, loc_y, loc_z) + rot_w = obj.animation_data.action.fcurves.new( + data_path='rotation_quaternion', + index=0, + action_group=anim_name) + rot_x = obj.animation_data.action.fcurves.new( + data_path='rotation_quaternion', + index=1, + action_group=anim_name) + rot_y = obj.animation_data.action.fcurves.new( + data_path='rotation_quaternion', + index=2, + action_group=anim_name) + rot_z = obj.animation_data.action.fcurves.new( + data_path='rotation_quaternion', + index=3, + action_group=anim_name) + rot = rotation(rot_x, rot_y, rot_z, rot_w) + sca_x = obj.animation_data.action.fcurves.new(data_path='scale', + index=0, + action_group=anim_name) + sca_y = obj.animation_data.action.fcurves.new(data_path='scale', + index=1, + action_group=anim_name) + sca_z = obj.animation_data.action.fcurves.new(data_path='scale', + index=2, + action_group=anim_name) + sca = scale(sca_x, sca_y, sca_z) + return (loc, rot, sca) diff --git a/ModelImporter/import_scene.py b/src/addon/nmsdk/ModelImporter/import_scene.py similarity index 71% rename from ModelImporter/import_scene.py rename to src/addon/nmsdk/ModelImporter/import_scene.py index eebc76d..5eaf798 100644 --- a/ModelImporter/import_scene.py +++ b/src/addon/nmsdk/ModelImporter/import_scene.py @@ -1,39 +1,53 @@ # stdlib imports -import time +import json +import os import os.path as op -from math import radians +import shutil import subprocess -from typing import cast -import numpy as np +import time +import traceback +from math import radians +from tempfile import mkdtemp +from typing import TYPE_CHECKING, Optional, cast + +import bmesh # Blender imports import bpy +import numpy as np from bpy.types import Armature -import bmesh # pylint: disable=import-error -from mathutils import Matrix, Vector, Quaternion # noqa pylint: disable=import-error +from hgpaktool.utils import normalise_path +from mathutils import Matrix, Quaternion, Vector + +from ..NMS.LOOKUPS import REV_SEMANTICS, VERT_TYPE_MAP +from ..NMS.material_node import create_material_node # Internal imports -from serialization.formats import np_read_int_2_10_10_10_rev -from NMS.LOOKUPS import COLOURS, REV_SEMANTICS -from NMS.material_node import create_material_node -from ModelImporter.readers import ( - read_gstream, read_entity_animation_data, gstream_info -) -from ModelImporter.SceneNodeData import SceneNodeData -from ModelImporter.mesh_utils import BB_transform_matrix -from utils.io import get_NMS_dir, base_path -from utils.bpyutils import SceneOp, edit_object, select_object -from serialization.NMS_Structures.Structures import ( - TkSceneNodeData, TkGeometryData, TkModelDescriptorList, NAMEHASH_MAPPING +if TYPE_CHECKING: + from .. import NMSDKPreferences +from ..serialization.formats import np_read_int_2_10_10_10_rev +from ..serialization.NMS_Structures.NMS_types import MBINHeader +from ..serialization.NMS_Structures.Structures import ( + NAMEHASH_MAPPING, + TkAnimationComponentData, + TkAnimationData, + TkAnimMetadata, + TkAttachmentData, + TkGeometryData, + TkJointBindingData, + TkModelDescriptorList, + TkSceneNodeData, + ctx_nonignored_namehashes, ) -from serialization.NMS_Structures.NMS_types import MBINHeader +from ..utils.bpyutils import SceneOp, edit_object, select_object +from ..utils.io import base_path, get_NMS_dir, load_file, load_file_unsafe, post_path +from ..utils.stopwitch import witch +from .animation_handler import add_animation_to_scene +from .mesh_utils import BB_transform_matrix +from .readers import gstream_info +from .SceneNodeData import SceneNodeData -VERT_TYPE_MAP = { - 5121: {'size': 1, 'np_fmt': "4B"}, - 5131: {'size': 2, 'np_fmt': "4e"}, - 36255: {'size': 1, 'np_fmt': np.int32} -} ROT_MATRIX = Matrix.Rotation(radians(90), 4, 'X') DATA_PATH_MAP = {'Rotation': 'rotation_quaternion', 'Translation': 'location', @@ -49,6 +63,11 @@ else: RENDER_ENGINE = "BLENDER_EEVEE" + +# Get the parent package name. +_package = __package__.rpartition(".")[0] + + class MeshError(Exception): pass @@ -70,33 +89,55 @@ class ImportScene(): A dictionary with the path to another scene as the key, and the blender object that has already been loaded as the value. """ - def __init__(self, fpath, parent_obj=None, ref_scenes=dict(), - settings=dict()): - self.local_directory, self.scene_basename = op.split(fpath) - # scene_basename is the final component of the scene path. - # Ie. the file name without the extension - self.scene_basename, ftype = op.splitext(self.scene_basename) - # determine the PCBANKS directory - self.PCBANKS_dir = get_NMS_dir(self.local_directory) + @witch.section("__init__") + def __init__( + self, + fpath: str, + parent_obj=None, + ref_scenes: Optional[dict] = None, + settings: Optional[dict] = None, + from_pak: bool = False, + ): + self.from_pak = from_pak + self.ref_scenes = ref_scenes or {} + self.parent_obj = parent_obj - # Determine the type of file provided and get the mxml and mbin file - # paths for that file. - if ftype.lower() == '.mxml': - mbin_fpath = (op.join(self.local_directory, self.scene_basename) + - '.MBIN') - mxml_fpath = fpath - elif ftype.lower() == '.mbin': - mbin_fpath = fpath + addon_prefs: NMSDKPreferences = bpy.context.preferences.addons[_package].preferences + if self.from_pak: + # Get the vfs path. + vfs_path = op.join(addon_prefs.pcbanks_dir, ".scene_vfs") + self.root_dir = addon_prefs.pcbanks_dir + if op.exists(op.join(vfs_path, "index.json")): + with open(op.join(vfs_path, "index.json")) as f: + self.pak_data_mapping = json.load(f) + else: + vfs_path = None + self.pak_data_mapping = {} + + mbincompiler_path = addon_prefs.mbincompiler_path + + if settings is None: + settings = {} + if self.from_pak is True: + self.local_directory = None + self.scene_path = fpath else: - raise TypeError('Selected file is of the wrong format.') + self.local_directory, _ = op.split(fpath) + self.root_dir = get_NMS_dir(fpath) + self.scene_path = post_path(fpath, self.root_dir) + if self.scene_path is None: + self.scene_path = fpath + # Scene_basename is the final component of the scene path. + # Ie. the file name without the extension + self.scene_basename, _ = op.splitext(self.scene_path) + if self.scene_basename.lower().endswith(".scene"): + self.scene_basename = self.scene_basename[:-6] # Annoyingly, some nodes may have the same name. As we traverse the # tree in order we should be able to just have the index stored here # and increment as needed. self.name_clash_orders = dict() - self.parent_obj = parent_obj - self.ref_scenes = ref_scenes self.settings = settings self.dep_graph = bpy.context.evaluated_depsgraph_get() # When scenes contain reference nodes there can be clashes with names. @@ -110,34 +151,45 @@ def __init__(self, fpath, parent_obj=None, ref_scenes=dict(), self.scn = bpy.context.scene self.scene_ctx = SceneOp(bpy.context) - # Find the local name of the scene (relative to the NMS PCBANKS dir) - # This needs to be read from the mbin file, so ensure we are either - # reading from it or construct the name. - with open(mbin_fpath, 'rb') as f: - MBINHeader.read(f) - t1 = time.perf_counter() - self._scene_node_data = TkSceneNodeData.read(f) - t2 = time.perf_counter() - self.scene_name = self._scene_node_data.Name - print(f"Loaded {mbin_fpath} in {t2 - t1:.03f}s") - print('Loading {0}'.format(self.scene_name)) - - # To optimise loading of referenced scenes, check to see if the current - # scene has already been loaded into blender. If so, simply make a copy - # of the mesh object and place it appropriately. - if self.scene_name in self.ref_scenes: + # To optimise loading of referenced scenes, check to see if the current scene has already been loaded + # into blender. If so, simply make a copy of the mesh object and place it appropriately. + if self.scene_path in self.ref_scenes: self._add_existing_to_scene() self.requires_render = False return - self.ref_scenes[self.scene_name] = list() + tmpdir = mkdtemp() + + # Determine the type of file provided and get the mxml and mbin file + # paths for that file. + if not self.scene_path.lower().endswith(".mbin"): + # Use the original full path to convert + # TODO: This will not work on linux. + cmd = [mbincompiler_path, f"--output-dir={normalise_path(tmpdir)}", fpath] + with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE): + pass + # There will be only one file here... + self.scene_path = op.join(tmpdir, os.listdir(tmpdir)[0]) + + # Find the local name of the scene (relative to the NMS PCBANKS dir) + # This needs to be read from the mbin file, so ensure we are either + # reading from it or construct the name. + with witch.section("read_scene"): + with load_file(self.scene_path, self.root_dir, self.from_pak, self.pak_data_mapping) as f: + MBINHeader.read(f) + self._scene_node_data = TkSceneNodeData.read(f) + self.scene_name = self._scene_node_data.Name + print(f"Loading {self.scene_name}") + shutil.rmtree(tmpdir) + + self.ref_scenes[self.scene_path] = list() self.data = None self.position_vertex_elements = [] self.vertex_elements = [] self.bh_data = [] self.materials = {} - self.entities = set() + self.entities: set[str] = set() self.animations = {} # This list of joints is used to add all the bones if needed self.joints: list[SceneNodeData] = [] @@ -152,17 +204,6 @@ def __init__(self, fpath, parent_obj=None, ref_scenes=dict(), # Change to render with cycles self.scn.render.engine = RENDER_ENGINE - if not op.exists(mbin_fpath): - retcode = subprocess.call( - [self.scn.nmsdk_default_settings.MBINCompiler_path, "-q", "-Q", fpath] - ) - if retcode != 0: - print('MBINCompiler failed to run. Please ensure it is registered on the path.') - print('Import failed') - self.requires_render = False - raise OSError("MBINCompiler failed to run. See System Console for more details. " - "(Window > Toggle System Console)") - self.scene_node_data = SceneNodeData(self._scene_node_data) # Once we have loaded this, we need to do a sanity check to make sure # that the scene file actually has an associated geometry file. @@ -171,36 +212,37 @@ def __init__(self, fpath, parent_obj=None, ref_scenes=dict(), if not self.scene_node_data.Attribute('GEOMETRY'): self.requires_render = False return - self.directory = op.dirname(self.scene_node_data.Name) - self.local_root_folder = base_path(self.local_directory, - self.directory) + if not self.from_pak: + self.directory = op.dirname(self.scene_node_data.Name) + self.local_root_folder = base_path(self.local_directory, self.directory) # remove the name of the top level object self.scene_node_data.info.Name = None # Try and find the geometry file locally. - self.geometry_file = op.join( - self.local_directory, - op.relpath( - self.scene_node_data.Attribute('GEOMETRY'), - self.directory) + '.PC') - # If this fails, try find it under the PCBANKS folder. - if not op.exists(self.geometry_file): - self.geometry_file = op.join( - self.PCBANKS_dir, - self.scene_node_data.Attribute('GEOMETRY') + '.PC') + self.geometry_fname = self.scene_node_data.Attribute("GEOMETRY").lower() + ".pc" + if not self.from_pak: + self.geometry_fname = op.join( + self.local_directory, + op.relpath( + self.scene_node_data.Attribute("GEOMETRY"), + self.directory + ) + ".PC" + ) + if op.exists(self.geometry_fname): + self.geometry_fname = op.join( + self.root_dir, + self.scene_node_data.Attribute("GEOMETRY") + ".PC" + ) self.descriptor_data = TkModelDescriptorList([]) - self.geometry_stream_file = self.geometry_file.replace('GEOMETRY', 'GEOMETRY.DATA') - - # get the information about what data the geometry file contains + self.geometry_stream_file = self.geometry_fname.lower().replace('geometry', 'geometry.data') - with open(self.geometry_file, "rb") as f: - header = MBINHeader.read(f) - assert header.header_namehash == NAMEHASH_MAPPING["TkGeometryData"] - t1 = time.perf_counter() - geometry_data = TkGeometryData.read(f) - t2 = time.perf_counter() - print(f"Loaded {self.geometry_file} in {t2 - t1:.03f}s") + # Get the information about what data the geometry file contains + with witch.section("read_geometry"): + with load_file(self.geometry_fname, self.root_dir, self.from_pak, self.pak_data_mapping) as f: + header = MBINHeader.read(f) + assert header.header_namehash == NAMEHASH_MAPPING["TkGeometryData"] + geometry_data = TkGeometryData.read(f) if geometry_data.Indices16Bit: self.mesh_indexes = [] @@ -259,30 +301,43 @@ def __init__(self, fpath, parent_obj=None, ref_scenes=dict(), # region public methods - def load_animations(self): + def load_animations(self, import_idle_anims: bool): """ Handle the loading of the animations. """ + # TODO: Fixme! This needs to read from the pak files too... _loadable_anim_data = self.scn.nmsdk_anim_data.loadable_anim_data # If there are no entities, there is no animation data. (Even implicit # animations have an entity associated.) if len(self.entities) == 0: + print("Error: Could not find any associated entity files.") return # Iterate over the entity files to collate all the animation data - local_anims = dict() + animation_data: dict[str, TkAnimationData] = {} + ctx_nonignored_namehashes.set({0x6E59DA5E}) for entity in self.entities: - entity_path = op.join(self.PCBANKS_dir, entity) - local_anims.update(read_entity_animation_data(entity_path)) - if len(local_anims) == 0: + with load_file(entity, self.root_dir, self.from_pak, self.pak_data_mapping) as f: + MBINHeader.read(f) + entity_data = TkAttachmentData.read(f) + anim_data: list[TkAnimationComponentData] = [att for att in entity_data.iter_attachments(TkAnimationComponentData)] + if anim_data: + print(f"{entity} has {len(entity_data.Components)} components which contains {len(anim_data)} TkAnimationComponentData's") + for anim in anim_data: + # Special case the idle data since there can also be an "IDLE" animation which is + # different (ie. an actual animation). + animation_data["__idle__"] = anim.Idle + for _anim in anim.Anims: + animation_data[_anim.Anim] = _anim + if len(animation_data) == 0: # If there are no animations added by this scene just return to # save time. return # Find out how many animations have been found in this scene. # If the total number is greater than 10 then we don't want to render # them all as it gets too slow to import a scene then. - print('Found {0} animations to be loaded!'.format(len(local_anims))) + print(f"Found {len(animation_data)} animations to be loaded") # Update the global animation data dictionary - _loadable_anim_data.update(local_anims) + # _loadable_anim_data.update(local_anims) max_anims = self.settings.get('max_anims', 10) if max_anims == 0: @@ -296,7 +351,19 @@ def load_animations(self): else: load_anims = True - self._fix_anim_data(local_anims, self.PCBANKS_dir) + if self.settings.get("import_idle_anims", False): + # We can import just the idle animation data... Should be fairly quick... + if (_idle_anim_data := animation_data.get("__idle__")) is not None: + if _idle_anim_data.Filename: + with load_file( + _idle_anim_data.Filename, self.root_dir, self.from_pak, self.pak_data_mapping + ) as f: + MBINHeader.read(f) + idle_anim_data = TkAnimMetadata.read(f) + add_animation_to_scene(bpy.context.scene, "", idle_anim_data, True) + return + + # self._fix_anim_data(local_anims, self.root_dir) if not self.scn.nmsdk_anim_data.anims_loaded: # Only update the value if going from False -> True @@ -322,9 +389,7 @@ def load_mesh(self, mesh_node: SceneNodeData): This will load the mesh data into memory then deserialize the actual vertex and index data from the gstream mbin. """ - self._load_mesh(mesh_node) - self._deserialize_vertex_data(mesh_node) - self._deserialize_index_data(mesh_node) + self._deserialize_gstream_data(mesh_node) mesh_node._generate_bounded_hull(self.bh_data) def load_collision_mesh(self, mesh_node: SceneNodeData): @@ -350,6 +415,7 @@ def render_mesh(self, mesh_ID: str): self._add_empty_to_scene(obj, standalone=True) self.state = {'FINISHED'} + @witch.section("render_scene") def render_scene(self): """ Render the scene in the blender view. """ # First, add the empty root object that everything will be a @@ -359,47 +425,63 @@ def render_scene(self): # First, remove everything else in the scene if self.settings.get('clear_scene', True): self._clear_prev_scene() - added_obj = self._add_empty_to_scene(self.scene_node_data) - # added_obj['scene_node'] = {'idx': 0, 'data': asdict(self.scene_node_data.info)} + self._add_empty_to_scene(self.scene_node_data) # Get all the joints in the scene for obj in self.scene_node_data.iter(): if obj.Type == 'JOINT': self.joints.append(obj) self.scn.nmsdk_anim_data.joints.append(obj.Name) t1 = time.perf_counter() - for i, obj in enumerate(self.scene_node_data.iter()): - added_obj = None - if obj.Type == 'MESH': - if obj.Name.upper() in self.mesh_metadata: - obj.metadata = self._handle_duplicate_mesh_names( - obj.Name.upper()) - else: - print('Failed to load {0}. Please make sure your scene ' - 'file and geometry data are the same ' - 'versions.'.format(obj.Name)) - continue - try: - self.load_mesh(obj) - added_obj = self._add_mesh_to_scene(obj) - except MeshError: - # In the case of a mesh error, we will pass and leave the - # `added_obj` as None to handle later. - pass - elif obj.Type in ('LOCATOR', 'JOINT', 'REFERENCE'): - added_obj = self._add_empty_to_scene(obj) - elif obj.Type == 'COLLISION': - if self.settings.get('import_collisions', True): - if obj.Attribute('TYPE') == 'Mesh': - self.load_collision_mesh(obj) - added_obj = self._add_mesh_collision_to_scene(obj) + + try: + self.geometry_stream_data = load_file_unsafe( + self.geometry_stream_file, + self.root_dir, + self.from_pak, + self.pak_data_mapping, + ) + + for i, obj in enumerate(self.scene_node_data.iter()): + added_obj = None + if obj.Type == 'MESH': + if obj.Name.upper() in self.mesh_metadata: + obj.metadata = self._handle_duplicate_mesh_names( + obj.Name.upper()) else: - added_obj = self._add_primitive_collision_to_scene(obj) - elif obj.Type == 'LIGHT': - added_obj = self._add_light_to_scene(obj) - # Get the added object and give it its scene node data so that it - # can be rexported in a more faithful way. - # if added_obj: - # added_obj['scene_node'] = {'idx': i, 'data': asdict(obj.info)} + print('Failed to load {0}. Please make sure your scene ' + 'file and geometry data are the same ' + 'versions.'.format(obj.Name)) + continue + try: + with witch.section("load_mesh"): + self.load_mesh(obj) + with witch.section("add_mesh_to_scene"): + added_obj = self._add_mesh_to_scene(obj) + + except MeshError: + # In the case of a mesh error, we will pass and leave the + # `added_obj` as None to handle later. + pass + elif obj.Type in ('LOCATOR', 'JOINT', 'REFERENCE'): + added_obj = self._add_empty_to_scene(obj) + elif obj.Type == 'COLLISION': + if self.settings.get('import_collisions', True): + if obj.Attribute('TYPE') == 'Mesh': + self.load_collision_mesh(obj) + added_obj = self._add_mesh_collision_to_scene(obj) + else: + added_obj = self._add_primitive_collision_to_scene(obj) + elif obj.Type == 'LIGHT': + added_obj = self._add_light_to_scene(obj) + # Get the added object and give it its scene node data so that it + # can be rexported in a more faithful way. + # if added_obj: + # added_obj['scene_node'] = {'idx': i, 'data': asdict(obj.info)} + except Exception: + print(f"An exception ocurred while rendering {self.scene_path}:") + print(traceback.format_exc()) + if not self.from_pak: + self.geometry_stream_data.close() # We will add an armature to the scene irrespective of whether we have # any animations, only if we are asked to import bones. @@ -409,7 +491,6 @@ def render_scene(self): if import_bones and self.joints: armature = self._add_armature_to_scene() for joint in self.joints: - print('Adding bone {0}'.format(joint.Name)) self._add_bone_to_scene(joint, armature) # Now that we have the armature set up, apply modifiers to each of # the meshes to bind them. @@ -417,7 +498,7 @@ def render_scene(self): mod = mesh_obj.modifiers.new('Armature', 'ARMATURE') mod.object = bpy.data.objects['Armature'] if import_anims and self.settings.get('max_anims', 10) != 0: - self.load_animations() + self.load_animations(self.settings.get('import_idle_anims', True)) bpy.ops.nmsdk._change_animation(anim_names='None') # If the loaded scene is a proc-gen scene, load the info in. @@ -425,7 +506,7 @@ def render_scene(self): self._apply_proc_gen_info() t2 = time.perf_counter() - print(f"Took {t2 - t1:.05f}s to fully render") + print(f"Took {t2 - t1:.05f}s to fully render {self.scene_path}") self.state = {'FINISHED'} @@ -437,7 +518,10 @@ def _add_armature_to_scene(self) -> Armature: obj = bpy.data.objects.new('Armature', armature) obj.NMSNode_props.node_types = 'None' self.scene_ctx.link_object(obj) - obj.parent = self.local_objects[self.scene_basename] + if self.parent_obj is None: + obj.parent = self.local_objects[self.scene_basename] + else: + obj.parent = self.local_objects[self.parent_obj] return obj def _add_bone_to_scene(self, scene_node: SceneNodeData, @@ -445,17 +529,16 @@ def _add_bone_to_scene(self, scene_node: SceneNodeData, # Let's get all the data collection out of the way if self.scn.nmsdk_anim_data.has_bound_mesh: joint_index = scene_node.Attribute('JOINTINDEX', int) - joint_binding_data = self.mesh_binding_data[ - 'JointBindings'][joint_index] - inv_bind_matrix = joint_binding_data['InvBindMatrix'] + joint_binding_data: TkJointBindingData = self.mesh_binding_data['JointBindings'][joint_index] + inv_bind_matrix = joint_binding_data.InvBindMatrix inv_bind_matrix = Matrix([inv_bind_matrix[:4], inv_bind_matrix[4:8], inv_bind_matrix[8:12], inv_bind_matrix[12:]]) inv_bind_matrix.transpose() - bind_trans = joint_binding_data['BindTranslate'] - bind_rot = joint_binding_data['BindRotate'] - bind_sca = joint_binding_data['BindScale'] + bind_trans = joint_binding_data.BindTranslate + bind_rot = joint_binding_data.BindRotate + bind_sca = joint_binding_data.BindScale # Assign the bind matrix so we can do easy lookup of it later for # applying animations. @@ -469,7 +552,7 @@ def _add_bone_to_scene(self, scene_node: SceneNodeData, with edit_object(armature) as data: bone = data.edit_bones.new(scene_node.Name) bone.use_inherit_rotation = True - bone.use_inherit_scale = True + bone.inherit_scale = "FIX_SHEAR" self.scn.objects[scene_node.Name]['bind_data'] = ( Vector(bind_trans[:3]), @@ -521,9 +604,9 @@ def _add_bone_to_scene(self, scene_node: SceneNodeData, return bone = data.edit_bones.new(scene_node.Name) bone.use_inherit_rotation = True - bone.use_inherit_scale = True + bone.inherit_scale = "FIX_SHEAR" # TODO: Investigate the other options... bone.use_local_location = True - bone.connected = True + bone.use_connect = True if scene_node.parent.Name in armature.data.edit_bones: bone.parent = armature.data.edit_bones[ scene_node.parent.Name] @@ -553,8 +636,7 @@ def _add_bounds_to_scene(self, scene_node: SceneNodeData): bbox_obj.NMSNode_props.node_types = 'None' self.scene_ctx.link_object(bbox_obj) - def _add_empty_to_scene(self, scene_node: SceneNodeData, - standalone: bool = False): + def _add_empty_to_scene(self, scene_node: SceneNodeData, standalone: bool = False): """ Adds the given scene node data to the Blender scene. Parameters @@ -575,8 +657,7 @@ def _add_empty_to_scene(self, scene_node: SceneNodeData, empty_obj = bpy.data.objects.new(self.scene_basename, empty_mesh) empty_obj.NMSNode_props.node_types = 'Reference' - empty_obj.NMSReference_props.reference_path = ( - self.scene_name + '.SCENE.MBIN') + empty_obj.NMSReference_props.reference_path = self.scene_name + '.SCENE.MBIN' empty_obj.matrix_world = ROT_MATRIX self.scene_ctx.link_object(empty_obj) select_object(empty_obj) @@ -603,23 +684,27 @@ def _add_empty_to_scene(self, scene_node: SceneNodeData, # always find this node easily. self.scn['scene_node'] = empty_obj # check if the scene is proc-gen - descriptor_name = self.scene_name + '.DESCRIPTOR' - short_scene_name = op.basename(descriptor_name) - # Try and find the descriptor locally - descriptor_path = op.join(self.local_directory, - short_scene_name) - print(f'Trying to find a descriptor at: {descriptor_path}') - # Otherwise fallback to looking relative to the PCBANKS directory. - if not op.exists(descriptor_path + '.MBIN'): - descriptor_path = op.join(self.PCBANKS_dir, descriptor_name) - print(f'Now trying to find a descriptor at: {descriptor_path}') - if op.exists(descriptor_path + '.MBIN'): + descriptor_path = normalise_path(self.scene_name + '.DESCRIPTOR.MBIN').lower() + if self.from_pak: + # See if the descriptor exists. + if descriptor_path not in self.pak_data_mapping: + descriptor_path = None + else: + if op.exists(op.join(self.local_directory, op.basename(descriptor_path))): + descriptor_path = op.join(self.local_directory, op.basename(descriptor_path)) + elif op.exists(op.join(self.root_dir, descriptor_path)): + descriptor_path = op.join(self.root_dir, descriptor_path) + else: + descriptor_path = None + + if descriptor_path is not None: empty_obj.NMSReference_props.is_proc = True - with open(descriptor_path + ".MBIN", 'rb') as f: + with load_file(descriptor_path, self.root_dir, self.from_pak, self.pak_data_mapping) as f: MBINHeader.read(f) self.descriptor_data = TkModelDescriptorList.read(f) else: - print("No descriptor found... Scene is not proc-gen") + pass + print(f"Adding {self.scene_basename} empty obj to local_objects") self.local_objects[self.scene_basename] = empty_obj return empty_obj @@ -645,7 +730,7 @@ def _add_empty_to_scene(self, scene_node: SceneNodeData, if self.parent_obj is not None and scene_node.parent.Name is None: # Direct child of the reference node empty_obj.parent = self.parent_obj - self.ref_scenes[self.scene_name].append(empty_obj) + self.ref_scenes[self.scene_path].append(empty_obj) elif scene_node.parent.Name is not None: # Other child if scene_node.parent in self.local_objects: @@ -675,24 +760,33 @@ def _add_empty_to_scene(self, scene_node: SceneNodeData, empty_obj.NMSEntity_props.name_or_path = entity_path if scene_node.Type == 'JOINT': - empty_obj.NMSJoint_props.joint_id = int(scene_node.Attribute( - 'JOINTINDEX')) + empty_obj.NMSJoint_props.joint_id = scene_node.Attribute('JOINTINDEX', int) if scene_node.Type == 'REFERENCE': - empty_obj.NMSReference_props.reference_path = scene_node.Attribute( - 'SCENEGRAPH') - ref_scene_path = op.join(self.PCBANKS_dir, - scene_node.Attribute('SCENEGRAPH')) - if op.exists(ref_scene_path): + print(f"Reference node name: {scene_node.Name}") + empty_obj.NMSReference_props.reference_path = scene_node.Attribute('SCENEGRAPH') + if self.from_pak: + ref_scene_path = scene_node.Attribute('SCENEGRAPH') + else: + ref_scene_path = op.join(self.root_dir, scene_node.Attribute('SCENEGRAPH')) + if self.from_pak or op.exists(ref_scene_path): if self.settings.get('import_recursively', True): print(f'loading referenced scene: {ref_scene_path}') - sub_scene = ImportScene(ref_scene_path, empty_obj, - self.ref_scenes, self.settings) - if sub_scene.requires_render: - sub_scene.render_scene() + with witch.section("import_referenced"): + sub_scene = ImportScene( + ref_scene_path, + empty_obj, + self.ref_scenes, + self.settings, + self.from_pak, + ) + if sub_scene.requires_render: + sub_scene.render_scene() else: - print("The reference node {0} has a reference to a path " - "that doesn't exist ({1})".format(name, ref_scene_path)) + print( + f"The reference node {name} has a reference to a path " + f"that doesn't exist ({ref_scene_path})" + ) return empty_obj @@ -728,7 +822,7 @@ def _add_light_to_scene(self, scene_node: SceneNodeData, if self.parent_obj is not None and scene_node.parent.Name is None: # Direct child of the reference node light_obj.parent = self.parent_obj - self.ref_scenes[self.scene_name].append(light_obj) + self.ref_scenes[self.scene_path].append(light_obj) elif scene_node.parent.Name is not None: # Other child if scene_node.parent in self.local_objects: @@ -768,7 +862,7 @@ def _add_mesh_collision_to_scene(self, scene_node: SceneNodeData): if self.parent_obj is not None and scene_node.parent.Name is None: # Direct child of reference node bh_obj.parent = self.parent_obj - self.ref_scenes[self.scene_name].append(bh_obj) + self.ref_scenes[self.scene_path].append(bh_obj) elif scene_node.parent.Name is not None: # Other child if scene_node.parent in self.local_objects: @@ -864,7 +958,7 @@ def _add_primitive_collision_to_scene(self, scene_node: SceneNodeData): if self.parent_obj is not None and scene_node.parent.Name is None: # Direct child of reference node coll_obj.parent = self.parent_obj - self.ref_scenes[self.scene_name].append(coll_obj) + self.ref_scenes[self.scene_path].append(coll_obj) elif scene_node.parent.Name is not None: # Other child if scene_node.parent in self.local_objects: @@ -893,16 +987,13 @@ def _add_primitive_collision_to_scene(self, scene_node: SceneNodeData): return coll_obj def _add_existing_to_scene(self): - # existing is a list of child objects to the reference - existing = self.ref_scenes[self.scene_name] - # for each object - for obj in existing: + # For each object under the existing scene path, copy and reparent. + for obj in self.ref_scenes[self.scene_path]: new_obj = obj.copy() new_obj.parent = self.parent_obj self.scn.collection.objects.link(new_obj) - def _add_mesh_to_scene(self, scene_node: SceneNodeData, - standalone: bool = False): + def _add_mesh_to_scene(self, scene_node: SceneNodeData, standalone: bool = False): """ Adds the given scene node data to the Blender scene. Parameters @@ -913,7 +1004,7 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, part is being rendered. """ name = scene_node.Name - mesh = bpy.data.meshes.new(name) + mesh: bpy.types.Mesh = bpy.data.meshes.new(name) vert_count = len(scene_node.np_verts) // 3 idx_count = len(scene_node.np_idxs) face_count = idx_count // 3 @@ -941,7 +1032,7 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, if scene_node.Attribute('ATTACHMENT') is not None: self.entities.add(scene_node.Attribute('ATTACHMENT')) - mesh_obj = bpy.data.objects.new(name, mesh) + mesh_obj: bpy.types.Object = bpy.data.objects.new(name, mesh) mesh_obj.NMSNode_props.node_types = 'Mesh' self.local_objects[scene_node] = mesh_obj @@ -951,7 +1042,7 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, if self.parent_obj is not None and scene_node.parent.Name is None: # Direct child of reference node mesh_obj.parent = self.parent_obj - self.ref_scenes[self.scene_name].append(mesh_obj) + self.ref_scenes[self.scene_path].append(mesh_obj) elif scene_node.parent.Name is not None: # Other child if scene_node.parent in self.local_objects: @@ -959,14 +1050,14 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, else: # In this case the parent object doesn't exist (maybe it is # corrupt?). Skip this object. - print(f"Warning: Couldn't find the approriate parent for {scene_node.Name}") + print(f"Warning: Couldn't find the appropriate parent for {scene_node.Name}") return else: # Direct child of loaded scene mesh_obj.parent = self.local_objects[self.scene_basename] mesh_obj.matrix_local = scene_node.matrix_local else: - mesh_obj.matrix_world = ROT_MATRIX * mesh_obj.matrix_world + mesh_obj.matrix_world = ROT_MATRIX @ mesh_obj.matrix_world # Set the rotation mode to be in quaternions so that anims work # correctly @@ -978,24 +1069,19 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, self.dep_graph.update() # Add vertex colour - if COLOURS in scene_node.verts: - colours = scene_node.verts[COLOURS] - if not mesh_obj.data.vertex_colors: - mesh_obj.data.vertex_colors.new() - colour_loops = mesh_obj.data.vertex_colors.active.data + if (colours := scene_node.np_colours) is not None: + colour_layer_name = f"{name}_colour" + if (colour_attribute := mesh.color_attributes.get(colour_layer_name)) is None: + colour_attribute = mesh.color_attributes.new(f"{name}_colour", "FLOAT_COLOR", "CORNER") for loop in mesh_obj.data.loops: - colour = colours[loop.vertex_index] - colour_loops[loop.index].color = (colour[0] / 255, - colour[1] / 255, - colour[2] / 255, - 0) + colour = colours[loop.vertex_index] / 255.0 + colour_attribute.data[loop.index].color = colour # Add vertexes to mesh groups if self.mesh_binding_data is not None: first_skin_mat = int(scene_node.Attribute('FIRSTSKINMAT')) last_skin_mat = int(scene_node.Attribute('LASTSKINMAT')) - skin_mats = self.mesh_binding_data[ - 'SkinMatrixLayout'][first_skin_mat: last_skin_mat] + skin_mats = self.mesh_binding_data['SkinMatrixLayout'][first_skin_mat: last_skin_mat] for skin_mat in skin_mats: joint = self._find_joint(skin_mat) mesh_obj.vertex_groups.new(name=joint.Name) @@ -1014,8 +1100,16 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, material = None if mat_path is not None: if mat_path not in self.materials: - material = create_material_node(mat_path, - self.local_root_folder) + if self.from_pak: + root_folder = self.root_dir + else: + root_folder = self.local_root_folder + material = create_material_node( + mat_path, + root_folder, + self.from_pak, + self.pak_data_mapping, + ) if material: self.materials[mat_path] = material else: @@ -1095,7 +1189,6 @@ def _clear_prev_scene(self): for obj in bpy.data.objects: # Don't remove the camera or lamp objects if obj.name not in ['Camera', 'Light']: - print('removing {0}'.format(obj.name)) bpy.data.objects.remove(obj) for mesh in bpy.data.meshes: bpy.data.meshes.remove(mesh) @@ -1128,12 +1221,19 @@ def _deserialize_index_data(self, mesh: SceneNodeData): + "Mesh name: {0}\n".format(mesh.Name) + "Mesh indexes: {0}\n".format(face_count * 3) + "Mesh metadata: {0}\n".format(mesh.metadata) - + "In geometry file: {0}".format(self.geometry_file)) + + "In geometry file: {0}".format(self.geometry_fname)) raise MeshError(err) - with open(self.geometry_stream_file, 'rb') as f: - mesh.np_idxs = np.fromfile(f, dtype=dtype, count=idx_count, offset=mesh.metadata.idx_off) + + metadata = cast(gstream_info, mesh.metadata) - def _deserialize_vertex_data(self, mesh: SceneNodeData): + if self.from_pak: + with load_file(self.geometry_stream_file, self.root_dir, self.from_pak, self.pak_data_mapping) as f: + mesh.np_idxs = np.frombuffer(f.getvalue(), dtype=dtype, count=idx_count, offset=metadata.idx_off + metadata.vert_off) + else: + with load_file(self.geometry_stream_file, self.root_dir, self.from_pak, self.pak_data_mapping) as f: + mesh.np_idxs = np.fromfile(f, dtype=dtype, count=idx_count, offset=metadata.idx_off + metadata.vert_off) + + def _deserialize_gstream_data(self, mesh: SceneNodeData): """ Take the raw vertex data and generate a list of actual vertex data. Parameters @@ -1141,6 +1241,7 @@ def _deserialize_vertex_data(self, mesh: SceneNodeData): mesh SceneNodeData of type MESH to get the vertex data of. """ + # Vertex data names: list[str] = [] pos_names: list[str] = [] np_fmts: list[str] = [] @@ -1162,6 +1263,23 @@ def _deserialize_vertex_data(self, mesh: SceneNodeData): np_fmts.append(f"S{_size}") names.append(REV_SEMANTICS[ve['semID']]) + # Index data + idx_count = mesh.Attribute('BATCHCOUNT', int) + face_count = idx_count // 3 + size = mesh.metadata.idx_size // face_count + if size // 3 == 4: + _dtype = np.uint32 + elif size // 3 == 2: + _dtype = np.uint16 + else: + err = ("An error has ocurred. Here is the object information:\n" + + "Mesh name: {0}\n".format(mesh.Name) + + "Mesh indexes: {0}\n".format(face_count * 3) + + "Mesh metadata: {0}\n".format(mesh.metadata) + + "In geometry file: {0}".format(self.geometry_fname)) + raise MeshError(err) + idx_dtype = np.dtype({"names": ["index"], "formats": [_dtype]}) + metadata = cast(gstream_info, mesh.metadata) num_verts = metadata.vert_size / self.vert_extras_stride @@ -1172,24 +1290,36 @@ def _deserialize_vertex_data(self, mesh: SceneNodeData): f'({metadata.vert_size}) isn\'t consistent ' 'with the stride value.') - with open(self.geometry_stream_file, "rb") as f: - vert_data = np.rec.fromfile(f, np_dtype, int(num_verts), offset=metadata.vert_off) - # NOTE: Because numpy is whack, offset is the relative offset to the current cursor location. - # Go back to the start to make our life easier... - f.seek(0) - pos_vert_data = np.rec.fromfile(f, np_pos_dtype, int(num_verts), offset=metadata.vert_pos_off) - if "Vertices" in pos_names: - mesh.np_verts = pos_vert_data.Vertices[:, :3].flatten() - if "UVs" in names: - vert_data.UVs[..., 1] = 1 - vert_data.UVs[..., 1] - mesh.np_uvs = vert_data.UVs[:, :2] - if "Normals" in names: - mesh.np_norms = np_read_int_2_10_10_10_rev(vert_data.Normals) - if "BlendIndex" in names: - mesh.np_blendIndex = vert_data.BlendIndex - if "BlendWeight" in names: - mesh.np_blendWeight = vert_data.BlendWeight - # TODO: Handle Colours as well + # Ensure we're always going from the start for each mesh metadata. + self.geometry_stream_data.seek(metadata.vert_off) + vert_data = np.rec.fromfile( + self.geometry_stream_data, + np_dtype, + int(num_verts), + ) + + # Load the index data now since it will be immediately after the vertex data. + mesh.np_idxs = np.rec.fromfile(self.geometry_stream_data, idx_dtype, idx_count).index + + self.geometry_stream_data.seek(metadata.vert_pos_off) + pos_vert_data = np.rec.fromfile( + self.geometry_stream_data, + np_pos_dtype, + int(num_verts), + ) + if "Vertices" in pos_names: + mesh.np_verts = pos_vert_data.Vertices[:, :3].flatten() + if "UVs" in pos_names: + pos_vert_data.UVs[..., 1] = 1 - pos_vert_data.UVs[..., 1] + mesh.np_uvs = pos_vert_data.UVs[:, :2] + if "Normals" in names: + mesh.np_norms = np_read_int_2_10_10_10_rev(vert_data.Normals) + if "BlendIndex" in names: + mesh.np_blendIndex = vert_data.BlendIndex + if "BlendWeight" in names: + mesh.np_blendWeight = vert_data.BlendWeight + if "Colours" in names: + mesh.np_colours = vert_data.Colours / 255.0 # divide by 255 to convert to floats def _find_joint(self, index=None, name=None): """ Return the joint with the specified index. """ @@ -1213,8 +1343,7 @@ def _fix_anim_data(self, local_anims: dict, mod_dir: str): for anim_name, anim_data in local_anims_copy.items(): if anim_data['Filename'] == '': # In this case we are using the implicit animation data - fpath = self.geometry_file.replace('GEOMETRY.MBIN.PC', - 'ANIM.MBIN') + fpath = self.geometry_fname.replace('geometry.mbin.pc', 'anim.mbin') # If the anim name is empty, replace it with a new one called # "_DEFAULT" if anim_name == '': @@ -1241,11 +1370,10 @@ def _fix_anim_data(self, local_anims: dict, mod_dir: str): local_anims[anim_name]['Filename'] = fpath def _get_material_path(self, scene_node: SceneNodeData): - real_path = None raw_path = scene_node.Attribute('MATERIAL') - if raw_path is not None: - real_path = self._get_path(raw_path) - return real_path + if raw_path is not None and not self.from_pak: + return self._get_path(raw_path) + return raw_path def _get_path(self, fpath): # First, try and find the file locally: @@ -1255,7 +1383,7 @@ def _get_path(self, fpath): # Otherwise, fallback to returning the filepath relative to the PCBANKS # folder. try: - return op.join(self.PCBANKS_dir, fpath) + return op.join(self.root_dir, fpath) except ValueError: return None @@ -1285,8 +1413,3 @@ def _handle_duplicate_mesh_names(self, node_name): return mesh_metadata[self.name_clash_orders[node_name]] else: return mesh_metadata - - def _load_mesh(self, mesh: SceneNodeData): - """ Load the mesh data from the geometry stream file.""" - mesh.raw_verts, mesh.raw_idxs = read_gstream(self.geometry_stream_file, - mesh.metadata) diff --git a/ModelImporter/mesh_utils.py b/src/addon/nmsdk/ModelImporter/mesh_utils.py similarity index 100% rename from ModelImporter/mesh_utils.py rename to src/addon/nmsdk/ModelImporter/mesh_utils.py diff --git a/ModelImporter/readers.py b/src/addon/nmsdk/ModelImporter/readers.py similarity index 59% rename from ModelImporter/readers.py rename to src/addon/nmsdk/ModelImporter/readers.py index ebf208d..4861239 100644 --- a/ModelImporter/readers.py +++ b/src/addon/nmsdk/ModelImporter/readers.py @@ -1,14 +1,13 @@ import struct -from typing import Tuple, NamedTuple -import os.path as op +from io import BufferedReader +from typing import NamedTuple -# TODO: move to the serialization folder? - -from serialization.utils import read_string, bytes_to_quat -from serialization.list_header import ListHeader -from utils.utils import mxml_to_dict +from ..serialization.list_header import ListHeader +from ..serialization.NMS_Structures import NAMEHASH_MAPPING, MBINHeader, TkAnimMetadata, TkMaterialData -from serialization.NMS_Structures import TkMaterialData, MBINHeader, NAMEHASH_MAPPING, TkAnimMetadata +# TODO: move to the serialization folder? +from ..serialization.utils import bytes_to_quat, read_string +from ..utils.utils import mxml_to_dict class gstream_info(NamedTuple): @@ -96,102 +95,6 @@ def read_anim(fname): # TODO: FIX! return anim_data -def read_entity_animation_data(fname: str) -> dict: # TODO: Fix - """ Read an entity file. - - This will currently only support reading the animation data from the - entity file as it's all we care about right now... - - Returns - ------- - anim_data - List of dictionaries containing the path and name of the contained - animation data. - """ - - anim_data = dict() - with open(fname, 'rb') as f: - f.seek(0x60) - has_anims = False - # Scan the list of components to see if we have a - # TkAnimationComponentData struct present. - with ListHeader(f) as components: - for _ in range(components.count): - return_offset = f.tell() - offset = struct.unpack(' Tuple[bytes, bytes]: - """ Read the requested info from the gstream file. - - Parameters - ---------- - fname - File path to the ~.GEOMETRY.DATA.MBIN.PC file. - info - namedtupled containing the vertex sizes and offset, and index sizes and - offsets. - - Returns - ------- - verts - Raw vertex data. - indexes - Raw index data. - """ - with open(fname, 'rb') as f: - f.seek(info.vert_off) - verts = f.read(info.vert_size) - f.seek(info.idx_off) - indexes = f.read(info.idx_size) - return verts, indexes - - -def read_TkAnimationData(f) -> dict: - """ Extract the animation name and path from the entity file. """ - data = dict() - data['Anim'] = read_string(f, 0x10) - data['Filename'] = read_string(f, 0x80) - # Move the pointer to the end of the TkAnimationComponentData struct - f.seek(0xA8, 1) - return data - - def read_TkModelDescriptorList(data: dict) -> dict: """ Take a dictionary of the model descriptor data and extract recursively just the useful info. """ diff --git a/NMS/LOOKUPS.py b/src/addon/nmsdk/NMS/LOOKUPS.py similarity index 100% rename from NMS/LOOKUPS.py rename to src/addon/nmsdk/NMS/LOOKUPS.py diff --git a/NMS/classes/Empty.py b/src/addon/nmsdk/NMS/classes/Empty.py similarity index 100% rename from NMS/classes/Empty.py rename to src/addon/nmsdk/NMS/classes/Empty.py diff --git a/NMS/classes/Errors.py b/src/addon/nmsdk/NMS/classes/Errors.py similarity index 100% rename from NMS/classes/Errors.py rename to src/addon/nmsdk/NMS/classes/Errors.py diff --git a/NMS/classes/GcAISpaceshipComponentData.py b/src/addon/nmsdk/NMS/classes/GcAISpaceshipComponentData.py similarity index 100% rename from NMS/classes/GcAISpaceshipComponentData.py rename to src/addon/nmsdk/NMS/classes/GcAISpaceshipComponentData.py diff --git a/NMS/classes/GcAISpaceshipTypes.py b/src/addon/nmsdk/NMS/classes/GcAISpaceshipTypes.py similarity index 100% rename from NMS/classes/GcAISpaceshipTypes.py rename to src/addon/nmsdk/NMS/classes/GcAISpaceshipTypes.py diff --git a/NMS/classes/GcActionTrigger.py b/src/addon/nmsdk/NMS/classes/GcActionTrigger.py similarity index 100% rename from NMS/classes/GcActionTrigger.py rename to src/addon/nmsdk/NMS/classes/GcActionTrigger.py diff --git a/NMS/classes/GcActionTriggerState.py b/src/addon/nmsdk/NMS/classes/GcActionTriggerState.py similarity index 100% rename from NMS/classes/GcActionTriggerState.py rename to src/addon/nmsdk/NMS/classes/GcActionTriggerState.py diff --git a/NMS/classes/GcAlienPuzzleMissionOverride.py b/src/addon/nmsdk/NMS/classes/GcAlienPuzzleMissionOverride.py similarity index 100% rename from NMS/classes/GcAlienPuzzleMissionOverride.py rename to src/addon/nmsdk/NMS/classes/GcAlienPuzzleMissionOverride.py diff --git a/NMS/classes/GcAlienRace.py b/src/addon/nmsdk/NMS/classes/GcAlienRace.py similarity index 100% rename from NMS/classes/GcAlienRace.py rename to src/addon/nmsdk/NMS/classes/GcAlienRace.py diff --git a/NMS/classes/GcAnimFrameEvent.py b/src/addon/nmsdk/NMS/classes/GcAnimFrameEvent.py similarity index 100% rename from NMS/classes/GcAnimFrameEvent.py rename to src/addon/nmsdk/NMS/classes/GcAnimFrameEvent.py diff --git a/NMS/classes/GcBeenShotEvent.py b/src/addon/nmsdk/NMS/classes/GcBeenShotEvent.py similarity index 100% rename from NMS/classes/GcBeenShotEvent.py rename to src/addon/nmsdk/NMS/classes/GcBeenShotEvent.py diff --git a/NMS/classes/GcCameraShakeAction.py b/src/addon/nmsdk/NMS/classes/GcCameraShakeAction.py similarity index 100% rename from NMS/classes/GcCameraShakeAction.py rename to src/addon/nmsdk/NMS/classes/GcCameraShakeAction.py diff --git a/NMS/classes/GcCustomInventoryComponentData.py b/src/addon/nmsdk/NMS/classes/GcCustomInventoryComponentData.py similarity index 100% rename from NMS/classes/GcCustomInventoryComponentData.py rename to src/addon/nmsdk/NMS/classes/GcCustomInventoryComponentData.py diff --git a/NMS/classes/GcDestroyAction.py b/src/addon/nmsdk/NMS/classes/GcDestroyAction.py similarity index 100% rename from NMS/classes/GcDestroyAction.py rename to src/addon/nmsdk/NMS/classes/GcDestroyAction.py diff --git a/NMS/classes/GcDestructableComponentData.py b/src/addon/nmsdk/NMS/classes/GcDestructableComponentData.py similarity index 100% rename from NMS/classes/GcDestructableComponentData.py rename to src/addon/nmsdk/NMS/classes/GcDestructableComponentData.py diff --git a/NMS/classes/GcDiscoveryTypes.py b/src/addon/nmsdk/NMS/classes/GcDiscoveryTypes.py similarity index 100% rename from NMS/classes/GcDiscoveryTypes.py rename to src/addon/nmsdk/NMS/classes/GcDiscoveryTypes.py diff --git a/NMS/classes/GcDisplayText.py b/src/addon/nmsdk/NMS/classes/GcDisplayText.py similarity index 100% rename from NMS/classes/GcDisplayText.py rename to src/addon/nmsdk/NMS/classes/GcDisplayText.py diff --git a/NMS/classes/GcEncounterComponentData.py b/src/addon/nmsdk/NMS/classes/GcEncounterComponentData.py similarity index 100% rename from NMS/classes/GcEncounterComponentData.py rename to src/addon/nmsdk/NMS/classes/GcEncounterComponentData.py diff --git a/NMS/classes/GcEncyclopediaComponentData.py b/src/addon/nmsdk/NMS/classes/GcEncyclopediaComponentData.py similarity index 100% rename from NMS/classes/GcEncyclopediaComponentData.py rename to src/addon/nmsdk/NMS/classes/GcEncyclopediaComponentData.py diff --git a/NMS/classes/GcGoToStateAction.py b/src/addon/nmsdk/NMS/classes/GcGoToStateAction.py similarity index 100% rename from NMS/classes/GcGoToStateAction.py rename to src/addon/nmsdk/NMS/classes/GcGoToStateAction.py diff --git a/NMS/classes/GcInteractionActivationCost.py b/src/addon/nmsdk/NMS/classes/GcInteractionActivationCost.py similarity index 100% rename from NMS/classes/GcInteractionActivationCost.py rename to src/addon/nmsdk/NMS/classes/GcInteractionActivationCost.py diff --git a/NMS/classes/GcInteractionComponentData.py b/src/addon/nmsdk/NMS/classes/GcInteractionComponentData.py similarity index 100% rename from NMS/classes/GcInteractionComponentData.py rename to src/addon/nmsdk/NMS/classes/GcInteractionComponentData.py diff --git a/NMS/classes/GcInteractionDof.py b/src/addon/nmsdk/NMS/classes/GcInteractionDof.py similarity index 100% rename from NMS/classes/GcInteractionDof.py rename to src/addon/nmsdk/NMS/classes/GcInteractionDof.py diff --git a/NMS/classes/GcInteractionType.py b/src/addon/nmsdk/NMS/classes/GcInteractionType.py similarity index 100% rename from NMS/classes/GcInteractionType.py rename to src/addon/nmsdk/NMS/classes/GcInteractionType.py diff --git a/NMS/classes/GcInventoryTechProbability.py b/src/addon/nmsdk/NMS/classes/GcInventoryTechProbability.py similarity index 100% rename from NMS/classes/GcInventoryTechProbability.py rename to src/addon/nmsdk/NMS/classes/GcInventoryTechProbability.py diff --git a/NMS/classes/GcNodeActivationAction.py b/src/addon/nmsdk/NMS/classes/GcNodeActivationAction.py similarity index 100% rename from NMS/classes/GcNodeActivationAction.py rename to src/addon/nmsdk/NMS/classes/GcNodeActivationAction.py diff --git a/NMS/classes/GcObjectPlacementComponentData.py b/src/addon/nmsdk/NMS/classes/GcObjectPlacementComponentData.py similarity index 100% rename from NMS/classes/GcObjectPlacementComponentData.py rename to src/addon/nmsdk/NMS/classes/GcObjectPlacementComponentData.py diff --git a/NMS/classes/GcPainAction.py b/src/addon/nmsdk/NMS/classes/GcPainAction.py similarity index 100% rename from NMS/classes/GcPainAction.py rename to src/addon/nmsdk/NMS/classes/GcPainAction.py diff --git a/NMS/classes/GcParticleAction.py b/src/addon/nmsdk/NMS/classes/GcParticleAction.py similarity index 100% rename from NMS/classes/GcParticleAction.py rename to src/addon/nmsdk/NMS/classes/GcParticleAction.py diff --git a/NMS/classes/GcPlayAnimAction.py b/src/addon/nmsdk/NMS/classes/GcPlayAnimAction.py similarity index 100% rename from NMS/classes/GcPlayAnimAction.py rename to src/addon/nmsdk/NMS/classes/GcPlayAnimAction.py diff --git a/NMS/classes/GcPlayAudioAction.py b/src/addon/nmsdk/NMS/classes/GcPlayAudioAction.py similarity index 100% rename from NMS/classes/GcPlayAudioAction.py rename to src/addon/nmsdk/NMS/classes/GcPlayAudioAction.py diff --git a/NMS/classes/GcPlayerNearbyEvent.py b/src/addon/nmsdk/NMS/classes/GcPlayerNearbyEvent.py similarity index 100% rename from NMS/classes/GcPlayerNearbyEvent.py rename to src/addon/nmsdk/NMS/classes/GcPlayerNearbyEvent.py diff --git a/NMS/classes/GcPrimaryAxis.py b/src/addon/nmsdk/NMS/classes/GcPrimaryAxis.py similarity index 100% rename from NMS/classes/GcPrimaryAxis.py rename to src/addon/nmsdk/NMS/classes/GcPrimaryAxis.py diff --git a/NMS/classes/GcProjectileImpactType.py b/src/addon/nmsdk/NMS/classes/GcProjectileImpactType.py similarity index 100% rename from NMS/classes/GcProjectileImpactType.py rename to src/addon/nmsdk/NMS/classes/GcProjectileImpactType.py diff --git a/NMS/classes/GcRarity.py b/src/addon/nmsdk/NMS/classes/GcRarity.py similarity index 100% rename from NMS/classes/GcRarity.py rename to src/addon/nmsdk/NMS/classes/GcRarity.py diff --git a/NMS/classes/GcRealitySubstanceCategory.py b/src/addon/nmsdk/NMS/classes/GcRealitySubstanceCategory.py similarity index 100% rename from NMS/classes/GcRealitySubstanceCategory.py rename to src/addon/nmsdk/NMS/classes/GcRealitySubstanceCategory.py diff --git a/NMS/classes/GcRewardAction.py b/src/addon/nmsdk/NMS/classes/GcRewardAction.py similarity index 100% rename from NMS/classes/GcRewardAction.py rename to src/addon/nmsdk/NMS/classes/GcRewardAction.py diff --git a/NMS/classes/GcScannableComponentData.py b/src/addon/nmsdk/NMS/classes/GcScannableComponentData.py similarity index 100% rename from NMS/classes/GcScannableComponentData.py rename to src/addon/nmsdk/NMS/classes/GcScannableComponentData.py diff --git a/NMS/classes/GcScannerIconTypes.py b/src/addon/nmsdk/NMS/classes/GcScannerIconTypes.py similarity index 100% rename from NMS/classes/GcScannerIconTypes.py rename to src/addon/nmsdk/NMS/classes/GcScannerIconTypes.py diff --git a/NMS/classes/GcShootableComponentData.py b/src/addon/nmsdk/NMS/classes/GcShootableComponentData.py similarity index 100% rename from NMS/classes/GcShootableComponentData.py rename to src/addon/nmsdk/NMS/classes/GcShootableComponentData.py diff --git a/NMS/classes/GcSimpleInteractionComponentData.py b/src/addon/nmsdk/NMS/classes/GcSimpleInteractionComponentData.py similarity index 100% rename from NMS/classes/GcSimpleInteractionComponentData.py rename to src/addon/nmsdk/NMS/classes/GcSimpleInteractionComponentData.py diff --git a/NMS/classes/GcSizeIndicator.py b/src/addon/nmsdk/NMS/classes/GcSizeIndicator.py similarity index 100% rename from NMS/classes/GcSizeIndicator.py rename to src/addon/nmsdk/NMS/classes/GcSizeIndicator.py diff --git a/NMS/classes/GcSpaceshipClasses.py b/src/addon/nmsdk/NMS/classes/GcSpaceshipClasses.py similarity index 100% rename from NMS/classes/GcSpaceshipClasses.py rename to src/addon/nmsdk/NMS/classes/GcSpaceshipClasses.py diff --git a/NMS/classes/GcSpaceshipComponentData.py b/src/addon/nmsdk/NMS/classes/GcSpaceshipComponentData.py similarity index 100% rename from NMS/classes/GcSpaceshipComponentData.py rename to src/addon/nmsdk/NMS/classes/GcSpaceshipComponentData.py diff --git a/NMS/classes/GcSpawnAction.py b/src/addon/nmsdk/NMS/classes/GcSpawnAction.py similarity index 100% rename from NMS/classes/GcSpawnAction.py rename to src/addon/nmsdk/NMS/classes/GcSpawnAction.py diff --git a/NMS/classes/GcStatTrackType.py b/src/addon/nmsdk/NMS/classes/GcStatTrackType.py similarity index 100% rename from NMS/classes/GcStatTrackType.py rename to src/addon/nmsdk/NMS/classes/GcStatTrackType.py diff --git a/NMS/classes/GcStateTimeEvent.py b/src/addon/nmsdk/NMS/classes/GcStateTimeEvent.py similarity index 100% rename from NMS/classes/GcStateTimeEvent.py rename to src/addon/nmsdk/NMS/classes/GcStateTimeEvent.py diff --git a/NMS/classes/GcStatsEnum.py b/src/addon/nmsdk/NMS/classes/GcStatsEnum.py similarity index 100% rename from NMS/classes/GcStatsEnum.py rename to src/addon/nmsdk/NMS/classes/GcStatsEnum.py diff --git a/NMS/classes/GcSubstanceAmount.py b/src/addon/nmsdk/NMS/classes/GcSubstanceAmount.py similarity index 100% rename from NMS/classes/GcSubstanceAmount.py rename to src/addon/nmsdk/NMS/classes/GcSubstanceAmount.py diff --git a/NMS/classes/GcTriggerActionComponentData.py b/src/addon/nmsdk/NMS/classes/GcTriggerActionComponentData.py similarity index 100% rename from NMS/classes/GcTriggerActionComponentData.py rename to src/addon/nmsdk/NMS/classes/GcTriggerActionComponentData.py diff --git a/NMS/classes/GcWarpAction.py b/src/addon/nmsdk/NMS/classes/GcWarpAction.py similarity index 100% rename from NMS/classes/GcWarpAction.py rename to src/addon/nmsdk/NMS/classes/GcWarpAction.py diff --git a/NMS/classes/List.py b/src/addon/nmsdk/NMS/classes/List.py similarity index 98% rename from NMS/classes/List.py rename to src/addon/nmsdk/NMS/classes/List.py index 9f0a299..c14ddb1 100644 --- a/NMS/classes/List.py +++ b/src/addon/nmsdk/NMS/classes/List.py @@ -2,7 +2,7 @@ # of a single structs from xml.etree.ElementTree import SubElement -from serialization.utils import list_header, serialize +from ...serialization.utils import list_header, serialize class List(): diff --git a/NMS/classes/NMSString0x10.py b/src/addon/nmsdk/NMS/classes/NMSString0x10.py similarity index 100% rename from NMS/classes/NMSString0x10.py rename to src/addon/nmsdk/NMS/classes/NMSString0x10.py diff --git a/NMS/classes/NMSString0x20.py b/src/addon/nmsdk/NMS/classes/NMSString0x20.py similarity index 100% rename from NMS/classes/NMSString0x20.py rename to src/addon/nmsdk/NMS/classes/NMSString0x20.py diff --git a/NMS/classes/NMSString0x80.py b/src/addon/nmsdk/NMS/classes/NMSString0x80.py similarity index 100% rename from NMS/classes/NMSString0x80.py rename to src/addon/nmsdk/NMS/classes/NMSString0x80.py diff --git a/NMS/classes/Object.py b/src/addon/nmsdk/NMS/classes/Object.py similarity index 94% rename from NMS/classes/Object.py rename to src/addon/nmsdk/NMS/classes/Object.py index 9df5fd9..68cd665 100644 --- a/NMS/classes/Object.py +++ b/src/addon/nmsdk/NMS/classes/Object.py @@ -2,13 +2,17 @@ # Each object in blender will be passed into this class. Any children are added # as child objects. +from collections import OrderedDict as odict from typing import Optional -from serialization.NMS_Structures.Structures import TkSceneNodeAttributeData, TkSceneNodeData, TkTransformData +from ...serialization.NMS_Structures.Structures import ( + TkSceneNodeAttributeData, + TkSceneNodeData, + TkTransformData, +) +from .List import List from .TkMaterialData import TkMaterialData from .TkPhysicsComponentData import TkPhysicsComponentData -from .List import List -from collections import OrderedDict as odict TYPES = ['MESH', 'LOCATOR', 'COLLISION', 'MODEL', 'REFERENCE'] @@ -147,11 +151,9 @@ def determine_included_streams(self): # which have been provided. # we will not include CHVerts as this will be given by default anyway # and we don't need to a semantic ID for it - for name in ['Vertices', 'Indexes', 'UVs', 'Normals', 'Tangents', - 'Colours']: + for name in ['Vertices', 'Indexes', 'UVs', 'Normals', 'Tangents', 'Colours']: if self.__dict__.get(name, None) is not None: - self.provided_streams = self.provided_streams.union( - set([name])) + self.provided_streams = self.provided_streams.union(set([name])) def get_data(self) -> TkSceneNodeData: # returns the NodeData attribute @@ -266,29 +268,20 @@ def __init__(self, Name: str, **kwargs): def create_attributes(self, data: dict, ignore_original: bool = False): self.Attributes = [ - TkSceneNodeAttributeData(Name='FOV', - Value=f'{self.FOV:.6f}'), - TkSceneNodeAttributeData(Name='FALLOFF', - Value='quadratic'), - TkSceneNodeAttributeData(Name='FALLOFF_RATE', - Value='2.000000'), - TkSceneNodeAttributeData(Name='INTENSITY', - Value=f'{self.Intensity:.6f}'), - TkSceneNodeAttributeData(Name='COL_R', - Value=f'{self.Colour[0]:.6f}'), - TkSceneNodeAttributeData(Name='COL_G', - Value=f'{self.Colour[1]:.6f}'), - TkSceneNodeAttributeData(Name='COL_B', - Value=f'{self.Colour[2]:.6f}'), + TkSceneNodeAttributeData(Name='FOV', Value=f'{self.FOV:.6f}'), + TkSceneNodeAttributeData(Name='FALLOFF', Value='2.000000'), + TkSceneNodeAttributeData(Name='INTENSITY', Value=f'{self.Intensity:.6f}'), + TkSceneNodeAttributeData(Name='RADIUS', Value='6.324555'), + TkSceneNodeAttributeData(Name='COL_R', Value=f'{self.Colour[0]:.6f}'), + TkSceneNodeAttributeData(Name='COL_G', Value=f'{self.Colour[1]:.6f}'), + TkSceneNodeAttributeData(Name='COL_B', Value=f'{self.Colour[2]:.6f}'), # These two values will be hard-coded until they are understood # well enough to modify them to be anything other than their # default values. - TkSceneNodeAttributeData(Name='COOKIE_IDX', - Value='-1'), - TkSceneNodeAttributeData(Name='VOLUMETRIC', - Value='0.000000'), - TkSceneNodeAttributeData(Name='MATERIAL', - Value='MATERIALS/LIGHT.MATERIAL.MBIN') + TkSceneNodeAttributeData(Name='COOKIE_IDX', Value='-1'), + TkSceneNodeAttributeData(Name='VOLUMETRIC', Value='0.000000'), + TkSceneNodeAttributeData(Name='LIGHTLAYERS', Value='3'), + TkSceneNodeAttributeData(Name='MATERIAL', Value='MATERIALS/LIGHT.MATERIAL.MBIN'), ] @@ -470,6 +463,9 @@ def create_attributes(self, data: dict, ignore_original: bool = False): self.Attributes = [TkSceneNodeAttributeData(Name="TYPE", Value=self.CType)] if self.CType == 'Mesh': + self.Attributes.append( + TkSceneNodeAttributeData(Value="FALSE", Name='NAVIGATION') + ) self.Attributes.append( TkSceneNodeAttributeData( Name='BATCHSTART', diff --git a/NMS/classes/PointLight.py b/src/addon/nmsdk/NMS/classes/PointLight.py similarity index 95% rename from NMS/classes/PointLight.py rename to src/addon/nmsdk/NMS/classes/PointLight.py index b2be8fe..b80216a 100644 --- a/NMS/classes/PointLight.py +++ b/src/addon/nmsdk/NMS/classes/PointLight.py @@ -1,6 +1,6 @@ # Custom TkSceneNodeData struct for PointLights -from serialization.NMS_Structures.Structures import TkSceneNodeAttributeData, TkTransformData +from ...serialization.NMS_Structures.Structures import TkSceneNodeAttributeData, TkTransformData class PointLight(): diff --git a/NMS/classes/Quaternion.py b/src/addon/nmsdk/NMS/classes/Quaternion.py similarity index 100% rename from NMS/classes/Quaternion.py rename to src/addon/nmsdk/NMS/classes/Quaternion.py diff --git a/NMS/classes/String.py b/src/addon/nmsdk/NMS/classes/String.py similarity index 100% rename from NMS/classes/String.py rename to src/addon/nmsdk/NMS/classes/String.py diff --git a/NMS/classes/Struct.py b/src/addon/nmsdk/NMS/classes/Struct.py similarity index 97% rename from NMS/classes/Struct.py rename to src/addon/nmsdk/NMS/classes/Struct.py index e9b3fe6..1c7f398 100644 --- a/NMS/classes/Struct.py +++ b/src/addon/nmsdk/NMS/classes/Struct.py @@ -8,10 +8,10 @@ import struct from binascii import hexlify # internal imports -from NMS.classes.String import String -from serialization.utils import to_chr -from NMS.classes.Empty import Empty -from NMS.classes.List import List +from .String import String +from ...serialization.utils import to_chr +from .Empty import Empty +from .List import List class Struct(): diff --git a/NMS/classes/TkAnimMetadata.py b/src/addon/nmsdk/NMS/classes/TkAnimMetadata.py similarity index 100% rename from NMS/classes/TkAnimMetadata.py rename to src/addon/nmsdk/NMS/classes/TkAnimMetadata.py diff --git a/NMS/classes/TkAnimNodeData.py b/src/addon/nmsdk/NMS/classes/TkAnimNodeData.py similarity index 100% rename from NMS/classes/TkAnimNodeData.py rename to src/addon/nmsdk/NMS/classes/TkAnimNodeData.py diff --git a/NMS/classes/TkAnimNodeFrameData.py b/src/addon/nmsdk/NMS/classes/TkAnimNodeFrameData.py similarity index 100% rename from NMS/classes/TkAnimNodeFrameData.py rename to src/addon/nmsdk/NMS/classes/TkAnimNodeFrameData.py diff --git a/NMS/classes/TkAnimationComponentData.py b/src/addon/nmsdk/NMS/classes/TkAnimationComponentData.py similarity index 100% rename from NMS/classes/TkAnimationComponentData.py rename to src/addon/nmsdk/NMS/classes/TkAnimationComponentData.py diff --git a/NMS/classes/TkAnimationData.py b/src/addon/nmsdk/NMS/classes/TkAnimationData.py similarity index 100% rename from NMS/classes/TkAnimationData.py rename to src/addon/nmsdk/NMS/classes/TkAnimationData.py diff --git a/NMS/classes/TkAnimationGameData.py b/src/addon/nmsdk/NMS/classes/TkAnimationGameData.py similarity index 100% rename from NMS/classes/TkAnimationGameData.py rename to src/addon/nmsdk/NMS/classes/TkAnimationGameData.py diff --git a/NMS/classes/TkAttachmentData.py b/src/addon/nmsdk/NMS/classes/TkAttachmentData.py similarity index 100% rename from NMS/classes/TkAttachmentData.py rename to src/addon/nmsdk/NMS/classes/TkAttachmentData.py diff --git a/NMS/classes/TkAudioAnimTrigger.py b/src/addon/nmsdk/NMS/classes/TkAudioAnimTrigger.py similarity index 100% rename from NMS/classes/TkAudioAnimTrigger.py rename to src/addon/nmsdk/NMS/classes/TkAudioAnimTrigger.py diff --git a/NMS/classes/TkAudioComponentData.py b/src/addon/nmsdk/NMS/classes/TkAudioComponentData.py similarity index 100% rename from NMS/classes/TkAudioComponentData.py rename to src/addon/nmsdk/NMS/classes/TkAudioComponentData.py diff --git a/NMS/classes/TkCameraWanderData.py b/src/addon/nmsdk/NMS/classes/TkCameraWanderData.py similarity index 100% rename from NMS/classes/TkCameraWanderData.py rename to src/addon/nmsdk/NMS/classes/TkCameraWanderData.py diff --git a/NMS/classes/TkGeometryData.py b/src/addon/nmsdk/NMS/classes/TkGeometryData.py similarity index 99% rename from NMS/classes/TkGeometryData.py rename to src/addon/nmsdk/NMS/classes/TkGeometryData.py index 8fbb6d2..cd7cdf4 100644 --- a/NMS/classes/TkGeometryData.py +++ b/src/addon/nmsdk/NMS/classes/TkGeometryData.py @@ -6,7 +6,7 @@ from .Struct import Struct from .List import List from .TkVertexLayout import TkVertexLayout -from serialization.utils import serialize, list_header +from ...serialization.utils import serialize, list_header PADDING_BYTE = b'\xFE' diff --git a/NMS/classes/TkMaterialData.py b/src/addon/nmsdk/NMS/classes/TkMaterialData.py similarity index 100% rename from NMS/classes/TkMaterialData.py rename to src/addon/nmsdk/NMS/classes/TkMaterialData.py diff --git a/NMS/classes/TkMaterialFlags.py b/src/addon/nmsdk/NMS/classes/TkMaterialFlags.py similarity index 100% rename from NMS/classes/TkMaterialFlags.py rename to src/addon/nmsdk/NMS/classes/TkMaterialFlags.py diff --git a/NMS/classes/TkMaterialSampler.py b/src/addon/nmsdk/NMS/classes/TkMaterialSampler.py similarity index 100% rename from NMS/classes/TkMaterialSampler.py rename to src/addon/nmsdk/NMS/classes/TkMaterialSampler.py diff --git a/NMS/classes/TkMaterialUniform_Float.py b/src/addon/nmsdk/NMS/classes/TkMaterialUniform_Float.py similarity index 100% rename from NMS/classes/TkMaterialUniform_Float.py rename to src/addon/nmsdk/NMS/classes/TkMaterialUniform_Float.py diff --git a/NMS/classes/TkMaterialUniform_UInt.py b/src/addon/nmsdk/NMS/classes/TkMaterialUniform_UInt.py similarity index 100% rename from NMS/classes/TkMaterialUniform_UInt.py rename to src/addon/nmsdk/NMS/classes/TkMaterialUniform_UInt.py diff --git a/NMS/classes/TkMeshData.py b/src/addon/nmsdk/NMS/classes/TkMeshData.py similarity index 100% rename from NMS/classes/TkMeshData.py rename to src/addon/nmsdk/NMS/classes/TkMeshData.py diff --git a/NMS/classes/TkMeshMetaData.py b/src/addon/nmsdk/NMS/classes/TkMeshMetaData.py similarity index 100% rename from NMS/classes/TkMeshMetaData.py rename to src/addon/nmsdk/NMS/classes/TkMeshMetaData.py diff --git a/NMS/classes/TkModelDescriptorList.py b/src/addon/nmsdk/NMS/classes/TkModelDescriptorList.py similarity index 100% rename from NMS/classes/TkModelDescriptorList.py rename to src/addon/nmsdk/NMS/classes/TkModelDescriptorList.py diff --git a/NMS/classes/TkModelRendererCameraData.py b/src/addon/nmsdk/NMS/classes/TkModelRendererCameraData.py similarity index 100% rename from NMS/classes/TkModelRendererCameraData.py rename to src/addon/nmsdk/NMS/classes/TkModelRendererCameraData.py diff --git a/NMS/classes/TkModelRendererData.py b/src/addon/nmsdk/NMS/classes/TkModelRendererData.py similarity index 100% rename from NMS/classes/TkModelRendererData.py rename to src/addon/nmsdk/NMS/classes/TkModelRendererData.py diff --git a/NMS/classes/TkPhysicsComponentData.py b/src/addon/nmsdk/NMS/classes/TkPhysicsComponentData.py similarity index 100% rename from NMS/classes/TkPhysicsComponentData.py rename to src/addon/nmsdk/NMS/classes/TkPhysicsComponentData.py diff --git a/NMS/classes/TkPhysicsData.py b/src/addon/nmsdk/NMS/classes/TkPhysicsData.py similarity index 100% rename from NMS/classes/TkPhysicsData.py rename to src/addon/nmsdk/NMS/classes/TkPhysicsData.py diff --git a/NMS/classes/TkResourceDescriptorData.py b/src/addon/nmsdk/NMS/classes/TkResourceDescriptorData.py similarity index 100% rename from NMS/classes/TkResourceDescriptorData.py rename to src/addon/nmsdk/NMS/classes/TkResourceDescriptorData.py diff --git a/NMS/classes/TkResourceDescriptorList.py b/src/addon/nmsdk/NMS/classes/TkResourceDescriptorList.py similarity index 100% rename from NMS/classes/TkResourceDescriptorList.py rename to src/addon/nmsdk/NMS/classes/TkResourceDescriptorList.py diff --git a/NMS/classes/TkRotationComponentData.py b/src/addon/nmsdk/NMS/classes/TkRotationComponentData.py similarity index 100% rename from NMS/classes/TkRotationComponentData.py rename to src/addon/nmsdk/NMS/classes/TkRotationComponentData.py diff --git a/NMS/classes/TkSceneNodeAttributeData.py b/src/addon/nmsdk/NMS/classes/TkSceneNodeAttributeData.py similarity index 100% rename from NMS/classes/TkSceneNodeAttributeData.py rename to src/addon/nmsdk/NMS/classes/TkSceneNodeAttributeData.py diff --git a/NMS/classes/TkSceneNodeData.py b/src/addon/nmsdk/NMS/classes/TkSceneNodeData.py similarity index 100% rename from NMS/classes/TkSceneNodeData.py rename to src/addon/nmsdk/NMS/classes/TkSceneNodeData.py diff --git a/NMS/classes/TkTextureResource.py b/src/addon/nmsdk/NMS/classes/TkTextureResource.py similarity index 100% rename from NMS/classes/TkTextureResource.py rename to src/addon/nmsdk/NMS/classes/TkTextureResource.py diff --git a/NMS/classes/TkTransformData.py b/src/addon/nmsdk/NMS/classes/TkTransformData.py similarity index 95% rename from NMS/classes/TkTransformData.py rename to src/addon/nmsdk/NMS/classes/TkTransformData.py index a83e776..d6f65a4 100644 --- a/NMS/classes/TkTransformData.py +++ b/src/addon/nmsdk/NMS/classes/TkTransformData.py @@ -2,7 +2,7 @@ from .Struct import Struct -from utils.misc import truncate_float +from ...utils.misc import truncate_float class TkTransformData(Struct): diff --git a/NMS/classes/TkVertexElement.py b/src/addon/nmsdk/NMS/classes/TkVertexElement.py similarity index 100% rename from NMS/classes/TkVertexElement.py rename to src/addon/nmsdk/NMS/classes/TkVertexElement.py diff --git a/NMS/classes/TkVertexLayout.py b/src/addon/nmsdk/NMS/classes/TkVertexLayout.py similarity index 100% rename from NMS/classes/TkVertexLayout.py rename to src/addon/nmsdk/NMS/classes/TkVertexLayout.py diff --git a/NMS/classes/TkVolumeTriggerType.py b/src/addon/nmsdk/NMS/classes/TkVolumeTriggerType.py similarity index 100% rename from NMS/classes/TkVolumeTriggerType.py rename to src/addon/nmsdk/NMS/classes/TkVolumeTriggerType.py diff --git a/NMS/classes/Vector4f.py b/src/addon/nmsdk/NMS/classes/Vector4f.py similarity index 100% rename from NMS/classes/Vector4f.py rename to src/addon/nmsdk/NMS/classes/Vector4f.py diff --git a/NMS/classes/Vector4i.py b/src/addon/nmsdk/NMS/classes/Vector4i.py similarity index 100% rename from NMS/classes/Vector4i.py rename to src/addon/nmsdk/NMS/classes/Vector4i.py diff --git a/NMS/classes/__init__.py b/src/addon/nmsdk/NMS/classes/__init__.py similarity index 100% rename from NMS/classes/__init__.py rename to src/addon/nmsdk/NMS/classes/__init__.py diff --git a/NMS/material_node.py b/src/addon/nmsdk/NMS/material_node.py similarity index 67% rename from NMS/material_node.py rename to src/addon/nmsdk/NMS/material_node.py index 0bf2228..e147899 100644 --- a/NMS/material_node.py +++ b/src/addon/nmsdk/NMS/material_node.py @@ -1,16 +1,30 @@ -import bpy -from mathutils import Vector - +import os import os.path as op -from NMS.LOOKUPS import DIFFUSE, MASKS, NORMAL, DIFFUSE2 -from ModelImporter.readers import read_material -from utils.io import realize_path +import bpy +from ..serialization.NMS_Structures import MBINHeader, TkMaterialData +from ..utils.io import load_file, normalise_path, realize_path +from .LOOKUPS import DIFFUSE, DIFFUSE2, MASKS, NORMAL -def create_material_node(mat_path: str, local_root_directory: str): + +def create_material_node( + mat_path: str, + local_root_directory: str, + from_pak: bool, + pak_data: dict[str, str], +): # Read the material data directly from the material MBIN - mat_data = read_material(mat_path) + mat_path = normalise_path(mat_path) + if from_pak: + if mat_path not in pak_data: + return + else: + if not op.exists(mat_path): + return + with load_file(mat_path, local_root_directory, from_pak, pak_data) as f: + MBINHeader.read(f) + mat_data = TkMaterialData.read(f) if mat_data is None: # no texture data so just exit this function. return @@ -64,11 +78,26 @@ def create_material_node(mat_path: str, local_root_directory: str): # create the diffuse, mask and normal nodes and give them their images for tex_type, tex_path in samplers.items(): img = None - if tex_type == DIFFUSE: - # texture + if from_pak: + try: + with load_file(tex_path, local_root_directory, from_pak, pak_data) as f: + dst_fpath = op.join(local_root_directory, ".scene_vfs", tex_path.lower()) + os.makedirs(op.dirname(dst_fpath), exist_ok=True) + with open(dst_fpath, "wb") as tmp: + tmp.write(f.getvalue()) + img = bpy.data.images.load(dst_fpath) + except ValueError as e: + print( + f"Warning: The material file {tex_path} had the following error when loading: {str(e)}\n" + "This file will not be loaded and textures may look broken" + ) + return + else: _path = realize_path(tex_path, local_root_directory) if _path is not None and op.exists(_path): img = bpy.data.images.load(_path) + if tex_type == DIFFUSE: + # texture diffuse_texture = nodes.new(type='ShaderNodeTexImage') diffuse_texture.name = diffuse_texture.label = 'Texture Image - Diffuse' # noqa diffuse_texture.image = img @@ -78,41 +107,49 @@ def create_material_node(mat_path: str, local_root_directory: str): # #ifdef _F16_DIFFUSE2MAP if 16 not in flags: # #ifndef _F17_MULTIPLYDIFFUSE2MAP - diffuse2_path = realize_path(samplers[DIFFUSE2], local_root_directory) - if op.exists(diffuse2_path): - img = bpy.data.images.load(diffuse2_path) + + if from_pak: + with load_file(samplers[DIFFUSE2], local_root_directory, from_pak, pak_data) as f: + dst_fpath = op.join( + local_root_directory, + ".scene_vfs", + samplers[DIFFUSE2].lower(), + ) + os.makedirs(op.dirname(dst_fpath), exist_ok=True) + with open(dst_fpath, "wb") as tmp: + tmp.write(f.getvalue()) + img = bpy.data.images.load(dst_fpath) + # img.filepath = tex_path + else: + diffuse2_path = realize_path(samplers[DIFFUSE2], local_root_directory) + if diffuse2_path is not None and op.exists(diffuse2_path): + img = bpy.data.images.load(diffuse2_path) + diffuse2_texture = nodes.new(type='ShaderNodeTexImage') diffuse2_texture.name = diffuse_texture.label = 'Texture Image - Diffuse2' # noqa diffuse2_texture.image = img diffuse2_texture.location = (-400, 300) mix_diffuse = nodes.new(type='ShaderNodeMixRGB') mix_diffuse.location = (-200, 300) - links.new(mix_diffuse.inputs['Color1'], - lColourVec4) - links.new(mix_diffuse.inputs['Color2'], - diffuse2_texture.outputs['Color']) - links.new(mix_diffuse.inputs['Fac'], - diffuse2_texture.outputs['Alpha']) + links.new(mix_diffuse.inputs['Color1'], lColourVec4) + links.new(mix_diffuse.inputs['Color2'], diffuse2_texture.outputs['Color']) + links.new(mix_diffuse.inputs['Fac'], diffuse2_texture.outputs['Alpha']) lColourVec4 = mix_diffuse.outputs['Color'] else: print('Note: Please post on discord the model you are' - ' importing so I can fix this!!!') + ' importing so I can fix this!!!') elif tex_type == MASKS: # texture - _path = realize_path(tex_path, local_root_directory) - if _path is not None and op.exists(_path): - img = bpy.data.images.load(_path) - img.colorspace_settings.name = 'Linear Rec.2020' + img.colorspace_settings.name = 'Linear Rec.2020' mask_texture = nodes.new(type='ShaderNodeTexImage') mask_texture.name = mask_texture.label = 'Texture Image - Mask' mask_texture.image = img mask_texture.location = (-700, 0) lfRoughness = None # RGB separation node - separate_rgb = nodes.new(type='ShaderNodeSeparateRGB') + separate_rgb = nodes.new(type='ShaderNodeSeparateColor') separate_rgb.location = (-400, 0) - links.new(separate_rgb.inputs['Image'], - mask_texture.outputs['Color']) + links.new(separate_rgb.inputs['Color'], mask_texture.outputs['Color']) if 43 not in flags: # #ifndef _F44_IMPOSTER if 24 in flags: @@ -126,11 +163,10 @@ def create_material_node(mat_path: str, local_root_directory: str): sub_1.inputs[0].default_value = 1.0 lfRoughness = sub_1.outputs['Value'] # link them up - links.new(sub_1.inputs[1], separate_rgb.outputs['R']) + links.new(sub_1.inputs[1], separate_rgb.outputs['Red']) # lfMetallic = lMasks.g; - links.new(principled_BSDF.inputs['Metallic'], - separate_rgb.outputs['G']) + links.new(principled_BSDF.inputs['Metallic'], separate_rgb.outputs['Green']) else: roughness_value = nodes.new(type='ShaderNodeValue') roughness_value.outputs[0].default_value = 1.0 @@ -143,16 +179,12 @@ def create_material_node(mat_path: str, local_root_directory: str): links.new(mult_param_x.inputs[0], lfRoughness) lfRoughness = mult_param_x.outputs['Value'] if lfRoughness is not None: - links.new(principled_BSDF.inputs['Roughness'], - lfRoughness) + links.new(principled_BSDF.inputs['Roughness'], lfRoughness) # If the roughness wasn't ever defined then the default value is 1 # which is what blender has as the default anyway elif tex_type == NORMAL: # texture - _path = realize_path(tex_path, local_root_directory) - if _path is not None and op.exists(_path): - img = bpy.data.images.load(_path) - img.colorspace_settings.name = 'Linear Rec.2020' + img.colorspace_settings.name = 'Linear Rec.2020' normal_texture = nodes.new(type='ShaderNodeTexImage') normal_texture.name = normal_texture.label = 'Texture Image - Normal' # noqa normal_texture.image = img @@ -163,23 +195,17 @@ def create_material_node(mat_path: str, local_root_directory: str): normal_com_xyz = nodes.new(type='ShaderNodeCombineXYZ') normal_com_xyz.location = (-200, -300) # swap X and Y channels - links.new(normal_com_xyz.inputs['X'], - normal_sep_xyz.outputs['Y']) - links.new(normal_com_xyz.inputs['Y'], - normal_sep_xyz.outputs['X']) - links.new(normal_com_xyz.inputs['Z'], - normal_sep_xyz.outputs['Z']) + links.new(normal_com_xyz.inputs['X'], normal_sep_xyz.outputs['Y']) + links.new(normal_com_xyz.inputs['Y'], normal_sep_xyz.outputs['X']) + links.new(normal_com_xyz.inputs['Z'], normal_sep_xyz.outputs['Z']) # normal map normal_map = nodes.new(type='ShaderNodeNormalMap') normal_map.location = (0, -300) # link them up - links.new(normal_sep_xyz.inputs['Vector'], - normal_texture.outputs['Color']) - links.new(normal_map.inputs['Color'], - normal_com_xyz.outputs['Vector']) - links.new(principled_BSDF.inputs['Normal'], - normal_map.outputs['Normal']) + links.new(normal_sep_xyz.inputs['Vector'], normal_texture.outputs['Color']) + links.new(normal_map.inputs['Color'], normal_com_xyz.outputs['Vector']) + links.new(principled_BSDF.inputs['Normal'], normal_map.outputs['Normal']) # Apply some final transforms to the data before connecting it to the # Material output node @@ -190,16 +216,12 @@ def create_material_node(mat_path: str, local_root_directory: str): col_attribute = nodes.new(type='ShaderNodeAttribute') col_attribute.attribute_name = 'Col' mix_colour = nodes.new(type='ShaderNodeMixRGB') - links.new(mix_colour.inputs['Color1'], - lColourVec4) - links.new(mix_colour.inputs['Color2'], - col_attribute.outputs['Color']) - links.new(principled_BSDF.inputs['Base Color'], - mix_colour.outputs['Color']) + links.new(mix_colour.inputs['Color1'], lColourVec4) + links.new(mix_colour.inputs['Color2'], col_attribute.outputs['Color']) + links.new(principled_BSDF.inputs['Base Color'], mix_colour.outputs['Color']) lColourVec4 = mix_colour.outputs['Color'] - if (8 in flags or 10 in flags or - 21 in flags): + if (8 in flags or 10 in flags or 21 in flags): # Handle transparency alpha_mix = nodes.new(type='ShaderNodeMixShader') alpha_shader = nodes.new(type='ShaderNodeBsdfTransparent') @@ -234,10 +256,8 @@ def create_material_node(mat_path: str, local_root_directory: str): links.new(alpha_shader.inputs['Color'], lColourVec4) - links.new(alpha_mix.inputs[1], - FRAGMENT_COLOUR0) - links.new(alpha_mix.inputs[2], - alpha_shader.outputs['BSDF']) + links.new(alpha_mix.inputs[1], FRAGMENT_COLOUR0) + links.new(alpha_mix.inputs[2], alpha_shader.outputs['BSDF']) FRAGMENT_COLOUR0 = alpha_mix.outputs['Shader'] @@ -246,22 +266,17 @@ def create_material_node(mat_path: str, local_root_directory: str): # FRAGMENT_COLOUR0 = vec4( lOutColours0Vec4.xyz, lColourVec4.a ); alpha_mix_decal = nodes.new(type='ShaderNodeMixShader') alpha_shader = nodes.new(type='ShaderNodeBsdfTransparent') - links.new(alpha_mix_decal.inputs['Fac'], - diffuse_texture.outputs['Alpha']) - links.new(alpha_mix_decal.inputs[1], - alpha_shader.outputs['BSDF']) - links.new(alpha_mix_decal.inputs[2], - FRAGMENT_COLOUR0) + links.new(alpha_mix_decal.inputs['Fac'], diffuse_texture.outputs['Alpha']) + links.new(alpha_mix_decal.inputs[1], alpha_shader.outputs['BSDF']) + links.new(alpha_mix_decal.inputs[2], FRAGMENT_COLOUR0) FRAGMENT_COLOUR0 = alpha_mix_decal.outputs['Shader'] # Link up the diffuse colour to the base colour on the prinicipled BSDF # shader. - links.new(principled_BSDF.inputs['Base Color'], - lColourVec4) + links.new(principled_BSDF.inputs['Base Color'], lColourVec4) # Finally, link the fragment colour to the output material. - links.new(output_material.inputs['Surface'], - FRAGMENT_COLOUR0) + links.new(output_material.inputs['Surface'], FRAGMENT_COLOUR0) # link some nodes up according to the uberfragment.bin shader # TODO: fix this at some point... diff --git a/NMSDK.py b/src/addon/nmsdk/NMSDK.py similarity index 85% rename from NMSDK.py rename to src/addon/nmsdk/NMSDK.py index 6fa83d0..6693b3f 100644 --- a/NMSDK.py +++ b/src/addon/nmsdk/NMSDK.py @@ -1,20 +1,28 @@ +# pyright: reportInvalidTypeForm=false + # stdlib imports -from math import radians import os.path as op +from math import radians +from typing import TYPE_CHECKING -# Blender imports -from bpy.props import (StringProperty, BoolProperty, EnumProperty, IntProperty) import bpy -from bpy_extras.io_utils import ExportHelper, ImportHelper + +# Blender imports +from bpy.props import BoolProperty, EnumProperty, IntProperty, StringProperty from bpy.types import Operator, PropertyGroup +from bpy_extras.io_utils import ExportHelper, ImportHelper from mathutils import Matrix -# internal imports -from ModelImporter.import_scene import ImportScene -from ModelExporter.addon_script import Exporter -from ModelExporter.utils import get_all_actions_in_scene, get_all_actions -from utils.settings import read_settings, write_settings -from BlenderExtensions.UIWidgets import ShowMessageBox +from .BlenderExtensions.UIWidgets import ShowMessageBox +from .ModelExporter.addon_script import Exporter +from .ModelExporter.utils import get_all_actions, get_all_actions_in_scene + +if TYPE_CHECKING: + from . import NMSDKPreferences +from .ModelImporter.import_scene import ImportScene +from .utils.io import is_subdir +from .utils.settings import read_settings, write_settings +from .utils.stopwitch import witch def set_import_export_defaults(cls, context): @@ -52,6 +60,19 @@ class ImportSceneOperator(Operator): 'blender.', default=True, ) + import_recursively: BoolProperty( + name='Import recursively', + description='Whether or not to import reference nodes automatically.\n' + 'For large scenes with many referenced scenes it is better' + ' to set this as False to avoid long wait times, and then ' + 'only import the scenes you want after it has loaded.', + default=True, + ) + dump_extracted_files: BoolProperty( + name="Store extracted files", + description="Whether or not to store any extracted files in the VFS.", + default=False, + ) draw_hulls: BoolProperty( name='Draw bounded hulls', @@ -70,20 +91,17 @@ class ImportSceneOperator(Operator): description='Whether or not to draw the collision objects.', default=False, ) - import_recursively: BoolProperty( - name='Import recursively', - description='Whether or not to import reference nodes automatically.\n' - 'For large scenes with many referenced scenes it is better' - ' to set this as False to avoid long wait times, and then ' - 'only import the scenes you want after it has loaded.', - default=True, - ) # Animation related properties import_bones: BoolProperty( name='Import bones', description="Whether or not to import the models' bones", default=False, ) + import_idle_anims: BoolProperty( + name='Import idle animations', + description='Whether or not to import idle animations for this scene', + default=True, + ) import_anims: BoolProperty( name='Import animations', description='Whether or not to import animations for this scene', @@ -98,8 +116,7 @@ class ImportSceneOperator(Operator): def execute(self, context): keywords = self.as_keywords() - importer = ImportScene(self.path, parent_obj=None, ref_scenes=dict(), - settings=keywords) + importer = ImportScene(self.path, parent_obj=None, ref_scenes=dict(), settings=keywords) importer.render_scene() return importer.state @@ -286,8 +303,7 @@ def execute(self, context): # imported scene. PCBANKS_dir = context.scene.nmsdk_default_settings.PCBANKS_directory full_path = op.join(PCBANKS_dir, scene_path) - importer = ImportScene(full_path, parent_obj=obj, ref_scenes=dict(), - settings={'clear_scene': False}) + importer = ImportScene(full_path, parent_obj=obj, ref_scenes=dict(), settings={'clear_scene': False}) importer.render_scene() return importer.state @@ -322,89 +338,6 @@ def execute(self, context): return {'FINISHED'} -class _GetPCBANKSFolder(Operator): - """Select the PCBANKS folder location""" - # Code modified from https://blender.stackexchange.com/a/126596 - bl_idname = "nmsdk._find_pcbanks" - bl_label = "Specify PCBANKS location" - - # Define this to tell 'fileselect_add' that we want a directoy - directory: StringProperty( - name="PCBANKS path", - description="Location of the PCBANKS folder") - - filter_folder: BoolProperty(default=True, options={'HIDDEN'}) - - def execute(self, context): - # Set the PCBANKS_directory value - context.scene.nmsdk_default_settings.PCBANKS_directory = self.directory - return {'FINISHED'} - - def invoke(self, context, event): - # Open browser, take reference to 'self' read the path to selected - # file, put path in predetermined self fields. - # See: - # https://docs.blender.org/api/current/bpy.types.WindowManager.html#bpy.types.WindowManager.fileselect_add - self.directory = context.scene.nmsdk_default_settings.PCBANKS_directory - context.window_manager.fileselect_add(self) - # Tells Blender to hang on for the slow user input - return {'RUNNING_MODAL'} - - -class _RemovePCBANKSFolder(Operator): - """Reset the PCBANKS folder location""" - bl_idname = "nmsdk._remove_pcbanks" - bl_label = "Remove PCBANKS location" - - def execute(self, context): - # Set the PCBANKS_directory as blank - context.scene.nmsdk_default_settings.PCBANKS_directory = "" - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_confirm(self, event) - - -class _GetMBINCompilerLocation(Operator): - """Select the MBINCompiler executable location""" - # Code modified from https://blender.stackexchange.com/a/126596 - bl_idname = "nmsdk._find_mbincompiler" - bl_label = "Specify MBINCompiler location" - - filepath: StringProperty( - name="MBINCompiler Location", - description="Location of the MBINCompiler executable") - - def execute(self, context): - # Set the PCBANKS_directory value - context.scene.nmsdk_default_settings.MBINCompiler_path = self.filepath - return {'FINISHED'} - - def invoke(self, context, event): - # Open browser, take reference to 'self' read the path to selected - # file, put path in predetermined self fields. - # See: - # https://docs.blender.org/api/current/bpy.types.WindowManager.html#bpy.types.WindowManager.fileselect_add - self.directory = context.scene.nmsdk_default_settings.MBINCompiler_path - context.window_manager.fileselect_add(self) - # Tells Blender to hang on for the slow user input - return {'RUNNING_MODAL'} - - -class _RemoveMBINCompilerLocation(Operator): - """Reset the MBINCompiler executable location""" - bl_idname = "nmsdk._remove_mbincompiler" - bl_label = "Remove MBINCompiler location" - - def execute(self, context): - # Set the PCBANKS_directory as blank - context.scene.nmsdk_default_settings.MBINCompiler_path = "" - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_confirm(self, event) - - # Animation classes and functions # TODO: move... @@ -504,6 +437,7 @@ def execute(self, context): loadable_anim_names = context.scene.nmsdk_anim_data.loadable_anim_data anim_name = self.loadable_anim_name anim_data = loadable_anim_names.pop(anim_name) + # TODO: Fix bpy.ops.nmsdk.animation_handler( anim_name=anim_name, anim_path=anim_data['Filename']) @@ -635,21 +569,13 @@ class NMSDKDefaultSettings(PropertyGroup): description="Group name so that models that all belong in the same " "folder are placed there (path becomes group_name/name)", default=default_settings.get('group_name', "")) - PCBANKS_directory: StringProperty( - name="PCBANKS directory", - description="Path to the PCBANKS folder", - default=default_settings.get('PCBANKS_directory', "")) - MBINCompiler_path: StringProperty( - name="MBINCompiler location", - description="Path to the Mbincompiler executable", - default=default_settings.get('MBINCompiler_path', "")) def save(self): """ Save the current settings. """ - settings = {'export_directory': self.export_directory, - 'group_name': self.group_name, - 'PCBANKS_directory': self.PCBANKS_directory, - 'MBINCompiler_path': self.MBINCompiler_path} + settings = { + 'export_directory': self.export_directory, + 'group_name': self.group_name, + } write_settings(settings) @@ -802,23 +728,33 @@ def draw(self, context): animations_box.prop(self, 'idle_anim') def execute(self, context): + addon_prefs: NMSDKPreferences = context.preferences.addons[__package__].preferences keywords = self.as_keywords() # Split the filepath provided as the final part is the name of the file export_path, scene_name = op.split(self.filepath) keywords.pop('export_directory') keywords.pop('group_name') - if not bpy.context.scene.nmsdk_default_settings.MBINCompiler_path: - ShowMessageBox("No MBINCompiler specified or found", "Error", - 'ERROR') - print("[ERROR]: No MBINCompiler specified or found") - return {'CANCELLED'} + no_convert = keywords.get("no_convert", False) + if not no_convert and ( + not addon_prefs.mbincompiler_path or not op.exists(addon_prefs.mbincompiler_path) + ): + ShowMessageBox( + "No MBINCompiler specified or found - Opening Preferences.", + "Error", + "ERROR", + ) + bpy.ops.screen.userpref_show() + bpy.context.preferences.active_section = "ADDONS" + bpy.ops.preferences.addon_show(module=__package__) + + print("[ERROR] No MBINCompiler specified or found - Opening Preferences.") + return {"CANCELLED"} main_exporter = Exporter(export_path, self.export_directory, self.group_name, scene_name, keywords) status = main_exporter.state if main_exporter.warnings: invalid_lights = main_exporter.warnings.get('light_is_mesh', []) - invalid_lights_msg = ('The following lights are meshes: ' - f'{", ".join(invalid_lights)}') + invalid_lights_msg = f'The following lights are meshes: {", ".join(invalid_lights)}' self.report({'WARNING'}, invalid_lights_msg) print(invalid_lights_msg) if status == {'FINISHED'}: @@ -850,6 +786,11 @@ class NMS_Import_Operator(Operator, ImportHelper): ' to set this as False to avoid long wait times, and then ' 'only import the scenes you want after it has loaded.', default=True) + dump_extracted_files: BoolProperty( + name="Store extracted files", + description="Whether or not to store any extracted files in the VFS.", + default=False, + ) # Collision related properties import_collisions: BoolProperty( @@ -866,6 +807,11 @@ class NMS_Import_Operator(Operator, ImportHelper): name='Import bones', description="Whether or not to import the models' bones", default=False) + import_idle_anims: BoolProperty( + name='Import idle animations', + description='Whether or not to import idle animations for this scene', + default=True, + ) import_anims: BoolProperty( name='Import animations', description='Whether or not to import animations for this scene', @@ -896,6 +842,7 @@ def draw(self, context): layout = self.layout layout.prop(self, 'clear_scene') layout.prop(self, 'import_recursively') + layout.prop(self, 'dump_extracted_files') coll_box = layout.box() coll_box.label(text='Collisions') coll_box.prop(self, 'import_collisions') @@ -904,9 +851,12 @@ def draw(self, context): animation_box = layout.box() animation_box.label(text='Animation') animation_box.prop(self, 'import_bones') - animation_box.prop(self, 'import_anims') - if self.import_anims: - animation_box.prop(self, 'max_anims') + if self.import_bones: + sub_anim_box = animation_box.box() + sub_anim_box.prop(self, "import_idle_anims") + sub_anim_box.prop(self, 'import_anims') + if self.import_anims: + sub_anim_box.prop(self, 'max_anims') debug_box = layout.box() debug_box.label(text='Debug') @@ -914,27 +864,55 @@ def draw(self, context): debug_box.prop(self, 'draw_bounding_box') def execute(self, context): + addon_prefs: NMSDKPreferences = context.preferences.addons[__package__].preferences keywords = self.as_keywords() # set the state of the show_collisions button from the value specified # when the import occurs context.scene.nmsdk_settings.show_collisions = self.show_collisions # Reset the animation data context.scene.nmsdk_anim_data.reset() - fdir = self.properties.filepath context.scene['_anim_names'] = ['None'] - print(fdir) - if not bpy.context.scene.nmsdk_default_settings.MBINCompiler_path: - ShowMessageBox("No MBINCompiler specified or found", "Error", - 'ERROR') - print("[ERROR]: No MBINCompiler specified or found") - return {'CANCELLED'} - importer = ImportScene(fdir, parent_obj=None, ref_scenes=dict(), - settings=keywords) + + # Check we have MBINCompiler found if we are importing an MXML file. + fpath: str = self.properties.filepath + if fpath.lower().endswith(".mxml"): + if not addon_prefs.mbincompiler_path or not op.exists(addon_prefs.mbincompiler_path): + ShowMessageBox( + "No MBINCompiler specified or found - Opening Preferences.", + "Error", + "ERROR", + ) + bpy.ops.screen.userpref_show() + bpy.context.preferences.active_section = "ADDONS" + bpy.ops.preferences.addon_show(module=__package__) + + print("[ERROR] No MBINCompiler specified or found - Opening Preferences.") + return {"CANCELLED"} + witch.start() + if is_subdir(fpath, op.join(addon_prefs.pcbanks_dir, ".scene_vfs")): + _fpath = op.relpath(fpath, op.join(addon_prefs.pcbanks_dir, ".scene_vfs")) + importer = ImportScene(_fpath, None, {}, keywords, True) + else: + importer = ImportScene(fpath, None, {}, keywords) importer.render_scene() status = importer.state self.report({'INFO'}, "Models Imported Successfully") - print('Scene imported!') + witch.stop() + # witch.results() if status: return {'FINISHED'} else: return {'CANCELLED'} + + def invoke(self, context, event): + addon_prefs: NMSDKPreferences = context.preferences.addons[__package__].preferences + if addon_prefs.unpacked_pcbanks_dir: + # Use the preferentially. + self.filepath = addon_prefs.unpacked_pcbanks_dir + # Otherwise, check to see if the pcbanks directory is set. + if addon_prefs.pcbanks_dir: + if op.exists(op.join(addon_prefs.pcbanks_dir, ".scene_vfs")): + self.filepath = op.join(addon_prefs.pcbanks_dir, ".scene_vfs", "models") + # Otherwise, let it just do its own thing. + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} diff --git a/tools/build.py b/src/addon/nmsdk/Tools/build.py similarity index 100% rename from tools/build.py rename to src/addon/nmsdk/Tools/build.py diff --git a/Tools/proc_gen.py b/src/addon/nmsdk/Tools/proc_gen.py similarity index 96% rename from Tools/proc_gen.py rename to src/addon/nmsdk/Tools/proc_gen.py index d55af48..6b10220 100644 --- a/Tools/proc_gen.py +++ b/src/addon/nmsdk/Tools/proc_gen.py @@ -15,17 +15,37 @@ # stdlib imports import os import subprocess -from tkinter import (Tk, StringVar, Frame, Label, Entry, LEFT, VERTICAL, RIGHT, - BOTH, Y, Button) -from tkinter import filedialog, simpledialog, ttk, messagebox +from tkinter import ( + BOTH, + LEFT, + RIGHT, + VERTICAL, + Button, + Entry, + Frame, + Label, + StringVar, + Tk, + Y, + filedialog, + messagebox, + simpledialog, + ttk, +) + # Internal imports -from ModelExporter.classes import (List, NMSString0x80, Model, Reference, - TkResourceDescriptorData, TkGeometryData, - TkResourceDescriptorList, - TkModelDescriptorList) +from ..NMS.classes import ( + List, + Model, + NMSString0x80, + Reference, + TkGeometryData, + TkModelDescriptorList, + TkResourceDescriptorData, + TkResourceDescriptorList, +) from .wckToolTips import ToolTipManager - tt = ToolTipManager() root = Tk() diff --git a/tools/process_file.bat b/src/addon/nmsdk/Tools/process_file.bat similarity index 100% rename from tools/process_file.bat rename to src/addon/nmsdk/Tools/process_file.bat diff --git a/tools/read_nmsdk_version.sh b/src/addon/nmsdk/Tools/read_nmsdk_version.sh similarity index 100% rename from tools/read_nmsdk_version.sh rename to src/addon/nmsdk/Tools/read_nmsdk_version.sh diff --git a/Tools/utilities/bh_converter.py b/src/addon/nmsdk/Tools/utilities/bh_converter.py similarity index 100% rename from Tools/utilities/bh_converter.py rename to src/addon/nmsdk/Tools/utilities/bh_converter.py diff --git a/Tools/utilities/enum_fix.py b/src/addon/nmsdk/Tools/utilities/enum_fix.py similarity index 100% rename from Tools/utilities/enum_fix.py rename to src/addon/nmsdk/Tools/utilities/enum_fix.py diff --git a/Tools/utilities/prettify.py b/src/addon/nmsdk/Tools/utilities/prettify.py similarity index 100% rename from Tools/utilities/prettify.py rename to src/addon/nmsdk/Tools/utilities/prettify.py diff --git a/Tools/utilities/struct_convert.py b/src/addon/nmsdk/Tools/utilities/struct_convert.py similarity index 100% rename from Tools/utilities/struct_convert.py rename to src/addon/nmsdk/Tools/utilities/struct_convert.py diff --git a/Tools/utilities/struct_gen.py b/src/addon/nmsdk/Tools/utilities/struct_gen.py similarity index 100% rename from Tools/utilities/struct_gen.py rename to src/addon/nmsdk/Tools/utilities/struct_gen.py diff --git a/Tools/wckToolTips.py b/src/addon/nmsdk/Tools/wckToolTips.py similarity index 100% rename from Tools/wckToolTips.py rename to src/addon/nmsdk/Tools/wckToolTips.py diff --git a/src/addon/nmsdk/__init__.py b/src/addon/nmsdk/__init__.py new file mode 100644 index 0000000..78736e2 --- /dev/null +++ b/src/addon/nmsdk/__init__.py @@ -0,0 +1,272 @@ +# pyright: reportInvalidTypeForm=false + +import json +import os +import os.path as op +import time + +import bpy +from bpy.types import Operator +from bpy.props import PointerProperty, StringProperty +from bpy.utils import register_class, unregister_class + +# extensions to blender UI +from .BlenderExtensions import ContextMenus, NMSEntities, NMSNodes, NMSPanels, SettingsPanels + +# External API operators +# Main IO operators +# NMSDK object node handling operators +# Internal operators +# Settings +# Animation classes +from .NMSDK import ( + AnimProperties, + CreateNMSDKScene, + ExportSceneOperator, + ImportMeshOperator, + ImportSceneOperator, + NMS_Export_Operator, + NMS_Import_Operator, + NMSDKDefaultSettings, + NMSDKSettings, + _ChangeAnimation, + _FixActionNames, + _FixOldFormat, + _ImportReferencedScene, + _LoadAnimation, + _PauseAnimation, + _PlayAnimation, + _RefreshAnimations, + _SaveDefaultSettings, + _StopAnimation, + _ToggleCollisionVisibility, +) +from .utils.io import hide_path +from .utils.settings import read_settings, write_settings + +customNodes = NMSNodes() + + +# @persistent +# def load_vfs_data(*args): +# addon_prefs = bpy.context.preferences.addons[__package__].preferences +# print(addon_prefs.pcbanks_dir) +# if addon_prefs.pcbanks_dir and op.exists(addon_prefs.pcbanks_dir): +# if op.exists(op.join(addon_prefs.pcbanks_dir, ".scene_vfs")): +# if op.exists(op.join(addon_prefs.pcbanks_dir, ".scene_vfs", "index.json")): +# with open(op.join(addon_prefs.pcbanks_dir, ".scene_vfs", "index.json")) as f: +# pak_data = json.load(f) +# print("loaded VFS data") + + +def save_preferences(cls: "NMSDKPreferences", context: bpy.types.Context): + preferences = cls.as_dict() + current_settings = read_settings() + current_pcbanks_dir = current_settings.get("pcbanks_dir") + new_pcbanks_dir = preferences.get("pcbanks_dir") + settings_file = write_settings(preferences) + print(f"Saved preferences to {settings_file}") + from hgpaktool import HGPAKFile + if new_pcbanks_dir and current_pcbanks_dir != new_pcbanks_dir: + t0 = time.perf_counter() + out_dir = op.join(new_pcbanks_dir, ".scene_vfs") + if not op.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + hide_path(out_dir) + # Load the data from the pak files. + print("Creating vfs... This may take a little while (but it will be worth it!)") + index = {} + counter = 0 + for pakfname in os.listdir(new_pcbanks_dir): + if pakfname.lower().endswith(".pak"): + with HGPAKFile(op.join(new_pcbanks_dir, pakfname)) as pak: + for fname in pak.filenames: + lfname = fname.lower() + if lfname.endswith(".scene.mbin"): + counter += 1 + dest_fname = op.join(out_dir, lfname) + if not op.exists(dest_fname): + dest_dir = op.join(out_dir, op.dirname(fname)) + os.makedirs(dest_dir, exist_ok=True) + with open(dest_fname, "w"): + pass + for fname in pak.filenames: + index[fname] = pakfname + cls.pak_mapping_data = index + with open(op.join(out_dir, "index.json"), "w") as f: + json.dump(index, f) + t1 = time.perf_counter() + print(f"Loaded {counter} scenes into VFS in {t1 - t0:.4f}s") + + +class IndexPAKPath(Operator): + """Index all the pak files""" + bl_idname = "nmsdk.index_paks" + bl_label = "" + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + from hgpaktool import HGPAKFile + + addon_prefs: NMSDKPreferences = context.preferences.addons[__package__].preferences + + t0 = time.perf_counter() + out_dir = op.join(addon_prefs.pcbanks_dir, ".scene_vfs") + if not op.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + hide_path(out_dir) + print("Creating vfs... This may take a little while (but it will be worth it!)") + index = {} + counter = 0 + for pakfname in os.listdir(addon_prefs.pcbanks_dir): + if pakfname.lower().endswith(".pak"): + with HGPAKFile(op.join(addon_prefs.pcbanks_dir, pakfname)) as pak: + for fname in pak.filenames: + lfname = fname.lower() + if lfname.endswith(".scene.mbin"): + counter += 1 + dest_fname = op.join(out_dir, lfname) + if not op.exists(dest_fname): + dest_dir = op.join(out_dir, op.dirname(fname)) + os.makedirs(dest_dir, exist_ok=True) + with open(dest_fname, "w"): + pass + for fname in pak.filenames: + index[fname] = pakfname + with open(op.join(out_dir, "index.json"), "w") as f: + json.dump(index, f) + t1 = time.perf_counter() + print(f"Loaded {counter} scenes into VFS in {t1 - t0:.4f}s") + + return {'FINISHED'} + + +class NMSDKPreferences(bpy.types.AddonPreferences): + # This must match the add-on name, use `__package__` + # when defining this for add-on extensions or a sub-module of a Python package. + bl_idname = __package__ + + default_settings = read_settings() + + pcbanks_dir: StringProperty( + name="PCBANKS Directory", + description="Path to your PCBANKS directory itself. This should contain the vanilla game .pak files", + subtype='DIR_PATH', + update=save_preferences, + default=default_settings.get("pcbanks_dir", "") + ) + unpacked_pcbanks_dir: StringProperty( + name="Unpacked PCBANKS Directory (Optional)", + description="Path to your unpacked game files. This is not required.", + subtype='DIR_PATH', + update=save_preferences, + default=default_settings.get("unpacked_pcbanks_dir", "") + ) + mbincompiler_path: StringProperty( + name="MBINCompiler Executable (Optional)", + description=( + "Path to the MBINCompiler executable. This is only required if you want to read/write MXML files" + ), + subtype='FILE_PATH', + update=save_preferences, + default=default_settings.get("mbincompiler_path", "") + ) + + pak_mapping_data: dict[str, str] + + def draw(self, context): + layout = self.layout + layout.label(text="NMSDK preferences") + row = layout.row(align=True) + row.prop(self, "pcbanks_dir") + row.operator("nmsdk.index_paks", icon="FILE_REFRESH", text_ctxt="Refresh pak index") + layout.prop(self, "unpacked_pcbanks_dir") + layout.prop(self, "mbincompiler_path") + + def as_dict(self): + return { + "pcbanks_dir": self.pcbanks_dir, + "unpacked_pcbanks_dir": self.unpacked_pcbanks_dir, + "mbincompiler_path": self.mbincompiler_path + } + + +# Only needed if you want to add into a dynamic menu +def menu_func_export(self, context): + self.layout.operator(NMS_Export_Operator.bl_idname, + text="Export to NMS XML Format ") + + +def menu_func_import(self, context): + self.layout.operator(NMS_Import_Operator.bl_idname, + text="Import NMS SCENE") + + +classes = ( + NMS_Export_Operator, + NMS_Import_Operator, + NMSDKSettings, + NMSDKDefaultSettings, + ImportSceneOperator, + ImportMeshOperator, + ExportSceneOperator, + CreateNMSDKScene, + _FixOldFormat, + _FixActionNames, + _ImportReferencedScene, + _ToggleCollisionVisibility, + _SaveDefaultSettings, + _ChangeAnimation, + _RefreshAnimations, + _LoadAnimation, + _PlayAnimation, + _PauseAnimation, + _StopAnimation, + AnimProperties, +) + + +def register(): + # bpy.app.handlers.load_post.append(load_vfs_data) + bpy.utils.register_class(IndexPAKPath) + bpy.utils.register_class(NMSDKPreferences) + for cls in classes: + register_class(cls) + bpy.types.Scene.nmsdk_settings = PointerProperty(type=NMSDKSettings) + bpy.types.Scene.nmsdk_default_settings = PointerProperty( + type=NMSDKDefaultSettings) + bpy.types.Scene.nmsdk_anim_data = PointerProperty(type=AnimProperties) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + NMSPanels.register() + # NMSShaderNode.register() + customNodes.register() + NMSEntities.register() + SettingsPanels.register() + ContextMenus.register() + + +def unregister(): + for cls in reversed(classes): + unregister_class(cls) + del bpy.types.Scene.nmsdk_settings + del bpy.types.Scene.nmsdk_default_settings + del bpy.types.Scene.nmsdk_anim_data + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + NMSPanels.unregister() + # NMSShaderNode.unregister() + customNodes.unregister() + NMSEntities.unregister() + SettingsPanels.unregister() + ContextMenus.unregister() + bpy.utils.unregister_class(NMSDKPreferences) + bpy.utils.unregister_class(IndexPAKPath) + # bpy.app.handlers.load_post.remove(load_vfs_data) + + +if __name__ == '__main__': + register() diff --git a/src/addon/nmsdk/blender_manifest.toml b/src/addon/nmsdk/blender_manifest.toml new file mode 100644 index 0000000..ea5fafc --- /dev/null +++ b/src/addon/nmsdk/blender_manifest.toml @@ -0,0 +1,36 @@ +schema_version = "1.0.0" + +id = "nmsdk" +version = "0.10.0-alpha13" +name = "NMSDK" +tagline = "NMS model importer and exporter" +maintainer = "monkeyman192" +type = "add-on" + +website = "https://github.com/monkeyman192/NMSDK" +tags = ["Import-Export"] + +blender_version_min = "5.0.0" + +license = [ + "SPDX:GPL-3.0-or-later", +] + +# HGPAKTool is a required dependency +wheels = [ + "./wheels/hgpaktool-1.1.3-py3-none-any.whl", +] + +[permissions] +files = "Import/export Scene files and read .pak files" + +# # Optional: advanced build settings. +# # https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build +# [build] +# # These are the default build excluded patterns. +# # You only need to edit them if you want different options. +# paths_exclude_pattern = [ +# "__pycache__/", +# "/.git/", +# "/*.zip", +# ] \ No newline at end of file diff --git a/serialization/NMS_Structures/NMS_types.py b/src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py similarity index 92% rename from serialization/NMS_Structures/NMS_types.py rename to src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py index 9e5670e..95c01c5 100644 --- a/serialization/NMS_Structures/NMS_types.py +++ b/src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py @@ -4,9 +4,9 @@ from typing import Annotated, Type, TypeVar, Optional, Union import types -from serialization.utils import bytes_to_quat -from serialization.cereal_bin.structdata import datatype, Field -import serialization.cereal_bin.basic_types as bt +from ..utils import bytes_to_quat +from ..cereal_bin.structdata import datatype, Field +from ..cereal_bin import basic_types as bt T = TypeVar("T", bound=datatype) @@ -163,3 +163,23 @@ class MBINHeader(datatype): class astring(bt.string): _alignment = 0x8 + + +class NMSString0x10(bt.string): + _format = "16s" + + +class NMSString0x20(bt.string): + _format = "32s" + + +class NMSString0x40(bt.string): + _format = "64s" + + +class NMSString0x80(bt.string): + _format = "128s" + + +class NMSString0x100(bt.string): + _format = "256s" \ No newline at end of file diff --git a/serialization/NMS_Structures/Structures.py b/src/addon/nmsdk/serialization/NMS_Structures/Structures.py similarity index 67% rename from serialization/NMS_Structures/Structures.py rename to src/addon/nmsdk/serialization/NMS_Structures/Structures.py index 4fd494a..ab24824 100644 --- a/serialization/NMS_Structures/Structures.py +++ b/src/addon/nmsdk/serialization/NMS_Structures/Structures.py @@ -1,15 +1,69 @@ -from typing import Annotated, Type -from io import BufferedWriter, BufferedReader -from dataclasses import dataclass import struct +from contextvars import ContextVar +from dataclasses import dataclass +from io import BufferedReader, BufferedWriter +from typing import Annotated, Type -from serialization.cereal_bin.structdata import datatype, Field -import serialization.cereal_bin.basic_types as bt - -from serialization.NMS_Structures.NMS_types import ( - Vector4f, NMS_list, astring, VariableSizeString, Quaternion_list, Vector4i +from ..cereal_bin import basic_types as bt +from ..cereal_bin.structdata import Field, datatype +from .NMS_types import ( + NMS_list, + NMSString0x10, + NMSString0x40, + Quaternion_list, + VariableSizeString, + Vector4f, + Vector4i, + astring, ) +ctx_nonignored_namehashes: ContextVar[set[int]] = ContextVar("ctx_nonignored_namehashes", default=set()) + + +class NMSTemplate(datatype): + _size = 0x10 + _alignment = 8 + _real_type: datatype + _end_padding: int = 0xEEEEEE01 + + @classmethod + def deserialize(cls, buf: BufferedReader) -> datatype: + start = buf.tell() + offset, namehash, _ = struct.unpack(" datatype: - start = buf.tell() - offset, namehash, _ = struct.unpack(" str: cls._skip_padding(buf) - fmt = cls._format.format(length=meta.length) + if meta.length: + fmt = cls._format.format(length=meta.length) + else: + fmt = cls._format encoding = meta.encoding or "utf-8" return struct.unpack(fmt, buf.read(meta.length))[0].decode(encoding).strip("\x00") @classmethod def _write(cls, buf: BufferedWriter, value: str, meta: Field): cls._write_padding(buf) - fmt = cls._format.format(length=meta.length) + if meta.length: + fmt = cls._format.format(length=meta.length) + else: + fmt = cls._format encoding = meta.encoding or "utf-8" buf.write(struct.pack(fmt, value.encode(encoding))) \ No newline at end of file diff --git a/serialization/cereal_bin/structdata.py b/src/addon/nmsdk/serialization/cereal_bin/structdata.py similarity index 94% rename from serialization/cereal_bin/structdata.py rename to src/addon/nmsdk/serialization/cereal_bin/structdata.py index 4173dcf..13aff8b 100644 --- a/serialization/cereal_bin/structdata.py +++ b/src/addon/nmsdk/serialization/cereal_bin/structdata.py @@ -1,9 +1,9 @@ -from io import BytesIO, BufferedWriter, BufferedReader -import struct import inspect -from typing import Optional, Any, TypeVar, Type +import struct from dataclasses import dataclass - +from io import BufferedReader, BufferedWriter, BytesIO +from types import GenericAlias +from typing import Any, Optional, Type, TypeVar, Union T = TypeVar("T", bound="datatype") N = TypeVar("N", bound=int) @@ -180,7 +180,7 @@ def _read(cls, buf: BufferedReader, meta: Optional["Field"] = None): return cls.read(buf) @classmethod - def read(cls: Type[T], buf: BufferedReader) -> T: + def read(cls: Type[T], buf: Union[BytesIO, BufferedReader]) -> T: cls_ = cls.__new__(cls) for name, pytype in cls_.__annotations__.items(): if name.startswith("_"): @@ -191,7 +191,11 @@ def read(cls: Type[T], buf: BufferedReader) -> T: print(name, pytype, type(pytype)) raise type_: datatype = meta.datatype - if isinstance(pytype.__origin__, list): + if ( + isinstance(origin := pytype.__origin__, GenericAlias) + and issubclass(origin.__origin__, list) + and meta.length + ): data = [] for _ in range(meta.length): data.append(type_._read(buf, meta)) @@ -207,7 +211,7 @@ def read(cls: Type[T], buf: BufferedReader) -> T: @dataclass class Field: - datatype: datatype + datatype: Type[datatype] length: Optional[int] = None encoding: Optional[str] = None deferred_loading: bool = False diff --git a/serialization/formats/INT_2_10_10_10_REV.py b/src/addon/nmsdk/serialization/formats/INT_2_10_10_10_REV.py similarity index 77% rename from serialization/formats/INT_2_10_10_10_REV.py rename to src/addon/nmsdk/serialization/formats/INT_2_10_10_10_REV.py index 55aaebd..a264309 100644 --- a/serialization/formats/INT_2_10_10_10_REV.py +++ b/src/addon/nmsdk/serialization/formats/INT_2_10_10_10_REV.py @@ -1,8 +1,10 @@ # some functions to process the openGL data type INT_2_10_10_10_REV import struct + import numpy as np + def bytes_to_int_2_10_10_10_rev(bytes_): return read_int_2_10_10_10_rev(struct.unpack('> i*10, 10)) + output.append(twos_complement((verts & (sel << i * 10)) >> i * 10, 10)) # read the w component seperately (don't need sign) output.append((verts & (sel << 30)) >> 30) # swap x and z components of output # output[0], output[2] = output[2], output[0] # calculate the norm of the x,y,z components of the array - norm = (output[0]**2 + output[1]**2 + output[2]**2)**0.5 + norm = (output[0] ** 2 + output[1] ** 2 + output[2] ** 2) ** 0.5 # then normalise if not norm: return [0, 0, 0, 1] for i in range(3): - output[i] = output[i]/norm + output[i] = output[i] / norm return output @@ -38,13 +40,13 @@ def np_read_int_2_10_10_10_rev(verts): """ Optimized version of the code to read the data type. The input array will be a row array and this will return a 2D array with 3 rows which will need to be flattened later.""" - a = fixed_twos_complement((verts & SEL_0) >> 0) - b = fixed_twos_complement((verts & SEL_10) >> 10) - c = fixed_twos_complement((verts & SEL_20) >> 20) - d = np.vstack([a, b, c]) + x = fixed_twos_complement((verts & SEL_0) >> 0) + y = fixed_twos_complement((verts & SEL_10) >> 10) + z = fixed_twos_complement((verts & SEL_20) >> 20) + v = np.vstack([x, y, z]) # Divide each element by the norm of the values. - norm = np.linalg.norm(d, axis=0) - return d / norm[None, :] + norm = np.linalg.norm(v, axis=0) + return v / norm[None, :] def fixed_twos_complement(input_value): @@ -54,7 +56,7 @@ def fixed_twos_complement(input_value): def twos_complement(input_value, num_bits): """Calculates a two's complement integer from the given input value""" - mask = 2**(num_bits - 1) + mask = 2 ** (num_bits - 1) return -(input_value & mask) + (input_value & ~mask) @@ -66,7 +68,7 @@ def write_int_2_10_10_10_rev(verts): out = 0 newverts = [0, 0, 0, 1] for i in range(3): - a = int(verts[i]*511) # maybe floor/ceil is needed?? + a = int(verts[i] * 511) # maybe floor/ceil is needed?? # implement a reverse twos compliment to get the signed binary # representation if abs(a) == a: @@ -75,5 +77,5 @@ def write_int_2_10_10_10_rev(verts): newverts[i] = (abs(a) ^ 0b1111111111) + 1 for i in range(4): - out = out | (newverts[i] << i*10) + out = out | (newverts[i] << i * 10) return struct.pack('