From 3c024b9970ef407d6bbe695d8158b5ce7205193e Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Fri, 13 Feb 2026 08:45:19 +1100 Subject: [PATCH 1/7] Initial commit for converting to blender plugin --- __init__.py | 123 ----------- build_cmf | 1 + pyproject.toml | 47 +++++ .../nmsdk/BlenderExtensions}/ContextMenu.py | 4 +- .../nmsdk/BlenderExtensions}/CustomNodes.py | 0 .../nmsdk/BlenderExtensions}/EntityPanels.py | 0 .../BlenderExtensions}/NMSObjectsPanels.py | 2 +- .../nmsdk/BlenderExtensions}/NMSShaderNode.py | 0 .../BlenderExtensions/SceneExplorer-maybe.py | 167 +++++++++++++++ .../nmsdk/BlenderExtensions/SceneExplorer.py | 44 ++++ .../nmsdk/BlenderExtensions}/SettingsPanel.py | 0 .../nmsdk/BlenderExtensions}/UIWidgets.py | 0 .../nmsdk/BlenderExtensions}/__init__.py | 0 .../ModelExporter}/ActionTriggerParser.py | 2 +- .../addon/nmsdk/ModelExporter}/Descriptor.py | 6 +- .../addon/nmsdk/ModelExporter}/__init__.py | 0 .../nmsdk/ModelExporter}/addon_script.py | 28 +-- .../addon/nmsdk/ModelExporter}/animations.py | 6 +- .../addon/nmsdk/ModelExporter}/cmd_export.py | 0 .../addon/nmsdk/ModelExporter}/export.py | 19 +- .../addon/nmsdk/ModelExporter}/utils.py | 27 --- .../nmsdk/ModelImporter}/SceneNodeData.py | 2 +- .../nmsdk/ModelImporter}/animation_handler.py | 4 +- .../nmsdk/ModelImporter}/import_scene.py | 20 +- .../addon/nmsdk/ModelImporter}/mesh_utils.py | 0 .../addon/nmsdk/ModelImporter}/readers.py | 8 +- {NMS => src/addon/nmsdk/NMS}/LOOKUPS.py | 0 {NMS => src/addon/nmsdk/NMS}/classes/Empty.py | 0 .../addon/nmsdk/NMS}/classes/Errors.py | 0 .../classes/GcAISpaceshipComponentData.py | 0 .../nmsdk/NMS}/classes/GcAISpaceshipTypes.py | 0 .../nmsdk/NMS}/classes/GcActionTrigger.py | 0 .../NMS}/classes/GcActionTriggerState.py | 0 .../classes/GcAlienPuzzleMissionOverride.py | 0 .../addon/nmsdk/NMS}/classes/GcAlienRace.py | 0 .../nmsdk/NMS}/classes/GcAnimFrameEvent.py | 0 .../nmsdk/NMS}/classes/GcBeenShotEvent.py | 0 .../nmsdk/NMS}/classes/GcCameraShakeAction.py | 0 .../classes/GcCustomInventoryComponentData.py | 0 .../nmsdk/NMS}/classes/GcDestroyAction.py | 0 .../classes/GcDestructableComponentData.py | 0 .../nmsdk/NMS}/classes/GcDiscoveryTypes.py | 0 .../addon/nmsdk/NMS}/classes/GcDisplayText.py | 0 .../NMS}/classes/GcEncounterComponentData.py | 0 .../classes/GcEncyclopediaComponentData.py | 0 .../nmsdk/NMS}/classes/GcGoToStateAction.py | 0 .../classes/GcInteractionActivationCost.py | 0 .../classes/GcInteractionComponentData.py | 0 .../nmsdk/NMS}/classes/GcInteractionDof.py | 0 .../nmsdk/NMS}/classes/GcInteractionType.py | 0 .../classes/GcInventoryTechProbability.py | 0 .../NMS}/classes/GcNodeActivationAction.py | 0 .../classes/GcObjectPlacementComponentData.py | 0 .../addon/nmsdk/NMS}/classes/GcPainAction.py | 0 .../nmsdk/NMS}/classes/GcParticleAction.py | 0 .../nmsdk/NMS}/classes/GcPlayAnimAction.py | 0 .../nmsdk/NMS}/classes/GcPlayAudioAction.py | 0 .../nmsdk/NMS}/classes/GcPlayerNearbyEvent.py | 0 .../addon/nmsdk/NMS}/classes/GcPrimaryAxis.py | 0 .../NMS}/classes/GcProjectileImpactType.py | 0 .../addon/nmsdk/NMS}/classes/GcRarity.py | 0 .../classes/GcRealitySubstanceCategory.py | 0 .../nmsdk/NMS}/classes/GcRewardAction.py | 0 .../NMS}/classes/GcScannableComponentData.py | 0 .../nmsdk/NMS}/classes/GcScannerIconTypes.py | 0 .../NMS}/classes/GcShootableComponentData.py | 0 .../GcSimpleInteractionComponentData.py | 0 .../nmsdk/NMS}/classes/GcSizeIndicator.py | 0 .../nmsdk/NMS}/classes/GcSpaceshipClasses.py | 0 .../NMS}/classes/GcSpaceshipComponentData.py | 0 .../addon/nmsdk/NMS}/classes/GcSpawnAction.py | 0 .../nmsdk/NMS}/classes/GcStatTrackType.py | 0 .../nmsdk/NMS}/classes/GcStateTimeEvent.py | 0 .../addon/nmsdk/NMS}/classes/GcStatsEnum.py | 0 .../nmsdk/NMS}/classes/GcSubstanceAmount.py | 0 .../classes/GcTriggerActionComponentData.py | 0 .../addon/nmsdk/NMS}/classes/GcWarpAction.py | 0 {NMS => src/addon/nmsdk/NMS}/classes/List.py | 2 +- .../addon/nmsdk/NMS}/classes/NMSString0x10.py | 0 .../addon/nmsdk/NMS}/classes/NMSString0x20.py | 0 .../addon/nmsdk/NMS}/classes/NMSString0x80.py | 0 .../addon/nmsdk/NMS}/classes/Object.py | 2 +- .../addon/nmsdk/NMS}/classes/PointLight.py | 2 +- .../addon/nmsdk/NMS}/classes/Quaternion.py | 0 .../addon/nmsdk/NMS}/classes/String.py | 0 .../addon/nmsdk/NMS}/classes/Struct.py | 8 +- .../nmsdk/NMS}/classes/TkAnimMetadata.py | 0 .../nmsdk/NMS}/classes/TkAnimNodeData.py | 0 .../nmsdk/NMS}/classes/TkAnimNodeFrameData.py | 0 .../NMS}/classes/TkAnimationComponentData.py | 0 .../nmsdk/NMS}/classes/TkAnimationData.py | 0 .../nmsdk/NMS}/classes/TkAnimationGameData.py | 0 .../nmsdk/NMS}/classes/TkAttachmentData.py | 0 .../nmsdk/NMS}/classes/TkAudioAnimTrigger.py | 0 .../NMS}/classes/TkAudioComponentData.py | 0 .../nmsdk/NMS}/classes/TkCameraWanderData.py | 0 .../nmsdk/NMS}/classes/TkGeometryData.py | 2 +- .../nmsdk/NMS}/classes/TkMaterialData.py | 0 .../nmsdk/NMS}/classes/TkMaterialFlags.py | 0 .../nmsdk/NMS}/classes/TkMaterialSampler.py | 0 .../NMS}/classes/TkMaterialUniform_Float.py | 0 .../NMS}/classes/TkMaterialUniform_UInt.py | 0 .../addon/nmsdk/NMS}/classes/TkMeshData.py | 0 .../nmsdk/NMS}/classes/TkMeshMetaData.py | 0 .../NMS}/classes/TkModelDescriptorList.py | 0 .../NMS}/classes/TkModelRendererCameraData.py | 0 .../nmsdk/NMS}/classes/TkModelRendererData.py | 0 .../NMS}/classes/TkPhysicsComponentData.py | 0 .../addon/nmsdk/NMS}/classes/TkPhysicsData.py | 0 .../NMS}/classes/TkResourceDescriptorData.py | 0 .../NMS}/classes/TkResourceDescriptorList.py | 0 .../NMS}/classes/TkRotationComponentData.py | 0 .../NMS}/classes/TkSceneNodeAttributeData.py | 0 .../nmsdk/NMS}/classes/TkSceneNodeData.py | 0 .../nmsdk/NMS}/classes/TkTextureResource.py | 0 .../nmsdk/NMS}/classes/TkTransformData.py | 2 +- .../nmsdk/NMS}/classes/TkVertexElement.py | 0 .../nmsdk/NMS}/classes/TkVertexLayout.py | 0 .../nmsdk/NMS}/classes/TkVolumeTriggerType.py | 0 .../addon/nmsdk/NMS}/classes/Vector4f.py | 0 .../addon/nmsdk/NMS}/classes/Vector4i.py | 0 .../addon/nmsdk/NMS}/classes/__init__.py | 0 {NMS => src/addon/nmsdk/NMS}/material_node.py | 7 +- NMSDK.py => src/addon/nmsdk/NMSDK.py | 22 +- {tools => src/addon/nmsdk/Tools}/build.py | 0 {Tools => src/addon/nmsdk/Tools}/proc_gen.py | 5 +- .../addon/nmsdk/Tools}/process_file.bat | 0 .../addon/nmsdk/Tools}/read_nmsdk_version.sh | 0 .../nmsdk/Tools}/utilities/bh_converter.py | 0 .../addon/nmsdk/Tools}/utilities/enum_fix.py | 0 .../addon/nmsdk/Tools}/utilities/prettify.py | 0 .../nmsdk/Tools}/utilities/struct_convert.py | 0 .../nmsdk/Tools}/utilities/struct_gen.py | 0 .../addon/nmsdk/Tools}/wckToolTips.py | 0 src/addon/nmsdk/__init__.py | 193 ++++++++++++++++++ src/addon/nmsdk/blender_manifest.toml | 36 ++++ .../NMS_Structures/NMS_types.py | 6 +- .../NMS_Structures/Structures.py | 6 +- .../serialization}/NMS_Structures/__init__.py | 2 +- .../nmsdk/serialization}/StreamCompiler.py | 2 +- .../serialization}/cereal_bin/basic_types.py | 0 .../serialization}/cereal_bin/structdata.py | 0 .../formats/INT_2_10_10_10_REV.py | 0 .../nmsdk/serialization}/formats/__init__.py | 0 .../nmsdk/serialization}/formats/half.py | 0 .../nmsdk/serialization}/formats/ubyte.py | 0 .../addon/nmsdk/serialization}/list_header.py | 0 .../nmsdk/serialization}/mbincompiler.py | 2 +- .../addon/nmsdk/serialization}/serializers.py | 4 +- .../nmsdk/serialization}/tests/__init__.py | 0 .../nmsdk/serialization}/tests/test_half.py | 0 .../tests/test_int_2_10_10_10_rev.py | 0 .../addon/nmsdk/serialization}/utils.py | 0 .../addon/nmsdk/utils}/DirectXTex/LICENSE | 0 .../addon/nmsdk/utils}/DirectXTex/texconv.exe | Bin {utils => src/addon/nmsdk/utils}/bpyutils.py | 0 .../addon/nmsdk/utils}/image_convert.py | 0 {utils => src/addon/nmsdk/utils}/io.py | 0 {utils => src/addon/nmsdk/utils}/misc.py | 0 src/addon/nmsdk/utils/pak_handler.py | 6 + {utils => src/addon/nmsdk/utils}/settings.py | 2 +- {utils => src/addon/nmsdk/utils}/utils.py | 0 .../wheels/hgpaktool-1.0.1-py3-none-any.whl | Bin 0 -> 21606 bytes uv.lock | 161 +++++++++++++++ 164 files changed, 744 insertions(+), 238 deletions(-) delete mode 100644 __init__.py create mode 100644 build_cmf create mode 100644 pyproject.toml rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/ContextMenu.py (99%) rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/CustomNodes.py (100%) rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/EntityPanels.py (100%) rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/NMSObjectsPanels.py (99%) rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/NMSShaderNode.py (100%) create mode 100644 src/addon/nmsdk/BlenderExtensions/SceneExplorer-maybe.py create mode 100644 src/addon/nmsdk/BlenderExtensions/SceneExplorer.py rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/SettingsPanel.py (100%) rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/UIWidgets.py (100%) rename {BlenderExtensions => src/addon/nmsdk/BlenderExtensions}/__init__.py (100%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/ActionTriggerParser.py (97%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/Descriptor.py (97%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/__init__.py (100%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/addon_script.py (98%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/animations.py (97%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/cmd_export.py (100%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/export.py (98%) rename {ModelExporter => src/addon/nmsdk/ModelExporter}/utils.py (88%) rename {ModelImporter => src/addon/nmsdk/ModelImporter}/SceneNodeData.py (98%) rename {ModelImporter => src/addon/nmsdk/ModelImporter}/animation_handler.py (99%) rename {ModelImporter => src/addon/nmsdk/ModelImporter}/import_scene.py (99%) rename {ModelImporter => src/addon/nmsdk/ModelImporter}/mesh_utils.py (100%) rename {ModelImporter => src/addon/nmsdk/ModelImporter}/readers.py (96%) rename {NMS => src/addon/nmsdk/NMS}/LOOKUPS.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/Empty.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/Errors.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcAISpaceshipComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcAISpaceshipTypes.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcActionTrigger.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcActionTriggerState.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcAlienPuzzleMissionOverride.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcAlienRace.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcAnimFrameEvent.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcBeenShotEvent.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcCameraShakeAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcCustomInventoryComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcDestroyAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcDestructableComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcDiscoveryTypes.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcDisplayText.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcEncounterComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcEncyclopediaComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcGoToStateAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcInteractionActivationCost.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcInteractionComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcInteractionDof.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcInteractionType.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcInventoryTechProbability.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcNodeActivationAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcObjectPlacementComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcPainAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcParticleAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcPlayAnimAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcPlayAudioAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcPlayerNearbyEvent.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcPrimaryAxis.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcProjectileImpactType.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcRarity.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcRealitySubstanceCategory.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcRewardAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcScannableComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcScannerIconTypes.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcShootableComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcSimpleInteractionComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcSizeIndicator.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcSpaceshipClasses.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcSpaceshipComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcSpawnAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcStatTrackType.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcStateTimeEvent.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcStatsEnum.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcSubstanceAmount.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcTriggerActionComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/GcWarpAction.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/List.py (98%) rename {NMS => src/addon/nmsdk/NMS}/classes/NMSString0x10.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/NMSString0x20.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/NMSString0x80.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/Object.py (99%) rename {NMS => src/addon/nmsdk/NMS}/classes/PointLight.py (95%) rename {NMS => src/addon/nmsdk/NMS}/classes/Quaternion.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/String.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/Struct.py (97%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAnimMetadata.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAnimNodeData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAnimNodeFrameData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAnimationComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAnimationData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAnimationGameData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAttachmentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAudioAnimTrigger.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkAudioComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkCameraWanderData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkGeometryData.py (99%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkMaterialData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkMaterialFlags.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkMaterialSampler.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkMaterialUniform_Float.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkMaterialUniform_UInt.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkMeshData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkMeshMetaData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkModelDescriptorList.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkModelRendererCameraData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkModelRendererData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkPhysicsComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkPhysicsData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkResourceDescriptorData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkResourceDescriptorList.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkRotationComponentData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkSceneNodeAttributeData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkSceneNodeData.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkTextureResource.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkTransformData.py (95%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkVertexElement.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkVertexLayout.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/TkVolumeTriggerType.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/Vector4f.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/Vector4i.py (100%) rename {NMS => src/addon/nmsdk/NMS}/classes/__init__.py (100%) rename {NMS => src/addon/nmsdk/NMS}/material_node.py (98%) rename NMSDK.py => src/addon/nmsdk/NMSDK.py (98%) rename {tools => src/addon/nmsdk/Tools}/build.py (100%) rename {Tools => src/addon/nmsdk/Tools}/proc_gen.py (98%) rename {tools => src/addon/nmsdk/Tools}/process_file.bat (100%) rename {tools => src/addon/nmsdk/Tools}/read_nmsdk_version.sh (100%) rename {Tools => src/addon/nmsdk/Tools}/utilities/bh_converter.py (100%) rename {Tools => src/addon/nmsdk/Tools}/utilities/enum_fix.py (100%) rename {Tools => src/addon/nmsdk/Tools}/utilities/prettify.py (100%) rename {Tools => src/addon/nmsdk/Tools}/utilities/struct_convert.py (100%) rename {Tools => src/addon/nmsdk/Tools}/utilities/struct_gen.py (100%) rename {Tools => src/addon/nmsdk/Tools}/wckToolTips.py (100%) create mode 100644 src/addon/nmsdk/__init__.py create mode 100644 src/addon/nmsdk/blender_manifest.toml rename {serialization => src/addon/nmsdk/serialization}/NMS_Structures/NMS_types.py (96%) rename {serialization => src/addon/nmsdk/serialization}/NMS_Structures/Structures.py (98%) rename {serialization => src/addon/nmsdk/serialization}/NMS_Structures/__init__.py (68%) rename {serialization => src/addon/nmsdk/serialization}/StreamCompiler.py (99%) rename {serialization => src/addon/nmsdk/serialization}/cereal_bin/basic_types.py (100%) rename {serialization => src/addon/nmsdk/serialization}/cereal_bin/structdata.py (100%) rename {serialization => src/addon/nmsdk/serialization}/formats/INT_2_10_10_10_REV.py (100%) rename {serialization => src/addon/nmsdk/serialization}/formats/__init__.py (100%) rename {serialization => src/addon/nmsdk/serialization}/formats/half.py (100%) rename {serialization => src/addon/nmsdk/serialization}/formats/ubyte.py (100%) rename {serialization => src/addon/nmsdk/serialization}/list_header.py (100%) rename {serialization => src/addon/nmsdk/serialization}/mbincompiler.py (97%) rename {serialization => src/addon/nmsdk/serialization}/serializers.py (91%) rename {serialization => src/addon/nmsdk/serialization}/tests/__init__.py (100%) rename {serialization => src/addon/nmsdk/serialization}/tests/test_half.py (100%) rename {serialization => src/addon/nmsdk/serialization}/tests/test_int_2_10_10_10_rev.py (100%) rename {serialization => src/addon/nmsdk/serialization}/utils.py (100%) rename {utils => src/addon/nmsdk/utils}/DirectXTex/LICENSE (100%) rename {utils => src/addon/nmsdk/utils}/DirectXTex/texconv.exe (100%) rename {utils => src/addon/nmsdk/utils}/bpyutils.py (100%) rename {utils => src/addon/nmsdk/utils}/image_convert.py (100%) rename {utils => src/addon/nmsdk/utils}/io.py (100%) rename {utils => src/addon/nmsdk/utils}/misc.py (100%) create mode 100644 src/addon/nmsdk/utils/pak_handler.py rename {utils => src/addon/nmsdk/utils}/settings.py (97%) rename {utils => src/addon/nmsdk/utils}/utils.py (100%) create mode 100644 src/addon/nmsdk/wheels/hgpaktool-1.0.1-py3-none-any.whl create mode 100644 uv.lock 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..ea7d63e --- /dev/null +++ b/build_cmf @@ -0,0 +1 @@ +blender --command extension build --source-dir "C:\Users\msval\Documents\GitHub\NMSDK\addons\NMSDK\src\addon\nmsdk" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a1f7716 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[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" +] +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 100% rename from BlenderExtensions/SettingsPanel.py rename to src/addon/nmsdk/BlenderExtensions/SettingsPanel.py 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 97% rename from ModelExporter/Descriptor.py rename to src/addon/nmsdk/ModelExporter/Descriptor.py index 49b183c..0bb01bc 100644 --- a/ModelExporter/Descriptor.py +++ b/src/addon/nmsdk/ModelExporter/Descriptor.py @@ -4,10 +4,10 @@ __author__ = "monkeyman192" -from utils.misc import get_obj_name -from NMS.classes import (List, TkModelDescriptorList, NMSString0x80, +from ..utils.misc import get_obj_name +from ..NMS.classes import (List, TkModelDescriptorList, NMSString0x80, TkResourceDescriptorList, TkResourceDescriptorData) -from ModelExporter.utils import get_children +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..51709bf 100644 --- a/ModelExporter/addon_script.py +++ b/src/addon/nmsdk/ModelExporter/addon_script.py @@ -11,22 +11,22 @@ 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, +from .utils import calc_tangents +from ..utils.misc import CompareMatrices, get_obj_name +from ..utils.image_convert import convert_image +from .animations import process_anims +from .export import Export +from .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 +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 .ActionTriggerParser import ParseNodes +from ..serialization.NMS_Structures.Structures import TkTransformData import numpy as np 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 98% rename from ModelExporter/export.py rename to src/addon/nmsdk/ModelExporter/export.py index cdde485..d09df05 100644 --- a/ModelExporter/export.py +++ b/src/addon/nmsdk/ModelExporter/export.py @@ -18,23 +18,22 @@ import os 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 ( +from ..NMS.classes import TkAttachmentData +from ..NMS.LOOKUPS import 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 ..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.StreamCompiler import StreamData +from ..serialization.serializers import serialize_vertex_stream +from .utils import nmsHash, traverse class Export(): diff --git a/ModelExporter/utils.py b/src/addon/nmsdk/ModelExporter/utils.py similarity index 88% rename from ModelExporter/utils.py rename to src/addon/nmsdk/ModelExporter/utils.py index 403639f..a5e2d2e 100644 --- a/ModelExporter/utils.py +++ b/src/addon/nmsdk/ModelExporter/utils.py @@ -7,8 +7,6 @@ # blender imports import bpy from mathutils import Matrix, Vector -# Internal imports -from NMS.classes import Vector4f ALL_TYPES = ['Reference', 'Mesh', 'Locator', 'Collision', 'Light', 'Joint'] @@ -171,31 +169,6 @@ def apply_local_transform(rotmat, data, normalize=True, use_norm_mat=False): data[i] = (_data[0], _data[1], _data[2], 1.0) -# TODO: !REMOVE -def apply_local_transforms(rotmat, verts, norms, tangents, chverts): - norm_mat = rotmat.inverted().transposed() - - for i in range(len(verts)): - # Load Vertex - vert = rotmat * Vector((verts[i])) - # Store Transformed - verts[i] = (vert[0], vert[1], vert[2], 1.0) - # Load Normal - norm = norm_mat * Vector((norms[i])) - norm.normalize() - # Store Transformed normal - norms[i] = (norm[0], norm[1], norm[2], 1.0) - # Load Tangent - tang = norm_mat * Vector((tangents[i])) - tang.normalize() - # Store Transformed tangent - tangents[i] = (tang[0], tang[1], tang[2], 1.0) - for i in range(len(chverts)): - chvert = rotmat * Vector((chverts[i])) - # chvert = chverts[i] - chverts[i] = Vector4f(x=chvert[0], y=chvert[1], z=chvert[2], t=1.0) - - def calc_tangents(verts: Tuple[Vector], uvs: Tuple[Vector], normal: Vector) -> Vector: diff --git a/ModelImporter/SceneNodeData.py b/src/addon/nmsdk/ModelImporter/SceneNodeData.py similarity index 98% rename from ModelImporter/SceneNodeData.py rename to src/addon/nmsdk/ModelImporter/SceneNodeData.py index d44d0f4..bfdbcec 100644 --- a/ModelImporter/SceneNodeData.py +++ b/src/addon/nmsdk/ModelImporter/SceneNodeData.py @@ -3,7 +3,7 @@ from mathutils import Matrix, Euler import numpy as np -from serialization.NMS_Structures import TkSceneNodeData +from ..serialization.NMS_Structures import TkSceneNodeData class SceneNodeData(): """ Our own internal representation of the TkSceneNodeData class. diff --git a/ModelImporter/animation_handler.py b/src/addon/nmsdk/ModelImporter/animation_handler.py similarity index 99% rename from ModelImporter/animation_handler.py rename to src/addon/nmsdk/ModelImporter/animation_handler.py index 42e21d0..4ea5f1c 100644 --- a/ModelImporter/animation_handler.py +++ b/src/addon/nmsdk/ModelImporter/animation_handler.py @@ -8,8 +8,8 @@ from mathutils import Vector, Quaternion # Internal imports -from ModelImporter.readers import read_anim -from serialization.NMS_Structures import TkAnimMetadata +from .readers import read_anim +from ..serialization.NMS_Structures import TkAnimMetadata DATA_PATH_MAP = {'Rotation': 'rotation_quaternion', diff --git a/ModelImporter/import_scene.py b/src/addon/nmsdk/ModelImporter/import_scene.py similarity index 99% rename from ModelImporter/import_scene.py rename to src/addon/nmsdk/ModelImporter/import_scene.py index eebc76d..2275f3b 100644 --- a/ModelImporter/import_scene.py +++ b/src/addon/nmsdk/ModelImporter/import_scene.py @@ -13,21 +13,21 @@ from mathutils import Matrix, Vector, Quaternion # noqa pylint: disable=import-error # 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 ( +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 .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 .SceneNodeData import SceneNodeData +from .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 ( +from ..serialization.NMS_Structures.Structures import ( TkSceneNodeData, TkGeometryData, TkModelDescriptorList, NAMEHASH_MAPPING ) -from serialization.NMS_Structures.NMS_types import MBINHeader +from ..serialization.NMS_Structures.NMS_types import MBINHeader VERT_TYPE_MAP = { 5121: {'size': 1, 'np_fmt': "4B"}, 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 96% rename from ModelImporter/readers.py rename to src/addon/nmsdk/ModelImporter/readers.py index ebf208d..be6fe8d 100644 --- a/ModelImporter/readers.py +++ b/src/addon/nmsdk/ModelImporter/readers.py @@ -4,11 +4,11 @@ # 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.utils import read_string, bytes_to_quat +from ..serialization.list_header import ListHeader +from ..utils.utils import mxml_to_dict -from serialization.NMS_Structures import TkMaterialData, MBINHeader, NAMEHASH_MAPPING, TkAnimMetadata +from ..serialization.NMS_Structures import TkMaterialData, MBINHeader, NAMEHASH_MAPPING, TkAnimMetadata class gstream_info(NamedTuple): 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 99% rename from NMS/classes/Object.py rename to src/addon/nmsdk/NMS/classes/Object.py index 9df5fd9..da6615c 100644 --- a/NMS/classes/Object.py +++ b/src/addon/nmsdk/NMS/classes/Object.py @@ -4,7 +4,7 @@ from typing import Optional -from serialization.NMS_Structures.Structures import TkSceneNodeAttributeData, TkSceneNodeData, TkTransformData +from ...serialization.NMS_Structures.Structures import TkSceneNodeAttributeData, TkSceneNodeData, TkTransformData from .TkMaterialData import TkMaterialData from .TkPhysicsComponentData import TkPhysicsComponentData from .List import List 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 98% rename from NMS/material_node.py rename to src/addon/nmsdk/NMS/material_node.py index 0bf2228..9b7d274 100644 --- a/NMS/material_node.py +++ b/src/addon/nmsdk/NMS/material_node.py @@ -1,11 +1,10 @@ import bpy -from mathutils import Vector 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 +from .LOOKUPS import DIFFUSE, MASKS, NORMAL, DIFFUSE2 +from ..ModelImporter.readers import read_material +from ..utils.io import realize_path def create_material_node(mat_path: str, local_root_directory: str): diff --git a/NMSDK.py b/src/addon/nmsdk/NMSDK.py similarity index 98% rename from NMSDK.py rename to src/addon/nmsdk/NMSDK.py index 6fa83d0..9bd89dd 100644 --- a/NMSDK.py +++ b/src/addon/nmsdk/NMSDK.py @@ -1,20 +1,24 @@ +# pyright: reportInvalidTypeForm=false + # stdlib imports -from math import radians import os.path as op +from math import radians -# 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 +from .BlenderExtensions.UIWidgets import ShowMessageBox +from .ModelExporter.addon_script import Exporter +from .ModelExporter.utils import get_all_actions, get_all_actions_in_scene + # 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 .ModelImporter.import_scene import ImportScene +from .utils.settings import read_settings, write_settings def set_import_export_defaults(cls, context): 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 98% rename from Tools/proc_gen.py rename to src/addon/nmsdk/Tools/proc_gen.py index d55af48..8ee4409 100644 --- a/Tools/proc_gen.py +++ b/src/addon/nmsdk/Tools/proc_gen.py @@ -15,11 +15,10 @@ # stdlib imports import os import subprocess -from tkinter import (Tk, StringVar, Frame, Label, Entry, LEFT, VERTICAL, RIGHT, - BOTH, Y, Button) +from tkinter import Tk, StringVar, Frame, Label, Entry, LEFT, VERTICAL, RIGHT, BOTH, Y, Button from tkinter import filedialog, simpledialog, ttk, messagebox # Internal imports -from ModelExporter.classes import (List, NMSString0x80, Model, Reference, +from ..NMS.classes import (List, NMSString0x80, Model, Reference, TkResourceDescriptorData, TkGeometryData, TkResourceDescriptorList, TkModelDescriptorList) 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..e72a474 --- /dev/null +++ b/src/addon/nmsdk/__init__.py @@ -0,0 +1,193 @@ +# pyright: reportInvalidTypeForm=false + +import os +import os.path as op +import time + +import bpy +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 +from .ModelImporter.animation_handler import AnimationHandler + +# 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, + _GetMBINCompilerLocation, + _GetPCBANKSFolder, + _ImportReferencedScene, + _LoadAnimation, + _PauseAnimation, + _PlayAnimation, + _RefreshAnimations, + _RemoveMBINCompilerLocation, + _RemovePCBANKSFolder, + _SaveDefaultSettings, + _StopAnimation, + _ToggleCollisionVisibility, +) +from .utils.pak_handler import PakInfo +from .utils.settings import read_settings, write_settings + +customNodes = NMSNodes() + + +def save_preferences(cls: "NMSDKPreferences", context: bpy.types.Context): + print("Saving preferences") + preferences = cls.as_dict() + current_settings = read_settings() + current_pcbanks_dir = current_settings.get("pcbanks_dir") + new_pcbanks_dir = preferences.get("pcbanks_dir") + write_settings(preferences) + from hgpaktool import HGPAKFile + if new_pcbanks_dir and current_pcbanks_dir != new_pcbanks_dir: + t0 = time.perf_counter() + # Load the data from the pak files. + 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"): + context.scene.nmsdk_pak_data.scene_paths.append(fname) + context.scene.nmsdk_pak_data.file_mapping[fname] = pakfname + # scene_paths.append(fname) + # file_mapping[fname] = pakfname + # elif lfname.endswith(".") + t1 = time.perf_counter() + print(f"Loaded {len(context.scene.nmsdk_pak_data.scene_paths)} scenes in {t1 - t0:.4f}s") + + +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', "") + ) + + def draw(self, context): + layout = self.layout + layout.label(text="NMSDK preferences") + layout.prop(self, "pcbanks_dir") + layout.prop(self, "unpacked_pcbanks_dir") + + def as_dict(self): + return { + "pcbanks_dir": self.pcbanks_dir, + "unpacked_pcbanks_dir": self.unpacked_pcbanks_dir + } + + +# 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, + PakInfo, +) + + +def register(): + bpy.utils.register_class(NMSDKPreferences) + for cls in classes: + register_class(cls) + bpy.types.Scene.nmsdk_pak_data = PointerProperty(type=PakInfo) + 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 + del bpy.types.Scene.nmsdk_pak_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) + + +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..9b27fed --- /dev/null +++ b/src/addon/nmsdk/blender_manifest.toml @@ -0,0 +1,36 @@ +schema_version = "1.0.0" + +id = "nmsdk" +version = "0.10.0" +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 = "4.2.0" + +license = [ + "SPDX:GPL-3.0-or-later", +] + +# HGPAKTool is a required dependency +wheels = [ + "./wheels/hgpaktool-1.0.1-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 96% rename from serialization/NMS_Structures/NMS_types.py rename to src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py index 9e5670e..acec40b 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) diff --git a/serialization/NMS_Structures/Structures.py b/src/addon/nmsdk/serialization/NMS_Structures/Structures.py similarity index 98% rename from serialization/NMS_Structures/Structures.py rename to src/addon/nmsdk/serialization/NMS_Structures/Structures.py index 4fd494a..c74ed27 100644 --- a/serialization/NMS_Structures/Structures.py +++ b/src/addon/nmsdk/serialization/NMS_Structures/Structures.py @@ -3,10 +3,10 @@ from dataclasses import dataclass import struct -from serialization.cereal_bin.structdata import datatype, Field -import serialization.cereal_bin.basic_types as bt +from ..cereal_bin.structdata import datatype, Field +from ..cereal_bin import basic_types as bt -from serialization.NMS_Structures.NMS_types import ( +from .NMS_types import ( Vector4f, NMS_list, astring, VariableSizeString, Quaternion_list, Vector4i ) diff --git a/serialization/NMS_Structures/__init__.py b/src/addon/nmsdk/serialization/NMS_Structures/__init__.py similarity index 68% rename from serialization/NMS_Structures/__init__.py rename to src/addon/nmsdk/serialization/NMS_Structures/__init__.py index 1a29460..4972462 100644 --- a/serialization/NMS_Structures/__init__.py +++ b/src/addon/nmsdk/serialization/NMS_Structures/__init__.py @@ -1,3 +1,3 @@ -from .Structures import TkMaterialData, TkSceneNodeData, TkGeometryData, TkAnimMetadata, NAMEHASH_MAPPING +from .Structures import TkMaterialData, TkSceneNodeData, TkGeometryData, TkAnimMetadata, NAMEHASH_MAPPING # noqa from .NMS_types import MBINHeader # noqa diff --git a/serialization/StreamCompiler.py b/src/addon/nmsdk/serialization/StreamCompiler.py similarity index 99% rename from serialization/StreamCompiler.py rename to src/addon/nmsdk/serialization/StreamCompiler.py index 5a080db..7747b86 100644 --- a/serialization/StreamCompiler.py +++ b/src/addon/nmsdk/serialization/StreamCompiler.py @@ -2,7 +2,7 @@ from struct import pack, unpack -from serialization.utils import pad, read_list_header, read_list_data, list_header +from .utils import pad, read_list_header, read_list_data, list_header class TkMeshMetaData(): diff --git a/serialization/cereal_bin/basic_types.py b/src/addon/nmsdk/serialization/cereal_bin/basic_types.py similarity index 100% rename from serialization/cereal_bin/basic_types.py rename to src/addon/nmsdk/serialization/cereal_bin/basic_types.py diff --git a/serialization/cereal_bin/structdata.py b/src/addon/nmsdk/serialization/cereal_bin/structdata.py similarity index 100% rename from serialization/cereal_bin/structdata.py rename to src/addon/nmsdk/serialization/cereal_bin/structdata.py 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 100% 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 diff --git a/serialization/formats/__init__.py b/src/addon/nmsdk/serialization/formats/__init__.py similarity index 100% rename from serialization/formats/__init__.py rename to src/addon/nmsdk/serialization/formats/__init__.py diff --git a/serialization/formats/half.py b/src/addon/nmsdk/serialization/formats/half.py similarity index 100% rename from serialization/formats/half.py rename to src/addon/nmsdk/serialization/formats/half.py diff --git a/serialization/formats/ubyte.py b/src/addon/nmsdk/serialization/formats/ubyte.py similarity index 100% rename from serialization/formats/ubyte.py rename to src/addon/nmsdk/serialization/formats/ubyte.py diff --git a/serialization/list_header.py b/src/addon/nmsdk/serialization/list_header.py similarity index 100% rename from serialization/list_header.py rename to src/addon/nmsdk/serialization/list_header.py diff --git a/serialization/mbincompiler.py b/src/addon/nmsdk/serialization/mbincompiler.py similarity index 97% rename from serialization/mbincompiler.py rename to src/addon/nmsdk/serialization/mbincompiler.py index 784ed62..c1ad704 100644 --- a/serialization/mbincompiler.py +++ b/src/addon/nmsdk/serialization/mbincompiler.py @@ -1,7 +1,7 @@ __author__ = "monkeyman192" __version__ = "0.5" -from serialization.utils import serialize, pad +from .utils import serialize, pad class mbinCompiler(): diff --git a/serialization/serializers.py b/src/addon/nmsdk/serialization/serializers.py similarity index 91% rename from serialization/serializers.py rename to src/addon/nmsdk/serialization/serializers.py index 2efab97..af0afbc 100644 --- a/serialization/serializers.py +++ b/src/addon/nmsdk/serialization/serializers.py @@ -1,8 +1,8 @@ from array import array from typing import List -from NMS.LOOKUPS import SERIALIZE_FMT_MAP, REV_SEMANTICS -from serialization.formats import write_half, write_int_2_10_10_10_rev, ubytes_to_bytes +from ..NMS.LOOKUPS import SERIALIZE_FMT_MAP, REV_SEMANTICS +from .formats import write_half, write_int_2_10_10_10_rev, ubytes_to_bytes def serialize_geometry_stream(data): diff --git a/serialization/tests/__init__.py b/src/addon/nmsdk/serialization/tests/__init__.py similarity index 100% rename from serialization/tests/__init__.py rename to src/addon/nmsdk/serialization/tests/__init__.py diff --git a/serialization/tests/test_half.py b/src/addon/nmsdk/serialization/tests/test_half.py similarity index 100% rename from serialization/tests/test_half.py rename to src/addon/nmsdk/serialization/tests/test_half.py diff --git a/serialization/tests/test_int_2_10_10_10_rev.py b/src/addon/nmsdk/serialization/tests/test_int_2_10_10_10_rev.py similarity index 100% rename from serialization/tests/test_int_2_10_10_10_rev.py rename to src/addon/nmsdk/serialization/tests/test_int_2_10_10_10_rev.py diff --git a/serialization/utils.py b/src/addon/nmsdk/serialization/utils.py similarity index 100% rename from serialization/utils.py rename to src/addon/nmsdk/serialization/utils.py diff --git a/utils/DirectXTex/LICENSE b/src/addon/nmsdk/utils/DirectXTex/LICENSE similarity index 100% rename from utils/DirectXTex/LICENSE rename to src/addon/nmsdk/utils/DirectXTex/LICENSE diff --git a/utils/DirectXTex/texconv.exe b/src/addon/nmsdk/utils/DirectXTex/texconv.exe similarity index 100% rename from utils/DirectXTex/texconv.exe rename to src/addon/nmsdk/utils/DirectXTex/texconv.exe diff --git a/utils/bpyutils.py b/src/addon/nmsdk/utils/bpyutils.py similarity index 100% rename from utils/bpyutils.py rename to src/addon/nmsdk/utils/bpyutils.py diff --git a/utils/image_convert.py b/src/addon/nmsdk/utils/image_convert.py similarity index 100% rename from utils/image_convert.py rename to src/addon/nmsdk/utils/image_convert.py diff --git a/utils/io.py b/src/addon/nmsdk/utils/io.py similarity index 100% rename from utils/io.py rename to src/addon/nmsdk/utils/io.py diff --git a/utils/misc.py b/src/addon/nmsdk/utils/misc.py similarity index 100% rename from utils/misc.py rename to src/addon/nmsdk/utils/misc.py diff --git a/src/addon/nmsdk/utils/pak_handler.py b/src/addon/nmsdk/utils/pak_handler.py new file mode 100644 index 0000000..1bef3d1 --- /dev/null +++ b/src/addon/nmsdk/utils/pak_handler.py @@ -0,0 +1,6 @@ +from bpy.types import PropertyGroup + + +class PakInfo(PropertyGroup): + scene_paths = [] + file_mapping: dict[str, str] = {} \ No newline at end of file diff --git a/utils/settings.py b/src/addon/nmsdk/utils/settings.py similarity index 97% rename from utils/settings.py rename to src/addon/nmsdk/utils/settings.py index 20670c5..2731eb1 100644 --- a/utils/settings.py +++ b/src/addon/nmsdk/utils/settings.py @@ -7,7 +7,7 @@ from platform import system as os_name # local imports -from utils.io import get_MBINCompiler_path +from .io import get_MBINCompiler_path _os_name = os_name() diff --git a/utils/utils.py b/src/addon/nmsdk/utils/utils.py similarity index 100% rename from utils/utils.py rename to src/addon/nmsdk/utils/utils.py diff --git a/src/addon/nmsdk/wheels/hgpaktool-1.0.1-py3-none-any.whl b/src/addon/nmsdk/wheels/hgpaktool-1.0.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..f491de863c647d6ffbeaeb1cb6c11b946884527b GIT binary patch literal 21606 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 er+ Date: Wed, 18 Feb 2026 10:20:55 +1100 Subject: [PATCH 2/7] Lots of performance improvements and support for 6.2X model format --- .gitignore | 1 + pyproject.toml | 3 +- src/addon/nmsdk/ModelExporter/export.py | 99 ++-- .../nmsdk/ModelImporter/SceneNodeData.py | 9 +- src/addon/nmsdk/ModelImporter/import_scene.py | 453 +++++++++++------- src/addon/nmsdk/ModelImporter/readers.py | 49 +- src/addon/nmsdk/NMS/classes/Object.py | 6 +- src/addon/nmsdk/NMS/material_node.py | 145 +++--- src/addon/nmsdk/NMSDK.py | 64 ++- src/addon/nmsdk/__init__.py | 47 +- src/addon/nmsdk/blender_manifest.toml | 6 +- .../NMS_Structures/Structures.py | 2 +- .../serialization/cereal_bin/structdata.py | 9 +- src/addon/nmsdk/serialization/serializers.py | 2 +- src/addon/nmsdk/utils/io.py | 94 +++- src/addon/nmsdk/utils/pak_handler.py | 3 + .../wheels/hgpaktool-1.0.1-py3-none-any.whl | Bin 21606 -> 0 bytes .../wheels/hgpaktool-1.1.3-py3-none-any.whl | Bin 0 -> 24212 bytes uv.lock | 311 +++++++----- 19 files changed, 800 insertions(+), 503 deletions(-) delete mode 100644 src/addon/nmsdk/wheels/hgpaktool-1.0.1-py3-none-any.whl create mode 100644 src/addon/nmsdk/wheels/hgpaktool-1.1.3-py3-none-any.whl 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/pyproject.toml b/pyproject.toml index a1f7716..655e5dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,8 @@ maintainers = [ ] dependencies = [ "fake-bpy-module", - "hgpaktool" + "hgpaktool", + "numpy", ] dynamic = ["version"] diff --git a/src/addon/nmsdk/ModelExporter/export.py b/src/addon/nmsdk/ModelExporter/export.py index d09df05..3b5fdc9 100644 --- a/src/addon/nmsdk/ModelExporter/export.py +++ b/src/addon/nmsdk/ModelExporter/export.py @@ -9,30 +9,31 @@ __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 -import struct from itertools import accumulate -# Internal imports + +import bpy +import numpy as np + from ..NMS.classes import TkAttachmentData -from ..NMS.LOOKUPS import SEMANTICS, STRIDES, VERTS from ..NMS.classes.Object import Model +from ..NMS.LOOKUPS import SEMANTICS, STRIDES, UVS, VERTS from ..serialization.NMS_Structures import MBINHeader from ..serialization.NMS_Structures.Structures import ( - TkMeshData, TkGeometryStreamData, TkVertexLayout, TkVertexElement, TkMeshMetaData + TkGeometryData as TkGeometryData_new, ) from ..serialization.NMS_Structures.Structures import ( - TkGeometryData as TkGeometryData_new, + TkGeometryStreamData, + TkMeshData, + TkMeshMetaData, + TkVertexElement, + TkVertexLayout, ) -from ..serialization.StreamCompiler import StreamData from ..serialization.serializers import serialize_vertex_stream +from ..serialization.StreamCompiler import StreamData from .utils import nmsHash, traverse @@ -228,21 +229,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. @@ -277,15 +275,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) @@ -347,7 +345,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(" Toggle System Console)") + # 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 @@ -171,36 +198,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') + self.geometry_stream_file = self.geometry_fname.lower().replace('geometry', 'geometry.data') - # get the information about what data the geometry file contains + # Get the information about what data the geometry file contains - with open(self.geometry_file, "rb") as f: + 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"] - t1 = time.perf_counter() geometry_data = TkGeometryData.read(f) - t2 = time.perf_counter() - print(f"Loaded {self.geometry_file} in {t2 - t1:.03f}s") if geometry_data.Indices16Bit: self.mesh_indexes = [] @@ -261,6 +289,7 @@ def __init__(self, fpath, parent_obj=None, ref_scenes=dict(), def load_animations(self): """ 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 @@ -270,7 +299,8 @@ def load_animations(self): # Iterate over the entity files to collate all the animation data local_anims = dict() for entity in self.entities: - entity_path = op.join(self.PCBANKS_dir, entity) + entity_path = op.join(self.root_dir, entity) + # print(f"Entity path: {entity_path}") local_anims.update(read_entity_animation_data(entity_path)) if len(local_anims) == 0: # If there are no animations added by this scene just return to @@ -296,7 +326,7 @@ def load_animations(self): else: load_anims = True - self._fix_anim_data(local_anims, self.PCBANKS_dir) + 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 +352,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): @@ -367,39 +395,53 @@ def render_scene(self): 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: + 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) + 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. @@ -425,7 +467,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'} @@ -521,9 +563,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] @@ -603,23 +645,26 @@ 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 self.local_objects[self.scene_basename] = empty_obj return empty_obj @@ -675,19 +720,25 @@ 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): + 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) + 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: @@ -901,8 +952,7 @@ def _add_existing_to_scene(self): 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 +963,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 +991,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 @@ -959,14 +1009,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,17 +1028,13 @@ 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: @@ -1014,8 +1060,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 +1149,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 +1181,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 +1201,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 +1223,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 +1250,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 +1303,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 +1330,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 +1343,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 +1373,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/src/addon/nmsdk/ModelImporter/readers.py b/src/addon/nmsdk/ModelImporter/readers.py index be6fe8d..dbd102c 100644 --- a/src/addon/nmsdk/ModelImporter/readers.py +++ b/src/addon/nmsdk/ModelImporter/readers.py @@ -1,14 +1,14 @@ -import struct -from typing import Tuple, NamedTuple import os.path as op +import struct +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.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): @@ -140,7 +140,7 @@ def read_entity_animation_data(fname: str) -> dict: # TODO: Fix return anim_data -def read_material(fname): +def read_material(f: BufferedReader): """ Reads the textures and types from a material file. Returns @@ -149,37 +149,8 @@ def read_material(fname): Mapping between the texture type (one of DIFFUSE, MASKS, NORMAL) and the path to the texture """ - if not op.exists(fname): - return None - with open(fname, "rb") as f: - MBINHeader.read(f) - return TkMaterialData.read(f) - - -def read_gstream(fname: str, info: gstream_info) -> 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 + MBINHeader.read(f) + return TkMaterialData.read(f) def read_TkAnimationData(f) -> dict: diff --git a/src/addon/nmsdk/NMS/classes/Object.py b/src/addon/nmsdk/NMS/classes/Object.py index da6615c..7d3e81f 100644 --- a/src/addon/nmsdk/NMS/classes/Object.py +++ b/src/addon/nmsdk/NMS/classes/Object.py @@ -147,11 +147,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 diff --git a/src/addon/nmsdk/NMS/material_node.py b/src/addon/nmsdk/NMS/material_node.py index 9b7d274..17b6fb5 100644 --- a/src/addon/nmsdk/NMS/material_node.py +++ b/src/addon/nmsdk/NMS/material_node.py @@ -1,15 +1,29 @@ -import bpy - +import os import os.path as op -from .LOOKUPS import DIFFUSE, MASKS, NORMAL, DIFFUSE2 +import bpy + from ..ModelImporter.readers import read_material -from ..utils.io import realize_path +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: + mat_data = read_material(f) if mat_data is None: # no texture data so just exit this function. return @@ -63,11 +77,20 @@ 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: + 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) + # img.filepath = tex_path + 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 @@ -77,41 +100,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: @@ -125,11 +156,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 @@ -142,16 +172,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 @@ -162,23 +188,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 @@ -189,16 +209,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') @@ -233,10 +249,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'] @@ -245,22 +259,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/src/addon/nmsdk/NMSDK.py b/src/addon/nmsdk/NMSDK.py index 9bd89dd..29421be 100644 --- a/src/addon/nmsdk/NMSDK.py +++ b/src/addon/nmsdk/NMSDK.py @@ -19,6 +19,8 @@ # internal imports from .ModelImporter.import_scene import ImportScene from .utils.settings import read_settings, write_settings +from .utils.io import is_subdir +from .utils.pak_handler import pak_data def set_import_export_defaults(cls, context): @@ -56,6 +58,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', @@ -74,14 +89,6 @@ 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', @@ -854,6 +861,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( @@ -900,6 +912,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') @@ -918,22 +931,26 @@ def draw(self, context): debug_box.prop(self, 'draw_bounding_box') def execute(self, context): + addon_prefs = 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 + fpath = self.properties.filepath + print(f"I want to import {fpath}") 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) + 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) + # 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.render_scene() status = importer.state self.report({'INFO'}, "Models Imported Successfully") @@ -942,3 +959,16 @@ def execute(self, context): return {'FINISHED'} else: return {'CANCELLED'} + + def invoke(self, context, event): + addon_prefs = 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/src/addon/nmsdk/__init__.py b/src/addon/nmsdk/__init__.py index e72a474..5962745 100644 --- a/src/addon/nmsdk/__init__.py +++ b/src/addon/nmsdk/__init__.py @@ -1,5 +1,6 @@ # pyright: reportInvalidTypeForm=false +import json import os import os.path as op import time @@ -7,6 +8,7 @@ import bpy from bpy.props import PointerProperty, StringProperty from bpy.utils import register_class, unregister_class +from bpy.app.handlers import persistent # extensions to blender UI from .BlenderExtensions import ContextMenus, NMSEntities, NMSNodes, NMSPanels, SettingsPanels @@ -44,12 +46,25 @@ _StopAnimation, _ToggleCollisionVisibility, ) -from .utils.pak_handler import PakInfo +from .utils.io import hide_path +from .utils.pak_handler import PakInfo, pak_data 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): print("Saving preferences") preferences = cls.as_dict() @@ -60,20 +75,34 @@ def save_preferences(cls: "NMSDKPreferences", context: bpy.types.Context): 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"): - context.scene.nmsdk_pak_data.scene_paths.append(fname) - context.scene.nmsdk_pak_data.file_mapping[fname] = pakfname - # scene_paths.append(fname) - # file_mapping[fname] = pakfname - # elif lfname.endswith(".") + 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 {len(context.scene.nmsdk_pak_data.scene_paths)} scenes in {t1 - t0:.4f}s") + print(f"Loaded {counter} scenes into VFS in {t1 - t0:.4f}s") class NMSDKPreferences(bpy.types.AddonPreferences): @@ -98,6 +127,8 @@ class NMSDKPreferences(bpy.types.AddonPreferences): default=default_settings.get('unpacked_pcbanks_dir', "") ) + pak_mapping_data: dict[str, str] + def draw(self, context): layout = self.layout layout.label(text="NMSDK preferences") @@ -153,6 +184,7 @@ def menu_func_import(self, context): def register(): + # bpy.app.handlers.load_post.append(load_vfs_data) bpy.utils.register_class(NMSDKPreferences) for cls in classes: register_class(cls) @@ -187,6 +219,7 @@ def unregister(): SettingsPanels.unregister() ContextMenus.unregister() bpy.utils.unregister_class(NMSDKPreferences) + # bpy.app.handlers.load_post.remove(load_vfs_data) if __name__ == '__main__': diff --git a/src/addon/nmsdk/blender_manifest.toml b/src/addon/nmsdk/blender_manifest.toml index 9b27fed..bba9532 100644 --- a/src/addon/nmsdk/blender_manifest.toml +++ b/src/addon/nmsdk/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "nmsdk" -version = "0.10.0" +version = "0.10.0-alpha8" name = "NMSDK" tagline = "NMS model importer and exporter" maintainer = "monkeyman192" @@ -10,7 +10,7 @@ type = "add-on" website = "https://github.com/monkeyman192/NMSDK" tags = ["Import-Export"] -blender_version_min = "4.2.0" +blender_version_min = "5.0.0" license = [ "SPDX:GPL-3.0-or-later", @@ -18,7 +18,7 @@ license = [ # HGPAKTool is a required dependency wheels = [ - "./wheels/hgpaktool-1.0.1-py3-none-any.whl", + "./wheels/hgpaktool-1.1.3-py3-none-any.whl", ] [permissions] diff --git a/src/addon/nmsdk/serialization/NMS_Structures/Structures.py b/src/addon/nmsdk/serialization/NMS_Structures/Structures.py index c74ed27..42769ed 100644 --- a/src/addon/nmsdk/serialization/NMS_Structures/Structures.py +++ b/src/addon/nmsdk/serialization/NMS_Structures/Structures.py @@ -231,10 +231,10 @@ class TkAnimNodeFrameData(datatype): @dataclass class TkAnimNodeData(datatype): + Node: Annotated[str, Field(VariableSizeString)] RotIndex: Annotated[int, Field(bt.int32)] ScaleIndex: Annotated[int, Field(bt.int32)] TransIndex: Annotated[int, Field(bt.int32)] - Node: Annotated[str, Field(bt.string, length=0x40)] @dataclass diff --git a/src/addon/nmsdk/serialization/cereal_bin/structdata.py b/src/addon/nmsdk/serialization/cereal_bin/structdata.py index 4173dcf..42ffe96 100644 --- a/src/addon/nmsdk/serialization/cereal_bin/structdata.py +++ b/src/addon/nmsdk/serialization/cereal_bin/structdata.py @@ -1,9 +1,8 @@ -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 typing import Any, Optional, Type, TypeVar T = TypeVar("T", bound="datatype") N = TypeVar("N", bound=int) @@ -207,7 +206,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/src/addon/nmsdk/serialization/serializers.py b/src/addon/nmsdk/serialization/serializers.py index af0afbc..58b483d 100644 --- a/src/addon/nmsdk/serialization/serializers.py +++ b/src/addon/nmsdk/serialization/serializers.py @@ -9,7 +9,7 @@ def serialize_geometry_stream(data): pass -def serialize_vertex_stream(requires: List[str], count: int, **kwargs): +def serialize_vertex_stream(requires: List[int], count: int, **kwargs): """ Return a serialized version of the vertex data diff --git a/src/addon/nmsdk/utils/io.py b/src/addon/nmsdk/utils/io.py index 9729e95..891c3c9 100644 --- a/src/addon/nmsdk/utils/io.py +++ b/src/addon/nmsdk/utils/io.py @@ -1,15 +1,96 @@ # stdlib imports +import ctypes import os import os.path as op -from pathlib import Path import subprocess +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Union # Blender imports import bpy +from hgpaktool import HGPAKFile +from hgpaktool.utils import normalise_path + +ctypes.windll.kernel32.SetFileAttributesW.argtypes = (ctypes.c_wchar_p, ctypes.c_uint32) + + +@contextmanager +def load_file(fpath: Union[str, os.PathLike[str]], root_dir: str, from_pak: bool, pak_data: dict): + fpath = normalise_path(fpath) + if from_pak: + if (pakfile_path := pak_data.get(fpath)) is not None: + if not op.isabs(pakfile_path): + pakfile_path = op.join(root_dir, pakfile_path) + # with HGPAKFile(pakfile_path) as pak: + # for _, fdata in pak.extract(fpath): + # print(len(fdata)) + # yield BytesIO(fdata) + pak = HGPAKFile(pakfile_path) + data = pak.extract_specific(fpath, True) + data.seek(0) + yield data + else: + raise ValueError(f"Could not find {fpath!r} in pak index...") + else: + if op.isabs(fpath): + with open(fpath, "rb") as f: + yield f + else: + with open(op.join(root_dir, fpath), "rb") as f: + yield f + + +def load_file_unsafe(fpath: Union[str, os.PathLike[str]], root_dir: str, from_pak: bool, pak_data: dict): + # Same as the above function, but without any potential clean up. + # When loading from a pak file this is fine, but for loading from a disk it is not and we need to be + # careful... + fpath = normalise_path(fpath) + if from_pak: + if (pakfile_path := pak_data.get(fpath)) is not None: + if not op.isabs(pakfile_path): + pakfile_path = op.join(root_dir, pakfile_path) + pak = HGPAKFile(pakfile_path) + return pak.extract_specific(fpath, True) + else: + raise ValueError(f"Could not find {fpath!r} in pak index...") + else: + if op.isabs(fpath): + return open(fpath, "rb") + else: + return open(op.join(root_dir, fpath), "rb") -def get_NMS_dir(fpath: str) -> str: +def hide_path(fpath: str): + FILE_ATTRIBUTE_HIDDEN = 0x02 + + ret = ctypes.windll.kernel32.SetFileAttributesW(fpath, FILE_ATTRIBUTE_HIDDEN) + if ret: + print('attribute set to Hidden') + else: # return code of zero indicates failure -- raise a Windows error + raise ctypes.WinError() + + +def is_subdir(path: str, root: str): + """ Check to see if `path` stems from `root`.""" + path = os.path.realpath(path) + root = os.path.realpath(root) + + if op.splitdrive(path)[0] != op.splitdrive(root)[0]: + return False + + relative = os.path.relpath(path, root) + + if relative.startswith(os.pardir): + return False + else: + return True + + +def get_NMS_dir(fpath: Optional[str]) -> Optional[str]: """ Returns the NMS file directory from the given filepath. """ + if not fpath: + return None # If a PCBANKS path is specified in the scene, return it instead. if bpy.context.scene.nmsdk_default_settings.PCBANKS_directory != "": return bpy.context.scene.nmsdk_default_settings.PCBANKS_directory @@ -62,6 +143,15 @@ def base_path(abs_path: str, rel_path: str): return '' +def post_path(full_path: str, base_path: str): + """ Return the part of `full_path` after the `base_part` component.""" + if not is_subdir(full_path, base_path): + return None + f_parts = list(Path(full_path.lower()).parts) + b_parts = Path(base_path.lower()).parts + return str(Path(*f_parts[len(b_parts):])) + + def realize_path(fpath: str, local_root_directory: str) -> str: """ Return the provided path relativized to the PCBANKS folder or the local root directory if it exists. """ diff --git a/src/addon/nmsdk/utils/pak_handler.py b/src/addon/nmsdk/utils/pak_handler.py index 1bef3d1..9536a17 100644 --- a/src/addon/nmsdk/utils/pak_handler.py +++ b/src/addon/nmsdk/utils/pak_handler.py @@ -1,6 +1,9 @@ from bpy.types import PropertyGroup +pak_data = {} + + class PakInfo(PropertyGroup): scene_paths = [] file_mapping: dict[str, str] = {} \ No newline at end of file diff --git a/src/addon/nmsdk/wheels/hgpaktool-1.0.1-py3-none-any.whl b/src/addon/nmsdk/wheels/hgpaktool-1.0.1-py3-none-any.whl deleted file mode 100644 index f491de863c647d6ffbeaeb1cb6c11b946884527b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21606 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 er+47D2%>L+ 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 Date: Wed, 11 Mar 2026 09:09:58 +1100 Subject: [PATCH 3/7] Latest batch of changes --- .../nmsdk/BlenderExtensions/SettingsPanel.py | 20 - src/addon/nmsdk/ModelExporter/Descriptor.py | 9 +- src/addon/nmsdk/ModelExporter/addon_script.py | 51 +- src/addon/nmsdk/ModelExporter/export.py | 31 +- src/addon/nmsdk/ModelExporter/utils.py | 44 +- .../nmsdk/ModelImporter/animation_handler.py | 550 +++++++++--------- src/addon/nmsdk/ModelImporter/import_scene.py | 228 +++++--- src/addon/nmsdk/ModelImporter/readers.py | 68 --- src/addon/nmsdk/NMS/classes/Object.py | 44 +- src/addon/nmsdk/NMS/material_node.py | 5 +- src/addon/nmsdk/NMSDK.py | 187 +++--- src/addon/nmsdk/Tools/proc_gen.py | 35 +- src/addon/nmsdk/__init__.py | 36 +- src/addon/nmsdk/blender_manifest.toml | 2 +- .../serialization/NMS_Structures/NMS_types.py | 20 + .../NMS_Structures/Structures.py | 222 +++++-- .../serialization/cereal_bin/basic_types.py | 10 +- .../serialization/cereal_bin/structdata.py | 11 +- src/addon/nmsdk/serialization/serializers.py | 8 +- src/addon/nmsdk/utils/io.py | 39 +- src/addon/nmsdk/utils/settings.py | 38 +- src/addon/nmsdk/utils/stopwitch.py | 157 +++++ 22 files changed, 1015 insertions(+), 800 deletions(-) create mode 100644 src/addon/nmsdk/utils/stopwitch.py diff --git a/src/addon/nmsdk/BlenderExtensions/SettingsPanel.py b/src/addon/nmsdk/BlenderExtensions/SettingsPanel.py index a661639..a13e872 100644 --- a/src/addon/nmsdk/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/src/addon/nmsdk/ModelExporter/Descriptor.py b/src/addon/nmsdk/ModelExporter/Descriptor.py index 0bb01bc..7ce6c9b 100644 --- a/src/addon/nmsdk/ModelExporter/Descriptor.py +++ b/src/addon/nmsdk/ModelExporter/Descriptor.py @@ -4,9 +4,14 @@ __author__ = "monkeyman192" +from ..NMS.classes import ( + List, + NMSString0x80, + TkModelDescriptorList, + TkResourceDescriptorData, + TkResourceDescriptorList, +) from ..utils.misc import get_obj_name -from ..NMS.classes import (List, TkModelDescriptorList, NMSString0x80, - TkResourceDescriptorList, TkResourceDescriptorData) from .utils import get_children diff --git a/src/addon/nmsdk/ModelExporter/addon_script.py b/src/addon/nmsdk/ModelExporter/addon_script.py index 51709bf..882ff7d 100644 --- a/src/addon/nmsdk/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 .utils import calc_tangents -from ..utils.misc import CompareMatrices, get_obj_name + +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 .export import Export from .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 .ActionTriggerParser import ParseNodes -from ..serialization.NMS_Structures.Structures import TkTransformData - -import numpy as np - +from .export import Export +from .utils import calc_tangents ROT_X_MAT = Matrix.Rotation(radians(-90), 4, 'X') diff --git a/src/addon/nmsdk/ModelExporter/export.py b/src/addon/nmsdk/ModelExporter/export.py index 3b5fdc9..f9e12df 100644 --- a/src/addon/nmsdk/ModelExporter/export.py +++ b/src/addon/nmsdk/ModelExporter/export.py @@ -14,12 +14,15 @@ import subprocess from collections import OrderedDict as odict from itertools import accumulate +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 +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 ( @@ -34,7 +37,10 @@ ) from ..serialization.serializers import serialize_vertex_stream from ..serialization.StreamCompiler import StreamData -from .utils import nmsHash, traverse +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) @@ -355,7 +361,7 @@ def serialize_data(self): for i, md in enumerate(mesh_datas): StreamMetaDataArray.append(TkMeshMetaData( IdString=md.IdString.upper(), - Hash=md.Hash, + Hash=jenkins_one_at_a_time(md.IdString), IndexDataOffset=offsets[i][1], IndexDataSize=index_sizes[i], VertexDataOffset=offsets[i][0], @@ -695,10 +701,10 @@ def mix_streams(self): else: dtype = np.uint16 - index_array = np.concatenate( - self.np_indexes[mesh_col_offset:] + self.np_indexes[:mesh_col_offset], - dtype=dtype, - ) + # Write the index data to an array of bytes then back so that we can fake it being 32bit for + # serialization purposes. This is a crappy hack... + + index_array = np.concatenate(self.np_indexes[mesh_col_offset:], dtype=dtype) index_array_bytes = index_array.tobytes() if (dfct := (len(index_array_bytes) % 4)) != 0: index_array_bytes += b"\x00" * (4 - dfct) @@ -825,11 +831,10 @@ def convert_to_mbin(self): if os.path.splitext(location)[1].lower() == '.mxml': # Force MBINCompiler to overwrite existing files and # ignore errors. - mbincompiler_path = bpy.context.scene.nmsdk_default_settings.MBINCompiler_path # noqa - retcode = subprocess.call( - [mbincompiler_path, "-y", "-f", "-Q", location]) + addon_prefs: NMSDKPreferences = bpy.context.preferences.addons[_package].preferences + mbincompiler_path = addon_prefs.mbincompiler_path + retcode = subprocess.call([mbincompiler_path, "-y", "-f", "-Q", location]) if retcode == 0: os.remove(location) else: - print('MBINCompiler failed to run. Please ensure it ' - 'is registered on the path.') + print(f"MBINCompiler failed to run with error code {retcode}") diff --git a/src/addon/nmsdk/ModelExporter/utils.py b/src/addon/nmsdk/ModelExporter/utils.py index a5e2d2e..8c5fca8 100644 --- a/src/addon/nmsdk/ModelExporter/utils.py +++ b/src/addon/nmsdk/ModelExporter/utils.py @@ -1,13 +1,9 @@ # Miscellaneous useful functions -# stdlib imports -from array import array -from hashlib import sha256 -from typing import Tuple # blender imports import bpy -from mathutils import Matrix, Vector - +from mathutils import Matrix, Vector, Quaternion +from ..serialization.NMS_Structures.NMS_types import Vector4f ALL_TYPES = ['Reference', 'Mesh', 'Locator', 'Collision', 'Light', 'Joint'] @@ -112,21 +108,6 @@ def movetoindex(lst, i, index): return lst[:index] + [k] + lst[index:] -def nmsHash(data): - """ - Lazy hash function for mesh data - This is simply the last 16 hexadecimal digits of a sha256 hash. - Also just take the first 16 verts to save time... - """ - if isinstance(data, list): - d = array('f') - for verts in data[:16]: - d.extend(verts) - else: - d = data - return int(sha256(d).hexdigest()[-16:], 16) - - def traverse(obj): # a custom generator to iterate over the tree of all the children on the # scene (including the Model object) @@ -169,8 +150,8 @@ def apply_local_transform(rotmat, data, normalize=True, use_norm_mat=False): data[i] = (_data[0], _data[1], _data[2], 1.0) -def calc_tangents(verts: Tuple[Vector], - uvs: Tuple[Vector], +def calc_tangents(verts: tuple[Vector, Vector, Vector], + uvs: tuple[Vector, Vector, Vector], normal: Vector) -> 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 @@ -191,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/src/addon/nmsdk/ModelImporter/animation_handler.py b/src/addon/nmsdk/ModelImporter/animation_handler.py index 4ea5f1c..c8add30 100644 --- a/src/addon/nmsdk/ModelImporter/animation_handler.py +++ b/src/addon/nmsdk/ModelImporter/animation_handler.py @@ -1,328 +1,308 @@ -# 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 +from mathutils import Quaternion, Vector -# Internal imports -from .readers import read_anim +from ..ModelExporter.utils import transform_to_matrix 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 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 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) +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 - # 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. + # 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 - 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 + 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) - # 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: + # 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)) - 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) + bone = armature.pose.bones[name] - # 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)) + still_data = node_data['still'] + animated_data = node_data['anim'] - bone = armature.pose.bones[name] + location = None + rotation = None + scale = None - 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 transforms as required - for key, value in still_data.items(): - data = anim_data['StillFrameData'][key][value] + # 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]]) + 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])) - 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: + _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) - 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. +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: - 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(): + 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': - loc.x.keyframe_points.add(num_frames) - loc.y.keyframe_points.add(num_frames) - loc.z.keyframe_points.add(num_frames) + _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': - 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) + _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': - 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) + _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(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_animframe_data(fcurve, data, frame): + fcurve.keyframe_points[int(frame)].co = float(frame), float(data) - 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. +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' - 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) +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/src/addon/nmsdk/ModelImporter/import_scene.py b/src/addon/nmsdk/ModelImporter/import_scene.py index e918c77..c975ccb 100644 --- a/src/addon/nmsdk/ModelImporter/import_scene.py +++ b/src/addon/nmsdk/ModelImporter/import_scene.py @@ -1,36 +1,51 @@ # stdlib imports import json +import os import os.path as op +import shutil +import subprocess import time -from math import radians -from typing import Optional, cast import traceback +from math import radians +from tempfile import mkdtemp +from typing import TYPE_CHECKING, Optional, cast -import bmesh # pylint: disable=import-error +import bmesh # Blender imports import bpy import numpy as np from bpy.types import Armature from hgpaktool.utils import normalise_path -from mathutils import Matrix, Quaternion, Vector # noqa pylint: disable=import-error +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 + +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 ..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, read_entity_animation_data +from .readers import gstream_info from .SceneNodeData import SceneNodeData ROT_MATRIX = Matrix.Rotation(radians(90), 4, 'X') @@ -74,6 +89,7 @@ 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. """ + @witch.section("__init__") def __init__( self, fpath: str, @@ -83,7 +99,9 @@ def __init__( from_pak: bool = False, ): self.from_pak = from_pak - addon_prefs = bpy.context.preferences.addons[_package].preferences + self.ref_scenes = ref_scenes or {} + + 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") @@ -95,8 +113,8 @@ def __init__( vfs_path = None self.pak_data_mapping = {} - if ref_scenes is None: - ref_scenes = {} + mbincompiler_path = addon_prefs.mbincompiler_path + if settings is None: settings = {} if self.from_pak is True: @@ -111,14 +129,28 @@ def __init__( # 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] + + # 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 + + 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"): - raise TypeError( - f"Only .MBIN scenes are currently supported. Please convert to {self.scene_path} MBIN and try" - " again." - ) + # 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]) # 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 @@ -126,7 +158,6 @@ def __init__( 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. @@ -143,28 +174,22 @@ def __init__( # 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 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 + 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) - # 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: - self._add_existing_to_scene() - self.requires_render = False - return - - self.ref_scenes[self.scene_name] = list() + 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] = [] @@ -179,17 +204,6 @@ def __init__( # 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. @@ -224,11 +238,11 @@ def __init__( self.geometry_stream_file = self.geometry_fname.lower().replace('geometry', 'geometry.data') # Get the information about what data the geometry file contains - - 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) + 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 = [] @@ -287,7 +301,7 @@ def __init__( # 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... @@ -295,24 +309,35 @@ def load_animations(self): # 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.root_dir, entity) - # print(f"Entity path: {entity_path}") - 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: @@ -326,7 +351,19 @@ def load_animations(self): else: load_anims = True - self._fix_anim_data(local_anims, self.root_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 @@ -378,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 @@ -387,8 +425,7 @@ 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': @@ -416,8 +453,11 @@ def render_scene(self): 'versions.'.format(obj.Name)) continue try: - self.load_mesh(obj) - added_obj = self._add_mesh_to_scene(obj) + 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. @@ -451,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. @@ -459,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. @@ -479,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, @@ -487,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. @@ -511,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]), @@ -595,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 @@ -617,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) @@ -665,6 +704,7 @@ def _add_empty_to_scene(self, scene_node: SceneNodeData, self.descriptor_data = TkModelDescriptorList.read(f) else: pass + print(f"Adding {self.scene_basename} empty obj to local_objects") self.local_objects[self.scene_basename] = empty_obj return empty_obj @@ -690,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: @@ -723,8 +763,8 @@ def _add_empty_to_scene(self, scene_node: SceneNodeData, 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') + 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: @@ -732,18 +772,21 @@ def _add_empty_to_scene(self, scene_node: SceneNodeData, 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, - self.from_pak, - ) - 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 @@ -779,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: @@ -819,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: @@ -915,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: @@ -944,10 +987,8 @@ 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) @@ -1001,7 +1042,7 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, standalone: bool = False 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: @@ -1040,8 +1081,7 @@ def _add_mesh_to_scene(self, scene_node: SceneNodeData, standalone: bool = False 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) diff --git a/src/addon/nmsdk/ModelImporter/readers.py b/src/addon/nmsdk/ModelImporter/readers.py index dbd102c..4861239 100644 --- a/src/addon/nmsdk/ModelImporter/readers.py +++ b/src/addon/nmsdk/ModelImporter/readers.py @@ -1,4 +1,3 @@ -import os.path as op import struct from io import BufferedReader from typing import NamedTuple @@ -96,73 +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(' 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/src/addon/nmsdk/NMS/classes/Object.py b/src/addon/nmsdk/NMS/classes/Object.py index 7d3e81f..68cd665 100644 --- a/src/addon/nmsdk/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'] @@ -264,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'), ] @@ -468,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/src/addon/nmsdk/NMS/material_node.py b/src/addon/nmsdk/NMS/material_node.py index 17b6fb5..ea13a34 100644 --- a/src/addon/nmsdk/NMS/material_node.py +++ b/src/addon/nmsdk/NMS/material_node.py @@ -3,7 +3,7 @@ import bpy -from ..ModelImporter.readers import read_material +from ..serialization.NMS_Structures import MBINHeader, TkMaterialData from ..utils.io import load_file, normalise_path, realize_path from .LOOKUPS import DIFFUSE, DIFFUSE2, MASKS, NORMAL @@ -23,7 +23,8 @@ def create_material_node( if not op.exists(mat_path): return with load_file(mat_path, local_root_directory, from_pak, pak_data) as f: - mat_data = read_material(f) + MBINHeader.read(f) + mat_data = TkMaterialData.read(f) if mat_data is None: # no texture data so just exit this function. return diff --git a/src/addon/nmsdk/NMSDK.py b/src/addon/nmsdk/NMSDK.py index 29421be..05c803a 100644 --- a/src/addon/nmsdk/NMSDK.py +++ b/src/addon/nmsdk/NMSDK.py @@ -3,6 +3,7 @@ # stdlib imports import os.path as op from math import radians +from typing import TYPE_CHECKING import bpy @@ -16,11 +17,12 @@ from .ModelExporter.addon_script import Exporter from .ModelExporter.utils import get_all_actions, get_all_actions_in_scene -# internal imports +if TYPE_CHECKING: + from . import NMSDKPreferences from .ModelImporter.import_scene import ImportScene -from .utils.settings import read_settings, write_settings from .utils.io import is_subdir -from .utils.pak_handler import pak_data +from .utils.settings import read_settings, write_settings +from .utils.stopwitch import witch def set_import_export_defaults(cls, context): @@ -95,6 +97,11 @@ class ImportSceneOperator(Operator): 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', @@ -109,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 @@ -297,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 @@ -333,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... @@ -515,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']) @@ -646,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) @@ -818,18 +733,27 @@ def execute(self, context): 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'}: @@ -882,6 +806,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', @@ -921,9 +850,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') @@ -931,37 +863,48 @@ def draw(self, context): debug_box.prop(self, 'draw_bounding_box') def execute(self, context): - addon_prefs = context.preferences.addons[__package__].preferences + 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() - fpath = self.properties.filepath - print(f"I want to import {fpath}") context.scene['_anim_names'] = ['None'] + + # 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) - # 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.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 = context.preferences.addons[__package__].preferences + addon_prefs: NMSDKPreferences = context.preferences.addons[__package__].preferences if addon_prefs.unpacked_pcbanks_dir: # Use the preferentially. self.filepath = addon_prefs.unpacked_pcbanks_dir diff --git a/src/addon/nmsdk/Tools/proc_gen.py b/src/addon/nmsdk/Tools/proc_gen.py index 8ee4409..6b10220 100644 --- a/src/addon/nmsdk/Tools/proc_gen.py +++ b/src/addon/nmsdk/Tools/proc_gen.py @@ -15,16 +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 ..NMS.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/src/addon/nmsdk/__init__.py b/src/addon/nmsdk/__init__.py index 5962745..4d55213 100644 --- a/src/addon/nmsdk/__init__.py +++ b/src/addon/nmsdk/__init__.py @@ -8,11 +8,9 @@ import bpy from bpy.props import PointerProperty, StringProperty from bpy.utils import register_class, unregister_class -from bpy.app.handlers import persistent # extensions to blender UI from .BlenderExtensions import ContextMenus, NMSEntities, NMSNodes, NMSPanels, SettingsPanels -from .ModelImporter.animation_handler import AnimationHandler # External API operators # Main IO operators @@ -33,21 +31,16 @@ _ChangeAnimation, _FixActionNames, _FixOldFormat, - _GetMBINCompilerLocation, - _GetPCBANKSFolder, _ImportReferencedScene, _LoadAnimation, _PauseAnimation, _PlayAnimation, _RefreshAnimations, - _RemoveMBINCompilerLocation, - _RemovePCBANKSFolder, _SaveDefaultSettings, _StopAnimation, _ToggleCollisionVisibility, ) from .utils.io import hide_path -from .utils.pak_handler import PakInfo, pak_data from .utils.settings import read_settings, write_settings customNodes = NMSNodes() @@ -66,12 +59,12 @@ def save_preferences(cls: "NMSDKPreferences", context: bpy.types.Context): - print("Saving preferences") preferences = cls.as_dict() current_settings = read_settings() current_pcbanks_dir = current_settings.get("pcbanks_dir") new_pcbanks_dir = preferences.get("pcbanks_dir") - write_settings(preferences) + 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() @@ -117,14 +110,23 @@ class NMSDKPreferences(bpy.types.AddonPreferences): 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', "") + 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', "") + 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] @@ -134,11 +136,13 @@ def draw(self, context): layout.label(text="NMSDK preferences") layout.prop(self, "pcbanks_dir") 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 + "unpacked_pcbanks_dir": self.unpacked_pcbanks_dir, + "mbincompiler_path": self.mbincompiler_path } @@ -165,10 +169,6 @@ def menu_func_import(self, context): _FixOldFormat, _FixActionNames, _ImportReferencedScene, - _GetPCBANKSFolder, - _RemovePCBANKSFolder, - _GetMBINCompilerLocation, - _RemoveMBINCompilerLocation, _ToggleCollisionVisibility, _SaveDefaultSettings, _ChangeAnimation, @@ -177,9 +177,7 @@ def menu_func_import(self, context): _PlayAnimation, _PauseAnimation, _StopAnimation, - AnimationHandler, AnimProperties, - PakInfo, ) @@ -188,7 +186,6 @@ def register(): bpy.utils.register_class(NMSDKPreferences) for cls in classes: register_class(cls) - bpy.types.Scene.nmsdk_pak_data = PointerProperty(type=PakInfo) bpy.types.Scene.nmsdk_settings = PointerProperty(type=NMSDKSettings) bpy.types.Scene.nmsdk_default_settings = PointerProperty( type=NMSDKDefaultSettings) @@ -209,7 +206,6 @@ def unregister(): del bpy.types.Scene.nmsdk_settings del bpy.types.Scene.nmsdk_default_settings del bpy.types.Scene.nmsdk_anim_data - del bpy.types.Scene.nmsdk_pak_data bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) NMSPanels.unregister() diff --git a/src/addon/nmsdk/blender_manifest.toml b/src/addon/nmsdk/blender_manifest.toml index bba9532..b347124 100644 --- a/src/addon/nmsdk/blender_manifest.toml +++ b/src/addon/nmsdk/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "nmsdk" -version = "0.10.0-alpha8" +version = "0.10.0-alpha9" name = "NMSDK" tagline = "NMS model importer and exporter" maintainer = "monkeyman192" diff --git a/src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py b/src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py index acec40b..95c01c5 100644 --- a/src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py +++ b/src/addon/nmsdk/serialization/NMS_Structures/NMS_types.py @@ -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/src/addon/nmsdk/serialization/NMS_Structures/Structures.py b/src/addon/nmsdk/serialization/NMS_Structures/Structures.py index 42769ed..ab24824 100644 --- a/src/addon/nmsdk/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 ..cereal_bin.structdata import datatype, Field from ..cereal_bin import basic_types as bt - +from ..cereal_bin.structdata import Field, datatype from .NMS_types import ( - Vector4f, NMS_list, astring, VariableSizeString, Quaternion_list, Vector4i + 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/src/addon/nmsdk/serialization/cereal_bin/structdata.py b/src/addon/nmsdk/serialization/cereal_bin/structdata.py index 42ffe96..13aff8b 100644 --- a/src/addon/nmsdk/serialization/cereal_bin/structdata.py +++ b/src/addon/nmsdk/serialization/cereal_bin/structdata.py @@ -2,7 +2,8 @@ import struct from dataclasses import dataclass from io import BufferedReader, BufferedWriter, BytesIO -from typing import Any, Optional, Type, TypeVar +from types import GenericAlias +from typing import Any, Optional, Type, TypeVar, Union T = TypeVar("T", bound="datatype") N = TypeVar("N", bound=int) @@ -179,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("_"): @@ -190,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)) diff --git a/src/addon/nmsdk/serialization/serializers.py b/src/addon/nmsdk/serialization/serializers.py index 58b483d..7094dab 100644 --- a/src/addon/nmsdk/serialization/serializers.py +++ b/src/addon/nmsdk/serialization/serializers.py @@ -1,12 +1,8 @@ from array import array from typing import List -from ..NMS.LOOKUPS import SERIALIZE_FMT_MAP, REV_SEMANTICS -from .formats import write_half, write_int_2_10_10_10_rev, ubytes_to_bytes - - -def serialize_geometry_stream(data): - pass +from ..NMS.LOOKUPS import REV_SEMANTICS, SERIALIZE_FMT_MAP +from .formats import ubytes_to_bytes, write_half, write_int_2_10_10_10_rev def serialize_vertex_stream(requires: List[int], count: int, **kwargs): diff --git a/src/addon/nmsdk/utils/io.py b/src/addon/nmsdk/utils/io.py index 891c3c9..83e4781 100644 --- a/src/addon/nmsdk/utils/io.py +++ b/src/addon/nmsdk/utils/io.py @@ -5,15 +5,22 @@ import subprocess from contextlib import contextmanager from pathlib import Path -from typing import Optional, Union +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from .. import NMSDKPreferences # Blender imports import bpy from hgpaktool import HGPAKFile from hgpaktool.utils import normalise_path +# This function is used to make files hidden on windows. ctypes.windll.kernel32.SetFileAttributesW.argtypes = (ctypes.c_wchar_p, ctypes.c_uint32) +# Get the parent package name. +_package = __package__.rpartition(".")[0] + @contextmanager def load_file(fpath: Union[str, os.PathLike[str]], root_dir: str, from_pak: bool, pak_data: dict): @@ -91,39 +98,24 @@ def get_NMS_dir(fpath: Optional[str]) -> Optional[str]: """ Returns the NMS file directory from the given filepath. """ if not fpath: return None - # If a PCBANKS path is specified in the scene, return it instead. - if bpy.context.scene.nmsdk_default_settings.PCBANKS_directory != "": - return bpy.context.scene.nmsdk_default_settings.PCBANKS_directory - # Otherwise, determine as normal + addon_prefs: NMSDKPreferences = bpy.context.preferences.addons[_package].preferences + # If the unpacked pcbanks directory is specified, use it + if addon_prefs.unpacked_pcbanks_dir: + return addon_prefs.unpacked_pcbanks_dir + # Otherwise, determine as normal. path = Path(fpath) parts = path.parts try: - PCBANKS_path = str(Path(*parts[:parts.index('MODELS')])) - bpy.context.scene.nmsdk_default_settings.PCBANKS_directory = PCBANKS_path # noqa - return PCBANKS_path + return str(Path(*parts[:parts.index('MODELS')])) except ValueError: # In this case we may be loading a model in a custom folder. # We need to go back until we find a folder with a 'MODELS' folder in # it. for parent in path.parents: if Path(parent, 'MODELS').exists(): - # Cache the PCBANKS folder - bpy.context.scene.nmsdk_default_settings.PCBANKS_directory = str(parent) # noqa return str(parent) -def get_MBINCompiler_path() -> str: - """ Return the location of the MBINCompiler exe if it is registered on the - path. """ - _path = '' - for p in os.environ['PATH'].split(';'): - test_path = op.join(p, 'MBINCompiler.exe') - if op.exists(test_path): - _path = test_path - break - return _path - - def base_path(abs_path: str, rel_path: str): """ Return the base path that the rel_path is contained in the abs_path For example, if abs_path is a/b/c/d, and rel_path is c/d, then this will @@ -173,7 +165,8 @@ def convert_file(fpath: str) -> str: path of the produced file. """ exts = {'.MBIN': '.MXML', '.MXML': '.MBIN'} - mbincompiler_path = bpy.context.scene.nmsdk_default_settings.MBINCompiler_path # noqa + addon_prefs: NMSDKPreferences = bpy.context.preferences.addons[_package].preferences + mbincompiler_path = addon_prefs.mbincompiler_path retcode = subprocess.call( [mbincompiler_path, "-y", "-f", "-Q", fpath]) if retcode == 0: diff --git a/src/addon/nmsdk/utils/settings.py b/src/addon/nmsdk/utils/settings.py index 2731eb1..b66a384 100644 --- a/src/addon/nmsdk/utils/settings.py +++ b/src/addon/nmsdk/utils/settings.py @@ -1,37 +1,33 @@ """ Module to handle loading and writing the default NMSDK settings file. """ -# stdlib imports import json -import os.path as op import os +import os.path as op from platform import system as os_name -# local imports -from .io import get_MBINCompiler_path - - _os_name = os_name() -if _os_name == 'Windows': - SETTINGS_DIR = op.join(os.getenv('APPDATA'), 'NMSDK') -elif _os_name == 'Linux': - SETTINGS_DIR = op.expanduser('~/NMSDK') -elif _os_name == 'Darwin': +if _os_name == "Windows": + SETTINGS_DIR = op.join(os.getenv("APPDATA", ""), "NMSDK") +elif _os_name == "Linux": + SETTINGS_DIR = op.expanduser("~/NMSDK") +elif _os_name == "Darwin": # UNTESTED! - SETTINGS_DIR = op.join(op.expanduser('~'), 'Library', 'NMSDK') + SETTINGS_DIR = op.join(op.expanduser("~"), "Library", "NMSDK") else: - raise ValueError('Current operating system is not supported sorry!') -SETTINGS_FNAME = 'settings.json' + raise ValueError("I don't know what OS you're running, but it's supported sorry!") +SETTINGS_FNAME = "settings.json" -DEFAULT_SETTINGS = {'export_directory': 'CUSTOMMODELS', - 'group_name': '', - 'MBINCompiler_path': get_MBINCompiler_path()} +DEFAULT_SETTINGS = { + "export_directory": "CUSTOMMODELS", + "group_name": "", +} def read_settings(): """ Read the settings from the settings file. """ if not op.exists(op.join(SETTINGS_DIR, SETTINGS_FNAME)): return DEFAULT_SETTINGS - with open(op.join(SETTINGS_DIR, SETTINGS_FNAME), 'r') as f: + with open(op.join(SETTINGS_DIR, SETTINGS_FNAME), "r") as f: data = json.load(f) # Update the settings with any default settings for key, value in DEFAULT_SETTINGS.items(): @@ -40,9 +36,11 @@ def read_settings(): return data -def write_settings(settings): +def write_settings(settings) -> str: """ Write the settings provided to the settings file. """ if not op.exists(SETTINGS_DIR): os.mkdir(SETTINGS_DIR) - with open(op.join(SETTINGS_DIR, SETTINGS_FNAME), 'w') as f: + dest = op.join(SETTINGS_DIR, SETTINGS_FNAME) + with open(dest, "w") as f: json.dump(settings, f) + return dest diff --git a/src/addon/nmsdk/utils/stopwitch.py b/src/addon/nmsdk/utils/stopwitch.py new file mode 100644 index 0000000..872ae7d --- /dev/null +++ b/src/addon/nmsdk/utils/stopwitch.py @@ -0,0 +1,157 @@ +import time +from contextlib import contextmanager +from itertools import accumulate + +INDENT = " " + + +class StopWitch(): + def __init__(self, auto_nest_sections: bool = True, timer=time.perf_counter): + self.auto_nest_sections = auto_nest_sections + self.timer = timer + + self._start_time: float = 0 + self._total_time: float = 0 + # Timing data is always stored raw to avoid any overhead processing it as we add data. + # This data will be processed later. + self._timing_data: dict[str, float] = {} + self._start_times: dict[str, float] = {} + self._section_name_stack: list[str] = [] + + # TODO: Could make a class ala https://stackoverflow.com/a/70059951 + @contextmanager + def section(self, name: str): + self.start_section(name) + yield + self.end_section(name) + + def start(self): + self._start_time = self.timer() + + def stop(self): + stop_time = self.timer() + # Loop over the items in the start times dict and end all of them. + for name, start_time in self._start_times.items(): + if name not in self._timing_data: + self._timing_data[name] = stop_time - start_time + else: + self._timing_data[name] += stop_time - start_time + self._start_times = {} + # Also set the total time. + self._total_time += stop_time - self._start_time + + def start_section(self, *args: str): + start_time = self.timer() + # For each name in args, start the section. If we are automatically nesting sections, get the parent + # from the stack and add it. + if self.auto_nest_sections: + if len(args) != 1: + raise ValueError( + "Only one section can be created per start_section call when auto section nesting is " + "enabled." + ) + self._section_name_stack.append(args[0]) + self._start_times[".".join(self._section_name_stack)] = start_time + else: + # For each name, split it by the . character and then for each sub-component, check to see if it + # exists in the start times list. + # This allows us to track the total time of parent groups even if they aren't created explicitly. + for name in args: + for subname in list(accumulate(name.split("."), lambda x, y: f"{x}.{y}")): + self._start_times[subname] = start_time + + def end_section(self, *args: str): + end_time = self.timer() + if self.auto_nest_sections: + if len(args) != 1: + raise ValueError( + "Only one section can be created per start_section call when auto section nesting is " + "enabled." + ) + name = ".".join(self._section_name_stack) + start_time = self._start_times.pop(name, self._start_time) + if name not in self._timing_data: + self._timing_data[name] = end_time - start_time + else: + self._timing_data[name] += end_time - start_time + self._section_name_stack.pop() + else: + # Ending a section will end all sections with the same prefix. + # If we request a section to be ended which wasn't ever started we just add the name as we have a + # catch for this later. + stop_names = set() + for name in args: + filtered_names = [x for x in filter(lambda x: x.startswith(name), self._start_times.keys())] + if filtered_names: + stop_names.update(filtered_names) + else: + stop_names.add(name) + for name in stop_names: + start_time = self._start_times.pop(name, self._start_time) + if name not in self._timing_data: + self._timing_data[name] = end_time - start_time + else: + self._timing_data[name] += end_time - start_time + + def results(self): + if self._total_time == 0: + return + structured_data = {} + curr_level = structured_data + curr_level_name = "" + # Reformat the data so that it's a set of nested dictionaries, split by the .'s in the names of the + # sections. + for name, time_ in self._timing_data.items(): + for part in name.split("."): + # Generate the current level name. + if not curr_level_name: + curr_level_name = part + else: + curr_level_name += f".{part}" + + # + if part not in curr_level: + curr_level[part] = {"total_time": -1, "children": {}} + if curr_level_name in self._timing_data: + curr_level[part]["total_time"] = time_ + elif curr_level_name == name: + curr_level[part]["total_time"] = time_ + curr_level = curr_level[part]["children"] + curr_level = structured_data + curr_level_name = "" + + def calc_percentage(data: dict, parent_time: float): + data["percentage"] = f"{100 * data['total_time'] / parent_time:.4f}%" + for child_data in data["children"].values(): + calc_percentage(child_data, data["total_time"]) + + for data in structured_data.values(): + calc_percentage(data, self._total_time) + + def format_data(data: dict, depth: int = 0): + for key, value in data.items(): + print(f"{INDENT * depth}{key}: {value['total_time']:.4f} ({value['percentage']})") + if (children := value.get("children")): + format_data(children, depth + 1) + + format_data(structured_data) + + +# Create a "global" object essentially which can be imported from other places. +witch = StopWitch() + +if __name__ == "__main__": + + @witch.section("bloop") + def wait_some(): + time.sleep(0.5) + + witch.start() + with witch.section("outer"): + with witch.section("test"): + time.sleep(2) + time.sleep(1) + for i in range(3): + wait_some() + witch.stop() + witch.results() From a62dffc71cb7e3e14904101d89d2002002c155bb Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Sat, 25 Apr 2026 17:28:02 +1000 Subject: [PATCH 4/7] More fixes - proper linux support, material fixes and export fixes --- .github/workflows/pipeline.yml | 3 +- build_cmf | 2 +- src/addon/nmsdk/ModelExporter/export.py | 11 ++-- src/addon/nmsdk/ModelImporter/import_scene.py | 38 +++++++------- src/addon/nmsdk/NMS/material_node.py | 20 ++++--- src/addon/nmsdk/NMSDK.py | 1 + src/addon/nmsdk/__init__.py | 52 ++++++++++++++++++- src/addon/nmsdk/blender_manifest.toml | 2 +- .../formats/INT_2_10_10_10_REV.py | 26 +++++----- src/addon/nmsdk/utils/io.py | 10 +++- 10 files changed, 116 insertions(+), 49 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 51ed10e..1e99096 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -24,10 +24,9 @@ jobs: python-version: "3.9" - name: Install dependencies run: python -m pip install flake8 mkdocs - # - name: Lint code - # run: flake8 . - name: Build docs run: mkdocs build + # TODO: This needs to be fixed. Probably pull blenderkit/headless-blender:blender-5.0 and use it for building... - name: Build release run: | python ./tools/build.py diff --git a/build_cmf b/build_cmf index ea7d63e..f476f01 100644 --- a/build_cmf +++ b/build_cmf @@ -1 +1 @@ -blender --command extension build --source-dir "C:\Users\msval\Documents\GitHub\NMSDK\addons\NMSDK\src\addon\nmsdk" \ No newline at end of file +blender --command extension build --source-dir "src\addon\nmsdk" \ No newline at end of file diff --git a/src/addon/nmsdk/ModelExporter/export.py b/src/addon/nmsdk/ModelExporter/export.py index f9e12df..9ebad90 100644 --- a/src/addon/nmsdk/ModelExporter/export.py +++ b/src/addon/nmsdk/ModelExporter/export.py @@ -704,10 +704,13 @@ def mix_streams(self): # Write the index data to an array of bytes then back so that we can fake it being 32bit for # serialization purposes. This is a crappy hack... - index_array = np.concatenate(self.np_indexes[mesh_col_offset:], dtype=dtype) - index_array_bytes = index_array.tobytes() - if (dfct := (len(index_array_bytes) % 4)) != 0: - index_array_bytes += b"\x00" * (4 - dfct) + if (mesh_cols := self.np_indexes[mesh_col_offset:]): + index_array = np.concatenate(mesh_cols, dtype=dtype) + index_array_bytes = index_array.tobytes() + if (dfct := (len(index_array_bytes) % 4)) != 0: + index_array_bytes += b"\x00" * (4 - dfct) + else: + index_array_bytes = b"" self.GeometryData['IndexBuffer'] = np.frombuffer(index_array_bytes, dtype=np.uint32) diff --git a/src/addon/nmsdk/ModelImporter/import_scene.py b/src/addon/nmsdk/ModelImporter/import_scene.py index c975ccb..5eaf798 100644 --- a/src/addon/nmsdk/ModelImporter/import_scene.py +++ b/src/addon/nmsdk/ModelImporter/import_scene.py @@ -100,6 +100,7 @@ def __init__( ): self.from_pak = from_pak self.ref_scenes = ref_scenes or {} + self.parent_obj = parent_obj addon_prefs: NMSDKPreferences = bpy.context.preferences.addons[_package].preferences if self.from_pak: @@ -132,6 +133,24 @@ def __init__( 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.settings = settings + self.dep_graph = bpy.context.evaluated_depsgraph_get() + # When scenes contain reference nodes there can be clashes with names. + # To ensure correct parenting of objects in blender, we will keep track + # of what objects exist within a scene as there will be no name clashes + # within each scene. + self.local_objects = dict() + + self.requires_render = True + self.has_errors = False + self.scn = bpy.context.scene + self.scene_ctx = SceneOp(bpy.context) + # 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: @@ -152,25 +171,6 @@ def __init__( # There will be only one file here... self.scene_path = op.join(tmpdir, os.listdir(tmpdir)[0]) - # 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.settings = settings - self.dep_graph = bpy.context.evaluated_depsgraph_get() - # When scenes contain reference nodes there can be clashes with names. - # To ensure correct parenting of objects in blender, we will keep track - # of what objects exist within a scene as there will be no name clashes - # within each scene. - self.local_objects = dict() - - self.requires_render = True - self.has_errors = False - 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. diff --git a/src/addon/nmsdk/NMS/material_node.py b/src/addon/nmsdk/NMS/material_node.py index ea13a34..e147899 100644 --- a/src/addon/nmsdk/NMS/material_node.py +++ b/src/addon/nmsdk/NMS/material_node.py @@ -79,13 +79,19 @@ def create_material_node( for tex_type, tex_path in samplers.items(): img = None if from_pak: - 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) - # img.filepath = tex_path + 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): diff --git a/src/addon/nmsdk/NMSDK.py b/src/addon/nmsdk/NMSDK.py index 05c803a..6693b3f 100644 --- a/src/addon/nmsdk/NMSDK.py +++ b/src/addon/nmsdk/NMSDK.py @@ -728,6 +728,7 @@ 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) diff --git a/src/addon/nmsdk/__init__.py b/src/addon/nmsdk/__init__.py index 4d55213..78736e2 100644 --- a/src/addon/nmsdk/__init__.py +++ b/src/addon/nmsdk/__init__.py @@ -6,6 +6,7 @@ import time import bpy +from bpy.types import Operator from bpy.props import PointerProperty, StringProperty from bpy.utils import register_class, unregister_class @@ -98,6 +99,51 @@ def save_preferences(cls: "NMSDKPreferences", context: bpy.types.Context): 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. @@ -134,7 +180,9 @@ class NMSDKPreferences(bpy.types.AddonPreferences): def draw(self, context): layout = self.layout layout.label(text="NMSDK preferences") - layout.prop(self, "pcbanks_dir") + 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") @@ -183,6 +231,7 @@ def menu_func_import(self, context): 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) @@ -215,6 +264,7 @@ def unregister(): SettingsPanels.unregister() ContextMenus.unregister() bpy.utils.unregister_class(NMSDKPreferences) + bpy.utils.unregister_class(IndexPAKPath) # bpy.app.handlers.load_post.remove(load_vfs_data) diff --git a/src/addon/nmsdk/blender_manifest.toml b/src/addon/nmsdk/blender_manifest.toml index b347124..ea5fafc 100644 --- a/src/addon/nmsdk/blender_manifest.toml +++ b/src/addon/nmsdk/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "nmsdk" -version = "0.10.0-alpha9" +version = "0.10.0-alpha13" name = "NMSDK" tagline = "NMS model importer and exporter" maintainer = "monkeyman192" diff --git a/src/addon/nmsdk/serialization/formats/INT_2_10_10_10_REV.py b/src/addon/nmsdk/serialization/formats/INT_2_10_10_10_REV.py index 55aaebd..a264309 100644 --- a/src/addon/nmsdk/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('