├── .coveragerc ├── .github └── workflows │ ├── ci.yml │ ├── format.yml │ └── publish.yml ├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── OpenSAGE.BlenderPlugin.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .pylintrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── art ├── AotR_Umbar_Buildings.jpg └── opensage-logo.png ├── io_mesh_w3d ├── __init__.py ├── bone_volume_export.py ├── common │ ├── structs │ │ ├── animation.py │ │ ├── collision_box.py │ │ ├── data_context.py │ │ ├── hierarchy.py │ │ ├── hlod.py │ │ ├── mesh.py │ │ ├── mesh_structs │ │ │ ├── aabbtree.py │ │ │ ├── shader_material.py │ │ │ ├── texture.py │ │ │ ├── triangle.py │ │ │ └── vertex_influence.py │ │ └── rgba.py │ └── utils │ │ ├── animation_export.py │ │ ├── animation_import.py │ │ ├── box_export.py │ │ ├── box_import.py │ │ ├── helpers.py │ │ ├── hierarchy_export.py │ │ ├── hierarchy_import.py │ │ ├── hlod_export.py │ │ ├── material_export.py │ │ ├── material_import.py │ │ ├── mesh_export.py │ │ ├── mesh_import.py │ │ └── primitives.py ├── custom_properties.py ├── export_utils.py ├── geometry_export.py ├── import_utils.py ├── utils.py ├── w3d │ ├── adaptive_delta.py │ ├── export_w3d.py │ ├── import_w3d.py │ ├── io_binary.py │ ├── structs │ │ ├── compressed_animation.py │ │ ├── dazzle.py │ │ ├── mesh_structs │ │ │ ├── material_info.py │ │ │ ├── material_pass.py │ │ │ ├── prelit.py │ │ │ ├── shader.py │ │ │ └── vertex_material.py │ │ └── version.py │ └── utils │ │ ├── dazzle_export.py │ │ ├── dazzle_import.py │ │ └── helpers.py └── w3x │ ├── export_w3x.py │ ├── import_w3x.py │ ├── io_xml.py │ └── structs │ ├── include.py │ └── mesh_structs │ ├── bounding_box.py │ └── bounding_sphere.py ├── runTests.bat ├── runTests.sh ├── runTestsWithPrefix.bat └── tests ├── __init__.py ├── common ├── __init__.py ├── cases │ ├── __init__.py │ ├── structs │ │ ├── __init__.py │ │ ├── mesh_structs │ │ │ ├── __init__.py │ │ │ ├── test_aabbtree.py │ │ │ ├── test_shader_material.py │ │ │ ├── test_texture.py │ │ │ ├── test_triangle.py │ │ │ └── test_vertex_influence.py │ │ ├── test_animation.py │ │ ├── test_collision_box.py │ │ ├── test_hierarchy.py │ │ ├── test_hlod.py │ │ ├── test_mesh.py │ │ └── test_rgba.py │ ├── test_addon.py │ ├── test_custom_properties.py │ ├── test_export_utils.py │ ├── test_import_utils.py │ ├── test_utils.py │ └── utils │ │ ├── __init__.py │ │ ├── test_animation_utils.py │ │ ├── test_box_export.py │ │ ├── test_box_import.py │ │ ├── test_helpers.py │ │ ├── test_hierarchy_utils.py │ │ ├── test_material_utils.py │ │ ├── test_mesh_export.py │ │ └── test_mesh_import.py └── helpers │ ├── animation.py │ ├── collision_box.py │ ├── hierarchy.py │ ├── hlod.py │ ├── mesh.py │ ├── mesh_structs │ ├── aabbtree.py │ ├── shader_material.py │ ├── texture.py │ ├── triangle.py │ └── vertex_influence.py │ └── rgba.py ├── mathutils.py ├── runner.py ├── test_bone_volume_export.py ├── test_geometry_export.py ├── testfiles ├── cube_with_modifiers.blend ├── multiuser_mesh_with_modifiers.blend ├── texture.dds └── unordered_bones.blend ├── utils.py ├── w3d ├── __init__.py ├── cases │ ├── __init__.py │ ├── structs │ │ ├── __init__.py │ │ ├── mesh_structs │ │ │ ├── __init__.py │ │ │ ├── test_material_info.py │ │ │ ├── test_material_pass.py │ │ │ ├── test_prelit.py │ │ │ ├── test_shader.py │ │ │ └── test_vertex_material.py │ │ ├── test_compressed_animation.py │ │ ├── test_dazzle.py │ │ └── test_version.py │ ├── test_adaptive_delta.py │ ├── test_export.py │ ├── test_import.py │ ├── test_import_utils.py │ ├── test_io_binary.py │ └── test_roundtrip.py └── helpers │ ├── compressed_animation.py │ ├── dazzle.py │ ├── mesh_structs │ ├── material_info.py │ ├── material_pass.py │ ├── prelit.py │ ├── shader.py │ └── vertex_material.py │ └── version.py └── w3x ├── __init__.py ├── cases ├── __init__.py ├── structs │ ├── __init__.py │ ├── mesh_structs │ │ ├── __init__.py │ │ ├── test_bounding_box.py │ │ └── test_bounding_sphere.py │ └── test_include.py ├── test_export.py ├── test_import.py ├── test_io_xml.py └── test_roundtrip.py └── helpers ├── include.py └── mesh_structs ├── bounding_box.py └── bounding_sphere.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = io_mesh_w3d -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | create: 9 | tags: 10 | - v* 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: [ ubuntu-latest ] 16 | strategy: 17 | matrix: 18 | blender-version: [ '2.93', '3.6', '4.0', '4.2' ] 19 | include: 20 | - blender-version: '2.93' 21 | blender-version-suffix: '13' 22 | python-version: '3.9.16' 23 | - blender-version: '3.6' 24 | blender-version-suffix: '0' 25 | python-version: '3.10.9' 26 | - blender-version: '4.0' 27 | blender-version-suffix: '0' 28 | python-version: '3.10.9' 29 | - blender-version: '4.2' 30 | blender-version-suffix: '0' 31 | python-version: '3.11.7' 32 | - blender-version: '4.4' 33 | blender-version-suffix: '3' 34 | python-version: '3.11.7' 35 | 36 | steps: 37 | - uses: actions/checkout@v3 38 | with: 39 | submodules: 'true' 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | - name: Install dependencies 45 | run: | 46 | sudo apt install xz-utils 47 | python -m pip install --upgrade pip 48 | python -m pip install codecov 49 | mkdir tmp && cd tmp 50 | wget http://ftp.halifax.rwth-aachen.de/blender/release/Blender${{ matrix.blender-version }}/blender-${{ matrix.blender-version }}.${{ matrix.blender-version-suffix }}-linux-x64.tar.xz 51 | tar xf blender-${{ matrix.blender-version }}.${{ matrix.blender-version-suffix }}-linux-x64.tar.xz 52 | mv blender-${{ matrix.blender-version }}.${{ matrix.blender-version-suffix }}-linux-x64 blender 53 | rm blender-${{ matrix.blender-version }}.${{ matrix.blender-version-suffix }}-linux-x64.tar.xz 54 | cd .. 55 | mkdir -p ${PWD}/tmp/blender/${{ matrix.blender-version }}/scripts/addons 56 | ln -s ${PWD}/io_mesh_w3d ${PWD}/tmp/blender/${{ matrix.blender-version }}/scripts/addons/io_mesh_w3d 57 | wget https://files.pythonhosted.org/packages/85/d5/818d0e603685c4a613d56f065a721013e942088047ff1027a632948bdae6/coverage-4.5.4.tar.gz#md5=c33cab2aed8780aac32880cb6c7616b7 58 | tar zxf coverage-4.5.4.tar.gz 59 | mv coverage-4.5.4/coverage "${PWD}/tmp/blender/${{ matrix.blender-version }}/scripts/modules" 60 | rm -rf coverage-4.5.4 61 | - name: Test 62 | run: | 63 | ./tmp/blender/blender --factory-startup -noaudio -b --python-exit-code 1 --python ./tests/runner.py -- --coverage 64 | - name: Coverage 65 | run: | 66 | codecov 67 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format python code 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | autopep8: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: autopep8 12 | uses: peter-evans/autopep8@v1 13 | with: 14 | args: --recursive --in-place --aggressive --aggressive . --max-line-length=120 15 | - name: Create Pull Request 16 | uses: peter-evans/create-pull-request@v3 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | commit-message: autopep8 action fixes 20 | committer: Stephan Vedder 21 | title: Fixes by autopep8 action 22 | body: This is an auto-generated PR with fixes by autopep8. 23 | labels: autopep8, automated pr 24 | reviewers: Tarcontar 25 | branch: autopep8-patches 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | create: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | runs-on: [ ubuntu-latest ] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: 'true' 16 | - name: Bundle archive 17 | run: | 18 | zip -r io_mesh_w3d.zip io_mesh_w3d -x io_mesh_w3d/blender_addon_updater/.git 19 | 20 | - name: Create Release 21 | id: create_release 22 | uses: ncipollo/release-action@v1 23 | with: 24 | artifacts: "io_mesh_w3d.zip" 25 | draft: false 26 | prerelease: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode 3 | .coverage 4 | coverage.xml 5 | htmlcov 6 | io_mesh_w3d.zip 7 | .vs 8 | .pytest_cache 9 | TestResults 10 | venv 11 | /virtual_env 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "io_mesh_w3d/blender_addon_updater"] 2 | path = io_mesh_w3d/blender_addon_updater 3 | url = https://github.com/OpenSAGE/blender-addon-updater.git 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | -------------------------------------------------------------------------------- /.idea/OpenSAGE.BlenderPlugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | no-docstring-rgx=. 3 | variable-rgx=([a-z_][a-z0-9_]{2,30})|(x|y|z|uv)$ 4 | max-line-length=120 5 | 6 | [TYPECHECK] 7 | ignored-modules=addon_utils, bgl, bmesh, bpy, bpy_extras, mathutils 8 | 9 | [MESSAGES CONTROL] 10 | disable=C0111,R0903,W0614,E0602,W0401 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![OpenSAGE](/art/opensage-logo.png) 2 | ============================================================ 3 | 4 | [![Build Status](https://github.com/OpenSAGE/OpenSAGE.BlenderPlugin/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenSAGE/OpenSAGE.BlenderPlugin/actions/workflows/ci.yml) 5 | [![Discord Chat](https://img.shields.io/discord/398393968234332161.svg?logo=discord)](https://discord.gg/G2FhZUT) 6 | [![codecov](https://codecov.io/gh/OpenSAGE/OpenSAGE.BlenderPlugin/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenSAGE/OpenSAGE.BlenderPlugin) 7 | 8 | ![Sample](/art/AotR_Umbar_Buildings.jpg) 9 | 10 | **OpenSAGE.BlenderPlugin**: a free, open source blender plugin for the [Westwood](https://de.wikipedia.org/wiki/Westwood_Studios) 3D 11 | format used in Command & Conquer™: Generals and other RTS titles from Westwood Studios and EA Pacific. 12 | 13 | ## Installing and activating 14 | 15 | Please see [Installing the plugin](https://github.com/OpenSAGE/OpenSAGE.BlenderPlugin/wiki/Installing-the-Plugin) 16 | 17 | ## Setting up for development 18 | 19 | Please see [Setting up for development](https://github.com/OpenSAGE/OpenSAGE.BlenderPlugin/wiki/Development-Setup) 20 | 21 | ## Note 22 | 23 | The plugin is still in beta and the behaviour may change between releases. Also bugs might still occur, which we'll try to fix as soon as possible. So feel free to report bugs and issues in the #w3d-blender-plugin channel on [OpenSAGE Discord](https://discord.gg/G2FhZUT). Also see [Troubleshoting](https://github.com/OpenSAGE/OpenSAGE.BlenderPlugin/wiki/Troubleshooting) for more information. 24 | 25 | ## Legal disclaimers 26 | 27 | * This project is not affiliated with or endorsed by EA in any way. Command & Conquer is a trademark of Electronic Arts. 28 | * This project is non-commercial. The source code is available for free and always will be. 29 | * If you want to contribute to this repository, your contribution must be either your own original code, or open source code with a 30 | clear acknowledgement of its origin. No code that was acquired through reverse engineering executable binaries will be accepted. 31 | * No assets from the original games are included in this repo. 32 | 33 | ## Community 34 | 35 | We have a growing [OpenSAGE Discord](https://discord.gg/G2FhZUT) community. If you have questions about the project or can't get it working, 36 | there's usually someone there who can help out. 37 | -------------------------------------------------------------------------------- /art/AotR_Umbar_Buildings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/art/AotR_Umbar_Buildings.jpg -------------------------------------------------------------------------------- /art/opensage-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/art/opensage-logo.png -------------------------------------------------------------------------------- /io_mesh_w3d/bone_volume_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | from io_mesh_w3d.utils import ReportHelper 6 | from bpy_extras.io_utils import ExportHelper 7 | from io_mesh_w3d.w3x.io_xml import * 8 | from io_mesh_w3d.common.utils.helpers import * 9 | 10 | 11 | def format_str(value): 12 | return '{:.3f}'.format(value) 13 | 14 | 15 | class ExportBoneVolumeData(bpy.types.Operator, ExportHelper, ReportHelper): 16 | bl_idname = 'scene.export_bone_volume_data' 17 | bl_label = 'Export Bone Volume Data' 18 | bl_options = {'REGISTER', 'UNDO'} 19 | 20 | filename_ext = '.xml' 21 | 22 | def execute(self, context): 23 | export_bone_volume_data(self, self.filepath) 24 | return {'FINISHED'} 25 | 26 | 27 | def export_bone_volume_data(context, filepath): 28 | context.info(f'exporting bone volume data to xml: {filepath}') 29 | 30 | root = create_named_root('BoneVolumes') 31 | 32 | for mesh in get_objects('MESH'): 33 | if not mesh.data.object_type == 'BONE_VOLUME': 34 | continue 35 | 36 | node = create_node(root, 'BoneVolume') 37 | node.set('BoneName', mesh.name) 38 | node.set('ContactTag', mesh.data.contact_tag) 39 | node.set('Mass', format_str(mesh.data.mass)) 40 | node.set('Spinniness', format_str(mesh.data.spinniness)) 41 | 42 | location, rotation, scale = mesh.matrix_world.decompose() 43 | extend = get_aa_box(mesh.data.vertices) 44 | halfX = extend.x * scale.x * 0.5 45 | halfY = extend.y * scale.y * 0.5 46 | halfZ = extend.z * scale.z * 0.5 47 | 48 | box = create_node(node, 'Box') 49 | box.set('HalfSizeX', format_str(halfX)) 50 | box.set('HalfSizeY', format_str(halfY)) 51 | box.set('HalfSizeZ', format_str(halfZ)) 52 | 53 | create_vector(location, box, 'Translation') 54 | create_quaternion(rotation, box, 'Rotation') 55 | 56 | write(root, filepath) 57 | context.info('exporting bone volume data finished') 58 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/structs/collision_box.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector 5 | from io_mesh_w3d.common.structs.rgba import RGBA 6 | from io_mesh_w3d.w3d.utils.helpers import * 7 | from io_mesh_w3d.w3d.structs.version import Version 8 | from io_mesh_w3d.w3x.io_xml import * 9 | 10 | W3D_CHUNK_BOX = 0x00000740 11 | ATTRIBUTE_MASK = 0xF 12 | COLLISION_TYPE_MASK = 0xFF0 13 | 14 | COLLISION_TYPE_PHYSICAL = 0x10 15 | COLLISION_TYPE_PROJECTILE = 0x20 16 | COLLISION_TYPE_VIS = 0x40 17 | COLLISION_TYPE_CAMERA = 0x80 18 | COLLISION_TYPE_VEHICLE = 0x100 19 | 20 | 21 | class CollisionBox: 22 | def __init__(self, version=Version(), box_type=0, collision_types=0, name_='', color=RGBA(), 23 | center=Vector((0.0, 0.0, 0.0)), extend=Vector((0.0, 0.0, 0.0)), joypad_picking_only=False): 24 | self.version = version 25 | self.box_type = box_type 26 | self.collision_types = collision_types 27 | self.name_ = name_ 28 | self.color = color 29 | self.center = center 30 | self.extend = extend 31 | self.joypad_picking_only = joypad_picking_only 32 | 33 | def validate(self, context): 34 | if context.file_format == 'W3X': 35 | return True 36 | if len(self.name_) >= LARGE_STRING_LENGTH: 37 | context.error(f'box name \'{self.name_}\' exceeds max length of: {LARGE_STRING_LENGTH}') 38 | return False 39 | return True 40 | 41 | def container_name(self): 42 | return self.name_.split('.', 1)[0] 43 | 44 | def name(self): 45 | return self.name_.split('.', 1)[-1] 46 | 47 | @staticmethod 48 | def read(io_stream): 49 | ver = Version.read(io_stream) 50 | flags = read_ulong(io_stream) 51 | return CollisionBox( 52 | version=ver, 53 | box_type=(flags & ATTRIBUTE_MASK), 54 | collision_types=(flags & COLLISION_TYPE_MASK), 55 | name_=read_long_fixed_string(io_stream), 56 | color=RGBA.read(io_stream), 57 | center=read_vector(io_stream), 58 | extend=read_vector(io_stream)) 59 | 60 | @staticmethod 61 | def size(include_head=True): 62 | return const_size(68, include_head) 63 | 64 | def write(self, io_stream): 65 | write_chunk_head(W3D_CHUNK_BOX, io_stream, self.size(False)) 66 | 67 | self.version.write(io_stream) 68 | write_ulong((self.box_type & ATTRIBUTE_MASK) | (self.collision_types & COLLISION_TYPE_MASK), io_stream) 69 | write_long_fixed_string(self.name_, io_stream) 70 | self.color.write(io_stream) 71 | write_vector(self.center, io_stream) 72 | write_vector(self.extend, io_stream) 73 | 74 | @staticmethod 75 | def parse(context, xml_collision_box): 76 | result = CollisionBox( 77 | name_=xml_collision_box.get('id'), 78 | joypad_picking_only=bool(xml_collision_box.get('JoypadPickingOnly', False))) 79 | 80 | for child in xml_collision_box: 81 | if child.tag == 'Center': 82 | result.center = parse_vector(child) 83 | elif child.tag == 'Extent': 84 | result.extend = parse_vector(child) 85 | else: 86 | context.warning(f'unhandled node \'{child.tag}\' in W3DCollisionBox!') 87 | return result 88 | 89 | def create(self, parent): 90 | result = create_node(parent, 'W3DCollisionBox') 91 | result.set('id', self.name_) 92 | create_vector(self.center, result, 'Center') 93 | create_vector(self.extend, result, 'Extent') 94 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/structs/data_context.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | 5 | class DataContext: 6 | def __init__(self, container_name='', rig=None, hierarchy=None, meshes=None, dazzles=None, hlod=None, textures=None, 7 | collision_boxes=None, animation=None, compressed_animation=None): 8 | self.container_name = container_name 9 | self.rig = rig 10 | self.hierarchy = hierarchy 11 | self.meshes = meshes if meshes is not None else [] 12 | self.dazzles = dazzles if dazzles is not None else [] 13 | self.hlod = hlod 14 | self.textures = textures if textures is not None else [] 15 | self.collision_boxes = collision_boxes if collision_boxes is not None else [] 16 | self.animation = animation 17 | self.compressed_animation = compressed_animation 18 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/structs/mesh_structs/texture.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.utils.helpers import * 5 | from io_mesh_w3d.w3x.io_xml import * 6 | 7 | W3D_CHUNK_TEXTURES = 0x00000030 8 | W3D_CHUNK_TEXTURE_INFO = 0x00000033 9 | 10 | 11 | class TextureInfo: 12 | def __init__(self, attributes=0, animation_type=0, frame_count=0, frame_rate=0): 13 | self.attributes = attributes 14 | self.animation_type = animation_type 15 | self.frame_count = frame_count 16 | self.frame_rate = frame_rate 17 | 18 | @staticmethod 19 | def read(io_stream): 20 | return TextureInfo( 21 | attributes=read_ushort(io_stream), 22 | animation_type=read_ushort(io_stream), 23 | frame_count=read_ulong(io_stream), 24 | frame_rate=read_float(io_stream)) 25 | 26 | @staticmethod 27 | def size(include_head=True): 28 | return const_size(12, include_head) 29 | 30 | def write(self, io_stream): 31 | write_chunk_head(W3D_CHUNK_TEXTURE_INFO, io_stream, self.size(False)) 32 | write_ushort(self.attributes, io_stream) 33 | write_ushort(self.animation_type, io_stream) 34 | write_ulong(self.frame_count, io_stream) 35 | write_float(self.frame_rate, io_stream) 36 | 37 | 38 | W3D_CHUNK_TEXTURE = 0x00000031 39 | W3D_CHUNK_TEXTURE_NAME = 0x00000032 40 | 41 | 42 | class Texture: 43 | def __init__(self, id='', file='', texture_info=None): 44 | self.id = id 45 | self.file = file 46 | self.texture_info = texture_info 47 | 48 | @staticmethod 49 | def read(context, io_stream, chunk_end): 50 | result = Texture() 51 | 52 | while io_stream.tell() < chunk_end: 53 | (chunk_type, chunk_size, _) = read_chunk_head(io_stream) 54 | 55 | if chunk_type == W3D_CHUNK_TEXTURE_NAME: 56 | result.file = read_string(io_stream) 57 | result.id = result.file 58 | elif chunk_type == W3D_CHUNK_TEXTURE_INFO: 59 | result.texture_info = TextureInfo.read(io_stream) 60 | else: 61 | skip_unknown_chunk(context, io_stream, chunk_type, chunk_size) 62 | return result 63 | 64 | def size(self, include_head=True): 65 | size = const_size(0, include_head) 66 | size += text_size(self.file) 67 | if self.texture_info is not None: 68 | size += self.texture_info.size() 69 | return size 70 | 71 | def write(self, io_stream): 72 | write_chunk_head(W3D_CHUNK_TEXTURE, io_stream, self.size(False), has_sub_chunks=True) 73 | write_chunk_head(W3D_CHUNK_TEXTURE_NAME, io_stream, text_size(self.file, False)) 74 | write_string(self.file, io_stream) 75 | 76 | if self.texture_info is not None: 77 | self.texture_info.write(io_stream) 78 | 79 | @staticmethod 80 | def parse(xml_texture): 81 | return Texture( 82 | id=xml_texture.get('id'), 83 | file=xml_texture.get('File'), 84 | texture_info=TextureInfo()) 85 | 86 | def create(self, parent): 87 | texture = create_node(parent, 'Texture') 88 | texture.set('id', self.id) 89 | texture.set('File', self.file) 90 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/structs/mesh_structs/triangle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector 5 | from io_mesh_w3d.w3d.io_binary import * 6 | from io_mesh_w3d.w3x.io_xml import * 7 | 8 | 9 | surface_types = [ 10 | 'LightMetal', 11 | 'HeavyMetal', 12 | 'Water', 13 | 'Sand', 14 | 'Dirt', 15 | 'Mud', 16 | 'Grass', 17 | 'Wood', 18 | 'Concrete', 19 | 'Flesh', 20 | 'Rock', 21 | 'Snow', 22 | 'Ice', 23 | 'Default', 24 | 'Glass', 25 | 'Cloth', 26 | 'TiberiumField', 27 | 'FoliagePermeable', 28 | 'GlassPermeable', 29 | 'IcePermeable', 30 | 'ClothPermeable', 31 | 'Electrical', 32 | 'Flammable', 33 | 'Steam', 34 | 'ElectricalPermeable', 35 | 'FlammablePermeable', 36 | 'SteamPermeable', 37 | 'WaterPermeable', 38 | 'TiberiumWater', 39 | 'TiberiumWaterPermeable', 40 | 'UnderwaterDirt', 41 | 'UnderwaterTiberiumDirt'] 42 | 43 | 44 | class Triangle: 45 | def __init__(self, vert_ids=None, surface_type=13, normal=Vector((0.0, 0.0, 0.0)), distance=0.0): 46 | self.vert_ids = vert_ids if vert_ids is not None else [] 47 | self.surface_type = surface_type 48 | self.normal = normal 49 | self.distance = distance 50 | 51 | @staticmethod 52 | def validate_face_map_names(context, face_map_names): 53 | for name in face_map_names: 54 | if name not in surface_types: 55 | context.warning(f'name of face map \'{name}\' is not one of valid surface types: {surface_types}') 56 | 57 | def get_surface_type_name(self, context, index): 58 | if self.surface_type >= len(surface_types): 59 | context.warning(f'triangle {index} has an invalid surface type \'{self.surface_type}\'') 60 | return 'Default' 61 | return surface_types[self.surface_type] 62 | 63 | def set_surface_type(self, name): 64 | if name not in surface_types: 65 | return 66 | self.surface_type = surface_types.index(name) 67 | 68 | @staticmethod 69 | def read(io_stream): 70 | return Triangle( 71 | vert_ids=[read_ulong(io_stream), read_ulong(io_stream), read_ulong(io_stream)], 72 | surface_type=read_ulong(io_stream), 73 | normal=read_vector(io_stream), 74 | distance=read_float(io_stream)) 75 | 76 | @staticmethod 77 | def size(): 78 | return 32 79 | 80 | def write(self, io_stream): 81 | write_ulong(self.vert_ids[0], io_stream) 82 | write_ulong(self.vert_ids[1], io_stream) 83 | write_ulong(self.vert_ids[2], io_stream) 84 | write_ulong(self.surface_type, io_stream) 85 | write_vector(self.normal, io_stream) 86 | write_float(self.distance, io_stream) 87 | 88 | @staticmethod 89 | def parse(xml_triangle): 90 | result = Triangle(vert_ids=[]) 91 | 92 | for i, xml_vert in enumerate(xml_triangle.findall('V')): 93 | result.vert_ids.append(int(xml_vert.text)) 94 | 95 | result.normal = parse_vector(xml_triangle.find('Nrm')) 96 | result.distance = get_float(xml_triangle.find('Dist').text) 97 | return result 98 | 99 | def create(self, parent): 100 | triangle = create_node(parent, 'T') 101 | for vert_id in self.vert_ids: 102 | xml_vert = create_node(triangle, 'V') 103 | xml_vert.text = str(vert_id) 104 | 105 | create_vector(self.normal, triangle, 'Nrm') 106 | xml_distance = create_node(triangle, 'Dist') 107 | xml_distance.text = format(self.distance) 108 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/structs/mesh_structs/vertex_influence.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.io_binary import * 5 | from io_mesh_w3d.w3x.io_xml import * 6 | 7 | 8 | class VertexInfluence: 9 | def __init__(self, bone_idx=0, xtra_idx=0, bone_inf=0.0, xtra_inf=0.0): 10 | self.bone_idx = bone_idx 11 | self.xtra_idx = xtra_idx 12 | self.bone_inf = bone_inf 13 | self.xtra_inf = xtra_inf 14 | 15 | @staticmethod 16 | def read(io_stream): 17 | return VertexInfluence( 18 | bone_idx=read_ushort(io_stream), 19 | xtra_idx=read_ushort(io_stream), 20 | bone_inf=read_ushort(io_stream) / 100, 21 | xtra_inf=read_ushort(io_stream) / 100) 22 | 23 | @staticmethod 24 | def size(): 25 | return 8 26 | 27 | def write(self, io_stream): 28 | write_ushort(self.bone_idx, io_stream) 29 | write_ushort(self.xtra_idx, io_stream) 30 | write_ushort(int(self.bone_inf * 100), io_stream) 31 | write_ushort(int(self.xtra_inf * 100), io_stream) 32 | 33 | @staticmethod 34 | def parse(xml_vertex_influence, xml_vertex_influence2=None): 35 | result = VertexInfluence( 36 | bone_idx=int(xml_vertex_influence.get('Bone')), 37 | bone_inf=parse_float(xml_vertex_influence, 'Weight')) 38 | 39 | if xml_vertex_influence2 is not None: 40 | result.xtra_idx = int(xml_vertex_influence2.get('Bone')) 41 | result.xtra_inf = parse_float(xml_vertex_influence2, 'Weight') 42 | return result 43 | 44 | def create(self, parent, parent2=None): 45 | influence = create_node(parent, 'I') 46 | influence.set('Bone', str(self.bone_idx)) 47 | influence.set('Weight', format(self.bone_inf)) 48 | 49 | if parent2 is not None: 50 | influence2 = create_node(parent2, 'I') 51 | influence2.set('Bone', str(self.xtra_idx)) 52 | influence2.set('Weight', format(self.xtra_inf)) 53 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/structs/rgba.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.io_binary import * 5 | from io_mesh_w3d.w3x.io_xml import * 6 | 7 | 8 | class RGBA: 9 | def __init__(self, vec=None, a=None, scale=255, r=0, g=0, b=0): 10 | if vec is None: 11 | self.r = r 12 | self.g = g 13 | self.b = b 14 | if a is not None: 15 | self.a = int(a) 16 | else: 17 | self.a = 0 18 | return 19 | 20 | self.r = int(vec[0] * scale) 21 | self.g = int(vec[1] * scale) 22 | self.b = int(vec[2] * scale) 23 | if a is not None: 24 | self.a = int(a) 25 | else: 26 | self.a = int(vec[3] * scale) 27 | 28 | @staticmethod 29 | def read(io_stream): 30 | return RGBA(r=read_ubyte(io_stream), 31 | g=read_ubyte(io_stream), 32 | b=read_ubyte(io_stream), 33 | a=read_ubyte(io_stream)) 34 | 35 | @staticmethod 36 | def read_f(io_stream): 37 | return RGBA(r=int(read_float(io_stream) * 255), 38 | g=int(read_float(io_stream) * 255), 39 | b=int(read_float(io_stream) * 255), 40 | a=int(read_float(io_stream) * 255)) 41 | 42 | @staticmethod 43 | def parse(xml_color): 44 | return RGBA(r=int(parse_float(xml_color, 'R', 0.0) * 255), 45 | g=int(parse_float(xml_color, 'G', 0.0) * 255), 46 | b=int(parse_float(xml_color, 'B', 0.0) * 255), 47 | a=int(parse_float(xml_color, 'A', 0.0) * 255)) 48 | 49 | @staticmethod 50 | def size(): 51 | return 4 52 | 53 | def write(self, io_stream): 54 | write_ubyte(self.r, io_stream) 55 | write_ubyte(self.g, io_stream) 56 | write_ubyte(self.b, io_stream) 57 | write_ubyte(self.a, io_stream) 58 | 59 | def write_f(self, io_stream): 60 | write_float(self.r / 255, io_stream) 61 | write_float(self.g / 255, io_stream) 62 | write_float(self.b / 255, io_stream) 63 | write_float(self.a / 255, io_stream) 64 | 65 | def create(self, parent): 66 | color = create_node(parent, 'C') 67 | color.set('R', format(self.r / 255)) 68 | color.set('G', format(self.g / 255)) 69 | color.set('B', format(self.b / 255)) 70 | color.set('A', format(self.a / 255)) 71 | 72 | def to_vector_rgba(self, scale=255.0): 73 | return self.r / scale, self.g / scale, self.b / scale, self.a / scale 74 | 75 | def to_vector_rgb(self, scale=255.0): 76 | return self.r / scale, self.g / scale, self.b / scale 77 | 78 | def __eq__(self, other): 79 | if isinstance(other, RGBA): 80 | return self.r == other.r and self.g == other.g and self.b == other.b and self.a == other.a 81 | return False 82 | 83 | def __str__(self): 84 | return f'RGBA({self.r}, {self.g}, {self.b}, {self.a})' 85 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/utils/box_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.utils.helpers import * 5 | from io_mesh_w3d.common.structs.collision_box import * 6 | 7 | 8 | def retrieve_boxes(container_name): 9 | boxes = [] 10 | 11 | for mesh_object in get_objects('MESH'): 12 | if mesh_object.data.object_type != 'BOX': 13 | continue 14 | name = container_name + '.' + mesh_object.name 15 | box = CollisionBox( 16 | name_=name, 17 | center=mesh_object.location) 18 | 19 | box.extend = get_aa_box(mesh_object.data.vertices) 20 | 21 | box.box_type = int(mesh_object.data.box_type) 22 | 23 | if 'PHYSICAL' in mesh_object.data.box_collision_types: 24 | box.collision_types |= COLLISION_TYPE_PHYSICAL 25 | if 'PROJECTILE' in mesh_object.data.box_collision_types: 26 | box.collision_types |= COLLISION_TYPE_PROJECTILE 27 | if 'VIS' in mesh_object.data.box_collision_types: 28 | box.collision_types |= COLLISION_TYPE_VIS 29 | if 'CAMERA' in mesh_object.data.box_collision_types: 30 | box.collision_types |= COLLISION_TYPE_CAMERA 31 | if 'VEHICLE' in mesh_object.data.box_collision_types: 32 | box.collision_types |= COLLISION_TYPE_VEHICLE 33 | 34 | for material in mesh_object.data.materials: 35 | box.color = RGBA(material.diffuse_color) 36 | boxes.append(box) 37 | return boxes 38 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/utils/box_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | from io_mesh_w3d.common.utils.helpers import * 6 | from io_mesh_w3d.common.structs.collision_box import * 7 | 8 | 9 | def create_box(box, coll): 10 | x = box.extend[0] / 2.0 11 | y = box.extend[1] / 2.0 12 | z = box.extend[2] 13 | 14 | verts = [(x, y, z), (-x, y, z), (-x, -y, z), (x, -y, z), 15 | (x, y, 0), (-x, y, 0), (-x, -y, 0), (x, -y, 0)] 16 | faces = [(0, 1, 2, 3), (4, 5, 6, 7), (0, 4, 5, 1), 17 | (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0)] 18 | 19 | cube = bpy.data.meshes.new(box.name()) 20 | cube.from_pydata(verts, [], faces) 21 | cube.update(calc_edges=True) 22 | box_object = bpy.data.objects.new(box.name(), cube) 23 | box_object.data.object_type = 'BOX' 24 | box_object.data.box_type = str(box.box_type) 25 | 26 | box_collision_types = {'DEFAULT'} 27 | if box.collision_types & COLLISION_TYPE_PHYSICAL: 28 | box_collision_types.add('PHYSICAL') 29 | if box.collision_types & COLLISION_TYPE_PROJECTILE: 30 | box_collision_types.add('PROJECTILE') 31 | if box.collision_types & COLLISION_TYPE_VIS: 32 | box_collision_types.add('VIS') 33 | if box.collision_types & COLLISION_TYPE_CAMERA: 34 | box_collision_types.add('CAMERA') 35 | if box.collision_types & COLLISION_TYPE_VEHICLE: 36 | box_collision_types.add('VEHICLE') 37 | 38 | box_object.data.box_collision_types = box_collision_types 39 | 40 | box_object.display_type = 'WIRE' 41 | mat = bpy.data.materials.new(box.name() + ".Material") 42 | 43 | mat.diffuse_color = box.color.to_vector_rgba() 44 | cube.materials.append(mat) 45 | box_object.location = box.center 46 | link_object_to_active_scene(box_object, coll) 47 | 48 | 49 | def rig_box(box, hierarchy, rig, sub_object): 50 | if sub_object.bone_index == 0: 51 | return 52 | pivot = hierarchy.pivots[sub_object.bone_index] 53 | box_object = bpy.data.objects[box.name()] 54 | box_object.parent = rig 55 | box_object.parent_bone = pivot.name 56 | box_object.parent_type = 'BONE' 57 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/utils/helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | import os 6 | import sys 7 | from mathutils import Quaternion, Matrix, Vector 8 | from bpy_extras.image_utils import load_image 9 | 10 | 11 | def make_transform_matrix(loc, rot): 12 | mat_loc = Matrix.Translation(loc) 13 | mat_rot = Quaternion(rot).to_matrix().to_4x4() 14 | return mat_loc @ mat_rot 15 | 16 | 17 | def get_objects(type, object_list=None): # MESH, ARMATURE 18 | if object_list is None: 19 | object_list = bpy.context.scene.objects 20 | return [obj for obj in object_list if obj.type == type] 21 | 22 | 23 | def switch_to_pose(rig, pose): 24 | if rig is not None: 25 | rig.data.pose_position = pose 26 | bpy.context.view_layer.update() 27 | 28 | 29 | def insensitive_path(path): 30 | # find the io_stream on unix 31 | directory = os.path.dirname(path) 32 | name = os.path.basename(path) 33 | 34 | for io_stream_name in os.listdir(directory): 35 | if io_stream_name.lower() == name.lower(): 36 | path = os.path.join(directory, io_stream_name) 37 | return path 38 | 39 | 40 | def get_collection(hlod=None, index=''): 41 | if hlod is not None: 42 | name = hlod.model_name() + index 43 | if name in bpy.data.collections: 44 | return bpy.data.collections[name] 45 | coll = bpy.data.collections.new(name) 46 | bpy.context.scene.collection.children.link(coll) 47 | return coll 48 | return bpy.context.scene.collection 49 | 50 | 51 | def link_object_to_active_scene(obj, coll): 52 | coll.objects.link(obj) 53 | bpy.context.view_layer.objects.active = obj 54 | obj.select_set(True) 55 | 56 | 57 | def rig_object(obj, hierarchy, rig, sub_object): 58 | obj.parent = rig 59 | obj.parent_type = 'ARMATURE' 60 | if sub_object.bone_index <= 0: 61 | return 62 | 63 | pivot = hierarchy.pivots[sub_object.bone_index] 64 | 65 | obj.parent_bone = pivot.name 66 | obj.parent_type = 'BONE' 67 | 68 | 69 | def create_uvlayer(context, mesh, b_mesh, tris, mat_pass): 70 | tx_coords = None 71 | if mat_pass.tx_coords: 72 | tx_coords = mat_pass.tx_coords 73 | else: 74 | if mat_pass.tx_stages: 75 | if len(mat_pass.tx_stages[0].tx_coords) == 0: 76 | context.warning('texture stage did not have texture coordinates!') 77 | return 78 | tx_coords = mat_pass.tx_stages[0].tx_coords[0] 79 | if len(mat_pass.tx_stages[0].tx_coords) > 1: 80 | context.warning('only one set of texture coordinates per texture stage supported') 81 | if len(mat_pass.tx_stages) > 1: 82 | context.warning('only one texture stage per material pass supported') 83 | 84 | if tx_coords is None: 85 | if mesh is not None: 86 | uv_layer = mesh.uv_layers.new(do_init=False) 87 | return 88 | 89 | uv_layer = mesh.uv_layers.new(do_init=False) 90 | for i, face in enumerate(b_mesh.faces): 91 | for loop in face.loops: 92 | idx = tris[i][loop.index % 3] 93 | uv_layer.data[loop.index].uv = tx_coords[idx].xy 94 | 95 | 96 | def create_uvlayer_2(context, mesh, b_mesh, tris, mat_pass): 97 | tx_coords_2 = None 98 | if mat_pass.tx_coords_2: 99 | tx_coords_2 = mat_pass.tx_coords_2 100 | else: 101 | uv_layer = mesh.uv_layers.new(do_init=False) 102 | return 103 | 104 | uv_layer = mesh.uv_layers.new(do_init=False) 105 | for i, face in enumerate(b_mesh.faces): 106 | for loop in face.loops: 107 | idx = tris[i][loop.index % 3] 108 | uv_layer.data[loop.index].uv = tx_coords_2[idx].xy 109 | 110 | 111 | extensions = ['.dds', '.tga', '.jpg', '.jpeg', '.png', '.bmp'] 112 | 113 | 114 | def find_texture(context, file, name=None): 115 | file = file.rsplit('.', 1)[0] 116 | if name is None: 117 | name = file 118 | else: 119 | name = name.rsplit('.', 1)[0] 120 | 121 | for extension in extensions: 122 | combined = name + extension 123 | if combined in bpy.data.images: 124 | return bpy.data.images[combined] 125 | 126 | path = insensitive_path(os.path.dirname(context.filepath)) 127 | filepath = path + os.path.sep + file 128 | 129 | img = None 130 | for extension in extensions: 131 | img = load_image(filepath + extension, check_existing=True) 132 | if img is not None: 133 | context.info('loaded texture: ' + filepath + extension) 134 | img.name = file 135 | break 136 | 137 | if img is None: 138 | context.warning( 139 | f'texture not found: {filepath} {extensions}. Make sure it is right next to the file you are importing!') 140 | img = bpy.data.images.new(name, width=2048, height=2048) 141 | img.generated_type = 'COLOR_GRID' 142 | img.source = 'GENERATED' 143 | img.name = name + extensions[0] 144 | 145 | img.alpha_mode = 'STRAIGHT' 146 | return img 147 | 148 | 149 | def get_aa_box(vertices): 150 | minX = sys.float_info.max 151 | maxX = sys.float_info.min 152 | 153 | minY = sys.float_info.max 154 | maxY = sys.float_info.min 155 | 156 | minZ = sys.float_info.max 157 | maxZ = sys.float_info.min 158 | 159 | for vertex in vertices: 160 | minX = min(vertex.co.x, minX) 161 | maxX = max(vertex.co.x, maxX) 162 | 163 | minY = min(vertex.co.y, minY) 164 | maxY = max(vertex.co.y, maxY) 165 | 166 | minZ = min(vertex.co.z, minZ) 167 | maxZ = max(vertex.co.z, maxZ) 168 | 169 | return Vector((maxX - minX, maxY - minY, maxZ - minZ)) 170 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/utils/hierarchy_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector 5 | from io_mesh_w3d.common.utils.helpers import * 6 | from io_mesh_w3d.common.structs.hierarchy import * 7 | 8 | 9 | pick_plane_names = ['PICK'] 10 | 11 | 12 | def retrieve_hierarchy(context, container_name): 13 | root = HierarchyPivot(name='ROOTTRANSFORM') 14 | 15 | hierarchy = Hierarchy( 16 | header=HierarchyHeader(), 17 | pivots=[root]) 18 | 19 | rig = None 20 | rigs = get_objects('ARMATURE') 21 | 22 | pivot_id_dict = dict() 23 | 24 | if len(rigs) == 0: 25 | hierarchy.header.name = container_name 26 | hierarchy.header.center_pos = Vector() 27 | context.warning('scene does not contain an armature object!') 28 | 29 | if len(rigs) > 0: 30 | rig = rigs[0] 31 | 32 | switch_to_pose(rig, 'REST') 33 | 34 | root.translation = rig.delta_location 35 | root.rotation = rig.delta_rotation_quaternion 36 | 37 | hierarchy.header.name = rig.data.name 38 | hierarchy.header.center_pos = rig.location 39 | 40 | for bone in rig.pose.bones: 41 | process_bone(bone, pivot_id_dict, hierarchy) 42 | 43 | switch_to_pose(rig, 'POSE') 44 | 45 | if len(rigs) > 1: 46 | context.error(f'only one armature per scene allowed! Exporting only the first one: {rigs[0].name}') 47 | 48 | meshes = get_objects('MESH') 49 | 50 | for mesh in meshes: 51 | process_mesh(context, mesh, hierarchy, pivot_id_dict) 52 | 53 | hierarchy.header.num_pivots = len(hierarchy.pivots) 54 | return hierarchy, rig 55 | 56 | 57 | def process_bone(bone, pivot_id_dict, hierarchy): 58 | if bone.name in pivot_id_dict.keys(): 59 | return 60 | 61 | pivot = HierarchyPivot(name=bone.name, parent_id=0) 62 | matrix = bone.matrix 63 | 64 | if bone.parent is not None: 65 | if bone.parent.name not in pivot_id_dict.keys(): 66 | process_bone(bone.parent, pivot_id_dict, hierarchy) 67 | 68 | pivot.parent_id = pivot_id_dict[bone.parent.name] 69 | matrix = bone.parent.matrix.inverted() @ matrix 70 | 71 | if bone.name in pivot_id_dict.keys(): 72 | return 73 | 74 | translation, rotation, _ = matrix.decompose() 75 | pivot.translation = translation 76 | pivot.rotation = rotation 77 | eulers = rotation.to_euler() 78 | pivot.euler_angles = Vector((eulers.x, eulers.y, eulers.z)) 79 | 80 | pivot_id_dict[pivot.name] = len(hierarchy.pivots) 81 | hierarchy.pivots.append(pivot) 82 | 83 | for child in bone.children: 84 | process_bone(child, pivot_id_dict, hierarchy) 85 | 86 | 87 | def process_mesh(context, mesh, hierarchy, pivot_id_dict): 88 | if mesh.vertex_groups \ 89 | or mesh.data.object_type == 'BOX' \ 90 | or mesh.name in pick_plane_names \ 91 | or mesh.name in pivot_id_dict.keys(): 92 | return 93 | 94 | if not mesh.parent_type == 'BONE': 95 | pivot = HierarchyPivot(name=mesh.name, parent_id=0) 96 | matrix = mesh.matrix_local 97 | 98 | if mesh.parent is not None and mesh.parent.type == 'MESH': 99 | context.warning(f'mesh \'{mesh.name}\' did have an object instead of a bone as parent!') 100 | if mesh.parent.name not in pivot_id_dict.keys(): 101 | process_mesh(context, mesh.parent, hierarchy, pivot_id_dict) 102 | return 103 | 104 | pivot.parent_id = pivot_id_dict[mesh.parent.name] 105 | matrix = mesh.parent.matrix_local.inverted() @ matrix 106 | 107 | if mesh.name in pivot_id_dict.keys(): 108 | return 109 | 110 | location, rotation, _ = matrix.decompose() 111 | eulers = rotation.to_euler() 112 | 113 | pivot.translation = location 114 | pivot.rotation = rotation 115 | pivot.euler_angles = Vector((eulers.x, eulers.y, eulers.z)) 116 | 117 | pivot_id_dict[pivot.name] = len(hierarchy.pivots) 118 | hierarchy.pivots.append(pivot) 119 | 120 | for child in mesh.children: 121 | process_mesh(context, child, hierarchy, pivot_id_dict) 122 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/utils/hierarchy_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | from mathutils import Vector, Quaternion, Matrix 6 | from io_mesh_w3d.common.utils.helpers import * 7 | from io_mesh_w3d.common.utils.primitives import * 8 | 9 | 10 | def get_or_create_skeleton(hierarchy, coll): 11 | if hierarchy is None: 12 | return None 13 | 14 | name = hierarchy.header.name.upper() 15 | for obj in bpy.data.objects: 16 | if obj.name.upper() == name and obj.type == 'ARMATURE': 17 | return obj 18 | 19 | return create_bone_hierarchy(hierarchy, coll) 20 | 21 | 22 | def create_rig(name, root, coll): 23 | armature = bpy.data.armatures.new(name) 24 | armature.show_names = False 25 | 26 | rig = bpy.data.objects.new(name, armature) 27 | rig.rotation_mode = 'QUATERNION' 28 | rig.delta_location = root.translation 29 | rig.delta_rotation_quaternion = root.rotation 30 | rig.track_axis = 'POS_X' 31 | link_object_to_active_scene(rig, coll) 32 | bpy.ops.object.mode_set(mode='EDIT') 33 | return rig, armature 34 | 35 | 36 | def create_bone_hierarchy(hierarchy, coll): 37 | root = hierarchy.pivots[0] 38 | rig, armature = create_rig(hierarchy.name(), root, coll) 39 | 40 | for pivot in hierarchy.pivots: 41 | if pivot.parent_id < 0: 42 | continue 43 | 44 | bone = armature.edit_bones.new(pivot.name) 45 | matrix = make_transform_matrix(pivot.translation, pivot.rotation) 46 | 47 | if pivot.parent_id > 0: 48 | parent_pivot = hierarchy.pivots[pivot.parent_id] 49 | bone.parent = armature.edit_bones[parent_pivot.name] 50 | matrix = bone.parent.matrix @ matrix 51 | 52 | bone.head = Vector((0.0, 0.0, 0.0)) 53 | bone.tail = Vector((0.0, 0.0, 0.01)) 54 | bone.matrix = matrix 55 | 56 | bpy.ops.object.mode_set(mode='POSE') 57 | (basic_sphere, sphere_mesh) = create_sphere() 58 | 59 | for bone in rig.pose.bones: 60 | bone.custom_shape = basic_sphere 61 | 62 | bpy.ops.object.mode_set(mode='OBJECT') 63 | 64 | bpy.data.objects.remove(basic_sphere) 65 | bpy.data.meshes.remove(sphere_mesh) 66 | return rig 67 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/utils/hlod_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | 6 | from io_mesh_w3d.common.utils.helpers import * 7 | from io_mesh_w3d.common.structs.hlod import * 8 | 9 | 10 | screen_sizes = [MAX_SCREEN_SIZE, 1.0, 0.3, 0.03] 11 | 12 | 13 | def create_lod_array(meshes, hierarchy, container_name, lod_arrays): 14 | if not meshes: 15 | return lod_arrays 16 | 17 | index = min(len(lod_arrays), len(screen_sizes) - 1) 18 | 19 | lod_array = HLodLodArray( 20 | header=HLodArrayHeader( 21 | model_count=len(meshes), 22 | max_screen_size=screen_sizes[index]), 23 | sub_objects=[]) 24 | 25 | for mesh in meshes: 26 | sub_object = HLodSubObject( 27 | name=mesh.name, 28 | identifier=container_name + '.' + mesh.name, 29 | bone_index=0, 30 | is_box=mesh.data.object_type == 'BOX') 31 | 32 | if not mesh.vertex_groups: 33 | for index, pivot in enumerate(hierarchy.pivots): 34 | if pivot.name == mesh.parent_bone or pivot.name == mesh.name: 35 | sub_object.bone_index = index 36 | 37 | lod_array.sub_objects.append(sub_object) 38 | 39 | lod_arrays.append(lod_array) 40 | return lod_arrays 41 | 42 | 43 | def create_hlod(hierarchy, container_name): 44 | hlod = HLod( 45 | header=HLodHeader( 46 | model_name=container_name, 47 | hierarchy_name=hierarchy.name()), 48 | lod_arrays=[]) 49 | 50 | meshes = get_objects('MESH', bpy.context.scene.collection.objects) 51 | lod_arrays = create_lod_array(meshes, hierarchy, container_name, []) 52 | 53 | for coll in bpy.data.collections: 54 | meshes = get_objects('MESH', coll.objects) 55 | lod_arrays = create_lod_array( 56 | meshes, hierarchy, container_name, lod_arrays) 57 | 58 | for lod_array in reversed(lod_arrays): 59 | hlod.lod_arrays.append(lod_array) 60 | hlod.header.lod_count = len(hlod.lod_arrays) 61 | return hlod 62 | -------------------------------------------------------------------------------- /io_mesh_w3d/common/utils/primitives.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | import bmesh 6 | 7 | 8 | def create_sphere(): 9 | mesh = bpy.data.meshes.new('Basic_Sphere') 10 | basic_sphere = bpy.data.objects.new("Basic_Sphere", mesh) 11 | 12 | b_mesh = bmesh.new() 13 | if bpy.app.version < (3, 0, 0): 14 | bmesh.ops.create_uvsphere(b_mesh, u_segments=12, v_segments=6, diameter=35) 15 | else: 16 | bmesh.ops.create_uvsphere(b_mesh, u_segments=12, v_segments=6, radius=17.5) 17 | b_mesh.to_mesh(mesh) 18 | b_mesh.free() 19 | 20 | return (basic_sphere, mesh) 21 | 22 | 23 | def create_cone(name): 24 | mesh = bpy.data.meshes.new(name) 25 | cone = bpy.data.objects.new(name, mesh) 26 | 27 | b_mesh = bmesh.new() 28 | if bpy.app.version < (3, 0, 0): 29 | bmesh.ops.create_cone(b_mesh, cap_ends=True, cap_tris=True, 30 | segments=10, diameter1=0, diameter2=1.0, depth=2.0, calc_uvs=True) 31 | else: 32 | bmesh.ops.create_cone(b_mesh, cap_ends=True, cap_tris=True, 33 | segments=10, radius1=0, radius2=0.5, depth=2.0, calc_uvs=True) 34 | b_mesh.to_mesh(mesh) 35 | b_mesh.free() 36 | 37 | return mesh, cone 38 | -------------------------------------------------------------------------------- /io_mesh_w3d/export_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.data_context import * 5 | 6 | from io_mesh_w3d.common.utils.mesh_export import * 7 | from io_mesh_w3d.common.utils.hierarchy_export import * 8 | from io_mesh_w3d.common.utils.animation_export import * 9 | from io_mesh_w3d.common.utils.hlod_export import * 10 | from io_mesh_w3d.common.utils.box_export import * 11 | from io_mesh_w3d.w3d.utils.dazzle_export import * 12 | 13 | 14 | def save_data(context, export_settings): 15 | data_context = retrieve_data(context, export_settings) 16 | 17 | if data_context is None: 18 | return {'CANCELLED'} 19 | 20 | if context.file_format == 'W3X': 21 | context.filename_ext = '.w3x' 22 | from .w3x.export_w3x import save 23 | return save(context, export_settings, data_context) 24 | 25 | context.filename_ext = '.w3d' 26 | from .w3d.export_w3d import save 27 | return save(context, export_settings, data_context) 28 | 29 | 30 | def retrieve_data(context, export_settings): 31 | export_mode = export_settings['mode'] 32 | 33 | if export_mode not in ['M', 'HM', 'HAM', 'H', 'A']: 34 | context.error(f'unsupported export mode: {export_mode}, aborting export!') 35 | return None 36 | 37 | container_name = os.path.basename(context.filepath).split('.')[0] 38 | 39 | if context.file_format == 'W3D' and len(container_name) > STRING_LENGTH: 40 | context.error(f'Filename is longer than {STRING_LENGTH} characters, aborting export!') 41 | return None 42 | 43 | hierarchy, rig, hlod = None, None, None 44 | 45 | if export_mode != 'M': 46 | hierarchy, rig = retrieve_hierarchy(context, container_name) 47 | hlod = create_hlod(hierarchy, container_name) 48 | 49 | data_context = DataContext( 50 | container_name=container_name, 51 | rig=rig, 52 | meshes=[], 53 | textures=[], 54 | collision_boxes=retrieve_boxes(container_name), 55 | dazzles=retrieve_dazzles(container_name), 56 | hierarchy=hierarchy, 57 | hlod=hlod) 58 | 59 | if 'M' in export_mode: 60 | (meshes, textures) = retrieve_meshes(context, hierarchy, rig, container_name) 61 | data_context.meshes = meshes 62 | data_context.textures = textures 63 | if not data_context.meshes: 64 | context.error('Scene does not contain any meshes, aborting export!') 65 | return None 66 | 67 | for mesh in data_context.meshes: 68 | if not mesh.validate(context): 69 | context.error('aborting export!') 70 | return None 71 | 72 | if 'H' in export_mode and not hierarchy.validate(context): 73 | context.error('aborting export!') 74 | return None 75 | 76 | if export_mode in ['HM', 'HAM']: 77 | if not data_context.hlod.validate(context): 78 | context.error('aborting export!') 79 | return None 80 | 81 | for box in data_context.collision_boxes: 82 | if not box.validate(context): 83 | context.error('aborting export!') 84 | return None 85 | 86 | if 'A' in export_mode: 87 | timecoded = export_settings['compression'] == 'TC' 88 | data_context.animation = retrieve_animation(context, container_name, hierarchy, rig, timecoded) 89 | if not data_context.animation.validate(context): 90 | context.error('aborting export!') 91 | return None 92 | return data_context 93 | -------------------------------------------------------------------------------- /io_mesh_w3d/geometry_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | from io_mesh_w3d.utils import ReportHelper 6 | from bpy_extras.io_utils import ExportHelper 7 | from io_mesh_w3d.w3x.io_xml import * 8 | from io_mesh_w3d.common.utils.helpers import * 9 | 10 | 11 | def format_str(value): 12 | return '{:.3f}'.format(value) 13 | 14 | 15 | class ExportGeometryData(bpy.types.Operator, ExportHelper, ReportHelper): 16 | bl_idname = 'scene.export_geometry_data' 17 | bl_label = 'Export Geometry Data' 18 | bl_options = {'REGISTER', 'UNDO'} 19 | 20 | filename_ext = '.xml' 21 | 22 | def execute(self, context): 23 | export_geometry_data(self, self.filepath) 24 | return {'FINISHED'} 25 | 26 | 27 | def export_geometry_data(context, filepath): 28 | inifilepath = filepath.replace('.xml', '.ini') 29 | context.info(f'exporting geometry data to xml: {filepath}') 30 | context.info(f'exporting geometry data to ini: {inifilepath}') 31 | 32 | file = open(inifilepath, 'w') 33 | 34 | root = create_named_root('Geometry') 35 | root.set('isSmall', str(False)) 36 | 37 | index = 0 38 | 39 | for mesh in get_objects('MESH'): 40 | if not mesh.data.object_type == 'GEOMETRY': 41 | continue 42 | 43 | type = str(mesh.data.geometry_type).upper() 44 | location, _, scale = mesh.matrix_world.decompose() 45 | extend = get_aa_box(mesh.data.vertices) 46 | majorRadius = extend.x * scale.x * 0.5 47 | minorRadius = extend.y * scale.y * 0.5 48 | height = extend.z * scale.z 49 | 50 | shape_node = create_node(root, 'Shape') 51 | shape_node.set('Type', type) 52 | if (index == 0): 53 | file.write(f'\tGeometry\t\t\t\t= {type}\n') 54 | file.write('\tGeometryIsSmall\t\t\t= No\n') 55 | else: 56 | file.write(f'\tAdditionalGeometry\t\t= {type}\n') 57 | 58 | file.write(f'\tGeometryName\t\t\t= {mesh.name}\n') 59 | 60 | shape_node.set('MajorRadius', format_str(majorRadius)) 61 | file.write(f'\tGeometryMajorRadius\t\t= {format_str(majorRadius)}\n') 62 | 63 | if (mesh.data.geometry_type != 'SPHERE'): 64 | shape_node.set('MinorRadius', format_str(minorRadius)) 65 | shape_node.set('Height', format_str(height)) 66 | file.write(f'\tGeometryMinorRadius\t\t= {format_str(minorRadius)}\n') 67 | file.write(f'\tGeometryHeight\t\t\t= {format_str(height)}\n') 68 | 69 | if (mesh.data.contact_points_type != 'NONE'): 70 | shape_node.set('ContactPointGeneration', str(mesh.data.contact_points_type).upper()) 71 | 72 | create_vector(location, shape_node, 'Offset') 73 | if (location.length > 0.01): 74 | file.write( 75 | f'\tGeometryOffset\t\t\t= X:{format_str(location.x)} Y:{format_str(location.y)} Z:{format_str(location.z)}\n') 76 | 77 | file.write('\n') 78 | index += 1 79 | 80 | for empty in get_objects('EMPTY'): 81 | contact_point_node = create_node(root, 'ContactPoint') 82 | location, _, _ = empty.matrix_world.decompose() 83 | create_vector(location, contact_point_node, 'Pos') 84 | file.write( 85 | f'\tGeometryContactPoint\t= X:{format_str(location.x)} Y:{format_str(location.y)} Z:{format_str(location.z)}\n') 86 | 87 | write(root, filepath) 88 | file.close() 89 | context.info('exporting geometry data finished') 90 | -------------------------------------------------------------------------------- /io_mesh_w3d/import_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | 6 | from io_mesh_w3d.common.utils.mesh_import import * 7 | from io_mesh_w3d.common.utils.hierarchy_import import * 8 | from io_mesh_w3d.common.utils.animation_import import * 9 | from io_mesh_w3d.common.utils.box_import import * 10 | from io_mesh_w3d.w3d.utils.dazzle_import import * 11 | 12 | 13 | def create_data(context, meshes, hlod=None, hierarchy=None, boxes=None, animation=None, compressed_animation=None, 14 | dazzles=None): 15 | boxes = boxes if boxes is not None else [] 16 | dazzles = dazzles if dazzles is not None else [] 17 | collection = get_collection(hlod) 18 | 19 | mesh_names_map = {} 20 | if hlod is not None: 21 | current_coll = collection 22 | for i, lod_array in enumerate(reversed(hlod.lod_arrays)): 23 | if i > 0: 24 | current_coll = get_collection(hlod, '.' + str(i)) 25 | current_coll.hide_viewport = True 26 | 27 | for sub_object in lod_array.sub_objects: 28 | for mesh in meshes: 29 | if mesh.name() == sub_object.name: 30 | newname = create_mesh(context, mesh, current_coll) 31 | mesh_names_map[mesh.name()] = newname 32 | 33 | for box in boxes: 34 | if box.name() == sub_object.name: 35 | create_box(box, collection) 36 | 37 | for dazzle in dazzles: 38 | if dazzle.name() == sub_object.name: 39 | create_dazzle(context, dazzle, collection) 40 | 41 | rig = get_or_create_skeleton(hierarchy, collection) 42 | 43 | if hlod is not None: 44 | for lod_array in reversed(hlod.lod_arrays): 45 | for sub_object in lod_array.sub_objects: 46 | for mesh in meshes: 47 | if mesh.name() == sub_object.name: 48 | mesh.header.mesh_name = mesh_names_map[mesh.name()] 49 | rig_mesh(mesh, hierarchy, rig, sub_object) 50 | for box in boxes: 51 | if box.name() == sub_object.name: 52 | rig_box(box, hierarchy, rig, sub_object) 53 | for dazzle in dazzles: 54 | if dazzle.name() == sub_object.name: 55 | dazzle_object = bpy.data.objects[dazzle.name()] 56 | rig_object(dazzle_object, hierarchy, rig, sub_object) 57 | 58 | else: 59 | for mesh in meshes: 60 | create_mesh(context, mesh, collection) 61 | 62 | create_animation(context, rig, animation, hierarchy) 63 | create_animation(context, rig, compressed_animation, hierarchy) 64 | -------------------------------------------------------------------------------- /io_mesh_w3d/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | 5 | class ReportHelper(): 6 | def info(self, msg): 7 | print(f'INFO: {msg}') 8 | self.report({'INFO'}, str(msg)) 9 | 10 | def warning(self, msg): 11 | print(f'WARNING: {msg}') 12 | self.report({'WARNING'}, str(msg)) 13 | 14 | def error(self, msg): 15 | print(f'ERROR: {msg}') 16 | self.report({'ERROR'}, str(msg)) 17 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/adaptive_delta.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import math 5 | 6 | 7 | def fill_with_exponents_of_10(table): 8 | for i in range(16): 9 | table.append(pow(10, i - 8)) 10 | 11 | 12 | def fill_with_sinus_function(table): 13 | for i in range(240): 14 | num = i / 240.0 15 | table.append(1.0 - math.sin(90.0 * num * math.pi / 180.0)) 16 | 17 | 18 | def calculate_table(): 19 | table = [] 20 | fill_with_exponents_of_10(table) 21 | fill_with_sinus_function(table) 22 | return table 23 | 24 | 25 | DELTA_TABLE = calculate_table() 26 | 27 | 28 | def get_deltas(delta_bytes, num_bits): 29 | deltas = [None] * 16 30 | 31 | for i, byte in enumerate(delta_bytes): 32 | if num_bits == 4: 33 | index = i * 2 34 | lower = byte & 0x0F 35 | upper = byte >> 4 36 | # Bitflip 37 | if lower >= 8: 38 | lower -= 16 39 | 40 | deltas[index] = lower 41 | deltas[index + 1] = upper 42 | else: 43 | # Bitflip 44 | byte += 128 45 | if byte >= 128: 46 | byte -= 256 47 | deltas[i] = byte 48 | return deltas 49 | 50 | 51 | def set_deltas(bytes, num_bits): 52 | result = [None] * num_bits * 2 53 | 54 | if num_bits == 4: 55 | for i in range(int(len(bytes) / 2)): 56 | lower = bytes[i * 2] 57 | # Bitflip 58 | if lower < 0: 59 | lower += 16 60 | upper = bytes[i * 2 + 1] 61 | result[i] = (upper << 4) | lower 62 | else: 63 | for i in range(len(bytes)): 64 | byte = bytes[i] 65 | # Bitflip 66 | byte -= 128 67 | if byte < -127: 68 | byte += 256 69 | result[i] = byte 70 | return result 71 | 72 | 73 | def decode(channel_type, vector_len, num_time_codes, scale, data): 74 | scale_factor = 1.0 75 | if data.bit_count == 8: 76 | scale_factor /= 16.0 77 | 78 | result = [None] * num_time_codes 79 | result[0] = data.initial_value 80 | 81 | for i, delta_block in enumerate(data.delta_blocks): 82 | delta_scale = scale * scale_factor * DELTA_TABLE[delta_block.block_index] 83 | deltas = get_deltas(delta_block.delta_bytes, data.bit_count) 84 | 85 | for j, delta in enumerate(deltas): 86 | idx = int(i / vector_len) * 16 + j + 1 87 | if idx >= num_time_codes: 88 | break 89 | 90 | if channel_type == 6: 91 | # shift from wxyz to xyzw 92 | index = (delta_block.vector_index + 1) % 4 93 | value = result[idx - 1][index] + delta_scale * delta 94 | if result[idx] is None: 95 | result[idx] = result[idx - 1].copy() 96 | result[idx][index] = value 97 | else: 98 | result[idx] = result[idx - 1] + delta_scale * delta 99 | return result 100 | 101 | 102 | def encode(channel, num_bits): 103 | scale_factor = 1.0 104 | if num_bits == 8: 105 | scale_factor /= 16.0 106 | 107 | scale = 0.07435 # how to get this ??? 108 | 109 | num_time_codes = len(channel.data) - 1 # minus initial value 110 | num_delta_blocks = int(num_time_codes / 16) + 1 111 | deltas = [0x00] * (num_delta_blocks * 16) 112 | 113 | default_value = None 114 | 115 | for i, value in enumerate(channel.data): 116 | if i == 0: 117 | default_value = value 118 | continue 119 | 120 | block_index = 33 # how to get this one? 121 | 122 | old = default_value 123 | # if i > 1: 124 | # channel.data[i - 1] 125 | delta = value - old 126 | delta /= (scale_factor * scale * DELTA_TABLE[block_index]) 127 | 128 | delta = int(delta) 129 | # print("delta: " + str(delta) + " index: " + str(block_index)) 130 | deltas[i - 1] = delta 131 | 132 | deltas = set_deltas(deltas, num_bits) 133 | return deltas 134 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/export_w3d.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | 5 | def save(context, export_settings, data_context): 6 | filepath = context.filepath 7 | if not filepath.lower().endswith(context.filename_ext): 8 | filepath += context.filename_ext 9 | 10 | context.info(f'Saving file: {filepath}') 11 | 12 | export_mode = export_settings['mode'] 13 | context.info(f'export mode: {export_mode}') 14 | 15 | file = open(filepath, 'wb') 16 | 17 | if export_mode == 'M': 18 | if len(data_context.meshes) > 1: 19 | context.warning('Scene does contain multiple meshes, exporting only the first with export mode M!') 20 | mesh = data_context.meshes[0] 21 | mesh.header.container_name = '' 22 | mesh.header.mesh_name = data_context.container_name 23 | mesh.write(file) 24 | 25 | elif export_mode == 'HM' or export_mode == 'HAM': 26 | if export_mode == 'HAM' \ 27 | or not export_settings['use_existing_skeleton']: 28 | data_context.hlod.header.hierarchy_name = data_context.container_name 29 | data_context.hierarchy.header.name = data_context.container_name 30 | data_context.hierarchy.write(file) 31 | 32 | for box in data_context.collision_boxes: 33 | box.write(file) 34 | 35 | for dazzle in data_context.dazzles: 36 | dazzle.write(file) 37 | 38 | for mesh in data_context.meshes: 39 | mesh.write(file) 40 | 41 | data_context.hlod.write(file) 42 | if export_mode == 'HAM': 43 | data_context.animation.header.hierarchy_name = data_context.container_name 44 | data_context.animation.write(file) 45 | 46 | elif export_mode == 'A': 47 | data_context.animation.write(file) 48 | 49 | elif export_mode == 'H': 50 | data_context.hierarchy.header.name = data_context.container_name.upper() 51 | data_context.hierarchy.write(file) 52 | else: 53 | context.error(f'unsupported export mode \'{export_mode}\', aborting export!') 54 | return {'CANCELLED'} 55 | 56 | file.close() 57 | context.info('finished') 58 | return {'FINISHED'} 59 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/structs/dazzle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.utils.helpers import * 5 | 6 | W3D_CHUNK_DAZZLE = 0x00000900 7 | W3D_CHUNK_DAZZLE_NAME = 0x00000901 8 | W3D_CHUNK_DAZZLE_TYPENAME = 0x00000902 9 | 10 | # The dazzle is always assumed to be at the pivot point 11 | # of the bone it is attached to (you should enable Export_Transform) for 12 | # dazzles. If the dazzle-type (from dazzle.ini) is directional, then the 13 | # coordinate-system of the bone will define the direction. 14 | 15 | 16 | class Dazzle: 17 | def __init__(self, name_='', type_name=''): 18 | self.name_ = name_ 19 | self.type_name = type_name 20 | 21 | def name(self): 22 | return self.name_.split('.')[-1] 23 | 24 | @staticmethod 25 | def read(context, io_stream, chunk_end): 26 | result = Dazzle(name_='', type_name='') 27 | 28 | while io_stream.tell() < chunk_end: 29 | (chunk_type, chunk_size, _) = read_chunk_head(io_stream) 30 | if chunk_type == W3D_CHUNK_DAZZLE_NAME: 31 | result.name_ = read_string(io_stream) 32 | elif chunk_type == W3D_CHUNK_DAZZLE_TYPENAME: 33 | result.type_name = read_string(io_stream) 34 | else: 35 | skip_unknown_chunk(context, io_stream, chunk_type, chunk_size) 36 | return result 37 | 38 | def size(self, include_head=True): 39 | size = const_size(0, include_head) 40 | size += text_size(self.name_) 41 | size += text_size(self.type_name) 42 | return size 43 | 44 | def write(self, io_stream): 45 | write_chunk_head(W3D_CHUNK_DAZZLE, io_stream, self.size(False)) 46 | 47 | write_chunk_head(W3D_CHUNK_DAZZLE_NAME, io_stream, text_size(self.name_, False)) 48 | write_string(self.name_, io_stream) 49 | 50 | write_chunk_head(W3D_CHUNK_DAZZLE_TYPENAME, io_stream, text_size(self.type_name, False)) 51 | write_string(self.type_name, io_stream) 52 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/structs/mesh_structs/material_info.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.utils.helpers import * 5 | 6 | W3D_CHUNK_MATERIAL_INFO = 0x00000028 7 | 8 | 9 | class MaterialInfo: 10 | def __init__(self, pass_count=0, vert_matl_count=0, shader_count=0, texture_count=0): 11 | self.pass_count = pass_count 12 | self.vert_matl_count = vert_matl_count 13 | self.shader_count = shader_count 14 | self.texture_count = texture_count 15 | 16 | @staticmethod 17 | def read(io_stream): 18 | return MaterialInfo( 19 | pass_count=read_ulong(io_stream), 20 | vert_matl_count=read_ulong(io_stream), 21 | shader_count=read_ulong(io_stream), 22 | texture_count=read_ulong(io_stream)) 23 | 24 | @staticmethod 25 | def size(include_head=True): 26 | return const_size(16, include_head) 27 | 28 | def write(self, io_stream): 29 | write_chunk_head(W3D_CHUNK_MATERIAL_INFO, io_stream, self.size(False)) 30 | write_ulong(self.pass_count, io_stream) 31 | write_ulong(self.vert_matl_count, io_stream) 32 | write_ulong(self.shader_count, io_stream) 33 | write_ulong(self.texture_count, io_stream) 34 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/structs/mesh_structs/prelit.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.mesh_structs.texture import * 5 | from io_mesh_w3d.w3d.structs.mesh_structs.material_info import * 6 | from io_mesh_w3d.w3d.structs.mesh_structs.material_pass import * 7 | from io_mesh_w3d.w3d.structs.mesh_structs.shader import * 8 | from io_mesh_w3d.w3d.structs.mesh_structs.vertex_material import * 9 | 10 | W3D_CHUNK_PRELIT_UNLIT = 0x00000023 11 | W3D_CHUNK_PRELIT_VERTEX = 0x00000024 12 | W3D_CHUNK_PRELIT_LIGHTMAP_MULTI_PASS = 0x00000025 13 | W3D_CHUNK_PRELIT_LIGHTMAP_MULTI_TEXTURE = 0x00000026 14 | 15 | 16 | class PrelitBase: 17 | def __init__(self, type=W3D_CHUNK_PRELIT_UNLIT, mat_info=MaterialInfo(), material_passes=None, 18 | vert_materials=None, textures=None, shaders=None): 19 | self.type = type 20 | self.mat_info = mat_info 21 | self.material_passes = material_passes if material_passes is not None else [] 22 | self.vert_materials = vert_materials if vert_materials is not None else [] 23 | self.textures = textures if textures is not None else [] 24 | self.shaders = shaders if shaders is not None else [] 25 | 26 | @staticmethod 27 | def read(context, io_stream, chunk_end, type): 28 | result = PrelitBase(type=type) 29 | 30 | while io_stream.tell() < chunk_end: 31 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 32 | 33 | if chunk_type == W3D_CHUNK_MATERIAL_INFO: 34 | result.mat_info = MaterialInfo.read(io_stream) 35 | elif chunk_type == W3D_CHUNK_SHADERS: 36 | result.shaders = read_list(io_stream, subchunk_end, Shader.read) 37 | elif chunk_type == W3D_CHUNK_VERTEX_MATERIALS: 38 | result.vert_materials = read_chunk_array( 39 | context, 40 | io_stream, 41 | subchunk_end, 42 | W3D_CHUNK_VERTEX_MATERIAL, 43 | VertexMaterial.read) 44 | elif chunk_type == W3D_CHUNK_TEXTURES: 45 | result.textures = read_chunk_array( 46 | context, io_stream, subchunk_end, W3D_CHUNK_TEXTURE, Texture.read) 47 | elif chunk_type == W3D_CHUNK_MATERIAL_PASS: 48 | result.material_passes.append(MaterialPass.read( 49 | context, io_stream, subchunk_end)) 50 | else: 51 | skip_unknown_chunk(context, io_stream, chunk_type, chunk_size) 52 | return result 53 | 54 | def size(self, include_head=True): 55 | size = const_size(0, include_head) 56 | size += self.mat_info.size() 57 | size += list_size(self.vert_materials) 58 | size += list_size(self.shaders) 59 | size += list_size(self.textures) 60 | size += list_size(self.material_passes, False) 61 | return size 62 | 63 | def write(self, io_stream): 64 | write_chunk_head(self.type, io_stream, 65 | self.size(False), has_sub_chunks=True) 66 | 67 | self.mat_info.write(io_stream) 68 | 69 | if self.vert_materials: 70 | write_chunk_head( 71 | W3D_CHUNK_VERTEX_MATERIALS, 72 | io_stream, 73 | list_size(self.vert_materials, False), 74 | has_sub_chunks=True) 75 | write_list(self.vert_materials, io_stream, VertexMaterial.write) 76 | 77 | if self.shaders: 78 | write_chunk_head(W3D_CHUNK_SHADERS, io_stream, 79 | list_size(self.shaders, False)) 80 | write_list(self.shaders, io_stream, Shader.write) 81 | 82 | if self.textures: 83 | write_chunk_head( 84 | W3D_CHUNK_TEXTURES, 85 | io_stream, 86 | list_size(self.textures, False), 87 | has_sub_chunks=True) 88 | write_list(self.textures, io_stream, Texture.write) 89 | 90 | if self.material_passes: 91 | write_list(self.material_passes, io_stream, MaterialPass.write) 92 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/structs/mesh_structs/shader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.io_binary import * 5 | 6 | W3D_CHUNK_SHADERS = 0x00000029 7 | 8 | 9 | class Shader: 10 | def __init__(self, depth_compare=0, depth_mask=0, color_mask=0, dest_blend=0, fog_func=0, pri_gradient=0, 11 | sec_gradient=0, src_blend=0, texturing=0, detail_color_func=0, detail_alpha_func=0, shader_preset=0, 12 | alpha_test=0, post_detail_color_func=0, post_detail_alpha_func=0, pad=0): 13 | self.depth_compare = depth_compare 14 | self.depth_mask = depth_mask 15 | self.color_mask = color_mask 16 | self.dest_blend = dest_blend 17 | self.fog_func = fog_func 18 | self.pri_gradient = pri_gradient 19 | self.sec_gradient = sec_gradient 20 | self.src_blend = src_blend 21 | self.texturing = texturing 22 | self.detail_color_func = detail_color_func 23 | self.detail_alpha_func = detail_alpha_func 24 | self.shader_preset = shader_preset 25 | self.alpha_test = alpha_test 26 | self.post_detail_color_func = post_detail_color_func 27 | self.post_detail_alpha_func = post_detail_alpha_func 28 | self.pad = pad 29 | 30 | @staticmethod 31 | def read(io_stream): 32 | return Shader( 33 | depth_compare=read_ubyte(io_stream), 34 | depth_mask=read_ubyte(io_stream), 35 | color_mask=read_ubyte(io_stream), 36 | dest_blend=read_ubyte(io_stream), 37 | fog_func=read_ubyte(io_stream), 38 | pri_gradient=read_ubyte(io_stream), 39 | sec_gradient=read_ubyte(io_stream), 40 | src_blend=read_ubyte(io_stream), 41 | texturing=read_ubyte(io_stream), 42 | detail_color_func=read_ubyte(io_stream), 43 | detail_alpha_func=read_ubyte(io_stream), 44 | shader_preset=read_ubyte(io_stream), 45 | alpha_test=read_ubyte(io_stream), 46 | post_detail_color_func=read_ubyte(io_stream), 47 | post_detail_alpha_func=read_ubyte(io_stream), 48 | pad=read_ubyte(io_stream)) 49 | 50 | @staticmethod 51 | def size(): 52 | return 16 53 | 54 | def write(self, io_stream): 55 | write_ubyte(self.depth_compare, io_stream) 56 | write_ubyte(self.depth_mask, io_stream) 57 | write_ubyte(self.color_mask, io_stream) 58 | write_ubyte(self.dest_blend, io_stream) 59 | write_ubyte(self.fog_func, io_stream) 60 | write_ubyte(self.pri_gradient, io_stream) 61 | write_ubyte(self.sec_gradient, io_stream) 62 | write_ubyte(self.src_blend, io_stream) 63 | write_ubyte(self.texturing, io_stream) 64 | write_ubyte(self.detail_color_func, io_stream) 65 | write_ubyte(self.detail_alpha_func, io_stream) 66 | write_ubyte(self.shader_preset, io_stream) 67 | write_ubyte(self.alpha_test, io_stream) 68 | write_ubyte(self.post_detail_color_func, io_stream) 69 | write_ubyte(self.post_detail_alpha_func, io_stream) 70 | write_ubyte(self.pad, io_stream) 71 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/structs/mesh_structs/vertex_material.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.rgba import RGBA 5 | from io_mesh_w3d.w3d.utils.helpers import * 6 | 7 | W3D_CHUNK_VERTEX_MATERIALS = 0x0000002A 8 | W3D_CHUNK_VERTEX_MATERIAL_INFO = 0x0000002D 9 | 10 | USE_DEPTH_CUE = 0x1 11 | ARGB_EMISSIVE_ONLY = 0x2 12 | COPY_SPECULAR_TO_DIFFUSE = 0x4 13 | DEPTH_CUE_TO_ALPHA = 0x8 14 | 15 | STAGE0_MAPPING_MASK = 0x00FF0000 16 | STAGE1_MAPPING_MASK = 0x0000FF00 17 | 18 | 19 | class VertexMaterialInfo: 20 | def __init__(self, attributes=0, ambient=RGBA(), diffuse=RGBA(), specular=RGBA(), emissive=RGBA(), shininess=0.0, 21 | opacity=1.0, translucency=0.0): 22 | self.attributes = attributes 23 | self.ambient = ambient # alpha is only padding in this and below 24 | self.diffuse = diffuse 25 | self.specular = specular 26 | self.emissive = emissive 27 | self.shininess = shininess 28 | self.opacity = opacity 29 | self.translucency = translucency 30 | 31 | @staticmethod 32 | def read(io_stream): 33 | return VertexMaterialInfo( 34 | attributes=read_long(io_stream), 35 | ambient=RGBA.read(io_stream), 36 | diffuse=RGBA.read(io_stream), 37 | specular=RGBA.read(io_stream), 38 | emissive=RGBA.read(io_stream), 39 | shininess=read_float(io_stream), 40 | opacity=read_float(io_stream), 41 | translucency=read_float(io_stream)) 42 | 43 | @staticmethod 44 | def size(include_head=True): 45 | return const_size(32, include_head) 46 | 47 | def write(self, io_stream): 48 | write_chunk_head(W3D_CHUNK_VERTEX_MATERIAL_INFO, io_stream, self.size(False)) 49 | write_long(self.attributes, io_stream) 50 | self.ambient.write(io_stream) 51 | self.diffuse.write(io_stream) 52 | self.specular.write(io_stream) 53 | self.emissive.write(io_stream) 54 | write_float(self.shininess, io_stream) 55 | write_float(self.opacity, io_stream) 56 | write_float(self.translucency, io_stream) 57 | 58 | 59 | W3D_CHUNK_VERTEX_MATERIAL = 0x0000002B 60 | W3D_CHUNK_VERTEX_MATERIAL_NAME = 0x0000002C 61 | W3D_CHUNK_VERTEX_MAPPER_ARGS0 = 0x0000002E 62 | W3D_CHUNK_VERTEX_MAPPER_ARGS1 = 0x0000002F 63 | 64 | 65 | class VertexMaterial: 66 | def __init__(self, vm_name='', vm_info=None, vm_args_0='', vm_args_1=''): 67 | self.vm_name = vm_name 68 | self.vm_info = vm_info 69 | self.vm_args_0 = vm_args_0 70 | self.vm_args_1 = vm_args_1 71 | 72 | @staticmethod 73 | def read(context, io_stream, chunk_end): 74 | result = VertexMaterial() 75 | 76 | while io_stream.tell() < chunk_end: 77 | (chunk_type, chunk_size, _) = read_chunk_head(io_stream) 78 | 79 | if chunk_type == W3D_CHUNK_VERTEX_MATERIAL_NAME: 80 | result.vm_name = read_string(io_stream) 81 | elif chunk_type == W3D_CHUNK_VERTEX_MATERIAL_INFO: 82 | result.vm_info = VertexMaterialInfo.read(io_stream) 83 | elif chunk_type == W3D_CHUNK_VERTEX_MAPPER_ARGS0: 84 | result.vm_args_0 = read_string(io_stream) 85 | elif chunk_type == W3D_CHUNK_VERTEX_MAPPER_ARGS1: 86 | result.vm_args_1 = read_string(io_stream) 87 | else: 88 | skip_unknown_chunk(context, io_stream, chunk_type, chunk_size) 89 | return result 90 | 91 | def size(self, include_head=True): 92 | size = const_size(0, include_head) 93 | size += text_size(self.vm_name) 94 | if self.vm_info is not None: 95 | size += self.vm_info.size() 96 | size += text_size(self.vm_args_0) 97 | size += text_size(self.vm_args_1) 98 | return size 99 | 100 | def write(self, io_stream): 101 | write_chunk_head(W3D_CHUNK_VERTEX_MATERIAL, io_stream, self.size(False), has_sub_chunks=True) 102 | write_chunk_head(W3D_CHUNK_VERTEX_MATERIAL_NAME, io_stream, text_size(self.vm_name, False)) 103 | write_string(self.vm_name, io_stream) 104 | 105 | if self.vm_info is not None: 106 | self.vm_info.write(io_stream) 107 | 108 | if self.vm_args_0 != '': 109 | write_chunk_head(W3D_CHUNK_VERTEX_MAPPER_ARGS0, io_stream, text_size(self.vm_args_0, False), io_stream) 110 | write_string(self.vm_args_0, io_stream) 111 | 112 | if self.vm_args_1 != '': 113 | write_chunk_head(W3D_CHUNK_VERTEX_MAPPER_ARGS1, io_stream, text_size(self.vm_args_1, False)) 114 | write_string(self.vm_args_1, io_stream) 115 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/structs/version.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.io_binary import * 5 | 6 | 7 | class Version: 8 | def __init__(self, major=5, minor=0): 9 | self.major = major 10 | self.minor = minor 11 | 12 | @staticmethod 13 | def read(io_stream): 14 | data = read_ulong(io_stream) 15 | return Version(major=data >> 16, 16 | minor=data & 0xFFFF) 17 | 18 | def write(self, io_stream): 19 | write_ulong((self.major << 16) | self.minor, io_stream) 20 | 21 | def __eq__(self, other): 22 | if isinstance(other, Version): 23 | return self.major == other.major and self.minor == other.minor 24 | return False 25 | 26 | def __ne__(self, other): 27 | return not self.__eq__(other) 28 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/utils/dazzle_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.utils.helpers import * 5 | from io_mesh_w3d.w3d.structs.dazzle import * 6 | 7 | 8 | def retrieve_dazzles(container_name): 9 | dazzles = [] 10 | 11 | for mesh_object in get_objects('MESH'): 12 | if mesh_object.data.object_type != 'DAZZLE': 13 | continue 14 | name = container_name + '.' + mesh_object.name 15 | dazzle = Dazzle( 16 | name_=name, 17 | type_name=mesh_object.data.dazzle_type) 18 | 19 | dazzles.append(dazzle) 20 | return dazzles 21 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/utils/dazzle_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | from bpy_extras import node_shader_utils 6 | from io_mesh_w3d.common.utils.primitives import * 7 | from io_mesh_w3d.common.utils.helpers import * 8 | 9 | 10 | def create_dazzle(context, dazzle, coll): 11 | # Todo: proper dimensions for cone 12 | (dazzle_mesh, dazzle_cone) = create_cone(dazzle.name()) 13 | dazzle_cone.data.object_type = 'DAZZLE' 14 | dazzle_cone.data.dazzle_type = dazzle.type_name 15 | link_object_to_active_scene(dazzle_cone, coll) 16 | 17 | material = bpy.data.materials.new(dazzle.name()) 18 | material.use_nodes = True 19 | material.blend_method = 'BLEND' 20 | material.show_transparent_back = False 21 | 22 | principled = node_shader_utils.PrincipledBSDFWrapper(material, is_readonly=False) 23 | principled.base_color = (255, 255, 255) 24 | principled.base_color_texture.image = find_texture(context, 'SunDazzle.tga') 25 | dazzle_mesh.materials.append(material) 26 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3d/utils/helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.io_binary import * 5 | 6 | 7 | def skip_unknown_chunk(context, io_stream, chunk_type, chunk_size): 8 | context.warning(f'unknown chunk_type in io_stream: {hex(chunk_type)}') 9 | io_stream.seek(chunk_size, 1) 10 | 11 | 12 | def read_chunk_array(context, io_stream, chunk_end, type_, read_func): 13 | result = [] 14 | 15 | while io_stream.tell() < chunk_end: 16 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 17 | 18 | if chunk_type == type_: 19 | result.append(read_func(context, io_stream, subchunk_end)) 20 | else: 21 | skip_unknown_chunk(context, io_stream, chunk_type, chunk_size) 22 | return result 23 | 24 | 25 | def const_size(size, include_head=True): 26 | if include_head: 27 | size += HEAD 28 | return size 29 | 30 | 31 | def text_size(text, include_head=True): 32 | if len(text) == 0: 33 | return 0 34 | size = len(text) + 1 35 | if include_head: 36 | size += HEAD 37 | return size 38 | 39 | 40 | def list_size(objects, include_head=True): 41 | if not objects: 42 | return 0 43 | size = 0 44 | if include_head: 45 | size += HEAD 46 | for obj in objects: 47 | size += obj.size() 48 | return size 49 | 50 | 51 | def vec_list_size(vec_list, include_head=True): 52 | return data_list_size(vec_list, include_head, 12) 53 | 54 | 55 | def vec2_list_size(vec_list, include_head=True): 56 | return data_list_size(vec_list, include_head, 8) 57 | 58 | 59 | def long_list_size(vec_list, include_head=True): 60 | return data_list_size(vec_list, include_head, 4) 61 | 62 | 63 | def data_list_size(data_list, include_head=True, data_size=1): 64 | if not data_list: 65 | return 0 66 | size = len(data_list) * data_size 67 | if include_head: 68 | size += HEAD 69 | return size 70 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3x/export_w3x.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.export_utils import * 5 | from io_mesh_w3d.w3x.structs.include import * 6 | 7 | 8 | def save(context, export_settings, data_context): 9 | filepath = context.filepath 10 | if not filepath.lower().endswith(context.filename_ext): 11 | filepath += context.filename_ext 12 | context.info(f'Saving file: {filepath}') 13 | 14 | export_mode = export_settings['mode'] 15 | context.info(f'export mode: {export_mode}') 16 | 17 | root = create_root() 18 | includes = create_node(root, 'Includes') 19 | 20 | directory = os.path.dirname(context.filepath) + os.path.sep 21 | 22 | if export_mode == 'M': 23 | if len(data_context.meshes) > 1: 24 | context.warning('Scene does contain multiple meshes, exporting only the first with export mode M!') 25 | data_context.meshes[0].header.container_name = '' 26 | data_context.meshes[0].header.mesh_name = data_context.container_name 27 | data_context.meshes[0].create(root) 28 | 29 | elif export_mode == 'HM': 30 | if export_settings['use_existing_skeleton'] or export_settings['individual_files']: 31 | hierarchy_include = Include(type='all', source='ART:' + data_context.hierarchy.name() + '.w3x') 32 | hierarchy_include.create(includes) 33 | else: 34 | data_context.hierarchy.create(root) 35 | 36 | if export_settings['individual_files']: 37 | if not export_settings['use_existing_skeleton']: 38 | path = directory + data_context.hierarchy.name() + context.filename_ext 39 | context.info('Saving file :' + path) 40 | write_struct(data_context.hierarchy, path) 41 | 42 | if export_settings['create_texture_xmls']: 43 | for texture in data_context.textures: 44 | id = texture.rsplit('.', 1)[0] 45 | texture_include = Include(type='all', source='ART:' + id + '.xml') 46 | texture_include.create(includes) 47 | path = directory + id + '.xml' 48 | context.info('Saving file :' + path) 49 | write_struct(Texture(id=id, file=texture), path) 50 | 51 | for box in data_context.collision_boxes: 52 | if export_settings['individual_files']: 53 | box_include = Include(type='all', source='ART:' + box.name_ + '.w3x') 54 | box_include.create(includes) 55 | path = directory + box.name_ + context.filename_ext 56 | context.info('Saving file :' + path) 57 | write_struct(box, path) 58 | else: 59 | box.create(root) 60 | 61 | for mesh in data_context.meshes: 62 | if export_settings['individual_files']: 63 | mesh_include = Include(type='all', source='ART:' + mesh.identifier() + '.w3x') 64 | mesh_include.create(includes) 65 | path = directory + mesh.identifier() + context.filename_ext 66 | context.info('Saving file :' + path) 67 | write_struct(mesh, path) 68 | else: 69 | mesh.create(root) 70 | 71 | data_context.hlod.create(root) 72 | 73 | elif export_mode == 'HAM': 74 | data_context.hierarchy.create(root) 75 | 76 | if export_settings['create_texture_xmls']: 77 | for texture in data_context.textures: 78 | id = texture.split('.')[0] 79 | path = directory + id + '.xml' 80 | context.info('Saving file :' + path) 81 | write_struct(Texture(id=id, file=texture), path) 82 | 83 | for texture in data_context.textures: 84 | id = texture.split('.')[0] 85 | texture_include = Include(type='all', source='ART:' + id + '.xml') 86 | texture_include.create(includes) 87 | 88 | for box in data_context.collision_boxes: 89 | box.create(root) 90 | 91 | for mesh in data_context.meshes: 92 | mesh.create(root) 93 | 94 | data_context.hlod.create(root) 95 | data_context.animation.create(root) 96 | 97 | elif export_mode == 'A': 98 | hierarchy_include = Include(type='all', source='ART:' + data_context.hierarchy.header.name + '.w3x') 99 | hierarchy_include.create(includes) 100 | data_context.animation.create(root) 101 | 102 | elif export_mode == 'H': 103 | data_context.hierarchy.header.name = data_context.container_name.upper() 104 | data_context.hierarchy.create(root) 105 | 106 | else: 107 | context.error(f'unsupported export mode: \'{export_mode}\', aborting export!') 108 | return {'CANCELLED'} 109 | 110 | write(root, filepath) 111 | 112 | context.info('finished') 113 | return {'FINISHED'} 114 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3x/io_xml.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import xml.etree.ElementTree as ET 5 | from mathutils import Vector, Quaternion, Matrix 6 | 7 | 8 | def create_node(self, identifier): 9 | return ET.SubElement(self, identifier) 10 | 11 | 12 | def write_struct(struct, path): 13 | root = create_root() 14 | struct.create(root) 15 | write(root, path) 16 | 17 | 18 | def pretty_print(elem, level=0): 19 | i = '\n' + level * ' ' 20 | if elem: 21 | elem.text = i + ' ' 22 | elem.tail = i 23 | for elem in elem: 24 | pretty_print(elem, level + 1) 25 | elem.tail = i 26 | else: 27 | if level and (not elem.tail or not elem.tail.strip()): 28 | elem.tail = i 29 | 30 | 31 | def write(root, path): 32 | pretty_print(root) 33 | xml_spec = '\n' 34 | data = bytes(xml_spec, 'utf-8') + ET.tostring(root) 35 | 36 | file = open(path, 'wb') 37 | file.write(data) 38 | file.close() 39 | 40 | 41 | def strip_namespaces(it): 42 | for _, el in it: 43 | el.tag = el.tag.split('}', 1)[-1] 44 | 45 | 46 | def find_root(context, source): 47 | try: 48 | it = ET.iterparse(source) 49 | strip_namespaces(it) 50 | root = it.root 51 | except BaseException: 52 | context.error(f'file: {source} does not contain valid XML data!') 53 | return None 54 | 55 | if root.tag != 'AssetDeclaration': 56 | context.error(f'file: {source} does not contain a AssetDeclaration node!') 57 | return None 58 | return root 59 | 60 | 61 | def create_named_root(name): 62 | root = ET.Element(name) 63 | return root 64 | 65 | 66 | def create_root(): 67 | root = ET.Element('AssetDeclaration') 68 | root.set('xmlns', 'uri:ea.com:eala:asset') 69 | root.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance') 70 | return root 71 | 72 | 73 | def create_value(value, parent, identifier): 74 | xml_obj = create_node(parent, identifier) 75 | xml_obj.text = str(value) 76 | 77 | 78 | def parse_objects(parent, name, parse_func): 79 | result = [] 80 | objects = parent.findall(name) 81 | if not objects: 82 | return result 83 | for obj in objects: 84 | result.append(parse_func(obj)) 85 | return result 86 | 87 | 88 | def create_object_list(parent, name, objects, write_func, par1=None): 89 | xml_objects_list = create_node(parent, name) 90 | for obj in objects: 91 | if par1 is not None: 92 | write_func(obj, xml_objects_list, par1) 93 | else: 94 | write_func(obj, xml_objects_list) 95 | 96 | 97 | def format(value): 98 | return '{:.6f}'.format(value) 99 | 100 | 101 | def parse_int_value(xml_obj): 102 | return int(xml_obj.text) 103 | 104 | 105 | def get_float(str): 106 | return float(str.replace(',', '.')) 107 | 108 | 109 | def parse_float_value(xml_obj): 110 | return get_float(xml_obj.text) 111 | 112 | 113 | def parse_float(xml_obj, id, default=0.0): 114 | return get_float(xml_obj.get(id, format(default))) 115 | 116 | 117 | def parse_vector2(xml_vector2): 118 | return Vector(( 119 | parse_float(xml_vector2, 'X'), 120 | parse_float(xml_vector2, 'Y'))) 121 | 122 | 123 | def create_vector2(vec2, parent, name): 124 | vector = create_node(parent, name) 125 | vector.set('X', format(vec2.x)) 126 | vector.set('Y', format(vec2.y)) 127 | 128 | 129 | def parse_vector(xml_vector): 130 | return Vector(( 131 | parse_float(xml_vector, 'X'), 132 | parse_float(xml_vector, 'Y'), 133 | parse_float(xml_vector, 'Z'))) 134 | 135 | 136 | def create_vector(vec, parent, name): 137 | vector = create_node(parent, name) 138 | vector.set('X', format(vec.x)) 139 | vector.set('Y', format(vec.y)) 140 | vector.set('Z', format(vec.z)) 141 | 142 | 143 | def parse_quaternion(xml_quaternion): 144 | return Quaternion(( 145 | parse_float(xml_quaternion, 'W', 1.0), 146 | parse_float(xml_quaternion, 'X'), 147 | parse_float(xml_quaternion, 'Y'), 148 | parse_float(xml_quaternion, 'Z'))) 149 | 150 | 151 | def create_quaternion(quat, parent, identifier='Rotation'): 152 | quaternion = create_node(parent, identifier) 153 | quaternion.set('W', format(quat[0])) 154 | quaternion.set('X', format(quat[1])) 155 | quaternion.set('Y', format(quat[2])) 156 | quaternion.set('Z', format(quat[3])) 157 | 158 | 159 | def parse_matrix(xml_matrix): 160 | return Matrix(( 161 | [parse_float(xml_matrix, 'M00', 1.0), 162 | parse_float(xml_matrix, 'M01'), 163 | parse_float(xml_matrix, 'M02'), 164 | parse_float(xml_matrix, 'M03')], 165 | 166 | [parse_float(xml_matrix, 'M10'), 167 | parse_float(xml_matrix, 'M11', 1.0), 168 | parse_float(xml_matrix, 'M12'), 169 | parse_float(xml_matrix, 'M13')], 170 | 171 | [parse_float(xml_matrix, 'M20'), 172 | parse_float(xml_matrix, 'M21'), 173 | parse_float(xml_matrix, 'M22', 1.0), 174 | parse_float(xml_matrix, 'M23')])) 175 | 176 | 177 | def create_matrix(mat, parent, identifier='FixupMatrix'): 178 | matrix = create_node(parent, identifier) 179 | matrix.set('M00', format(mat[0][0])) 180 | matrix.set('M01', format(mat[0][1])) 181 | matrix.set('M02', format(mat[0][2])) 182 | matrix.set('M03', format(mat[0][3])) 183 | 184 | matrix.set('M10', format(mat[1][0])) 185 | matrix.set('M11', format(mat[1][1])) 186 | matrix.set('M12', format(mat[1][2])) 187 | matrix.set('M13', format(mat[1][3])) 188 | 189 | matrix.set('M20', format(mat[2][0])) 190 | matrix.set('M21', format(mat[2][1])) 191 | matrix.set('M22', format(mat[2][2])) 192 | matrix.set('M23', format(mat[2][3])) 193 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3x/structs/include.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3x.io_xml import * 5 | 6 | 7 | class Include: 8 | def __init__(self, type='', source=''): 9 | self.type = type 10 | self.source = source 11 | 12 | @staticmethod 13 | def parse(xml_include): 14 | return Include( 15 | type=xml_include.get('type'), 16 | source=xml_include.get('source')) 17 | 18 | def create(self, parent): 19 | include = create_node(parent, 'Include') 20 | include.set('type', self.type) 21 | include.set('source', self.source) 22 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3x/structs/mesh_structs/bounding_box.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector 5 | from io_mesh_w3d.w3x.io_xml import * 6 | 7 | 8 | class BoundingBox: 9 | def __init__(self, min=Vector((0, 0, 0)), max=Vector((0, 0, 0))): 10 | self.min = min 11 | self.max = max 12 | 13 | @staticmethod 14 | def parse(xml_bounding_box): 15 | return BoundingBox( 16 | min=parse_vector(xml_bounding_box.find('Min')), 17 | max=parse_vector(xml_bounding_box.find('Max'))) 18 | 19 | def create(self, parent): 20 | result = create_node(parent, 'BoundingBox') 21 | create_vector(self.min, result, 'Min') 22 | create_vector(self.max, result, 'Max') 23 | -------------------------------------------------------------------------------- /io_mesh_w3d/w3x/structs/mesh_structs/bounding_sphere.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector 5 | from io_mesh_w3d.w3x.io_xml import * 6 | 7 | 8 | class BoundingSphere: 9 | def __init__(self, radius=0.0, center=Vector((0, 0, 0))): 10 | self.radius = radius 11 | self.center = center 12 | 13 | @staticmethod 14 | def parse(xml_bounding_sphere): 15 | return BoundingSphere( 16 | radius=parse_float(xml_bounding_sphere, 'Radius'), 17 | center=parse_vector(xml_bounding_sphere.find('Center'))) 18 | 19 | def create(self, parent): 20 | result = create_node(parent, 'BoundingSphere') 21 | result.set('Radius', format(self.radius)) 22 | create_vector(self.center, result, 'Center') 23 | -------------------------------------------------------------------------------- /runTests.bat: -------------------------------------------------------------------------------- 1 | "C:\Program Files\Blender Foundation\Blender\blender.exe" --factory-startup -noaudio -b --python-exit-code 1 --python ./tests/runner.py -- --coverage -- --save-html-report 2 | Pause -------------------------------------------------------------------------------- /runTests.sh: -------------------------------------------------------------------------------- 1 | blender --factory-startup -noaudio -b --python-exit-code 1 --python ./tests/runner.py -- --coverage 2 | -------------------------------------------------------------------------------- /runTestsWithPrefix.bat: -------------------------------------------------------------------------------- 1 | set /P PREFIX=Please enter prefix of tests to run: 2 | 3 | "C:\Program Files\Blender Foundation\Blender\blender.exe" --factory-startup -noaudio -b --python-exit-code 1 --python ./tests/runner.py -- --prefix %PREFIX% 4 | Pause -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/__init__.py -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/common/__init__.py -------------------------------------------------------------------------------- /tests/common/cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/common/cases/__init__.py -------------------------------------------------------------------------------- /tests/common/cases/structs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/common/cases/structs/__init__.py -------------------------------------------------------------------------------- /tests/common/cases/structs/mesh_structs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/common/cases/structs/mesh_structs/__init__.py -------------------------------------------------------------------------------- /tests/common/cases/structs/mesh_structs/test_aabbtree.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.common.helpers.mesh_structs.aabbtree import * 6 | from tests.utils import TestCase 7 | 8 | 9 | class TestAABBTree(TestCase): 10 | def test_write_read(self): 11 | expected = get_aabbtree() 12 | 13 | self.assertEqual(40, expected.header.size()) 14 | self.assertEqual(1284, expected.size()) 15 | 16 | self.write_read_test(expected, W3D_CHUNK_AABBTREE, AABBTree.read, compare_aabbtrees, self, True) 17 | 18 | def test_write_read_empty(self): 19 | expected = get_aabbtree_empty() 20 | 21 | self.assertEqual(40, expected.header.size()) 22 | 23 | self.write_read_test(expected, W3D_CHUNK_AABBTREE, AABBTree.read, compare_aabbtrees, self, True) 24 | 25 | def test_unknown_chunk_skip(self): 26 | output = io.BytesIO() 27 | write_chunk_head(W3D_CHUNK_AABBTREE, output, 9, has_sub_chunks=True) 28 | 29 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 30 | write_ubyte(0x00, output) 31 | 32 | io_stream = io.BytesIO(output.getvalue()) 33 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 34 | 35 | self.assertEqual(W3D_CHUNK_AABBTREE, chunk_type) 36 | 37 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x0', text) 38 | AABBTree.read(self, io_stream, subchunk_end) 39 | 40 | def test_chunk_sizes(self): 41 | expected = get_aabbtree_minimal() 42 | 43 | self.assertEqual(32, expected.header.size(False)) 44 | self.assertEqual(40, expected.header.size()) 45 | 46 | self.assertEqual(4, long_list_size(expected.poly_indices, False)) 47 | 48 | self.assertEqual(32, list_size(expected.nodes, False)) 49 | 50 | self.assertEqual(92, expected.size(False)) 51 | self.assertEqual(100, expected.size()) 52 | 53 | def test_write_read_xml(self): 54 | self.write_read_xml_test(get_aabbtree(xml=True), 'AABTree', AABBTree.parse, compare_aabbtrees) 55 | 56 | def test_write_read_minimal_xml(self): 57 | self.write_read_xml_test(get_aabbtree_minimal(), 'AABTree', AABBTree.parse, compare_aabbtrees) 58 | -------------------------------------------------------------------------------- /tests/common/cases/structs/mesh_structs/test_texture.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.common.helpers.mesh_structs.texture import * 6 | from tests.utils import TestCase 7 | 8 | 9 | class TestTexture(TestCase): 10 | def test_write_read(self): 11 | expected = get_texture() 12 | 13 | self.assertEqual(20, expected.texture_info.size()) 14 | self.assertEqual(48, expected.size()) 15 | 16 | self.write_read_test(expected, W3D_CHUNK_TEXTURE, Texture.read, compare_textures, self, True) 17 | 18 | def test_write_read_no_texture_info(self): 19 | expected = get_texture() 20 | expected.texture_info = None 21 | 22 | self.assertEqual(28, expected.size()) 23 | 24 | self.write_read_test(expected, W3D_CHUNK_TEXTURE, Texture.read, compare_textures, self, True) 25 | 26 | def test_unknown_chunk_skip(self): 27 | output = io.BytesIO() 28 | write_chunk_head(W3D_CHUNK_TEXTURE, output, 9, has_sub_chunks=True) 29 | 30 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 31 | write_ubyte(0x00, output) 32 | 33 | io_stream = io.BytesIO(output.getvalue()) 34 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 35 | 36 | self.assertEqual(W3D_CHUNK_TEXTURE, chunk_type) 37 | 38 | Texture.read(self, io_stream, subchunk_end) 39 | 40 | def test_chunk_sizes(self): 41 | tex = get_texture_minimal() 42 | 43 | self.assertEqual(12, tex.texture_info.size(False)) 44 | self.assertEqual(12 + HEAD, tex.texture_info.size()) 45 | 46 | self.assertEqual(30, tex.size(False)) 47 | self.assertEqual(30 + HEAD, tex.size()) 48 | 49 | def test_write_read_xml(self): 50 | self.write_read_xml_test(get_texture(), 'Texture', Texture.parse, compare_textures) 51 | -------------------------------------------------------------------------------- /tests/common/cases/structs/mesh_structs/test_triangle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.common.helpers.mesh_structs.triangle import * 6 | from tests.utils import * 7 | 8 | 9 | class TestTriangle(TestCase): 10 | def test_write_read_bin(self): 11 | expected = get_triangle() 12 | 13 | self.assertEqual(32, expected.size()) 14 | 15 | io_stream = io.BytesIO() 16 | expected.write(io_stream) 17 | io_stream = io.BytesIO(io_stream.getvalue()) 18 | 19 | actual = Triangle.read(io_stream) 20 | compare_triangles(self, expected, actual) 21 | 22 | def test_write_read_xml(self): 23 | self.write_read_xml_test(get_triangle(), 'T', Triangle.parse, compare_triangles) 24 | -------------------------------------------------------------------------------- /tests/common/cases/structs/mesh_structs/test_vertex_influence.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.common.helpers.mesh_structs.vertex_influence import * 6 | from tests.utils import TestCase 7 | from io_mesh_w3d.w3x.io_xml import * 8 | 9 | 10 | class TestVertexInfluence(TestCase): 11 | def test_write_read(self): 12 | expected = get_vertex_influence() 13 | 14 | self.assertEqual(8, expected.size()) 15 | 16 | io_stream = io.BytesIO() 17 | expected.write(io_stream) 18 | io_stream = io.BytesIO(io_stream.getvalue()) 19 | 20 | actual = VertexInfluence.read(io_stream) 21 | compare_vertex_influences(self, expected, actual) 22 | 23 | def test_write_read_xml(self): 24 | expected = get_vertex_influence() 25 | root = create_root() 26 | bone_infs = create_node(root, 'BoneInfluences') 27 | bone_infs2 = create_node(root, 'BoneInfluences') 28 | expected.create(bone_infs, bone_infs2) 29 | 30 | xml_objects = root.findall('BoneInfluences') 31 | self.assertEqual(2, len(xml_objects)) 32 | 33 | actual = VertexInfluence.parse(xml_objects[0].find('I'), xml_objects[1].find('I')) 34 | compare_vertex_influences(self, expected, actual) 35 | 36 | def test_write_read_xml_only_one_bone(self): 37 | expected = get_vertex_influence(bone=3, xtra=0, bone_inf=1.0, xtra_inf=0.0) 38 | root = create_root() 39 | bone_infs = create_node(root, 'BoneInfluences') 40 | expected.create(bone_infs) 41 | 42 | xml_objects = root.findall('BoneInfluences') 43 | self.assertEqual(1, len(xml_objects)) 44 | 45 | actual = VertexInfluence.parse(xml_objects[0].find('I')) 46 | compare_vertex_influences(self, expected, actual) 47 | -------------------------------------------------------------------------------- /tests/common/cases/structs/test_animation.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | import bpy 6 | from tests.common.helpers.animation import * 7 | from tests.utils import TestCase 8 | from unittest.mock import patch, call 9 | 10 | 11 | class TestAnimation(TestCase): 12 | def test_write_read(self): 13 | expected = get_animation() 14 | 15 | self.assertEqual(52, expected.header.size()) 16 | if bpy.app.version == (4, 4, 3): 17 | self.assertEqual(645, expected.size(False)) 18 | self.assertEqual(653, expected.size()) 19 | else: 20 | self.assertEqual(683, expected.size(False)) 21 | self.assertEqual(691, expected.size()) 22 | 23 | self.write_read_test(expected, W3D_CHUNK_ANIMATION, Animation.read, compare_animations, self, True) 24 | 25 | def test_write_read_empty(self): 26 | expected = get_animation_empty() 27 | 28 | self.assertEqual(52, expected.header.size()) 29 | self.assertEqual(52, expected.size(False)) 30 | self.assertEqual(60, expected.size()) 31 | 32 | self.write_read_test(expected, W3D_CHUNK_ANIMATION, Animation.read, compare_animations, self, True) 33 | 34 | def test_unknown_chunk_skip(self): 35 | output = io.BytesIO() 36 | write_chunk_head(W3D_CHUNK_ANIMATION, output, 9, has_sub_chunks=True) 37 | 38 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 39 | write_ubyte(0x00, output) 40 | 41 | io_stream = io.BytesIO(output.getvalue()) 42 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 43 | 44 | self.assertEqual(W3D_CHUNK_ANIMATION, chunk_type) 45 | 46 | with (patch.object(self, 'warning')) as report_func: 47 | Animation.read(self, io_stream, subchunk_end) 48 | report_func.assert_called_with('unknown chunk_type in io_stream: 0x0') 49 | 50 | def test_validate(self): 51 | ani = get_animation() 52 | self.file_format = 'W3D' 53 | self.assertTrue(ani.validate(self)) 54 | self.file_format = 'W3X' 55 | self.assertTrue(ani.validate(self)) 56 | 57 | ani.header.name = 'tooooolonganiname' 58 | self.file_format = 'W3D' 59 | self.assertFalse(ani.validate(self)) 60 | self.file_format = 'W3X' 61 | self.assertTrue(ani.validate(self)) 62 | 63 | ani = get_animation() 64 | ani.header.hierarchy_name = 'tooooolonganiname' 65 | self.file_format = 'W3D' 66 | self.assertFalse(ani.validate(self)) 67 | self.file_format = 'W3X' 68 | self.assertTrue(ani.validate(self)) 69 | 70 | ani = get_animation() 71 | ani.channels = [] 72 | self.file_format = 'W3D' 73 | self.assertFalse(ani.validate(self)) 74 | self.file_format = 'W3X' 75 | self.assertFalse(ani.validate(self)) 76 | 77 | def test_chunk_sizes(self): 78 | ani = get_animation_minimal() 79 | 80 | self.assertEqual(44, ani.header.size(False)) 81 | 82 | self.assertEqual(43, list_size(ani.channels, False)) 83 | 84 | self.assertEqual(95, ani.size(False)) 85 | self.assertEqual(103, ani.size()) 86 | 87 | data = [0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0] 88 | bit_channel = AnimationBitChannel(data=data) 89 | self.assertEqual(10, bit_channel.size(False)) 90 | 91 | def test_write_read_xml(self): 92 | self.write_read_xml_test(get_animation(xml=True), 'W3DAnimation', Animation.parse, compare_animations, self) 93 | 94 | def test_write_read_minimal_xml(self): 95 | self.write_read_xml_test(get_animation_minimal(), 'W3DAnimation', Animation.parse, compare_animations, self) 96 | 97 | def test_parse_invalid_identifier(self): 98 | root = create_root() 99 | xml_animation = create_node(root, 'W3DAnimation') 100 | xml_animation.set('id', 'fakeIdentifier') 101 | xml_animation.set('Hierarchy', 'fakeHiera') 102 | xml_animation.set('NumFrames', '1') 103 | xml_animation.set('FrameRate', '22') 104 | 105 | create_node(xml_animation, 'InvalidIdentifier') 106 | 107 | channels = create_node(xml_animation, 'Channels') 108 | create_node(channels, 'InvalidIdentifier') 109 | 110 | xml_objects = root.findall('W3DAnimation') 111 | self.assertEqual(1, len(xml_objects)) 112 | 113 | with (patch.object(self, 'warning')) as report_func: 114 | Animation.parse(self, xml_objects[0]) 115 | 116 | report_func.assert_has_calls([call('unhandled node \'InvalidIdentifier\' in W3DAnimation!'), 117 | call('unhandled node \'InvalidIdentifier\' in W3DAnimation Channels!')]) 118 | -------------------------------------------------------------------------------- /tests/common/cases/structs/test_collision_box.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from tests.common.helpers.collision_box import * 5 | from tests.utils import TestCase 6 | from unittest.mock import patch 7 | 8 | 9 | class TestCollisionBox(TestCase): 10 | def test_write_read(self): 11 | expected = get_collision_box() 12 | 13 | self.assertEqual(68, expected.size(False)) 14 | self.assertEqual(76, expected.size()) 15 | 16 | self.write_read_test(expected, W3D_CHUNK_BOX, CollisionBox.read, compare_collision_boxes) 17 | 18 | def test_validate(self): 19 | box = get_collision_box() 20 | self.file_format = 'W3D' 21 | self.assertTrue(box.validate(self)) 22 | self.file_format = 'W3X' 23 | self.assertTrue(box.validate(self)) 24 | 25 | box.name_ = 'containerNameeee.BOUNDINGBOX00000' 26 | self.file_format = 'W3D' 27 | self.assertFalse(box.validate(self)) 28 | self.file_format = 'W3X' 29 | self.assertTrue(box.validate(self)) 30 | 31 | def test_name(self): 32 | box = get_collision_box() 33 | 34 | self.assertEqual('containerName.BOUNDINGBOX', box.name_) 35 | self.assertEqual('BOUNDINGBOX', box.name()) 36 | 37 | box.name_ = 'BOUNDINGBOX' 38 | self.assertEqual('BOUNDINGBOX', box.name()) 39 | 40 | def test_write_read_xml(self): 41 | self.write_read_xml_test(get_collision_box(xml=True), 'W3DCollisionBox', CollisionBox.parse, 42 | compare_collision_boxes, self) 43 | 44 | def test_parse_invalid_identifier(self): 45 | root = create_root() 46 | xml_box = create_node(root, 'W3DCollisionBox') 47 | xml_box.set('id', 'fakeIdentifier') 48 | 49 | create_node(xml_box, 'InvalidIdentifier') 50 | 51 | xml_objects = root.findall('W3DCollisionBox') 52 | self.assertEqual(1, len(xml_objects)) 53 | 54 | with (patch.object(self, 'warning')) as report_func: 55 | CollisionBox.parse(self, xml_objects[0]) 56 | report_func.assert_called_with('unhandled node \'InvalidIdentifier\' in W3DCollisionBox!') 57 | -------------------------------------------------------------------------------- /tests/common/cases/structs/test_hierarchy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.common.helpers.hierarchy import * 6 | from tests.utils import TestCase 7 | from unittest.mock import patch, call 8 | 9 | 10 | class TestHierarchy(TestCase): 11 | def test_write_read(self): 12 | expected = get_hierarchy() 13 | 14 | self.assertEqual(44, expected.header.size()) 15 | self.assertEqual(636, expected.size(False)) 16 | self.assertEqual(644, expected.size()) 17 | 18 | self.write_read_test(expected, W3D_CHUNK_HIERARCHY, Hierarchy.read, compare_hierarchies, self, True) 19 | 20 | def test_write_read_minimal(self): 21 | expected = get_hierarchy_minimal() 22 | 23 | self.assertEqual(44, expected.header.size()) 24 | self.assertEqual(132, expected.size(False)) 25 | self.assertEqual(140, expected.size()) 26 | 27 | self.write_read_test(expected, W3D_CHUNK_HIERARCHY, Hierarchy.read, compare_hierarchies, self, True) 28 | 29 | def test_write_read_empty(self): 30 | expected = get_hierarchy_empty() 31 | 32 | self.assertEqual(44, expected.header.size()) 33 | self.assertEqual(44, expected.size(False)) 34 | self.assertEqual(52, expected.size()) 35 | 36 | self.write_read_test(expected, W3D_CHUNK_HIERARCHY, Hierarchy.read, compare_hierarchies, self, True) 37 | 38 | def test_validate(self): 39 | hierarchy = get_hierarchy() 40 | self.file_format = 'W3D' 41 | self.assertTrue(hierarchy.validate(self)) 42 | self.file_format = 'W3X' 43 | self.assertTrue(hierarchy.validate(self)) 44 | 45 | hierarchy.header.name = 'tooolonghieraname' 46 | self.file_format = 'W3D' 47 | self.assertFalse(hierarchy.validate(self)) 48 | self.file_format = 'W3X' 49 | self.assertTrue(hierarchy.validate(self)) 50 | 51 | hierarchy = get_hierarchy() 52 | hierarchy.pivots[1].name = 'tooolongpivotname' 53 | self.file_format = 'W3D' 54 | self.assertFalse(hierarchy.validate(self)) 55 | self.file_format = 'W3X' 56 | self.assertTrue(hierarchy.validate(self)) 57 | 58 | def test_unknown_chunk_skip(self): 59 | output = io.BytesIO() 60 | write_chunk_head(W3D_CHUNK_HIERARCHY, output, 9, has_sub_chunks=True) 61 | 62 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 63 | write_ubyte(0x00, output) 64 | 65 | io_stream = io.BytesIO(output.getvalue()) 66 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 67 | 68 | self.assertEqual(W3D_CHUNK_HIERARCHY, chunk_type) 69 | 70 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x0', text) 71 | Hierarchy.read(self, io_stream, subchunk_end) 72 | 73 | def test_chunk_sizes(self): 74 | hierarchy = get_hierarchy_minimal() 75 | 76 | self.assertEqual(36, hierarchy.header.size(False)) 77 | self.assertEqual(44, hierarchy.header.size()) 78 | 79 | self.assertEqual(60, list_size(hierarchy.pivots, False)) 80 | 81 | self.assertEqual(12, vec_list_size(hierarchy.pivot_fixups, False)) 82 | 83 | self.assertEqual(132, hierarchy.size(False)) 84 | self.assertEqual(140, hierarchy.size()) 85 | 86 | def test_write_read_xml(self): 87 | self.write_read_xml_test(get_hierarchy(xml=True), 'W3DHierarchy', Hierarchy.parse, compare_hierarchies, self) 88 | 89 | def test_write_read_minimal_xml(self): 90 | self.write_read_xml_test(get_hierarchy_minimal(xml=True), 'W3DHierarchy', Hierarchy.parse, compare_hierarchies, 91 | self) 92 | 93 | def test_create_parse_xml_pivot_name_is_None(self): 94 | hiera = get_hierarchy(xml=True) 95 | hiera.pivots[0].name = None 96 | self.write_read_xml_test(hiera, 'W3DHierarchy', Hierarchy.parse, compare_hierarchies, self) 97 | 98 | def test_parse_invalid_identifier(self): 99 | root = create_root() 100 | xml_hierarchy = create_node(root, 'W3DHierarchy') 101 | xml_hierarchy.set('id', 'fakeIdentifier') 102 | 103 | create_node(xml_hierarchy, 'InvalidIdentifier') 104 | pivot = create_node(xml_hierarchy, 'Pivot') 105 | pivot.set('Parent', '1') 106 | create_node(pivot, 'InvalidIdentifier') 107 | 108 | xml_objects = root.findall('W3DHierarchy') 109 | self.assertEqual(1, len(xml_objects)) 110 | 111 | with (patch.object(self, 'warning')) as report_func: 112 | actual = Hierarchy.parse(self, xml_objects[0]) 113 | 114 | report_func.assert_has_calls([call('unhandled node \'InvalidIdentifier\' in W3DHierarchy!'), 115 | call('unhandled node \'InvalidIdentifier\' in Pivot!')]) 116 | -------------------------------------------------------------------------------- /tests/common/cases/structs/test_hlod.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.common.helpers.hlod import * 6 | from tests.utils import TestCase 7 | from unittest.mock import patch, call 8 | 9 | 10 | class TestHLod(TestCase): 11 | def test_write_read(self): 12 | expected = get_hlod() 13 | 14 | self.assertEqual(48, expected.header.size()) 15 | self.assertEqual(912, expected.size(False)) 16 | self.assertEqual(920, expected.size()) 17 | 18 | self.write_read_test(expected, W3D_CHUNK_HLOD, HLod.read, compare_hlods, self, True) 19 | 20 | def test_write_read_4_levels(self): 21 | expected = get_hlod_4_levels() 22 | 23 | self.assertEqual(48, expected.header.size()) 24 | self.assertEqual(672, expected.size(False)) 25 | self.assertEqual(680, expected.size()) 26 | 27 | self.write_read_test(expected, W3D_CHUNK_HLOD, HLod.read, compare_hlods, self, True) 28 | 29 | def test_validate(self): 30 | hlod = get_hlod() 31 | self.file_format = 'W3D' 32 | self.assertTrue(hlod.validate(self)) 33 | self.file_format = 'W3X' 34 | self.assertTrue(hlod.validate(self)) 35 | 36 | hlod.lod_arrays[0].sub_objects[0].identifier = 'containerName.tooooolongsuObjname' 37 | self.file_format = 'W3D' 38 | self.assertFalse(hlod.validate(self)) 39 | self.file_format = 'W3X' 40 | self.assertTrue(hlod.validate(self)) 41 | 42 | def test_unknown_chunk_skip(self): 43 | output = io.BytesIO() 44 | write_chunk_head(W3D_CHUNK_HLOD, output, 26, has_sub_chunks=True) 45 | 46 | write_chunk_head(W3D_CHUNK_HLOD_LOD_ARRAY, 47 | output, 9, has_sub_chunks=True) 48 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 49 | write_ubyte(0x00, output) 50 | 51 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 52 | write_ubyte(0x00, output) 53 | 54 | io_stream = io.BytesIO(output.getvalue()) 55 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 56 | 57 | self.assertEqual(W3D_CHUNK_HLOD, chunk_type) 58 | 59 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x0', text) 60 | HLod.read(self, io_stream, subchunk_end) 61 | 62 | def test_name(self): 63 | sub_object = get_hlod_sub_object() 64 | 65 | self.assertEqual('containerName.default', sub_object.identifier) 66 | self.assertEqual('default', sub_object.name) 67 | 68 | def test_chunk_sizes(self): 69 | hlod = get_hlod_minimal() 70 | 71 | self.assertEqual(40, hlod.header.size(False)) 72 | self.assertEqual(48, hlod.header.size()) 73 | 74 | self.assertEqual(8, hlod.lod_arrays[0].header.size(False)) 75 | self.assertEqual(16, hlod.lod_arrays[0].header.size()) 76 | 77 | self.assertEqual(36, hlod.lod_arrays[0].sub_objects[0].size(False)) 78 | self.assertEqual(44, hlod.lod_arrays[0].sub_objects[0].size()) 79 | 80 | self.assertEqual(60, hlod.lod_arrays[0].size(False)) 81 | self.assertEqual(68, hlod.lod_arrays[0].size()) 82 | 83 | self.assertEqual(60, hlod.aggregate_array.size(False)) 84 | self.assertEqual(68, hlod.aggregate_array.size()) 85 | 86 | self.assertEqual(60, hlod.proxy_array.size(False)) 87 | self.assertEqual(68, hlod.proxy_array.size()) 88 | 89 | self.assertEqual(252, hlod.size(False)) 90 | self.assertEqual(260, hlod.size()) 91 | 92 | def test_write_read_xml(self): 93 | self.write_read_xml_test(get_hlod(), 'W3DContainer', HLod.parse, compare_hlods, self) 94 | 95 | def test_write_read_minimal_xml(self): 96 | self.write_read_xml_test(get_hlod_minimal(), 'W3DContainer', HLod.parse, compare_hlods, self) 97 | 98 | def test_parse_invalid_identifier(self): 99 | root = create_root() 100 | xml_hlod = create_node(root, 'W3DContainer') 101 | xml_hlod.set('id', 'fakeIdentifier') 102 | xml_hlod.set('Hierarchy', 'fakeHierarchy') 103 | 104 | create_node(xml_hlod, 'InvalidIdentifier') 105 | sub_object = create_node(xml_hlod, 'SubObject') 106 | sub_object.set('SubObjectID', 'fakeID') 107 | sub_object.set('BoneIndex', '2') 108 | create_node(sub_object, 'InvalidIdentifier') 109 | obj = create_node(sub_object, 'RenderObject') 110 | create_node(obj, 'InvalidIdentifier') 111 | 112 | xml_objects = root.findall('W3DContainer') 113 | self.assertEqual(1, len(xml_objects)) 114 | 115 | with (patch.object(self, 'warning')) as report_func: 116 | HLod.parse(self, xml_objects[0]) 117 | 118 | report_func.assert_has_calls([call('unhandled node \'InvalidIdentifier\' in W3DContainer!'), 119 | call('unhandled node \'InvalidIdentifier\' in W3DContainer SubObject!'), 120 | call('unhandled node \'InvalidIdentifier\' in W3DContainer RenderObject!')]) 121 | -------------------------------------------------------------------------------- /tests/common/cases/structs/test_rgba.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.common.helpers.rgba import * 6 | from tests.utils import TestCase 7 | 8 | 9 | class TestRGBA(TestCase): 10 | def test_write_read(self): 11 | expected = get_rgba() 12 | 13 | io_stream = io.BytesIO() 14 | expected.write(io_stream) 15 | io_stream = io.BytesIO(io_stream.getvalue()) 16 | 17 | compare_rgbas(self, expected, RGBA.read(io_stream)) 18 | 19 | def test_write_read_f(self): 20 | expected = RGBA(r=244, g=123, b=33, a=99) 21 | 22 | io_stream = io.BytesIO() 23 | expected.write_f(io_stream) 24 | io_stream = io.BytesIO(io_stream.getvalue()) 25 | 26 | compare_rgbas(self, expected, RGBA.read_f(io_stream)) 27 | 28 | def test_eq_true(self): 29 | rgba = RGBA(r=244, g=222, b=1, a=0) 30 | self.assertEqual(rgba, rgba) 31 | 32 | def test_eq_false(self): 33 | rgba = RGBA(r=2, g=3, b=0, a=0) 34 | self.assertNotEqual(rgba, 'test') 35 | self.assertNotEqual(rgba, 1) 36 | 37 | def test_to_string(self): 38 | rgba = RGBA(r=244, g=123, b=33, a=99) 39 | expected = 'RGBA(244, 123, 33, 99)' 40 | self.assertEqual(expected, str(rgba)) 41 | -------------------------------------------------------------------------------- /tests/common/cases/test_addon.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io_mesh_w3d 5 | from tests.utils import TestCase 6 | 7 | 8 | class TestAddon(TestCase): 9 | def test_addon_enabled(self): 10 | self.assertIsNotNone(io_mesh_w3d.bl_info) 11 | -------------------------------------------------------------------------------- /tests/common/cases/test_import_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | import io 6 | from mathutils import Vector 7 | from tests.common.helpers.mesh import * 8 | from tests.utils import * 9 | 10 | 11 | class TestImportUtils(TestCase): 12 | def test_read_chunk_array(self): 13 | output = io.BytesIO() 14 | 15 | mat_pass = get_material_pass() 16 | mat_pass.write(output) 17 | mat_pass.write(output) 18 | mat_pass.write(output) 19 | 20 | write_chunk_head(0x00, output, 9, has_sub_chunks=False) 21 | write_ubyte(0x00, output) 22 | 23 | io_stream = io.BytesIO(output.getvalue()) 24 | read_chunk_array(self, io_stream, mat_pass.size() 25 | * 3 + 9, W3D_CHUNK_MATERIAL_PASS, MaterialPass.read) 26 | 27 | def test_bone_visibility_channel_creation(self): 28 | armature = bpy.data.armatures.new('armature') 29 | rig = bpy.data.objects.new('rig', armature) 30 | bpy.context.scene.collection.objects.link(rig) 31 | bpy.context.view_layer.objects.active = rig 32 | rig.select_set(True) 33 | 34 | if rig.mode != 'EDIT': 35 | bpy.ops.object.mode_set(mode='EDIT') 36 | 37 | bone = armature.edit_bones.new('bone') 38 | bone.head = Vector((0.0, 0.0, 0.0)) 39 | bone.tail = Vector((0.0, 1.0, 0.0)) 40 | 41 | if rig.mode != 'OBJECT': 42 | bpy.ops.object.mode_set(mode='OBJECT') 43 | 44 | self.assertTrue('bone' in armature.bones) 45 | self.assertTrue('bone' in rig.data.bones) 46 | 47 | bone = rig.data.bones['bone'] 48 | bone.hide = True 49 | bone.keyframe_insert(data_path='hide', frame=0) 50 | 51 | results = [fcu for fcu in armature.animation_data.action.fcurves if 'hide' in fcu.data_path] 52 | self.assertEqual(1, len(results)) 53 | -------------------------------------------------------------------------------- /tests/common/cases/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/common/cases/utils/__init__.py -------------------------------------------------------------------------------- /tests/common/cases/utils/test_box_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | import bmesh 6 | 7 | from io_mesh_w3d.common.utils.mesh_export import * 8 | from io_mesh_w3d.common.utils.box_export import * 9 | from tests.common.helpers.collision_box import * 10 | from tests.common.helpers.hlod import * 11 | from tests.common.helpers.hierarchy import * 12 | from tests.utils import * 13 | 14 | 15 | class TestBoxExportUtils(TestCase): 16 | def test_box_export(self): 17 | box = bpy.data.meshes.new('box') 18 | 19 | b_mesh = bmesh.new() 20 | bmesh.ops.create_cube(b_mesh, size=1) 21 | b_mesh.to_mesh(box) 22 | 23 | prepare_bmesh(self, box) 24 | 25 | box_object = bpy.data.objects.new(box.name, box) 26 | box_object.data.object_type = 'BOX' 27 | 28 | link_object_to_active_scene(box_object, bpy.context.scene.collection) 29 | 30 | boxes = retrieve_boxes('anything') 31 | 32 | self.assertEqual(1, len(boxes)) 33 | actual = boxes[0] 34 | 35 | compare_vectors(self, Vector((0, 0, 0)), actual.center) 36 | compare_vectors(self, Vector((1, 1, 1)), actual.extend) 37 | 38 | def test_box_export_collision_types(self): 39 | box = bpy.data.meshes.new('box') 40 | box.box_collision_types = {'PHYSICAL', 'PROJECTILE', 'VIS', 'CAMERA', 'VEHICLE'} 41 | 42 | b_mesh = bmesh.new() 43 | bmesh.ops.create_cube(b_mesh, size=1) 44 | b_mesh.to_mesh(box) 45 | 46 | prepare_bmesh(self, box) 47 | 48 | box_object = bpy.data.objects.new(box.name, box) 49 | box_object.data.object_type = 'BOX' 50 | 51 | link_object_to_active_scene(box_object, bpy.context.scene.collection) 52 | 53 | boxes = retrieve_boxes('anything') 54 | 55 | self.assertEqual(1, len(boxes)) 56 | actual = boxes[0] 57 | 58 | compare_vectors(self, Vector((0, 0, 0)), actual.center) 59 | compare_vectors(self, Vector((1, 1, 1)), actual.extend) 60 | -------------------------------------------------------------------------------- /tests/common/cases/utils/test_box_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | 6 | from io_mesh_w3d.common.utils.box_import import * 7 | from tests.common.helpers.collision_box import * 8 | from tests.common.helpers.hlod import * 9 | from tests.common.helpers.hierarchy import * 10 | from tests.utils import * 11 | 12 | 13 | class TestBoxImportUtils(TestCase): 14 | def test_import_box(self): 15 | box = get_collision_box() 16 | hlod = get_hlod() 17 | sub_object = get_hlod_sub_object(bone=1, name='containerName.box') 18 | hlod.lod_arrays[0].sub_objects = [sub_object] 19 | 20 | hierarchy = get_hierarchy() 21 | 22 | fake_rig = bpy.data.objects.new('rig', bpy.data.armatures.new('rig')) 23 | 24 | create_box(box, bpy.context.scene.collection) 25 | rig_box(box, hierarchy, fake_rig, sub_object) 26 | 27 | self.assertTrue('BOUNDINGBOX' in bpy.data.objects) 28 | 29 | def test_import_box_collision_types(self): 30 | box = get_collision_box() 31 | box.collision_types = COLLISION_TYPE_PHYSICAL | COLLISION_TYPE_PROJECTILE | COLLISION_TYPE_VIS | COLLISION_TYPE_CAMERA | COLLISION_TYPE_VEHICLE 32 | hlod = get_hlod() 33 | sub_object = get_hlod_sub_object(bone=1, name='containerName.box') 34 | hlod.lod_arrays[0].sub_objects = [sub_object] 35 | 36 | hierarchy = get_hierarchy() 37 | 38 | fake_rig = bpy.data.objects.new('rig', bpy.data.armatures.new('rig')) 39 | 40 | create_box(box, bpy.context.scene.collection) 41 | rig_box(box, hierarchy, fake_rig, sub_object) 42 | 43 | self.assertTrue('BOUNDINGBOX' in bpy.data.objects) 44 | -------------------------------------------------------------------------------- /tests/common/cases/utils/test_helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | from tests.utils import TestCase 6 | from shutil import copyfile 7 | from os.path import dirname as up 8 | from io_mesh_w3d.common.utils.helpers import * 9 | from unittest.mock import patch, call 10 | 11 | 12 | class FakeClass: 13 | def __init__(self): 14 | self.tx_coords = [] 15 | self.tx_stages = [] 16 | 17 | 18 | class TestHelpers(TestCase): 19 | def test_texture_file_extensions(self): 20 | extensions = ['.dds', '.tga', '.jpg', '.jpeg', '.png', '.bmp'] 21 | 22 | with (patch.object(self, 'info')) as report_func: 23 | for extension in extensions: 24 | copyfile(up(up(up(self.relpath()))) + '/testfiles/texture.dds', 25 | self.outpath() + 'texture' + extension) 26 | 27 | find_texture(self, 'texture') 28 | 29 | # reset scene 30 | bpy.ops.wm.read_homefile(use_empty=True) 31 | os.remove(self.outpath() + 'texture' + extension) 32 | 33 | report_func.assert_called() 34 | 35 | def test_texture_file_extensions_is_dds_if_file_is_dds_but_tga_referenced(self): 36 | with (patch.object(self, 'info')) as report_func: 37 | for extension in extensions: 38 | copyfile(up(up(up(self.relpath()))) + '/testfiles/texture.dds', 39 | self.outpath() + 'texture.dds') 40 | 41 | find_texture(self, 'texture', 'texture.tga') 42 | 43 | # reset scene 44 | bpy.ops.wm.read_homefile(use_empty=True) 45 | os.remove(self.outpath() + 'texture.dds') 46 | 47 | report_func.assert_called_with(f'loaded texture: {self.outpath()}texture.dds') 48 | 49 | def test_texture_file_extensions_is_tga_if_file_is_tga_but_dds_referenced(self): 50 | with (patch.object(self, 'info')) as report_func: 51 | for extension in extensions: 52 | copyfile(up(up(up(self.relpath()))) + '/testfiles/texture.dds', 53 | self.outpath() + 'texture.tga') 54 | 55 | find_texture(self, 'texture', 'texture.dds') 56 | 57 | # reset scene 58 | bpy.ops.wm.read_homefile(use_empty=True) 59 | os.remove(self.outpath() + 'texture.tga') 60 | 61 | report_func.assert_called_with(f'loaded texture: {self.outpath()}texture.tga') 62 | 63 | def test_invalid_texture_file_extension(self): 64 | extensions = ['.invalid'] 65 | 66 | with (patch.object(self, 'warning')) as report_func: 67 | for extension in extensions: 68 | copyfile(up(up(up(self.relpath()))) + '/testfiles/texture.dds', 69 | self.outpath() + 'texture' + extension) 70 | 71 | find_texture(self, 'texture') 72 | 73 | # reset scene 74 | bpy.ops.wm.read_homefile(use_empty=True) 75 | os.remove(self.outpath() + 'texture' + extension) 76 | 77 | report_func.assert_called() 78 | 79 | def test_call_create_uv_layer_without_tx_coords(self): 80 | fake_mat_pass = FakeClass() 81 | 82 | create_uvlayer(self, None, None, None, fake_mat_pass) 83 | -------------------------------------------------------------------------------- /tests/common/cases/utils/test_material_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from tests.utils import TestCase 5 | from unittest.mock import patch 6 | 7 | from tests.common.helpers.mesh_structs.shader_material import * 8 | from io_mesh_w3d.common.utils.material_import import * 9 | 10 | 11 | class TestMaterialUtils(TestCase): 12 | def test_shader_material_creation_unimplemented_property(self): 13 | shader_mat = get_shader_material() 14 | shader_mat.properties.append(get_shader_material_property(2, 'UnimplementedProp')) 15 | 16 | with (patch.object(self, 'error')) as report_func: 17 | create_material_from_shader_material(self, 'lorem ipsum', shader_mat) 18 | report_func.assert_called_with('shader property not implemented: UnimplementedProp') 19 | -------------------------------------------------------------------------------- /tests/common/helpers/collision_box.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.collision_box import * 5 | from tests.mathutils import * 6 | from tests.common.helpers.rgba import get_rgba, compare_rgbas 7 | from tests.w3d.helpers.version import get_version, compare_versions 8 | 9 | 10 | def get_collision_box(name='containerName.BOUNDINGBOX', xml=False): 11 | box = CollisionBox( 12 | version=get_version(), 13 | box_type=2, 14 | collision_types=0, 15 | name_=name, 16 | color=None, 17 | center=get_vec(1.0, 2.0, 3.0), 18 | extend=get_vec(4.0, 5.0, 6.0)) 19 | 20 | if not xml: 21 | box.collision_types = 0x70 22 | box.color = get_rgba() 23 | return box 24 | 25 | 26 | def compare_collision_boxes(self, expected, actual): 27 | compare_versions(self, expected.version, actual.version) 28 | if not self.file_format == 'W3X': # box type and collision type not supported in W3X 29 | self.assertEqual(expected.box_type, actual.box_type) 30 | self.assertEqual(expected.collision_types, actual.collision_types) 31 | self.assertEqual(expected.name_, actual.name_) 32 | if expected.color is not None: 33 | compare_rgbas(self, expected.color, actual.color) 34 | compare_vectors(self, expected.center, actual.center) 35 | compare_vectors(self, expected.extend, actual.extend) 36 | -------------------------------------------------------------------------------- /tests/common/helpers/hierarchy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.hierarchy import * 5 | from tests.mathutils import * 6 | from tests.w3d.helpers.version import get_version, compare_versions 7 | 8 | 9 | def get_hierarchy_header(name='TestHierarchy'): 10 | return HierarchyHeader( 11 | version=get_version(major=4, minor=1), 12 | name=name, 13 | num_pivots=0, 14 | center_pos=get_vec(0.0, 0.0, 0.0)) 15 | 16 | 17 | def compare_hierarchy_headers(self, expected, actual): 18 | compare_versions(self, expected.version, actual.version) 19 | self.assertEqual(expected.name, actual.name) 20 | self.assertEqual(expected.num_pivots, actual.num_pivots) 21 | compare_vectors(self, expected.center_pos, actual.center_pos) 22 | 23 | 24 | def get_hierarchy_pivot(name='', name_id=None, parent=-1): 25 | return HierarchyPivot( 26 | name=name, 27 | name_id=name_id, 28 | parent_id=parent, 29 | translation=get_vec(22.0, 33.0, 1.0), 30 | euler_angles=get_vec(0.32, -0.65, 0.67), 31 | rotation=get_quat(0.86, 0.25, -0.25, 0.36), 32 | fixup_matrix=get_mat()) 33 | 34 | 35 | def get_roottransform(): 36 | return HierarchyPivot( 37 | name='ROOTTRANSFORM', 38 | name_id=None, 39 | parent_id=-1, 40 | translation=get_vec(2.0, 3.0, -1.0), 41 | euler_angles=get_vec(), 42 | rotation=get_quat(0.86, 0.25, -0.25, 0.36), 43 | fixup_matrix=get_mat()) 44 | 45 | 46 | def compare_hierarchy_pivots(self, expected, actual): 47 | self.assertEqual(expected.name, actual.name) 48 | if expected.name_id is not None: 49 | self.assertEqual(expected.name_id, actual.name_id) 50 | self.assertEqual(expected.parent_id, actual.parent_id) 51 | 52 | compare_vectors(self, expected.translation, actual.translation) 53 | # dont care for those 54 | # if expected.euler_angles : 55 | # compare_vectors(self, expected.euler_angles, actual.euler_angles) 56 | compare_quats(self, expected.rotation, actual.rotation) 57 | compare_mats(self, expected.fixup_matrix, actual.fixup_matrix) 58 | 59 | 60 | def get_hierarchy(name='TestHierarchy', xml=False): 61 | hierarchy = Hierarchy( 62 | header=get_hierarchy_header(name), 63 | pivots=[], 64 | pivot_fixups=[]) 65 | 66 | hierarchy.pivots = [ 67 | get_roottransform(), 68 | get_hierarchy_pivot(name='b_waist', parent=0), 69 | get_hierarchy_pivot(name='b_hip', parent=1), 70 | get_hierarchy_pivot(name='shoulderl', parent=2), 71 | get_hierarchy_pivot(name='arml', parent=3), 72 | get_hierarchy_pivot(name='armr', parent=3), 73 | get_hierarchy_pivot(name='TRUNK', parent=5), 74 | get_hierarchy_pivot(name='sword_bone', parent=0)] 75 | 76 | if xml: 77 | hierarchy.pivots.append( 78 | get_hierarchy_pivot(name_id=4, parent=0)) 79 | else: 80 | hierarchy.header.num_pivots = len(hierarchy.pivots) 81 | hierarchy.pivot_fixups = [ 82 | get_vec(), 83 | get_vec(), 84 | get_vec(), 85 | get_vec(), 86 | get_vec(), 87 | get_vec(), 88 | get_vec(), 89 | get_vec()] 90 | 91 | return hierarchy 92 | 93 | 94 | def get_hierarchy_minimal(xml=False): 95 | hierarchy = Hierarchy( 96 | header=get_hierarchy_header(), 97 | pivots=[get_hierarchy_pivot()], 98 | pivot_fixups=[]) 99 | 100 | if not xml: 101 | hierarchy.pivot_fixups = [get_vec()] 102 | return hierarchy 103 | 104 | 105 | def get_hierarchy_empty(): 106 | return Hierarchy( 107 | header=get_hierarchy_header(), 108 | pivots=[], 109 | pivot_fixups=[]) 110 | 111 | 112 | def compare_hierarchies(self, expected, actual): 113 | compare_hierarchy_headers(self, expected.header, actual.header) 114 | 115 | self.assertEqual(len(expected.pivots), len(actual.pivots)) 116 | for i in range(len(expected.pivots)): 117 | compare_hierarchy_pivots(self, expected.pivots[i], actual.pivots[i]) 118 | 119 | if expected.pivot_fixups: 120 | self.assertEqual(len(expected.pivot_fixups), len(actual.pivot_fixups)) 121 | for i in range(len(expected.pivot_fixups)): 122 | compare_vectors( 123 | self, expected.pivot_fixups[i], actual.pivot_fixups[i]) 124 | -------------------------------------------------------------------------------- /tests/common/helpers/mesh_structs/aabbtree.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.mesh_structs.aabbtree import * 5 | from tests.mathutils import * 6 | 7 | 8 | def get_aabbtree_header(num_nodes=33, num_polys=41): 9 | return AABBTreeHeader( 10 | node_count=num_nodes, 11 | poly_count=num_polys) 12 | 13 | 14 | def compare_aabbtree_headers(self, expected, actual): 15 | self.assertEqual(expected.node_count, actual.node_count) 16 | self.assertEqual(expected.poly_count, actual.poly_count) 17 | 18 | 19 | def get_aabbtree_poly_indices(num_polys=41): 20 | result = [] 21 | for i in range(num_polys): 22 | result.append(i) 23 | return result 24 | 25 | 26 | def get_aabbtree_node(xml=False, i=0): 27 | node = AABBTreeNode( 28 | min=get_vec(1.0, 2.0, 3.0), 29 | max=get_vec(4.0, 5.0, 6.0)) 30 | 31 | if xml and i % 2 == 0: 32 | node.polys = Polys(begin=3, count=44) 33 | else: 34 | node.children = Children(front=5, back=9) 35 | return node 36 | 37 | 38 | def compare_aabbtree_nodes(self, expected, actual): 39 | compare_vectors(self, expected.min, actual.min) 40 | compare_vectors(self, expected.max, actual.max) 41 | if expected.children is not None: 42 | self.assertEqual(expected.children.front, actual.children.front) 43 | self.assertEqual(expected.children.front, actual.children.front) 44 | if expected.polys is not None: 45 | self.assertEqual(expected.polys.begin, actual.polys.begin) 46 | self.assertEqual(expected.polys.count, actual.polys.count) 47 | 48 | 49 | def get_aabbtree_nodes(num_nodes=33, xml=False): 50 | nodes = [] 51 | for i in range(num_nodes): 52 | nodes.append(get_aabbtree_node(xml, i)) 53 | return nodes 54 | 55 | 56 | def get_aabbtree(num_nodes=33, num_polys=41, xml=False): 57 | return AABBTree( 58 | header=get_aabbtree_header(num_nodes, num_polys), 59 | poly_indices=get_aabbtree_poly_indices(num_polys), 60 | nodes=get_aabbtree_nodes(num_nodes, xml)) 61 | 62 | 63 | def get_aabbtree_minimal(): 64 | return AABBTree( 65 | header=get_aabbtree_header(num_nodes=1, num_polys=1), 66 | poly_indices=[1], 67 | nodes=[get_aabbtree_node()]) 68 | 69 | 70 | def get_aabbtree_empty(): 71 | return AABBTree(header=get_aabbtree_header(num_nodes=0, num_polys=0)) 72 | 73 | 74 | def compare_aabbtrees(self, expected, actual): 75 | compare_aabbtree_headers(self, expected.header, actual.header) 76 | self.assertEqual(len(expected.poly_indices), len(actual.poly_indices)) 77 | self.assertEqual(expected.poly_indices, actual.poly_indices) 78 | 79 | self.assertEqual(len(expected.nodes), len(actual.nodes)) 80 | for i in range(len(expected.nodes)): 81 | compare_aabbtree_nodes(self, expected.nodes[i], actual.nodes[i]) 82 | -------------------------------------------------------------------------------- /tests/common/helpers/mesh_structs/texture.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.mesh_structs.texture import * 5 | 6 | 7 | def get_texture_info(): 8 | return TextureInfo() 9 | 10 | 11 | def get_texture(name='texture.dds'): 12 | return Texture( 13 | id=name, 14 | file=name, 15 | texture_info=get_texture_info()) 16 | 17 | 18 | def get_texture_minimal(): 19 | return Texture( 20 | id='a', 21 | file='a', 22 | texture_info=get_texture_info()) 23 | 24 | 25 | def compare_textures(self, expected, actual): 26 | self.assertEqual(expected.id, actual.id) 27 | self.assertEqual(expected.file.split('.')[0], actual.file.split('.')[0]) 28 | self.assertTrue('.tga' in actual.file or '.dds' in actual.file) 29 | if expected.texture_info is not None: 30 | compare_texture_infos(self, expected.texture_info, actual.texture_info) 31 | 32 | 33 | def compare_texture_infos(self, expected, actual): 34 | self.assertEqual(expected.attributes, actual.attributes) 35 | self.assertEqual(expected.animation_type, actual.animation_type) 36 | self.assertEqual(expected.frame_count, actual.frame_count) 37 | self.assertEqual(expected.frame_rate, actual.frame_rate) 38 | -------------------------------------------------------------------------------- /tests/common/helpers/mesh_structs/triangle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.mesh_structs.triangle import * 5 | from tests.mathutils import * 6 | 7 | 8 | def get_triangle(vert_ids=None, 9 | surface_type=13, 10 | normal=get_vec(x=1.0, y=22.0, z=-5.0), 11 | distance=103.0): 12 | vert_ids = vert_ids if vert_ids is not None else [1, 2, 3] 13 | return Triangle( 14 | vert_ids=vert_ids, 15 | surface_type=surface_type, 16 | normal=normal, 17 | distance=distance) 18 | 19 | 20 | def compare_triangles(self, expected, actual, is_skin=False): 21 | self.assertEqual(expected.vert_ids, actual.vert_ids) 22 | if not self.file_format == 'W3X': # surface type is not supported in W3X file format 23 | self.assertEqual(expected.surface_type, actual.surface_type) 24 | 25 | if not is_skin: # generated/changed by blender 26 | compare_vectors(self, expected.normal, actual.normal) 27 | self.assertAlmostEqual(expected.distance, actual.distance, 1) 28 | -------------------------------------------------------------------------------- /tests/common/helpers/mesh_structs/vertex_influence.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.mesh_structs.vertex_influence import VertexInfluence 5 | 6 | 7 | def get_vertex_influence(bone=3, xtra=4, bone_inf=0.25, xtra_inf=0.75): 8 | return VertexInfluence( 9 | bone_idx=bone, 10 | xtra_idx=xtra, 11 | bone_inf=bone_inf, 12 | xtra_inf=xtra_inf) 13 | 14 | 15 | def compare_vertex_influences(self, expected, actual): 16 | self.assertEqual(expected.bone_idx, actual.bone_idx) 17 | self.assertEqual(expected.xtra_idx, actual.xtra_idx) 18 | if expected.bone_inf < 0.01: 19 | if actual.bone_inf < 0.01: 20 | self.assertAlmostEqual(expected.bone_inf, actual.bone_inf, 2) 21 | else: 22 | self.assertAlmostEqual(1.0, actual.bone_inf, 2) 23 | else: 24 | self.assertAlmostEqual(expected.bone_inf, actual.bone_inf, 2) 25 | self.assertAlmostEqual(expected.xtra_inf, actual.xtra_inf, 2) 26 | -------------------------------------------------------------------------------- /tests/common/helpers/rgba.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.common.structs.rgba import RGBA 5 | 6 | 7 | def get_rgba(a=0): 8 | return RGBA(r=3, g=200, b=44, a=a) 9 | 10 | 11 | def compare_rgbas(self, expected, actual, delta=0): 12 | self.assertTrue(abs(expected.r - actual.r) <= delta) 13 | self.assertTrue(abs(expected.g - actual.g) <= delta) 14 | self.assertTrue(abs(expected.b - actual.b) <= delta) 15 | self.assertTrue(abs(expected.a - actual.a) <= delta) 16 | -------------------------------------------------------------------------------- /tests/mathutils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector, Quaternion, Matrix 5 | 6 | 7 | def get_vec(x=0.0, y=0.0, z=0.0): 8 | return Vector((x, y, z)) 9 | 10 | 11 | def compare_vectors(self, expected, actual): 12 | self.assertAlmostEqual(expected.x, actual.x, 1) 13 | self.assertAlmostEqual(expected.y, actual.y, 1) 14 | self.assertAlmostEqual(expected.z, actual.z, 1) 15 | 16 | 17 | def get_vec4(x=0.0, y=2.0, z=3.14, w=2): 18 | return Vector((x, y, z, w)) 19 | 20 | 21 | def compare_vectors4(self, expected, actual): 22 | self.assertAlmostEqual(expected.x, actual.x, 1) 23 | self.assertAlmostEqual(expected.y, actual.y, 1) 24 | self.assertAlmostEqual(expected.z, actual.z, 1) 25 | self.assertAlmostEqual(expected.w, actual.w, 1) 26 | 27 | 28 | def get_vec2(x=0.0, y=0.0): 29 | return Vector((x, y)) 30 | 31 | 32 | def compare_vectors2(self, expected, actual): 33 | self.assertAlmostEqual(expected.x, actual.x, 1) 34 | self.assertAlmostEqual(expected.y, actual.y, 1) 35 | 36 | 37 | def get_quat(w=1.0, x=0.0, y=0.0, z=0.0): 38 | return Quaternion((w, x, y, z)) 39 | 40 | 41 | def compare_quats(self, expected, actual): 42 | self.assertAlmostEqual(expected.w, actual.w, 1) 43 | self.assertAlmostEqual(expected.x, actual.x, 1) 44 | self.assertAlmostEqual(expected.y, actual.y, 1) 45 | self.assertAlmostEqual(expected.z, actual.z, 1) 46 | 47 | 48 | def get_mat(row0=None, row1=None, row2=None): 49 | mat = Matrix(([1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0])) 50 | if row0 is not None: 51 | mat[0][0] = row0[0] 52 | mat[0][1] = row0[1] 53 | mat[0][2] = row0[2] 54 | mat[0][3] = row0[3] 55 | 56 | if row1 is not None: 57 | mat[1][0] = row1[0] 58 | mat[1][1] = row1[1] 59 | mat[1][2] = row1[2] 60 | mat[1][3] = row1[3] 61 | 62 | if row2 is not None: 63 | mat[2][0] = row2[0] 64 | mat[2][1] = row2[1] 65 | mat[2][2] = row2[2] 66 | mat[2][3] = row2[3] 67 | 68 | return mat 69 | 70 | 71 | def compare_mats(self, expected, actual): 72 | self.assertAlmostEqual(expected[0][0], actual[0][0], 1) 73 | self.assertAlmostEqual(expected[0][1], actual[0][1], 1) 74 | self.assertAlmostEqual(expected[0][2], actual[0][2], 1) 75 | self.assertAlmostEqual(expected[0][3], actual[0][3], 1) 76 | 77 | self.assertAlmostEqual(expected[1][0], actual[1][0], 1) 78 | self.assertAlmostEqual(expected[1][1], actual[1][1], 1) 79 | self.assertAlmostEqual(expected[1][2], actual[1][2], 1) 80 | self.assertAlmostEqual(expected[1][3], actual[1][3], 1) 81 | 82 | self.assertAlmostEqual(expected[2][0], actual[2][0], 1) 83 | self.assertAlmostEqual(expected[2][1], actual[2][1], 1) 84 | self.assertAlmostEqual(expected[2][2], actual[2][2], 1) 85 | self.assertAlmostEqual(expected[2][3], actual[2][3], 1) 86 | -------------------------------------------------------------------------------- /tests/runner.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import sys 5 | import unittest 6 | 7 | if '--coverage' in sys.argv: 8 | import coverage 9 | # Start collecting coverage 10 | cov = coverage.Coverage() 11 | cov.start() 12 | 13 | loader = unittest.defaultTestLoader 14 | 15 | if '--prefix' in sys.argv: 16 | prefix = sys.argv[sys.argv.index('--prefix') + 1] 17 | if not prefix == '': 18 | loader.testMethodPrefix = prefix 19 | 20 | print(f'running all tests prefixed with \'{loader.testMethodPrefix}\'') 21 | 22 | suite = loader.discover('.') 23 | if not unittest.TextTestRunner().run(suite).wasSuccessful(): 24 | exit(1) 25 | 26 | if '--coverage' in sys.argv: 27 | cov.stop() 28 | cov.xml_report() 29 | 30 | if '--save-html-report' in sys.argv: 31 | cov.html_report() 32 | -------------------------------------------------------------------------------- /tests/test_bone_volume_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import os 5 | import bmesh 6 | from io_mesh_w3d.common.utils.mesh_export import * 7 | from io_mesh_w3d.bone_volume_export import * 8 | from tests.utils import * 9 | 10 | 11 | class TestBoneVolumeExport(TestCase): 12 | def test_bone_volume_export(self): 13 | create_volume(self, 'volume1', 33, 1.22, 'DEBRIS') 14 | create_volume(self, 'volume2', 21, 3.22, 'DEBRIS', Vector((22.332, 2.11, -5))) 15 | 16 | bpy.context.view_layer.update() 17 | 18 | xmlFile = None 19 | 20 | try: 21 | export_bone_volume_data(self, 'output.xml') 22 | 23 | xmlFile = open('output.xml', 'r') 24 | 25 | self.assertEqual('\n', xmlFile.readline()) 26 | self.assertEqual('\n', xmlFile.readline()) 27 | self.assertEqual( 28 | ' \n', 29 | xmlFile.readline()) 30 | self.assertEqual(' \n', xmlFile.readline()) 31 | self.assertEqual(' \n', xmlFile.readline()) 32 | self.assertEqual( 33 | ' \n', 34 | xmlFile.readline()) 35 | self.assertEqual(' \n', xmlFile.readline()) 36 | self.assertEqual(' \n', xmlFile.readline()) 37 | self.assertEqual( 38 | ' \n', 39 | xmlFile.readline()) 40 | self.assertEqual(' \n', xmlFile.readline()) 41 | self.assertEqual(' \n', xmlFile.readline()) 42 | self.assertEqual( 43 | ' \n', 44 | xmlFile.readline()) 45 | self.assertEqual(' \n', xmlFile.readline()) 46 | self.assertEqual(' \n', 47 | xmlFile.readline()) 48 | self.assertEqual('\n', xmlFile.readline()) 49 | 50 | xmlFile.close() 51 | 52 | except Exception as e: 53 | raise e 54 | finally: 55 | xmlFile.close() 56 | os.remove('output.xml') 57 | 58 | 59 | def create_volume(context, name, mass, spinniness, contact_tag, location=Vector((0, 0, 0))): 60 | mesh = bpy.data.meshes.new(name) 61 | mesh.mass = mass 62 | mesh.spinniness = spinniness 63 | mesh.contact_tag = contact_tag 64 | mesh.object_type = 'BONE_VOLUME' 65 | 66 | b_mesh = bmesh.new() 67 | bmesh.ops.create_cube(b_mesh, size=1) 68 | b_mesh.to_mesh(mesh) 69 | 70 | prepare_bmesh(context, mesh) 71 | 72 | object = bpy.data.objects.new(mesh.name, mesh) 73 | object.location = location 74 | 75 | link_object_to_active_scene(object, bpy.context.scene.collection) 76 | mesh.update() 77 | -------------------------------------------------------------------------------- /tests/testfiles/cube_with_modifiers.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/testfiles/cube_with_modifiers.blend -------------------------------------------------------------------------------- /tests/testfiles/multiuser_mesh_with_modifiers.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/testfiles/multiuser_mesh_with_modifiers.blend -------------------------------------------------------------------------------- /tests/testfiles/texture.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/testfiles/texture.dds -------------------------------------------------------------------------------- /tests/testfiles/unordered_bones.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/testfiles/unordered_bones.blend -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | import inspect 6 | import os 7 | import io 8 | import shutil 9 | import sys 10 | import tempfile 11 | import unittest 12 | from unittest.mock import patch 13 | 14 | import addon_utils 15 | 16 | from io_mesh_w3d.w3x.io_xml import * 17 | from io_mesh_w3d.w3d.io_binary import * 18 | 19 | 20 | def almost_equal(self, x, y, threshold=0.0001): 21 | self.assertTrue(abs(x - y) < threshold) 22 | 23 | 24 | class TestCase(unittest.TestCase): 25 | __save_test_data = '--save-test-data' in sys.argv 26 | __tmp_base = os.path.join(tempfile.gettempdir(), 'io_mesh_w3d-tests') 27 | __filepath = os.path.join(__tmp_base, 'out' + os.path.sep) 28 | 29 | filepath = '' 30 | file_format = 'W3D' 31 | filename_ext = '.w3d' 32 | 33 | def log(con, level, text): return text 34 | 35 | def set_format(self, format): 36 | self.file_format = format 37 | if format == 'W3D': 38 | self.filename_ext = '.w3d' 39 | else: 40 | self.filename_ext = '.w3x' 41 | 42 | def enable_logging(self): 43 | self.log = print 44 | 45 | def info(self, msg): 46 | self.log({'INFO'}, msg) 47 | 48 | def warning(self, msg): 49 | self.log({'WARNING'}, msg) 50 | 51 | def error(self, msg): 52 | self.log({'ERROR'}, msg) 53 | 54 | @classmethod 55 | def relpath(cls, path=None): 56 | result = os.path.dirname(inspect.getfile(cls)) 57 | if path is not None: 58 | result = os.path.join(result, path) 59 | return result 60 | 61 | @classmethod 62 | def outpath(cls, path=''): 63 | if not os.path.exists(cls.__filepath): 64 | os.makedirs(cls.__filepath) 65 | return os.path.join(cls.__filepath, path) 66 | 67 | def loadBlend(self, blend_file): 68 | bpy.ops.wm.open_mainfile(filepath=self.relpath(blend_file)) 69 | 70 | def setUp(self): 71 | namespace = self.id().split('.') 72 | print(namespace[-2] + '.' + namespace[-1]) 73 | 74 | self.filepath = self.outpath() 75 | if not os.path.exists(self.__filepath): 76 | os.makedirs(self.__filepath) 77 | bpy.ops.wm.read_homefile(use_empty=True) 78 | addon_utils.enable('io_mesh_w3d', default_set=True) 79 | 80 | def tearDown(self): 81 | if os.path.exists(self.__filepath): 82 | if self.__save_test_data: 83 | bpy.ops.wm.save_mainfile( 84 | __filepath=os.path.join(self.__filepath, 'result.blend')) 85 | new_path = os.path.join( 86 | self.__tmp_base, 87 | self.__class__.__name__, 88 | self._testMethodName) 89 | os.renames(self.__filepath, new_path) 90 | else: 91 | shutil.rmtree(self.__filepath) 92 | addon_utils.disable('io_mesh_w3d') 93 | 94 | def write_read_test( 95 | self, 96 | expected, 97 | chunk_id, 98 | read, 99 | compare, 100 | context=None, 101 | pass_end=False, 102 | adapt=lambda x: x): 103 | io_stream = io.BytesIO() 104 | expected.write(io_stream) 105 | 106 | self.assertEqual(expected.size(), io_stream.tell()) 107 | 108 | io_stream = io.BytesIO(io_stream.getvalue()) 109 | 110 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 111 | self.assertEqual(chunk_id, chunkType) 112 | self.assertEqual(expected.size(False), chunkSize) 113 | 114 | actual = None 115 | if context is None: 116 | if not pass_end: 117 | actual = read(io_stream) 118 | else: 119 | actual = read(io_stream, chunkEnd) 120 | else: 121 | if not pass_end: 122 | actual = read(self, io_stream) 123 | else: 124 | actual = read(self, io_stream, chunkEnd) 125 | 126 | adapt(expected) 127 | 128 | compare(self, expected, actual) 129 | 130 | def write_read_xml_test(self, expected, identifier, parse, compare, context=None): 131 | self.file_format = 'W3X' 132 | root = create_root() 133 | expected.create(root) 134 | 135 | # pretty_print(root) 136 | # print(ET.tostring(root)) 137 | 138 | xml_objects = root.findall(identifier) 139 | self.assertEqual(1, len(xml_objects)) 140 | 141 | if context is not None: 142 | actual = parse(context, xml_objects[0]) 143 | else: 144 | actual = parse(xml_objects[0]) 145 | compare(self, expected, actual) 146 | -------------------------------------------------------------------------------- /tests/w3d/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3d/__init__.py -------------------------------------------------------------------------------- /tests/w3d/cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3d/cases/__init__.py -------------------------------------------------------------------------------- /tests/w3d/cases/structs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3d/cases/structs/__init__.py -------------------------------------------------------------------------------- /tests/w3d/cases/structs/mesh_structs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3d/cases/structs/mesh_structs/__init__.py -------------------------------------------------------------------------------- /tests/w3d/cases/structs/mesh_structs/test_material_info.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | 6 | from tests.utils import TestCase 7 | from tests.w3d.helpers.mesh_structs.material_info import * 8 | 9 | 10 | class TestMaterialInfo(TestCase): 11 | def test_write_read(self): 12 | expected = get_material_info() 13 | 14 | self.assertEqual(24, expected.size()) 15 | 16 | io_stream = io.BytesIO() 17 | expected.write(io_stream) 18 | io_stream = io.BytesIO(io_stream.getvalue()) 19 | 20 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 21 | self.assertEqual(W3D_CHUNK_MATERIAL_INFO, chunkType) 22 | self.assertEqual(expected.size(False), chunkSize) 23 | 24 | actual = MaterialInfo.read(io_stream) 25 | 26 | compare_material_infos(self, expected, actual) 27 | 28 | def test_chunk_size(self): 29 | expected = get_material_info() 30 | 31 | self.assertEqual(16, expected.size(False)) 32 | self.assertEqual(24, expected.size()) 33 | -------------------------------------------------------------------------------- /tests/w3d/cases/structs/mesh_structs/test_material_pass.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | 6 | from tests.utils import TestCase 7 | from tests.w3d.helpers.mesh_structs.material_pass import * 8 | 9 | 10 | class TestMaterialPass(TestCase): 11 | def test_write_read(self): 12 | expected = get_material_pass() 13 | 14 | self.assertEqual(348, expected.size()) 15 | 16 | io_stream = io.BytesIO() 17 | expected.write(io_stream) 18 | io_stream = io.BytesIO(io_stream.getvalue()) 19 | 20 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 21 | self.assertEqual(W3D_CHUNK_MATERIAL_PASS, chunkType) 22 | self.assertEqual(expected.size(False), chunkSize) 23 | 24 | actual = MaterialPass.read(self, io_stream, chunkEnd) 25 | compare_material_passes(self, expected, actual) 26 | 27 | def test_write_read_empty(self): 28 | expected = get_material_pass_empty() 29 | 30 | self.assertEqual(8, expected.size()) 31 | 32 | io_stream = io.BytesIO() 33 | expected.write(io_stream) 34 | io_stream = io.BytesIO(io_stream.getvalue()) 35 | 36 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 37 | self.assertEqual(W3D_CHUNK_MATERIAL_PASS, chunkType) 38 | self.assertEqual(expected.size(False), chunkSize) 39 | 40 | actual = MaterialPass.read(self, io_stream, chunkEnd) 41 | compare_material_passes(self, expected, actual) 42 | 43 | def test_unknown_chunk_skip(self): 44 | output = io.BytesIO() 45 | write_chunk_head(W3D_CHUNK_MATERIAL_PASS, 46 | output, 9, has_sub_chunks=True) 47 | 48 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 49 | write_ubyte(0x00, output) 50 | 51 | io_stream = io.BytesIO(output.getvalue()) 52 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 53 | 54 | self.assertEqual(W3D_CHUNK_MATERIAL_PASS, chunk_type) 55 | 56 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x0', text) 57 | MaterialPass.read(self, io_stream, subchunk_end) 58 | 59 | def test_chunk_sizes(self): 60 | mpass = get_material_pass_minimal() 61 | 62 | self.assertEqual(4, long_list_size(mpass.vertex_material_ids, False)) 63 | 64 | self.assertEqual(4, long_list_size(mpass.shader_ids, False)) 65 | 66 | self.assertEqual(4, list_size(mpass.dcg, False)) 67 | self.assertEqual(4, list_size(mpass.dig, False)) 68 | self.assertEqual(4, list_size(mpass.scg, False)) 69 | 70 | self.assertEqual(4, long_list_size(mpass.shader_material_ids, False)) 71 | 72 | self.assertEqual(196, list_size(mpass.tx_stages, False)) 73 | 74 | self.assertEqual(284, mpass.size(False)) 75 | self.assertEqual(292, mpass.size()) 76 | 77 | 78 | class TestTextureStage(TestCase): 79 | def test_write_read(self): 80 | expected = get_texture_stage() 81 | 82 | self.assertEqual(196, expected.size()) 83 | 84 | io_stream = io.BytesIO() 85 | expected.write(io_stream) 86 | io_stream = io.BytesIO(io_stream.getvalue()) 87 | 88 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 89 | self.assertEqual(W3D_CHUNK_TEXTURE_STAGE, chunkType) 90 | self.assertEqual(expected.size(False), chunkSize) 91 | 92 | actual = TextureStage.read(self, io_stream, chunkEnd) 93 | compare_texture_stages(self, expected, actual) 94 | 95 | def test_write_read_empty(self): 96 | expected = get_texture_stage_empty() 97 | 98 | self.assertEqual(8, expected.size()) 99 | 100 | io_stream = io.BytesIO() 101 | expected.write(io_stream) 102 | io_stream = io.BytesIO(io_stream.getvalue()) 103 | 104 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 105 | self.assertEqual(W3D_CHUNK_TEXTURE_STAGE, chunkType) 106 | self.assertEqual(expected.size(False), chunkSize) 107 | 108 | actual = TextureStage.read(self, io_stream, chunkEnd) 109 | compare_texture_stages(self, expected, actual) 110 | 111 | def test_unsupported_chunk_skip(self): 112 | output = io.BytesIO() 113 | write_chunk_head(W3D_CHUNK_TEXTURE_STAGE, 114 | output, 9, has_sub_chunks=True) 115 | 116 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 117 | write_ubyte(0x00, output) 118 | 119 | io_stream = io.BytesIO(output.getvalue()) 120 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 121 | 122 | self.assertEqual(W3D_CHUNK_TEXTURE_STAGE, chunk_type) 123 | 124 | TextureStage.read(self, io_stream, subchunk_end) 125 | 126 | def test_chunk_sizes(self): 127 | stage = get_texture_stage_minimal() 128 | 129 | self.assertEqual(4, long_list_size(stage.tx_ids, False)) 130 | 131 | self.assertEqual(12, vec_list_size(stage.per_face_tx_coords, False)) 132 | 133 | self.assertEqual(8, vec2_list_size(stage.tx_coords, False)) 134 | 135 | self.assertEqual(48, stage.size(False)) 136 | self.assertEqual(56, stage.size()) 137 | -------------------------------------------------------------------------------- /tests/w3d/cases/structs/mesh_structs/test_prelit.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.utils import TestCase 6 | from tests.w3d.helpers.mesh_structs.prelit import * 7 | 8 | 9 | class TestPrelit(TestCase): 10 | def test_write_read(self): 11 | type = W3D_CHUNK_PRELIT_VERTEX 12 | expected = get_prelit(type=type) 13 | 14 | self.assertEqual(665, expected.size()) 15 | 16 | io_stream = io.BytesIO() 17 | expected.write(io_stream) 18 | io_stream = io.BytesIO(io_stream.getvalue()) 19 | 20 | (chunkType, chunkSize, _) = read_chunk_head(io_stream) 21 | self.assertEqual(type, chunkType) 22 | self.assertEqual(expected.size(False), chunkSize) 23 | 24 | actual = PrelitBase.read(self, io_stream, chunkSize, type) 25 | compare_prelits(self, expected, actual) 26 | 27 | def test_write_read_minimal(self): 28 | type = W3D_CHUNK_PRELIT_VERTEX 29 | expected = get_prelit_minimal(type=type) 30 | 31 | self.assertEqual(32, expected.size()) 32 | 33 | io_stream = io.BytesIO() 34 | expected.write(io_stream) 35 | io_stream = io.BytesIO(io_stream.getvalue()) 36 | 37 | (chunkType, chunkSize, _) = read_chunk_head(io_stream) 38 | self.assertEqual(type, chunkType) 39 | self.assertEqual(expected.size(False), chunkSize) 40 | 41 | actual = PrelitBase.read(self, io_stream, chunkSize, type) 42 | compare_prelits(self, expected, actual) 43 | 44 | def test_chunk_size(self): 45 | type = W3D_CHUNK_PRELIT_VERTEX 46 | expected = get_prelit(type=type) 47 | 48 | self.assertEqual(657, expected.size(False)) 49 | self.assertEqual(665, expected.size()) 50 | 51 | def test_unknown_chunk_skip(self): 52 | output = io.BytesIO() 53 | write_chunk_head(W3D_CHUNK_PRELIT_VERTEX, output, 9, has_sub_chunks=True) 54 | 55 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 56 | write_ubyte(0x00, output) 57 | 58 | io_stream = io.BytesIO(output.getvalue()) 59 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 60 | 61 | self.assertEqual(W3D_CHUNK_PRELIT_VERTEX, chunk_type) 62 | 63 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x0', text) 64 | PrelitBase.read(self, io_stream, subchunk_end, W3D_CHUNK_PRELIT_VERTEX) 65 | -------------------------------------------------------------------------------- /tests/w3d/cases/structs/mesh_structs/test_shader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | 6 | from tests.utils import TestCase 7 | from tests.w3d.helpers.mesh_structs.shader import * 8 | 9 | 10 | class TestShader(TestCase): 11 | def test_write_read(self): 12 | expected = get_shader() 13 | 14 | self.assertEqual(16, expected.size()) 15 | 16 | io_stream = io.BytesIO() 17 | expected.write(io_stream) 18 | io_stream = io.BytesIO(io_stream.getvalue()) 19 | 20 | actual = Shader.read(io_stream) 21 | compare_shaders(self, expected, actual) 22 | -------------------------------------------------------------------------------- /tests/w3d/cases/structs/mesh_structs/test_vertex_material.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | 6 | from tests.utils import TestCase 7 | from tests.w3d.helpers.mesh_structs.vertex_material import * 8 | 9 | 10 | class TestVertexMaterial(TestCase): 11 | def test_write_read(self): 12 | expected = get_vertex_material() 13 | 14 | self.assertEqual(40, expected.vm_info.size()) 15 | self.assertEqual(180, expected.size()) 16 | 17 | io_stream = io.BytesIO() 18 | expected.write(io_stream) 19 | io_stream = io.BytesIO(io_stream.getvalue()) 20 | 21 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 22 | self.assertEqual(W3D_CHUNK_VERTEX_MATERIAL, chunkType) 23 | self.assertEqual(expected.size(False), chunkSize) 24 | 25 | actual = VertexMaterial.read(self, io_stream, chunkEnd) 26 | compare_vertex_materials(self, expected, actual) 27 | 28 | def test_write_read_empty(self): 29 | expected = get_vertex_material_empty() 30 | 31 | self.assertEqual(18, expected.size()) 32 | 33 | io_stream = io.BytesIO() 34 | expected.write(io_stream) 35 | io_stream = io.BytesIO(io_stream.getvalue()) 36 | 37 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 38 | self.assertEqual(W3D_CHUNK_VERTEX_MATERIAL, chunkType) 39 | self.assertEqual(expected.size(False), chunkSize) 40 | 41 | actual = VertexMaterial.read(self, io_stream, chunkEnd) 42 | compare_vertex_materials(self, expected, actual) 43 | 44 | def test_unknown_chunk_skip(self): 45 | output = io.BytesIO() 46 | write_chunk_head(W3D_CHUNK_VERTEX_MATERIAL, 47 | output, 9, has_sub_chunks=True) 48 | 49 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 50 | write_ubyte(0x00, output) 51 | 52 | io_stream = io.BytesIO(output.getvalue()) 53 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 54 | 55 | self.assertEqual(W3D_CHUNK_VERTEX_MATERIAL, chunk_type) 56 | 57 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x0', text) 58 | VertexMaterial.read(self, io_stream, subchunk_end) 59 | 60 | def test_chunk_sizes(self): 61 | vm = get_vertex_material_minimal() 62 | 63 | self.assertEqual(2, text_size(vm.vm_name, False)) 64 | 65 | self.assertEqual(32, vm.vm_info.size(False)) 66 | self.assertEqual(40, vm.vm_info.size()) 67 | 68 | self.assertEqual(2, text_size(vm.vm_args_0, False)) 69 | 70 | self.assertEqual(2, text_size(vm.vm_args_1, False)) 71 | 72 | self.assertEqual(70, vm.size(False)) 73 | self.assertEqual(78, vm.size()) 74 | -------------------------------------------------------------------------------- /tests/w3d/cases/structs/test_compressed_animation.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | 6 | from tests.utils import TestCase 7 | from tests.w3d.helpers.compressed_animation import * 8 | 9 | 10 | class TestCompressedAnimation(TestCase): 11 | def test_write_read(self): 12 | expected = get_compressed_animation() 13 | 14 | self.assertEqual(3740, expected.size()) 15 | 16 | io_stream = io.BytesIO() 17 | expected.write(io_stream) 18 | io_stream = io.BytesIO(io_stream.getvalue()) 19 | 20 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 21 | self.assertEqual(W3D_CHUNK_COMPRESSED_ANIMATION, chunkType) 22 | self.assertEqual(expected.size(), chunkSize) 23 | 24 | actual = CompressedAnimation.read(self, io_stream, chunkEnd) 25 | compare_compressed_animations(self, expected, actual) 26 | 27 | def test_write_read_adaptive_delta(self): 28 | expected = get_compressed_animation(flavor=1) 29 | 30 | self.assertEqual(3639, expected.size()) 31 | 32 | io_stream = io.BytesIO() 33 | expected.write(io_stream) 34 | io_stream = io.BytesIO(io_stream.getvalue()) 35 | 36 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 37 | self.assertEqual(W3D_CHUNK_COMPRESSED_ANIMATION, chunkType) 38 | self.assertEqual(expected.size(), chunkSize) 39 | 40 | actual = CompressedAnimation.read(self, io_stream, chunkEnd) 41 | compare_compressed_animations(self, expected, actual) 42 | 43 | def test_write_read_empty(self): 44 | expected = get_compressed_animation_empty() 45 | 46 | self.assertEqual(52, expected.size()) 47 | 48 | io_stream = io.BytesIO() 49 | expected.write(io_stream) 50 | io_stream = io.BytesIO(io_stream.getvalue()) 51 | 52 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 53 | self.assertEqual(W3D_CHUNK_COMPRESSED_ANIMATION, chunkType) 54 | self.assertEqual(expected.size(), chunkSize) 55 | 56 | actual = CompressedAnimation.read(self, io_stream, chunkEnd) 57 | compare_compressed_animations(self, expected, actual) 58 | 59 | def test_validate(self): 60 | ani = get_compressed_animation() 61 | self.assertTrue(ani.validate(self)) 62 | self.assertTrue(ani.validate(self, w3x=True)) 63 | 64 | ani.header.name = 'tooooolonganiname' 65 | self.assertFalse(ani.validate(self)) 66 | self.assertTrue(ani.validate(self, w3x=True)) 67 | 68 | ani = get_compressed_animation() 69 | ani.header.hierarchy_name = 'tooooolonganiname' 70 | self.assertFalse(ani.validate(self)) 71 | self.assertTrue(ani.validate(self, w3x=True)) 72 | 73 | ani = get_compressed_animation() 74 | ani.time_coded_channels = [] 75 | self.assertFalse(ani.validate(self)) 76 | self.assertFalse(ani.validate(self, w3x=True)) 77 | 78 | def test_unknown_chunk_skip(self): 79 | output = io.BytesIO() 80 | write_chunk_head(W3D_CHUNK_COMPRESSED_ANIMATION, 81 | output, 70, has_sub_chunks=True) 82 | 83 | header = get_compressed_animation_header(flavor=2) 84 | header.write(output) 85 | 86 | write_chunk_head(W3D_CHUNK_COMPRESSED_ANIMATION_CHANNEL, 87 | output, 1, has_sub_chunks=False) 88 | write_ubyte(0x00, output) 89 | 90 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 91 | write_ubyte(0x00, output) 92 | 93 | io_stream = io.BytesIO(output.getvalue()) 94 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 95 | 96 | self.assertEqual(W3D_CHUNK_COMPRESSED_ANIMATION, chunk_type) 97 | 98 | warning_texts = [] 99 | self.warning = lambda text: warning_texts.append(text) 100 | CompressedAnimation.read(self, io_stream, subchunk_end) 101 | 102 | self.assertEqual('unknown chunk_type in io_stream: 0x282', warning_texts[0]) 103 | self.assertEqual('unknown chunk_type in io_stream: 0x0', warning_texts[1]) 104 | 105 | def test_chunk_sizes(self): 106 | ani = get_compressed_animation_minimal() 107 | 108 | self.assertEqual(44, ani.header.size(False)) 109 | 110 | self.assertEqual(24, list_size(ani.time_coded_channels, False)) 111 | self.assertEqual(36, list_size(ani.adaptive_delta_channels, False)) 112 | self.assertEqual(20, list_size(ani.time_coded_bit_channels, False)) 113 | self.assertEqual(24, list_size(ani.motion_channels, False)) 114 | 115 | self.assertEqual(156, ani.size()) 116 | -------------------------------------------------------------------------------- /tests/w3d/cases/structs/test_dazzle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | from tests.utils import TestCase 6 | from tests.w3d.helpers.dazzle import * 7 | 8 | 9 | class TestDazzle(TestCase): 10 | def test_write_read(self): 11 | expected = get_dazzle() 12 | 13 | self.assertEqual(64, expected.size()) 14 | 15 | io_stream = io.BytesIO() 16 | expected.write(io_stream) 17 | io_stream = io.BytesIO(io_stream.getvalue()) 18 | 19 | (chunkType, chunkSize, chunkEnd) = read_chunk_head(io_stream) 20 | self.assertEqual(W3D_CHUNK_DAZZLE, chunkType) 21 | self.assertEqual(expected.size(False), chunkSize) 22 | 23 | actual = Dazzle.read(self, io_stream, chunkEnd) 24 | 25 | compare_dazzles(self, expected, actual) 26 | 27 | def test_unknown_chunk_skip(self): 28 | output = io.BytesIO() 29 | write_chunk_head(W3D_CHUNK_DAZZLE, output, 9, has_sub_chunks=True) 30 | 31 | write_chunk_head(0x00, output, 1, has_sub_chunks=False) 32 | write_ubyte(0x00, output) 33 | 34 | io_stream = io.BytesIO(output.getvalue()) 35 | (chunk_type, chunk_size, subchunk_end) = read_chunk_head(io_stream) 36 | 37 | self.assertEqual(W3D_CHUNK_DAZZLE, chunk_type) 38 | 39 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x0', text) 40 | Dazzle.read(self, io_stream, subchunk_end) 41 | 42 | def test_chunk_sizes(self): 43 | dazzle = get_dazzle() 44 | 45 | self.assertEqual(64, dazzle.size()) 46 | self.assertEqual(56, dazzle.size(False)) 47 | -------------------------------------------------------------------------------- /tests/w3d/cases/structs/test_version.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import io 5 | 6 | from tests.utils import TestCase 7 | from tests.w3d.helpers.version import * 8 | 9 | 10 | class TestVersion(TestCase): 11 | def test_write_read(self): 12 | expected = get_version() 13 | 14 | io_stream = io.BytesIO() 15 | expected.write(io_stream) 16 | io_stream = io.BytesIO(io_stream.getvalue()) 17 | 18 | compare_versions(self, expected, Version.read(io_stream)) 19 | 20 | def test_eq_true(self): 21 | ver = get_version() 22 | self.assertEqual(ver, ver) 23 | 24 | def test_eq_false(self): 25 | ver = get_version() 26 | other = Version(major=2, minor=1) 27 | 28 | self.assertNotEqual(ver, other) 29 | self.assertNotEqual(ver, 1) 30 | self.assertNotEqual(ver, 'test') 31 | -------------------------------------------------------------------------------- /tests/w3d/cases/test_adaptive_delta.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import unittest 5 | 6 | from io_mesh_w3d.w3d.adaptive_delta import * 7 | from tests.common.helpers.animation import * 8 | from tests.w3d.helpers.compressed_animation import * 9 | from tests.utils import TestCase 10 | 11 | 12 | class TestAdaptiveDelta(TestCase): 13 | def test_get_deltas_4bit(self): 14 | deltaBytes = [-3, 17, -32, -101, 120, 88, -20, -1] 15 | deltas = get_deltas(deltaBytes, 4) 16 | expected = [-3, -1, 1, 1, 0, -2, -5, -7, -8, 7, -8, 5, -4, -2, -1, -1] 17 | self.assertEqual(expected, deltas) 18 | 19 | def test_get_deltas_8bit(self): 20 | deltaBytes = [-49, -50, -53, -57, -62, -69, -73, -75, -82, -94, -111, 123, 82, 37, 12, 4] 21 | deltas = get_deltas(deltaBytes, 8) 22 | expected = [79, 78, 75, 71, 66, 59, 55, 53, 46, 34, 17, -5, -46, -91, -116, -124] 23 | self.assertEqual(expected, deltas) 24 | 25 | def test_set_deltas_4bit(self): 26 | bytes = [-3, -1, 1, 1, 0, -2, -5, -7, -8, 7, -8, 5, -4, -2, -1, -1] 27 | expected = [-3, 17, -32, -101, 120, 88, -20, -1] 28 | actual = set_deltas(bytes, 4) 29 | 30 | self.assertEqual(expected, actual) 31 | 32 | def test_set_deltas_8bit(self): 33 | bytes = [79, 78, 75, 71, 66, 59, 55, 53, 46, 34, 17, -5, -46, -91, -116, -124] 34 | expected = [-49, -50, -53, -57, -62, -69, -73, -75, -82, -94, -111, 123, 82, 37, 12, 4] 35 | actual = set_deltas(bytes, 8) 36 | 37 | self.assertEqual(expected, actual) 38 | 39 | def test_decode_channel_ad(self): 40 | channel = get_adaptive_delta_animation_channel(type=0) 41 | expected = [4.3611, 15.5264, 29.4832, 49.0226, 68.5621] 42 | 43 | actual = decode(channel.type, channel.vector_len, channel.num_time_codes, channel.scale, channel.data) 44 | 45 | self.assertEqual(len(expected), len(actual)) 46 | for i, value in enumerate(expected): 47 | self.assertAlmostEqual(value, actual[i], 3) 48 | 49 | def test_decode_motion_channel_ad(self): 50 | channel = get_motion_channel(type=0, delta_type=1, num_time_codes=5) 51 | expected = [4.3611, 4.6254, 4.9559, 5.4186, 5.8812] 52 | 53 | actual = decode(channel.type, channel.vector_len, channel.num_time_codes, channel.data.scale, channel.data.data) 54 | 55 | self.assertEqual(len(expected), len(actual)) 56 | for i, value in enumerate(expected): 57 | self.assertAlmostEqual(value, actual[i], 3) 58 | 59 | def test_encode_8bit(self): 60 | channel = AnimationChannel( 61 | first_frame=0, 62 | last_frame=7, 63 | type=1, 64 | pivot=2, 65 | unknown=0, 66 | data=[4.3611, 4.3611, 4.6254, 4.9559, 5.4186, 5.8812]) 67 | expected = [95, 44, 12, 2, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 68 | 69 | actual = encode(channel, num_bits=8) 70 | 71 | # self.assertEqual(len(expected), len(actual)) 72 | # self.assertEqual(expected, actual) 73 | 74 | def test_encode_4bit(self): 75 | channel = AnimationChannel( 76 | first_frame=0, 77 | last_frame=7, 78 | type=1, 79 | pivot=2, 80 | unknown=0, 81 | data=[4.3611, 4.3611, 4.6254, 4.9559, 5.4186, 5.8812]) 82 | expected = [] # ? 83 | 84 | actual = encode(channel, num_bits=4) 85 | 86 | # self.assertEqual(len(expected), len(actual)) 87 | # self.assertEqual(expected, actual) 88 | -------------------------------------------------------------------------------- /tests/w3d/cases/test_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.import_w3d import * 5 | from tests.common.helpers.collision_box import get_collision_box 6 | from tests.common.helpers.hlod import get_hlod 7 | from tests.common.helpers.mesh import get_mesh 8 | from tests.common.helpers.hierarchy import get_hierarchy 9 | from tests.common.helpers.animation import get_animation 10 | from tests.w3d.helpers.compressed_animation import get_compressed_animation 11 | from tests.utils import * 12 | from unittest.mock import patch, call 13 | 14 | 15 | class TestImport(TestCase): 16 | def test_import_no_skeleton_file_found(self): 17 | hierarchy_name = 'TestHiera_SKL' 18 | meshes = [ 19 | get_mesh(name='sword', skin=True), 20 | get_mesh(name='soldier', skin=True), 21 | get_mesh(name='TRUNK')] 22 | hlod = get_hlod('TestModelName', hierarchy_name) 23 | box = get_collision_box() 24 | 25 | # write to file 26 | skn = open(self.outpath() + 'base_skn.w3d', 'wb') 27 | for mesh in meshes: 28 | mesh.write(skn) 29 | hlod.write(skn) 30 | box.write(skn) 31 | skn.close() 32 | 33 | # import 34 | self.filepath = self.outpath() + 'base_skn.w3d' 35 | load(self) 36 | 37 | def test_skips_multiple_hlod_chunks(self): 38 | hlod = get_hlod() 39 | skn = open(self.outpath() + 'output.w3d', 'wb') 40 | 41 | hlod.write(skn) 42 | hlod.write(skn) 43 | skn.close() 44 | 45 | # import 46 | with (patch.object(self, 'warning')) as warning_func: 47 | self.filepath = self.outpath() + 'output.w3d' 48 | load(self) 49 | warning_func.assert_called_with('-> already got one hlod chunk (skipping this one)!') 50 | 51 | def test_skips_multiple_hierarchy_chunks(self): 52 | hierarchy = get_hierarchy() 53 | skl = open(self.outpath() + 'output.w3d', 'wb') 54 | 55 | hierarchy.write(skl) 56 | hierarchy.write(skl) 57 | skl.close() 58 | 59 | # import 60 | with (patch.object(self, 'warning')) as warning_func: 61 | self.filepath = self.outpath() + 'output.w3d' 62 | self.assertEqual({'FINISHED'}, load(self)) 63 | warning_func.assert_called_with('-> already got one hierarchy chunk (skipping this one)!') 64 | 65 | def test_skips_multiple_animation_chunks(self): 66 | animation = get_animation() 67 | comp_animation = get_compressed_animation() 68 | ani = open(self.outpath() + 'output.w3d', 'wb') 69 | 70 | animation.write(ani) 71 | animation.write(ani) 72 | comp_animation.write(ani) 73 | ani.close() 74 | 75 | # import 76 | with (patch.object(self, 'warning')) as warning_func: 77 | self.filepath = self.outpath() + 'output.w3d' 78 | load(self) 79 | 80 | msg = '-> already got one animation chunk (skipping this one)!' 81 | warning_func.assert_has_calls([call(msg), call(msg)]) 82 | 83 | def test_skips_multiple_compressed_animation_chunks(self): 84 | animation = get_animation() 85 | comp_animation = get_compressed_animation() 86 | ani = open(self.outpath() + 'output.w3d', 'wb') 87 | 88 | comp_animation.write(ani) 89 | animation.write(ani) 90 | animation.write(ani) 91 | ani.close() 92 | 93 | # import 94 | with (patch.object(self, 'warning')) as warning_func: 95 | self.filepath = self.outpath() + 'output.w3d' 96 | load(self) 97 | 98 | msg = '-> already got one animation chunk (skipping this one)!' 99 | warning_func.assert_has_calls([call(msg), call(msg)]) 100 | 101 | def test_animation_import_no_skeleton_file_found(self): 102 | hierarchy_name = 'TestHiera_SKL' 103 | animation = get_animation(hierarchy_name) 104 | 105 | # write to file 106 | ani = open(self.outpath() + 'animation.w3d', 'wb') 107 | animation.write(ani) 108 | ani.close() 109 | 110 | # import 111 | self.filepath = self.outpath() + 'animation.w3d' 112 | load(self) 113 | 114 | def test_unsupported_chunk_skip(self): 115 | output = open(self.outpath() + 'output.w3d', 'wb') 116 | 117 | write_chunk_head(W3D_CHUNK_MORPH_ANIMATION, output, 0) 118 | write_chunk_head(W3D_CHUNK_HMODEL, output, 0) 119 | write_chunk_head(W3D_CHUNK_LODMODEL, output, 0) 120 | write_chunk_head(W3D_CHUNK_COLLECTION, output, 0) 121 | write_chunk_head(W3D_CHUNK_POINTS, output, 0) 122 | write_chunk_head(W3D_CHUNK_LIGHT, output, 0) 123 | write_chunk_head(W3D_CHUNK_EMITTER, output, 0) 124 | write_chunk_head(W3D_CHUNK_AGGREGATE, output, 0) 125 | write_chunk_head(W3D_CHUNK_NULL_OBJECT, output, 0) 126 | write_chunk_head(W3D_CHUNK_LIGHTSCAPE, output, 0) 127 | write_chunk_head(W3D_CHUNK_SOUNDROBJ, output, 0) 128 | output.close() 129 | 130 | self.filepath = self.outpath() + 'output.w3d' 131 | load(self) 132 | 133 | def test_unkown_chunk_skip(self): 134 | path = self.outpath() + 'output.w3d' 135 | file = open(path, 'wb') 136 | write_chunk_head(0x01, file, 1, has_sub_chunks=False) 137 | write_ubyte(0x00, file) 138 | file.close() 139 | 140 | self.warning = lambda text: self.assertEqual('unknown chunk_type in io_stream: 0x1', text) 141 | load_file(self, None, path) 142 | -------------------------------------------------------------------------------- /tests/w3d/cases/test_import_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | import bmesh 6 | from shutil import copyfile 7 | 8 | from io_mesh_w3d.import_utils import * 9 | from tests.common.helpers.hierarchy import * 10 | from tests.common.helpers.hlod import * 11 | from tests.common.helpers.mesh import * 12 | from tests.utils import * 13 | from tests.w3d.helpers.compressed_animation import * 14 | from tests.w3d.helpers.mesh_structs.material_pass import * 15 | from os.path import dirname as up 16 | 17 | 18 | class TestImportUtilsW3D(TestCase): 19 | def test_material_pass_with_2_texture_stages(self): 20 | mesh_struct = get_mesh() 21 | triangles = [] 22 | 23 | for triangle in mesh_struct.triangles: 24 | triangles.append(triangle.vert_ids) 25 | 26 | verts = mesh_struct.verts.copy() 27 | mesh = bpy.data.meshes.new(mesh_struct.header.mesh_name) 28 | mesh.from_pydata(verts, [], triangles) 29 | mesh.update() 30 | mesh.validate() 31 | b_mesh = bmesh.new() 32 | b_mesh.from_mesh(mesh) 33 | 34 | mesh_struct.material_passes[0].tx_stages.append(get_texture_stage()) 35 | 36 | for mat_pass in mesh_struct.material_passes: 37 | create_uvlayer(self, mesh, b_mesh, triangles, mat_pass) 38 | 39 | def test_mesh_import_2_textures_1_vertex_material(self): 40 | mesh = get_mesh_two_textures() 41 | 42 | copyfile(up(up(self.relpath())) + '/testfiles/texture.dds', 43 | self.outpath() + 'texture.dds') 44 | copyfile(up(up(self.relpath())) + '/testfiles/texture.dds', 45 | self.outpath() + 'texture2.dds') 46 | 47 | create_mesh(self, mesh, bpy.context.collection) 48 | 49 | def test_prelit_mesh_import(self): 50 | mesh = get_mesh(prelit=True) 51 | 52 | create_mesh(self, mesh, bpy.context.collection) 53 | 54 | def test_duplicate_vertex_material_creation(self): 55 | vert_mats = [get_vertex_material(vm_name='VM_NAME'), get_vertex_material(vm_name='VM_NAME')] 56 | 57 | for mat in vert_mats: 58 | create_material_from_vertex_material('meshName', mat) 59 | 60 | self.assertEqual(1, len(bpy.data.materials)) 61 | self.assertTrue('meshName.VM_NAME' in bpy.data.materials) 62 | 63 | def test_only_needed_keyframe_creation(self): 64 | animation = get_compressed_animation_empty() 65 | 66 | channel = TimeCodedAnimationChannel( 67 | num_time_codes=5, 68 | pivot=1, 69 | type=1, 70 | time_codes=[TimeCodedDatum(time_code=0, value=3.0), 71 | TimeCodedDatum(time_code=1, value=3.0), 72 | TimeCodedDatum(time_code=2, value=3.0), 73 | TimeCodedDatum(time_code=3, value=3.0), 74 | TimeCodedDatum(time_code=4, value=3.0)]) 75 | animation.time_coded_channels = [channel] 76 | 77 | hlod = get_hlod() 78 | hlod.lod_arrays[0].sub_objects = [ 79 | get_hlod_sub_object(bone=1, name='containerName.MESH')] 80 | 81 | hierarchy = get_hierarchy() 82 | pivot = HierarchyPivot( 83 | name='MESH', 84 | parent_id=0) 85 | 86 | hierarchy.pivots = [get_roottransform(), pivot] 87 | 88 | meshes = [get_mesh(name='MESH_Obj')] 89 | 90 | expected_frames = [0, 4] 91 | if bpy.app.version >= (4, 2, 0): 92 | expected_frames = [0] 93 | expected = [3.0, 3.0] 94 | 95 | self.filepath = self.outpath() + 'output' 96 | create_data(self, meshes, hlod, hierarchy, [], None, animation) 97 | 98 | obj = bpy.data.objects['TestHierarchy'] 99 | for fcu in obj.animation_data.action.fcurves: 100 | self.assertEqual(len(expected_frames), len(fcu.keyframe_points)) 101 | for i, keyframe in enumerate(fcu.keyframe_points): 102 | frame = int(keyframe.co.x) 103 | self.assertEqual(expected_frames[i], frame) 104 | val = keyframe.co.y 105 | self.assertEqual(expected[i], val) 106 | -------------------------------------------------------------------------------- /tests/w3d/helpers/dazzle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.structs.dazzle import * 5 | 6 | 7 | def get_dazzle(name='containerName.Brakelight', type='REN_BRAKELIGHT'): 8 | return Dazzle( 9 | name_=name, 10 | type_name=type) 11 | 12 | 13 | def compare_dazzles(self, expected, actual): 14 | self.assertEqual(expected.name_, actual.name_) 15 | self.assertEqual(expected.type_name, actual.type_name) 16 | -------------------------------------------------------------------------------- /tests/w3d/helpers/mesh_structs/material_info.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.structs.mesh_structs.material_info import * 5 | 6 | 7 | def get_material_info(mesh=None): 8 | info = MaterialInfo( 9 | pass_count=0, 10 | vert_matl_count=0, 11 | shader_count=0, 12 | texture_count=0) 13 | 14 | if mesh is not None: 15 | info.pass_count = len(mesh.material_passes) 16 | info.vert_matl_count = len(mesh.vert_materials) 17 | info.shader_count = len(mesh.shaders) 18 | info.texture_count = len(mesh.textures) 19 | return info 20 | 21 | 22 | def compare_material_infos(self, expected, actual): 23 | self.assertEqual(expected.pass_count, actual.pass_count) 24 | self.assertEqual(expected.vert_matl_count, actual.vert_matl_count) 25 | self.assertEqual(expected.shader_count, actual.shader_count) 26 | self.assertEqual(expected.texture_count, actual.texture_count) 27 | -------------------------------------------------------------------------------- /tests/w3d/helpers/mesh_structs/material_pass.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.structs.mesh_structs.material_pass import * 5 | from tests.mathutils import * 6 | from tests.common.helpers.rgba import get_rgba, compare_rgbas 7 | 8 | 9 | def get_uvs(): 10 | return [get_vec2(0.0, 0.1), 11 | get_vec2(0.0, 0.4), 12 | get_vec2(1.0, 0.6), 13 | get_vec2(0.3, 0.1), 14 | get_vec2(0.2, 0.2), 15 | get_vec2(0.6, 0.6), 16 | get_vec2(0.1, 0.8), 17 | get_vec2(0.7, 0.7)] 18 | 19 | 20 | def get_per_face_txcoords(): 21 | return [get_vec(1.0, 0.0, -1.0), 22 | get_vec(1.0, 0.0, -1.0), 23 | get_vec(1.0, 0.0, -1.0), 24 | get_vec(1.0, 0.0, -1.0), 25 | get_vec(1.0, 0.0, -1.0), 26 | get_vec(1.0, 0.0, -1.0), 27 | get_vec(1.0, 0.0, -1.0), 28 | get_vec(1.0, 0.0, -1.0)] 29 | 30 | 31 | def get_texture_stage(index=0): 32 | return TextureStage( 33 | tx_ids=[[index] + [index] * max(0, index - 1)], 34 | per_face_tx_coords=[get_per_face_txcoords()], 35 | tx_coords=[get_uvs()]) 36 | 37 | 38 | def get_texture_stage_minimal(): 39 | return TextureStage( 40 | tx_ids=[[0]], 41 | per_face_tx_coords=[[get_vec()]], 42 | tx_coords=[[get_vec()]]) 43 | 44 | 45 | def get_texture_stage_empty(): 46 | return TextureStage() 47 | 48 | 49 | def compare_texture_stages(self, expected, actual): 50 | if actual.tx_ids: # roundtrip not yet supported 51 | self.assertEqual(len(expected.tx_ids), len(actual.tx_ids)) 52 | for i in range(len(expected.tx_ids)): 53 | for j, tex_id in enumerate(expected.tx_ids[i]): 54 | self.assertEqual(tex_id, actual.tx_ids[i][j]) 55 | 56 | self.assertEqual(len(expected.tx_coords), len(actual.tx_coords)) 57 | for i in range(len(expected.tx_coords)): 58 | for j, tx_coord in enumerate(expected.tx_coords[i]): 59 | compare_vectors2(self, tx_coord, actual.tx_coords[i][j]) 60 | 61 | if actual.per_face_tx_coords: # roundtrip not yet supported 62 | self.assertEqual(len(expected.per_face_tx_coords), len(actual.per_face_tx_coords)) 63 | for i in range(len(expected.per_face_tx_coords)): 64 | for j, tx_coord in enumerate(expected.per_face_tx_coords[i]): 65 | compare_vectors(self, tx_coord, actual.per_face_tx_coords[i][j]) 66 | 67 | 68 | def get_material_pass(index=0, shader_mat=False, num_stages=1): 69 | matpass = MaterialPass( 70 | vertex_material_ids=[], 71 | shader_ids=[], 72 | dcg=[], 73 | dig=[], 74 | scg=[], 75 | shader_material_ids=[], 76 | tx_stages=[], 77 | tx_coords=[]) 78 | 79 | if shader_mat: 80 | matpass.shader_material_ids = [index] 81 | matpass.tx_coords = get_uvs() 82 | else: 83 | matpass.shader_ids = [index] 84 | matpass.vertex_material_ids = [index] 85 | for i in range(num_stages): 86 | matpass.tx_stages.append(get_texture_stage(index=index + i)) 87 | 88 | for _ in range(8): 89 | matpass.dcg.append(get_rgba()) 90 | matpass.dig.append(get_rgba()) 91 | matpass.scg.append(get_rgba()) 92 | 93 | return matpass 94 | 95 | 96 | def get_material_pass_minimal(): 97 | return MaterialPass( 98 | vertex_material_ids=[0], 99 | shader_ids=[0], 100 | dcg=[get_rgba()], 101 | dig=[get_rgba()], 102 | scg=[get_rgba()], 103 | shader_material_ids=[0], 104 | tx_stages=[get_texture_stage()], 105 | tx_coords=[get_vec()]) 106 | 107 | 108 | def get_material_pass_empty(): 109 | return MaterialPass( 110 | vertex_material_ids=[], 111 | shader_ids=[], 112 | dcg=[], 113 | dig=[], 114 | scg=[], 115 | shader_material_ids=[], 116 | tx_stages=[], 117 | tx_coords=[]) 118 | 119 | 120 | def compare_material_passes(self, expected, actual): 121 | self.assertEqual(expected.vertex_material_ids, actual.vertex_material_ids) 122 | self.assertEqual(expected.shader_ids, actual.shader_ids) 123 | 124 | delta = 1 125 | # because the color floats are truncated to 6 decimal places we 126 | # have rounding errors on a roundtrip 127 | if actual.dcg: 128 | self.assertEqual(len(expected.dcg), len(actual.dcg)) 129 | for i in range(len(expected.dcg)): 130 | compare_rgbas(self, expected.dcg[i], actual.dcg[i], delta) 131 | 132 | if actual.dig: 133 | self.assertEqual(len(expected.dig), len(actual.dig)) 134 | for i in range(len(expected.dig)): 135 | compare_rgbas(self, expected.dig[i], actual.dig[i], delta) 136 | 137 | if actual.scg: 138 | self.assertEqual(len(expected.scg), len(actual.scg)) 139 | for i in range(len(expected.scg)): 140 | compare_rgbas(self, expected.scg[i], actual.scg[i], delta) 141 | 142 | self.assertEqual(expected.shader_material_ids, actual.shader_material_ids) 143 | 144 | self.assertEqual(len(expected.tx_coords), len(actual.tx_coords)) 145 | for i in range(len(expected.tx_coords)): 146 | compare_vectors2(self, expected.tx_coords[i], actual.tx_coords[i]) 147 | 148 | self.assertEqual(len(expected.tx_stages), len(actual.tx_stages)) 149 | for i in range(len(expected.tx_stages)): 150 | compare_texture_stages( 151 | self, expected.tx_stages[i], actual.tx_stages[i]) 152 | -------------------------------------------------------------------------------- /tests/w3d/helpers/mesh_structs/prelit.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.structs.mesh_structs.prelit import * 5 | from tests.common.helpers.mesh_structs.texture import * 6 | from tests.w3d.helpers.mesh_structs.material_info import * 7 | from tests.w3d.helpers.mesh_structs.material_pass import * 8 | from tests.w3d.helpers.mesh_structs.shader import * 9 | from tests.w3d.helpers.mesh_structs.vertex_material import * 10 | 11 | 12 | def get_prelit(type=W3D_CHUNK_PRELIT_UNLIT, count=1): 13 | result = PrelitBase( 14 | type=type, 15 | mat_info=None, 16 | material_passes=[], 17 | vert_materials=[], 18 | textures=[], 19 | shaders=[]) 20 | 21 | result.mat_info = MaterialInfo( 22 | pass_count=count, 23 | vert_matl_count=count, 24 | shader_count=count, 25 | texture_count=count) 26 | 27 | vm_name = 'INVALID_TYPE' 28 | if type == W3D_CHUNK_PRELIT_UNLIT: 29 | vm_name = 'W3D_CHUNK_PRELIT_UNLIT' 30 | elif type == W3D_CHUNK_PRELIT_VERTEX: 31 | vm_name = 'W3D_CHUNK_PRELIT_VERTEX' 32 | elif type == W3D_CHUNK_PRELIT_LIGHTMAP_MULTI_PASS: 33 | vm_name = 'W3D_CHUNK_PRELIT_LIGHTMAP_MULTI_PASS' 34 | elif type == W3D_CHUNK_PRELIT_LIGHTMAP_MULTI_TEXTURE: 35 | vm_name = 'W3D_CHUNK_PRELIT_LIGHTMAP_MULTI_TEXTURE' 36 | 37 | for i in range(count): 38 | result.material_passes.append(get_material_pass()) 39 | name = vm_name + str(i) 40 | result.vert_materials.append(get_vertex_material(vm_name=name)) 41 | result.textures.append(get_texture()) 42 | result.shaders.append(get_shader()) 43 | return result 44 | 45 | 46 | def get_prelit_minimal(type=W3D_CHUNK_PRELIT_UNLIT): 47 | return PrelitBase( 48 | type=type, 49 | mat_info=get_material_info(), 50 | material_passes=[], 51 | vert_materials=[], 52 | textures=[], 53 | shaders=[]) 54 | 55 | 56 | def compare_prelits(self, expected, actual): 57 | self.assertEqual(expected.type, actual.type) 58 | compare_material_infos(self, expected.mat_info, actual.mat_info) 59 | 60 | self.assertEqual(len(expected.material_passes), 61 | len(actual.material_passes)) 62 | for i, mat_pass in enumerate(expected.material_passes): 63 | compare_material_passes(self, mat_pass, actual.material_passes[i]) 64 | 65 | self.assertEqual(len(expected.vert_materials), len(actual.vert_materials)) 66 | for i, vert_mat in enumerate(expected.vert_materials): 67 | compare_vertex_materials(self, vert_mat, actual.vert_materials[i]) 68 | 69 | self.assertEqual(len(expected.textures), len(actual.textures)) 70 | for i, tex in enumerate(expected.textures): 71 | compare_textures(self, tex, actual.textures[i]) 72 | 73 | self.assertEqual(len(expected.shaders), len(actual.shaders)) 74 | for i, shader in enumerate(expected.shaders): 75 | compare_shaders(self, shader, actual.shaders[i]) 76 | -------------------------------------------------------------------------------- /tests/w3d/helpers/mesh_structs/shader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.structs.mesh_structs.shader import Shader 5 | 6 | 7 | def get_shader(): 8 | return Shader( 9 | depth_compare=3, 10 | depth_mask=1, 11 | color_mask=0, 12 | dest_blend=0, 13 | fog_func=0, 14 | pri_gradient=1, 15 | sec_gradient=0, 16 | src_blend=1, 17 | texturing=1, 18 | detail_color_func=0, 19 | detail_alpha_func=0, 20 | shader_preset=0, 21 | alpha_test=0, 22 | post_detail_color_func=0, 23 | post_detail_alpha_func=0, 24 | pad=0) 25 | 26 | 27 | def compare_shaders(self, expected, actual): 28 | self.assertEqual(expected.depth_compare, actual.depth_compare) 29 | self.assertEqual(expected.depth_mask, actual.depth_mask) 30 | self.assertEqual(expected.color_mask, actual.color_mask) 31 | self.assertEqual(expected.dest_blend, actual.dest_blend) 32 | self.assertEqual(expected.fog_func, actual.fog_func) 33 | self.assertEqual(expected.pri_gradient, actual.pri_gradient) 34 | self.assertEqual(expected.sec_gradient, actual.sec_gradient) 35 | self.assertEqual(expected.src_blend, actual.src_blend) 36 | self.assertEqual(expected.texturing, actual.texturing) 37 | self.assertEqual(expected.detail_color_func, actual.detail_color_func) 38 | self.assertEqual(expected.detail_alpha_func, actual.detail_alpha_func) 39 | 40 | self.assertEqual(expected.shader_preset, actual.shader_preset) 41 | self.assertEqual(expected.alpha_test, actual.alpha_test) 42 | self.assertEqual(expected.post_detail_color_func, 43 | actual.post_detail_color_func) 44 | self.assertEqual(expected.post_detail_alpha_func, 45 | actual.post_detail_alpha_func) 46 | self.assertEqual(expected.pad, actual.pad) 47 | -------------------------------------------------------------------------------- /tests/w3d/helpers/mesh_structs/vertex_material.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.structs.mesh_structs.vertex_material import * 5 | 6 | from tests.common.helpers.rgba import get_rgba, compare_rgbas 7 | 8 | STAGE0_MAPPING_LINEAR_OFFSET = 0x00040000 9 | STAGE1_MAPPING_LINEAR_OFFSET = 0x00000400 10 | 11 | 12 | def get_vertex_material_info(attributes=0): 13 | return VertexMaterialInfo( 14 | attributes=attributes, 15 | ambient=get_rgba(a=0), # alpha is only padding in this and below 16 | diffuse=get_rgba(a=0), 17 | specular=get_rgba(a=0), 18 | emissive=get_rgba(a=0), 19 | shininess=0.5, 20 | opacity=0.32, 21 | translucency=0.12) 22 | 23 | 24 | def compare_vertex_material_infos(self, expected, actual): 25 | self.assertEqual(expected.attributes, actual.attributes) 26 | compare_rgbas(self, expected.ambient, actual.ambient) 27 | compare_rgbas(self, expected.diffuse, actual.diffuse) 28 | compare_rgbas(self, expected.specular, actual.specular) 29 | compare_rgbas(self, expected.emissive, actual.emissive) 30 | self.assertAlmostEqual(expected.shininess, actual.shininess, 5) 31 | self.assertAlmostEqual(expected.opacity, actual.opacity, 5) 32 | self.assertAlmostEqual(expected.translucency, actual.translucency, 5) 33 | 34 | 35 | def get_vertex_material(vm_name='VM_NAME'): 36 | attrs = USE_DEPTH_CUE | ARGB_EMISSIVE_ONLY | COPY_SPECULAR_TO_DIFFUSE \ 37 | | DEPTH_CUE_TO_ALPHA | STAGE0_MAPPING_LINEAR_OFFSET | STAGE1_MAPPING_LINEAR_OFFSET 38 | return VertexMaterial( 39 | vm_name=vm_name, 40 | vm_info=get_vertex_material_info( 41 | attributes=attrs), 42 | vm_args_0='UPerSec=-2.0\r\nVPerSec=0.0\r\nUScale=1.0\r\nVScale=1.0', 43 | vm_args_1='UPerSec=-2.0\r\nVPerSec=0.0\r\nUScale=1.0\r\nVScale=1.0') 44 | 45 | 46 | def get_vertex_material_minimal(): 47 | return VertexMaterial( 48 | vm_name='a', 49 | vm_info=get_vertex_material_info(), 50 | vm_args_0='a', 51 | vm_args_1='a') 52 | 53 | 54 | def get_vertex_material_empty(): 55 | return VertexMaterial( 56 | vm_name='a', 57 | vm_info=None, 58 | vm_args_0='', 59 | vm_args_1='') 60 | 61 | 62 | def compare_vertex_materials(self, expected, actual): 63 | self.assertEqual(expected.vm_name.split('.')[0], actual.vm_name.split('.')[0]) 64 | self.assertEqual(expected.vm_args_0, actual.vm_args_0) 65 | self.assertEqual(expected.vm_args_1, actual.vm_args_1) 66 | if expected.vm_info is not None: 67 | compare_vertex_material_infos(self, expected.vm_info, actual.vm_info) 68 | else: 69 | self.assertIsNone(actual.vm_info) 70 | -------------------------------------------------------------------------------- /tests/w3d/helpers/version.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3d.structs.version import Version 5 | 6 | 7 | def get_version(major=5, minor=0): 8 | return Version(major=major, minor=minor) 9 | 10 | 11 | def compare_versions(self, expected, actual): 12 | self.assertEqual(expected.major, actual.major) 13 | self.assertEqual(expected.minor, actual.minor) 14 | -------------------------------------------------------------------------------- /tests/w3x/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3x/__init__.py -------------------------------------------------------------------------------- /tests/w3x/cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3x/cases/__init__.py -------------------------------------------------------------------------------- /tests/w3x/cases/structs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3x/cases/structs/__init__.py -------------------------------------------------------------------------------- /tests/w3x/cases/structs/mesh_structs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSAGE/OpenSAGE.BlenderPlugin/e9b652c6eda5fcfee86aeb71d9cdb9686412d56e/tests/w3x/cases/structs/mesh_structs/__init__.py -------------------------------------------------------------------------------- /tests/w3x/cases/structs/mesh_structs/test_bounding_box.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from tests.w3x.helpers.mesh_structs.bounding_box import * 5 | from tests.utils import TestCase 6 | 7 | 8 | class TestBoundingBox(TestCase): 9 | def test_write_read_xml(self): 10 | self.write_read_xml_test(get_box(), 'BoundingBox', BoundingBox.parse, compare_boxes) 11 | -------------------------------------------------------------------------------- /tests/w3x/cases/structs/mesh_structs/test_bounding_sphere.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from tests.w3x.helpers.mesh_structs.bounding_sphere import * 5 | from tests.utils import TestCase 6 | 7 | 8 | class TestBoundingSphere(TestCase): 9 | def test_write_read_xml(self): 10 | self.write_read_xml_test(get_sphere(), 'BoundingSphere', BoundingSphere.parse, compare_spheres) 11 | -------------------------------------------------------------------------------- /tests/w3x/cases/structs/test_include.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from tests.w3x.helpers.include import * 5 | from tests.utils import TestCase 6 | 7 | 8 | class TestInclude(TestCase): 9 | def test_write_read_xml(self): 10 | self.write_read_xml_test(get_include(), 'Include', Include.parse, compare_includes) 11 | -------------------------------------------------------------------------------- /tests/w3x/cases/test_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | import bpy 5 | from unittest.mock import patch 6 | 7 | from io_mesh_w3d.w3x.import_w3x import * 8 | from tests.common.helpers.hierarchy import get_hierarchy 9 | from tests.common.helpers.mesh import get_mesh 10 | from tests.common.helpers.hlod import get_hlod 11 | from tests.common.helpers.animation import get_animation 12 | from tests.utils import * 13 | 14 | 15 | class TestObjectImport(TestCase): 16 | def test_hlod_import_no_skeleton_file(self): 17 | hierarchy_name = 'TestHiera_SKL' 18 | hlod = get_hlod('testmodelname', hierarchy_name) 19 | 20 | # write to file 21 | write_struct(hlod, self.outpath() + 'testmodelname.w3x') 22 | 23 | # import 24 | self.set_format('W3X') 25 | self.filepath = self.outpath() + 'testmodelname.w3x' 26 | load(self) 27 | 28 | def test_animation_import_no_skeleton_file(self): 29 | hierarchy_name = 'TestHiera_SKL' 30 | animation = get_animation(hierarchy_name) 31 | 32 | # write to file 33 | write_struct(animation, self.outpath() + 'animation.w3x') 34 | 35 | # import 36 | self.set_format('W3X') 37 | self.filepath = self.outpath() + 'animation.w3x' 38 | load(self) 39 | 40 | def test_import_animation_only_no_include(self): 41 | hierarchy_name = 'TestHiera_SKL' 42 | hierarchy = get_hierarchy(hierarchy_name) 43 | animation = get_animation(hierarchy_name) 44 | 45 | # write to file 46 | write_struct(hierarchy, self.outpath() + hierarchy_name + '.w3x') 47 | write_struct(animation, self.outpath() + 'animation.w3x') 48 | 49 | # import 50 | self.set_format('W3X') 51 | self.filepath = self.outpath() + 'animation.w3x' 52 | load(self) 53 | 54 | self.assertTrue(hierarchy_name in bpy.data.objects) 55 | self.assertTrue(hierarchy_name in bpy.data.armatures) 56 | 57 | def test_load_file_file_does_not_exist(self): 58 | path = self.outpath() + 'output.w3x' 59 | self.filepath = path 60 | self.error = lambda text: self.assertEqual(r'file not found: ' + path, text) 61 | load(self) 62 | 63 | @patch('io_mesh_w3d.w3x.import_w3x.os.path.dirname', return_value='') 64 | @patch('io_mesh_w3d.w3x.import_w3x.find_root', return_value=None) 65 | def test_load_file_root_is_none(self, root, dirname): 66 | path = self.outpath() + 'output.w3x' 67 | 68 | file = open(path, 'w') 69 | file.write('lorem ipsum') 70 | file.close() 71 | 72 | self.error = lambda text: self.fail(r'no error should be thrown!') 73 | load_file(self, None, path) 74 | 75 | dirname.assert_not_called() 76 | 77 | def test_load_file_invalid_node(self): 78 | path = self.outpath() + 'output.w3x' 79 | data = ' ' 81 | 82 | file = open(path, 'w') 83 | file.write(data) 84 | file.close() 85 | 86 | self.filepath = path 87 | self.warning = lambda text: self.assertEqual('unsupported node Invalid in file: ' + path, text) 88 | load(self) 89 | 90 | @patch('io_mesh_w3d.w3x.import_w3x.create_data') 91 | @patch.object(Mesh, 'container_name', return_value='') 92 | def test_mesh_only_import(self, mesh_mock, create): 93 | mesh = get_mesh() 94 | 95 | # write to file 96 | write_struct(mesh, self.outpath() + 'mesh.w3x') 97 | 98 | # import 99 | self.set_format('W3X') 100 | self.filepath = self.outpath() + 'mesh.w3x' 101 | load(self) 102 | 103 | mesh_mock.assert_called() 104 | create.assert_called() 105 | -------------------------------------------------------------------------------- /tests/w3x/helpers/include.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from io_mesh_w3d.w3x.structs.include import * 5 | 6 | 7 | def get_include(): 8 | return Include( 9 | type='type', 10 | source='source') 11 | 12 | 13 | def compare_includes(self, expected, actual): 14 | self.assertEqual(expected.type, actual.type) 15 | self.assertEqual(expected.source, actual.source) 16 | -------------------------------------------------------------------------------- /tests/w3x/helpers/mesh_structs/bounding_box.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector 5 | from io_mesh_w3d.w3x.structs.mesh_structs.bounding_box import * 6 | 7 | 8 | def get_box(): 9 | return BoundingBox( 10 | min=Vector((1.0, -2.0, 3.0)), 11 | max=Vector((2.0, 4.0, 3.33))) 12 | 13 | 14 | def compare_boxes(self, expected, actual): 15 | self.assertEqual(expected.min, actual.min) 16 | self.assertEqual(expected.max, actual.max) 17 | -------------------------------------------------------------------------------- /tests/w3x/helpers/mesh_structs/bounding_sphere.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Stephan Vedder and Michael Schnabel 3 | 4 | from mathutils import Vector 5 | from io_mesh_w3d.w3x.structs.mesh_structs.bounding_sphere import * 6 | 7 | 8 | def get_sphere(): 9 | return BoundingSphere( 10 | radius=9.314, 11 | center=Vector((2.0, 4.0, 3.33))) 12 | 13 | 14 | def compare_spheres(self, expected, actual): 15 | self.assertEqual(expected.radius, actual.radius) 16 | self.assertEqual(expected.center, actual.center) 17 | --------------------------------------------------------------------------------