From c83f14f88b4618ab4fa1f20b4f9fd5603dca5e0d Mon Sep 17 00:00:00 2001 From: 989onan Date: Fri, 13 Dec 2024 17:26:34 -0500 Subject: [PATCH 1/4] move methods around this is separate to prevent confusion --- core/resonite_utils.py | 153 +++++++++++++++++++++++++++++++++++++++++ ui/quick_access.py | 2 +- ui/tools.py | 2 +- 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 core/resonite_utils.py diff --git a/core/resonite_utils.py b/core/resonite_utils.py new file mode 100644 index 0000000..741fb49 --- /dev/null +++ b/core/resonite_utils.py @@ -0,0 +1,153 @@ +import bpy + +from typing import List, Optional +from .common import get_armature +from bpy.types import Object, ShapeKey, Mesh, Context, Operator +from functools import lru_cache +from ..core.register import register_wrap +from ..functions.translations import t + + +@register_wrap +class AvatarToolKit_OT_ExportResonite(Operator): + bl_idname = 'avatar_toolkit.export_resonite' + bl_label = t("Importer.export_resonite.label") + bl_description = t("Importer.export_resonite.desc") + bl_options = {'REGISTER', 'UNDO'} + filepath: bpy.props.StringProperty() + + + @classmethod + def poll(cls, context: Context): + if get_armature(context) is None: + return False + return True + + def execute(self, context: Context): + #settings stolen from cats. + bpy.ops.export_scene.gltf('INVOKE_AREA', + export_image_format = 'WEBP', + export_image_quality = 75, + export_materials = 'EXPORT', + export_animations = True, + export_animation_mode = 'ACTIONS', + export_nla_strips_merged_animation_name = 'Animation', + export_nla_strips = True) + return {'FINISHED'} + + +import bpy +from ..core.register import register_wrap +from typing import List, Optional +import re +from bpy.types import Operator, Context, Object +from ..core.dictionaries import bone_names +from ..core.common import get_selected_armature, simplify_bonename, is_valid_armature +from ..functions.translations import t + +@register_wrap +class AvatarToolKit_OT_ConvertToResonite(Operator): + bl_idname = 'avatar_toolkit.convert_to_resonite' + bl_label = t('Tools.convert_to_resonite.label') + bl_description = t('Tools.convert_to_resonite.desc') + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + armature = get_selected_armature(context) + return armature is not None and is_valid_armature(armature) + + def execute(self, context: Context) -> set: + armature = get_selected_armature(context) + if not armature: + self.report({'WARNING'}, t("Tools.no_armature_selected")) + return {'CANCELLED'} + + translate_bone_fails = 0 + untranslated_bones = set() + + reverse_bone_lookup = dict() + for (preferred_name, name_list) in bone_names.items(): + for name in name_list: + reverse_bone_lookup[name] = preferred_name + + resonite_translations = { + 'hips': "Hips", + 'spine': "Spine", + 'chest': "Chest", + 'neck': "Neck", + 'head': "Head", + 'left_eye': "Eye.L", + 'right_eye': "Eye.R", + 'right_leg': "UpperLeg.R", + 'right_knee': "Calf.R", + 'right_ankle': "Foot.R", + 'right_toe': 'Toes.R', + 'right_shoulder': "Shoulder.R", + 'right_arm': "UpperArm.R", + 'right_elbow': "ForeArm.R", + 'right_wrist': "Hand.R", + 'left_leg': "UpperLeg.L", + 'left_knee': "Calf.L", + 'left_ankle': "Foot.L", + 'left_toe': "Toes.L", + 'left_shoulder': "Shoulder.L", + 'left_arm': "UpperArm.L", + 'left_elbow': "ForeArm.L", + 'left_wrist': "Hand.R", + + 'pinkie_1_l': "pinkie1.L", + 'pinkie_2_l': "pinkie2.L", + 'pinkie_3_l': "pinkie3.L", + 'ring_1_l': "ring1.L", + 'ring_2_l': "ring2.L", + 'ring_3_l': "ring3.L", + 'middle_1_l': "middle1.L", + 'middle_2_l': "middle2.L", + 'middle_3_l': "middle3.L", + 'index_1_l': "index1.L", + 'index_2_l': "index2.L", + 'index_3_l': "index3.L", + 'thumb_1_l': "thumb1.L", + 'thumb_2_l': "thumb2.L", + 'thumb_3_l': "thumb3.L", + + 'pinkie_1_r': "pinkie1.R", + 'pinkie_2_r': "pinkie2.R", + 'pinkie_3_r': "pinkie3.R", + 'ring_1_r': "ring1.R", + 'ring_2_r': "ring2.R", + 'ring_3_r': "ring3.R", + 'middle_1_r': "middle1.R", + 'middle_2_r': "middle2.R", + 'middle_3_r': "middle3.R", + 'index_1_r': "index1.R", + 'index_2_r': "index2.R", + 'index_3_r': "index3.R", + 'thumb_1_r': "thumb1.R", + 'thumb_2_r': "thumb2.R", + 'thumb_3_r': "thumb3.R" + } + + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + bpy.ops.object.mode_set(mode='OBJECT') + bone.name = re.compile(re.escape(""), re.IGNORECASE).sub("",bone.name) #remove "NOIK" from bones before translating again, in case an update was done that fixes a translation. + for bone in armature.data.bones: + if simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[simplify_bonename(bone.name)] in resonite_translations: + bone.name = resonite_translations[reverse_bone_lookup[simplify_bonename(bone.name)]] + else: + untranslated_bones.add(bone.name) + + bone.name = bone.name+"" + translate_bone_fails += 1 + + bpy.ops.object.mode_set(mode='OBJECT') + + if translate_bone_fails > 0: + self.report({'INFO'}, t("Tools.bones_translated_with_fails").format(translate_bone_fails=translate_bone_fails)) + else: + self.report({'INFO'}, t("Tools.bones_translated_success")) + + return {'FINISHED'} diff --git a/ui/quick_access.py b/ui/quick_access.py index 26a33ef..a200c6a 100644 --- a/ui/quick_access.py +++ b/ui/quick_access.py @@ -1,7 +1,7 @@ import bpy from ..core.register import register_wrap from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME -from ..core.export_resonite import AvatarToolKit_OT_ExportResonite +from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite from bpy.types import Context, Mesh, Panel, Operator from ..functions.translations import t diff --git a/ui/tools.py b/ui/tools.py index df218c7..ea9524c 100644 --- a/ui/tools.py +++ b/ui/tools.py @@ -3,7 +3,7 @@ from ..core.register import register_wrap from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from bpy.types import Context from ..functions.digitigrade_legs import AvatarToolKit_OT_CreateDigitigradeLegs -from ..functions.resonite_functions import AvatarToolKit_OT_ConvertToResonite +from ..core.resonite_utils import AvatarToolKit_OT_ConvertToResonite from ..functions.translations import t from ..core.common import get_selected_armature from ..functions.mesh_tools import AvatarToolkit_OT_RemoveUnusedShapekeys From 758cf0e7609afe697469361ec82baf6d0ce0374e Mon Sep 17 00:00:00 2001 From: 989onan Date: Sat, 14 Dec 2024 18:21:32 -0500 Subject: [PATCH 2/4] almost there with this one, LZMA and LZ4 don't work --- .vscode/settings.json | 1 + core/common.py | 65 ++- core/export_resonite.py | 36 -- core/importer.py | 1 + core/preferences.json | 3 +- core/resonite_loader/common.py | 56 +++ core/resonite_loader/resonite_animx.py | 472 ++++++++++++++++++ core/resonite_loader/resonite_types.py | 655 +++++++++++++++++++++++++ core/resonite_utils.py | 114 ++++- functions/resonite_functions.py | 115 ----- functions/uv_tools.py | 3 + 11 files changed, 1356 insertions(+), 165 deletions(-) delete mode 100644 core/export_resonite.py create mode 100644 core/resonite_loader/common.py create mode 100644 core/resonite_loader/resonite_animx.py create mode 100644 core/resonite_loader/resonite_types.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 2021d12..37a3b13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\scripts\\addons", "C:\\Users\\Onan\\AppData\\Roaming\\Blender Foundation\\Blender\\4.3\\extensions\\user_default\\",//C:/Users/Onan/AppData/Roaming/Blender Foundation/Blender/4.0/scripts/addons "D:\\blender stuff\\blendercodestuff\\4.3", + "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\python\\lib\\site-packages", "/Users/frankche/Documents/blendercoding/4.1/", "/Users/frankche/Library/Application Support/Blender/4.3/extensions/user_default/" ], diff --git a/core/common.py b/core/common.py index 30cd235..bc06b41 100644 --- a/core/common.py +++ b/core/common.py @@ -1,11 +1,14 @@ import bpy + import numpy as np -from .dictionaries import bone_names import threading import time import webbrowser import typing +import struct +from io import BytesIO +from .dictionaries import bone_names from ..core.register import register_wrap from typing import List, Optional, Tuple from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup @@ -497,3 +500,63 @@ def transfer_vertex_weights(context: Context, obj: bpy.types.Object, source_grou return True +#Binary tools + +import ctypes +def ReadCSharp_str(data: BytesIO) -> str: + return data.read(read7bitEncoded_int(data)).decode('utf-16-le') + +def WriteCSharp_str(data: BytesIO, string: str) -> str: + write7bitEncoded_int(len(string)*2) + return data.write(string.encode("utf-16-le")) + +def read7bitEncoded_ulong(data: BytesIO) -> np.int64: + num: ctypes.c_uint = ctypes.c_uint(0) + num2: int = 0 + flag: bool = True + + while (flag): + b: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(' 0) + num |= ((b & 127) << num2) + num2 += 7 + if not flag: + break + + return num + +def read7bitEncoded_int(data: BytesIO) -> ctypes.c_int: + num: ctypes.c_int = ctypes.c_int(0) + num2:ctypes.c_int = ctypes.c_int(0) + while (num2 != 35): + b: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(' None: + while integer > ctypes.c_ulong(0): + b: ctypes.c_ubyte = ctypes.c_ubyte(integer & ctypes.c_ulong(127)) + integer >>= 7 + if integer > ctypes.c_ulong(0): + b |= 128 + data.write(b) + if integer <= ctypes.c_ulong(0): + return + +def write7bitEncoded_int(data: BytesIO, value: ctypes.c_int) -> None: + num: ctypes.c_uint = ctypes.c_uint(value) + while(num >= ctypes.c_ubyte(128)): + data.write(ctypes.c_ubyte(num | ctypes.c_ubyte(128))) + num >>= 7 + data.Write(ctypes.c_ubyte(num)) + + +#encoding FrooxEngine/C# types in binary: + + + + + diff --git a/core/export_resonite.py b/core/export_resonite.py deleted file mode 100644 index c5a668f..0000000 --- a/core/export_resonite.py +++ /dev/null @@ -1,36 +0,0 @@ -import bpy - -from typing import List, Optional -from .common import get_armature -from bpy.types import Object, ShapeKey, Mesh, Context, Operator -from functools import lru_cache -from ..core.register import register_wrap -from ..functions.translations import t - - -@register_wrap -class AvatarToolKit_OT_ExportResonite(Operator): - bl_idname = 'avatar_toolkit.export_resonite' - bl_label = t("Importer.export_resonite.label") - bl_description = t("Importer.export_resonite.desc") - bl_options = {'REGISTER', 'UNDO'} - filepath: bpy.props.StringProperty() - - - @classmethod - def poll(cls, context: Context): - if get_armature(context) is None: - return False - return True - - def execute(self, context: Context): - #settings stolen from cats. - bpy.ops.export_scene.gltf('INVOKE_AREA', - export_image_format = 'WEBP', - export_image_quality = 75, - export_materials = 'EXPORT', - export_animations = True, - export_animation_mode = 'ACTIONS', - export_nla_strips_merged_animation_name = 'Animation', - export_nla_strips = True) - return {'FINISHED'} \ No newline at end of file diff --git a/core/importer.py b/core/importer.py index 976c7dd..76f9b59 100644 --- a/core/importer.py +++ b/core/importer.py @@ -43,6 +43,7 @@ import_types: dict[str, typing.Callable[[str, list[dict[str,str]], str], None]] "vrm": (lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath)), "pmx": (lambda directory, files, filepath : import_pmx(filepath)), "pmd": (lambda directory, files, filepath : import_pmd(filepath)), + "animx": (lambda directory, files, filepath : bpy.ops.avatar_toolkit.animx_importer(directory=directory,files=files,filepath=filepath)), } def concat_imports_filter(imports): diff --git a/core/preferences.json b/core/preferences.json index b0ca7bf..4fd9b5f 100644 --- a/core/preferences.json +++ b/core/preferences.json @@ -1,3 +1,4 @@ { - "language": 0 + "language": 0, + "last_update_check": 1734208835.8049936 } \ No newline at end of file diff --git a/core/resonite_loader/common.py b/core/resonite_loader/common.py new file mode 100644 index 0000000..5c5a943 --- /dev/null +++ b/core/resonite_loader/common.py @@ -0,0 +1,56 @@ +import ctypes +import typing +import struct +from io import BytesIO + +def ReadCSharp_str(data: BytesIO) -> str: + charamount = read7bitEncoded_int(data) + print(charamount) + return data.read(charamount).decode('utf-8', errors="replace") + +def WriteCSharp_str(data: BytesIO, string: str) -> str: + write7bitEncoded_int(len(string)) + return data.write(string.encode("utf-8", errors="replace")) + +def read7bitEncoded_ulong(data: BytesIO) -> int: + num: int = int(0) + num2: int = 0 + flag: bool = True + + while (flag): + b: int = int(struct.unpack(' 0) + num |= ((b & 127) << num2) + num2 += 7 + if not flag: + break + + return num + +def read7bitEncoded_int(data: BytesIO) -> int: + num: int= int(0) + num2:int = int(0) + while (num2 != 35): + b: int = int(struct.unpack(' None: + while integer > int(0): + b: int = ctypes.c_ubyte(integer & int(127)) + integer >>= 7 + if integer > int(0): + b |= 128 + data.write(b) + if integer <= int(0): + return + +def write7bitEncoded_int(data: BytesIO, value: int) -> None: + num: int = int(value) + while(num >= int(128)): + data.write(int(num | int(128))) + num >>= 7 + data.Write(int(num)) \ No newline at end of file diff --git a/core/resonite_loader/resonite_animx.py b/core/resonite_loader/resonite_animx.py new file mode 100644 index 0000000..4c84e41 --- /dev/null +++ b/core/resonite_loader/resonite_animx.py @@ -0,0 +1,472 @@ +from __future__ import annotations + +import lz4.block +from . import resonite_types +from . import common + +import ctypes +import typing +import struct +from io import BytesIO + +KeyframeInterpolation: dict[str, int] = { + "Hold": 1, + "Linear": 2, + "Tangent": 3, + "CubicBezier": 4 +} + +class KeyFrame(): + time: resonite_types.float = 0 + interpolation: resonite_types.int = 0 + value: resonite_types.ResoType + left_tan: resonite_types.ResoType + right_tan: resonite_types.ResoType + + + def __getattr__(self, name: str): + if name == "interpolation": + interp: int = 0 + if (self.__dict__["left_tan"] != None and self.__dict__["right_tan"] != None): + interp = 3 + + + return resonite_types.int(interp) + return self.__dict__[name] + def __setattr__(self, name, value): + self.__dict__[name] = value + + + + + def __init__(self): + pass + + + def RequiresTangents(self) -> bool: + if KeyframeInterpolation[self.interpolation.x] == "Tangent" or KeyframeInterpolation[self.interpolation.x] == "CubicBezier": + return True + return False + +class ResoTrack(resonite_types.ResoType): + _node: resonite_types.string + _property: resonite_types.string + Owner: AnimX + FrameType: type[resonite_types.ResoType] + keyframes: list[KeyFrame] = [] + + def __init__(self,FrameType): + self.FrameType = FrameType + + def write(self, data: BytesIO): + self._node.write(data) + self._property.write(data) + common.write7bitEncoded_ulong(data, len(self.keyframes)) + + def read(self, data:BytesIO): + self._node.read(data) + self._property.read(data) + track_amount: int = int(common.read7bitEncoded_ulong(data)) + for i in range(0, track_amount): + self.keyframes.append(KeyFrame()) + + def removeKeyframe(self, time: float | int) -> bool: + """Takes a time and removes one with the same time""" + if (time < 0): + raise IndexError("Keyframe time cannot be lower than 0. Value: " + str(time)) + if(type(time) == float): + num: int = -1 + for num2 in range(0,len(self.keyframes)): + if (self.keyframes[num2].time.x == float(time)): + num = num2 + + if num == -1: + return False + + self.keyframes.remove(self.keyframes[num]) + return True + else: + if (int(time) >= len(self.keyframes)): + raise IndexError("Keyframe time cannot be bigger than the amount of keyframes. Value: " + str(time)) + self.keyframes.remove(self.keyframes[int(time)]) + + + + + def replaceKeyframe(self, keyframe: KeyFrame) -> bool: + """Takes a keyframe and replaces one with the same time""" + if (keyframe.time.x < 0): + raise IndexError("Keyframe time cannot be lower than 0. Value: " + str(keyframe.time.x)) + + + num: int = 0 + + if (keyframe.time.x == self.keyframes[self.GetKeyframeIndex(keyframe.time.x)].time.x): + num = len(self.keyframes) + else: + return False + + self.keyframes[num] = keyframe + return True + + def addKeyframe(self, keyframe: KeyFrame) -> int: + if (keyframe.time.x < 0): + raise IndexError("Keyframe time cannot be lower than 0. Value: " + str(keyframe.time.x)) + num: int + if (len(self.keyframes) == 0): + num = 0 + elif (keyframe.time.x >= self.keyframes[-1].time.x): + num = len(self.keyframes) + else: + num = self.GetKeyframeIndex(keyframe.time.x) + 1 + + self.keyframes.insert(num, keyframe) + + return num + + def GetKeyframeIndex(self, time:float | int)-> int: + if(type(time) == float): + if (len(self.keyframes) > 0): + num: int = 0 + if (self.keyframes[-1].time < float(time)): + num = len(self.keyframes) + + while (num < len(self.keyframes) and self.keyframes[num].time < time): + num += 1 + + return num - 1 + + return -1 + else: + return int(time) + + +class RawTrack(ResoTrack): + interval: resonite_types.float = 0 + + def __getattr__(self, name: str): + if name == "interval": + return self.Owner.interval.x + return self.__dict__[name] + + def __init__(self, FrameType): + super().__init__(FrameType) + + def write(self, data: BytesIO): + super().write(data) + self.interval.write(data) + for key in self.keyframes: + key.value.write(data) + + + def read(self, data:BytesIO): + super().read(data) + self.interval.read(data) + for key in self.keyframes: + key.value.read(data) + + def addKeyframe(self, keyframe: KeyFrame) -> int: + num: int = super().addKeyframe(keyframe) + for i in range(0,len(self.keyframes)): + self.keyframes[i].time = i + return num + def removeKeyframe(self, time: float | int) -> bool: + success: bool = super().removeKeyframe(int(time)) + for i in range(0,len(self.keyframes)): + self.keyframes[i].time = i + return success + + + + +class DiscreteTrack(ResoTrack): + + def __init__(self, FrameType): + super().__init__(FrameType) + + def write(self, data: BytesIO): + super().write(data) + + + def read(self, data:BytesIO): + super().read(data) + + def addKeyframe(self, keyframe: KeyFrame) -> int: + num: int = super().addKeyframe(keyframe) + return num + def removeKeyframe(self, time: float | int) -> bool: + success: bool = super().removeKeyframe(time) + return success + + + + +class CurveTrack(ResoTrack): + interpolations: bool = False + tangents: bool = False + sharedinterpolation: resonite_types.int = -1 + + def __getattr__(self, name: str): + if name == "interpolations": + for key in self.keyframes: + if key.interpolation.x != self.sharedinterpolation.x: + return True + elif name == "tangents": + for key in self.keyframes: + if key.interpolation.x == 3 or key.interpolation.x == 4: + return True + return self.__dict__[name] + + def __init__(self, FrameType): + super().__init__(FrameType) + + def write(self, data: BytesIO): + super().write(data) + resonite_types.byte((1 if self.interpolations else 0) | (2 if self.tangents else 0)).write(data) + + + if(self.interpolations): + for key in self.keyframes: + key.interpolation.write(data) + else: + self.sharedinterpolation.write(data) + + for key in self.keyframes: + key.value.write(data) + key.time.write(data) + + if(self.tangents): + for key in self.keyframes: + key.left_tan.write(data) + key.right_tan.write(data) + + def read(self, data:BytesIO): + super().read(data) + flags: int = struct.unpack(" bool: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + + def replaceKeyframe(self, keyframe: KeyFrame) -> bool: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + def addKeyframe(self, keyframe: KeyFrame) -> int: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + + def GetKeyframeIndex(self, time:float)-> int: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") +#This is weird, but thank you python - @989onan +TrackTypes: list[type[ResoTrack]] = [ + RawTrack, + DiscreteTrack, + CurveTrack, + BezierTrack +] + +#TODO: add all types here +#wooooo - @989onan +elementTypes: list[type[resonite_types.ResoType]] = [ + resonite_types.bool, + resonite_types.bool2, + resonite_types.bool3, + resonite_types.bool4, + resonite_types.byte, + resonite_types.ushort, + resonite_types.uint, + resonite_types.ulong, + resonite_types.sbyte, + resonite_types.short, + resonite_types.int, + resonite_types.long, + resonite_types.int2, + resonite_types.int3, + resonite_types.int4, + resonite_types.uint2, + resonite_types.uint3, + resonite_types.uint4, + resonite_types.long2, + resonite_types.long3, + resonite_types.long4, + resonite_types.float, + resonite_types.float2, + resonite_types.float3, + resonite_types.float4, + resonite_types.floatQ, + resonite_types.float2x2, + resonite_types.float3x3, + resonite_types.float4x4, + resonite_types.double, + resonite_types.double2, + resonite_types.double3, + resonite_types.double4, + resonite_types.doubleQ, + resonite_types.double2x2, + resonite_types.double3x3, + resonite_types.double4x4, + resonite_types.color, + resonite_types.color32, + resonite_types.string + ] + + +most_recent_AnimX_vers: int = 1 + +class AnimX(): + + + """ + To use Raw Track properly, please set interval (seconds between frames) after reading/creating.\n + Represents data to be written to or read from an AnimX file. + """ + + file_version: resonite_types.int = resonite_types.int() + track_amount: resonite_types.int = resonite_types.int() + global_duration: resonite_types.float = resonite_types.float() + name: resonite_types.string = resonite_types.string() + + tracks: list[ResoTrack] = [] + + interval: resonite_types.float = resonite_types.float(1/25) #default value + + def __init__(self): + pass + + + + def read(self, file: str) -> bool: + """ + Takes an absolute file path and reads a binary animx file with it, and populates this class object with the data. + """ + with open(file, 'rb') as filecontents: + data: BytesIO = BytesIO(filecontents.read()) + magic_word = common.ReadCSharp_str(data) + if magic_word != 'AnimX': + print("AnimX != "+magic_word) + return False + self.file_version.read(data) + if self.file_version.x > 1: + raise Exception("AnimX version is higher than the supported one") + + self.track_amount.x = common.read7bitEncoded_ulong(data) + self.global_duration.read(data) + print(self.track_amount.x) + + self.name.read(data) + + + match (struct.unpack('> 1 + else: + trackType2 = int(struct.unpack(' bool: + """ + Takes an absolute file path and writes a binary animx file into it's contents, replacing them using this class's data. + """ + with open(file, 'rb') as filecontents: + data: BytesIO = BytesIO(filecontents) + common.WriteCSharp_str(data, 'AnimX') + self.file_version.x = most_recent_AnimX_vers #we wanna write an up to date file version type. + self.file_version.write(data) + + self.track_amount.x = len(self.tracks) + common.write7bitEncoded_ulong(self.track_amount.x) + self.global_duration.write(data) + + + self.name.write(data) + + data.write(struct.pack(' ResoTrack: + TrackType: type[ResoTrack] = TrackTypes[trackType2] + Track: ResoTrack = TrackType[elementTypes[value_type]](elementTypes[value_type]) + Track.read(data) + return Track + diff --git a/core/resonite_loader/resonite_types.py b/core/resonite_loader/resonite_types.py new file mode 100644 index 0000000..a5c3abe --- /dev/null +++ b/core/resonite_loader/resonite_types.py @@ -0,0 +1,655 @@ +import ctypes +import typing +from io import BytesIO +import struct +from . import common + +class ResoType(): + + + + + def __init__(self): + pass + + def write(self, data: BytesIO): + pass + + def read(cls, data: BytesIO): + pass + +#These below are collection of the basic resonite typing made from C#. This is in order to store data in a sane way and decode/encode it. + +class color(ResoType): + r: float = 0 + g: float = 0 + b: float = 0 + a: float = 0 + + + def __init__(self): + pass + + def write(self, data: BytesIO): + data.write(struct.pack(" str: + return self.x + + def __init__(self,value=""): + self.x = value + + def write(self, data: BytesIO): + common.WriteCSharp_str(data,self.x) + + def read(self,data): + + self.x = common.ReadCSharp_str(data) + + +class byte(ResoType): + x: int = 0 + + def __int__(self): + return self.x + + def __init__(self): + pass + + def write(self, data: BytesIO): + data.write(struct.pack(" bool: + return self.x + def __init__(self,value=False): + self.x = value + + def write(self, data: BytesIO): + data.write(struct.pack("?", self.x)) + + def read(self,data): + self.x = struct.unpack("?", data.read(1))[0] + +class bool2(bool): + y: bool = False + + def __init__(self): + pass + + def read(self,data: BytesIO): + byte: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(" 0 + self.y = (byte & 2) > 0 + + def createflags(self) -> ctypes.c_byte: + flags: ctypes.c_ubyte = ctypes.c_ubyte(0) + flags |= (1 if self.x else 0) + flags |= (2 if self.y else 0) + return flags + + def write(self, data: BytesIO): + data.write(struct.pack(" 0 + self.y = (byte & 2) > 0 + self.z = (byte & 4) > 0 + + def createflags(self) -> ctypes.c_byte: + flags: ctypes.c_ubyte = ctypes.c_ubyte(0) + flags |= (1 if self.x else 0) + flags |= (2 if self.y else 0) + flags |= (3 if self.z else 0) + return flags + + def write(self, data: BytesIO): + data.write(struct.pack(" 0 + self.y = (byte & 2) > 0 + self.z = (byte & 4) > 0 + self.w = (byte & 8) > 0 + + def createflags(self) -> ctypes.c_ubyte: + flags: ctypes.c_ubyte = ctypes.c_ubyte(0) + flags |= (1 if self.x else 0) + flags |= (2 if self.y else 0) + flags |= (4 if self.z else 0) + flags |= (8 if self.w else 0) + return flags + + def write(self, data: BytesIO): + data.write(struct.pack(" bool: + return True + + def execute(self, context: Context) -> set: + + Froox_animations: list[resonite_animx.AnimX] = [] + + #decoding using self contained library: + files = [file.name for file in self.files] + files.append(self.filepath) + for file in files: + froox_animation: resonite_animx.AnimX = resonite_animx.AnimX() + froox_animation.interval.x = 1/25 + froox_animation.read(file = os.path.join(self.directory,file)) + Froox_animations.append(froox_animation) + + #Load data into Blender Animations. + for froox_animation in Froox_animations: + action: bpy.types.Action = bpy.data.actions.new(froox_animation.name.x) + action.use_fake_user = True + for track in froox_animation.tracks: + print("hit here1") + print(track.FrameType) + if(track.FrameType != type(resonite_types.float) and track.FrameType != type(resonite_types.double)): + continue + + data_path: str = track._node.x+"."+track._property.x + fcurve_reso = action.fcurves.new(data_path,None,track._node.x) + + print("hit here2") + match(type(track)): + case (type(resonite_animx.RawTrack)): + rawtrack: resonite_animx.RawTrack = track + + + + for frame in rawtrack.keyframes: + key: bpy.types.Keyframe = fcurve_reso.keyframe_points.insert(frame.time, float(frame.value)) + + + case (type(resonite_animx.DiscreteTrack)): + discretetrack: resonite_animx.RawTrack = track + for frame in rawtrack.keyframes: + key: bpy.types.Keyframe = fcurve_reso.keyframe_points.insert(frame.time, float(frame.value)) + + + case(type(resonite_animx.CurveTrack)): + curvetrack: resonite_animx.RawTrack = track + for frame in curvetrack.keyframes: + key: bpy.types.Keyframe = fcurve_reso.keyframe_points.insert(frame.time, float(frame.value)) + key.handle_left = float(frame.left_tan) + key.handle_left = float(frame.right_tan) + + + case(type(resonite_animx.BezierTrack)): + beziertrack: resonite_animx.RawTrack = track + # Bezier is not supported rn, ignore. + + + + + + + + return {'FINISHED'} + + + + + + + + diff --git a/functions/resonite_functions.py b/functions/resonite_functions.py index 1f32e2e..e69de29 100644 --- a/functions/resonite_functions.py +++ b/functions/resonite_functions.py @@ -1,115 +0,0 @@ -import bpy -from ..core.register import register_wrap -from typing import List, Optional -import re -from bpy.types import Operator, Context, Object -from ..core.dictionaries import bone_names -from ..core.common import get_selected_armature, simplify_bonename, is_valid_armature -from ..functions.translations import t - -@register_wrap -class AvatarToolKit_OT_ConvertToResonite(Operator): - bl_idname = 'avatar_toolkit.convert_to_resonite' - bl_label = t('Tools.convert_to_resonite.label') - bl_description = t('Tools.convert_to_resonite.desc') - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context: Context) -> bool: - armature = get_selected_armature(context) - return armature is not None and is_valid_armature(armature) - - def execute(self, context: Context) -> set: - armature = get_selected_armature(context) - if not armature: - self.report({'WARNING'}, t("Tools.no_armature_selected")) - return {'CANCELLED'} - - translate_bone_fails = 0 - untranslated_bones = set() - - reverse_bone_lookup = dict() - for (preferred_name, name_list) in bone_names.items(): - for name in name_list: - reverse_bone_lookup[name] = preferred_name - - resonite_translations = { - 'hips': "Hips", - 'spine': "Spine", - 'chest': "Chest", - 'neck': "Neck", - 'head': "Head", - 'left_eye': "Eye.L", - 'right_eye': "Eye.R", - 'right_leg': "UpperLeg.R", - 'right_knee': "Calf.R", - 'right_ankle': "Foot.R", - 'right_toe': 'Toes.R', - 'right_shoulder': "Shoulder.R", - 'right_arm': "UpperArm.R", - 'right_elbow': "ForeArm.R", - 'right_wrist': "Hand.R", - 'left_leg': "UpperLeg.L", - 'left_knee': "Calf.L", - 'left_ankle': "Foot.L", - 'left_toe': "Toes.L", - 'left_shoulder': "Shoulder.L", - 'left_arm': "UpperArm.L", - 'left_elbow': "ForeArm.L", - 'left_wrist': "Hand.R", - - 'pinkie_1_l': "pinkie1.L", - 'pinkie_2_l': "pinkie2.L", - 'pinkie_3_l': "pinkie3.L", - 'ring_1_l': "ring1.L", - 'ring_2_l': "ring2.L", - 'ring_3_l': "ring3.L", - 'middle_1_l': "middle1.L", - 'middle_2_l': "middle2.L", - 'middle_3_l': "middle3.L", - 'index_1_l': "index1.L", - 'index_2_l': "index2.L", - 'index_3_l': "index3.L", - 'thumb_1_l': "thumb1.L", - 'thumb_2_l': "thumb2.L", - 'thumb_3_l': "thumb3.L", - - 'pinkie_1_r': "pinkie1.R", - 'pinkie_2_r': "pinkie2.R", - 'pinkie_3_r': "pinkie3.R", - 'ring_1_r': "ring1.R", - 'ring_2_r': "ring2.R", - 'ring_3_r': "ring3.R", - 'middle_1_r': "middle1.R", - 'middle_2_r': "middle2.R", - 'middle_3_r': "middle3.R", - 'index_1_r': "index1.R", - 'index_2_r': "index2.R", - 'index_3_r': "index3.R", - 'thumb_1_r': "thumb1.R", - 'thumb_2_r': "thumb2.R", - 'thumb_3_r': "thumb3.R" - } - - context.view_layer.objects.active = armature - bpy.ops.object.mode_set(mode='EDIT') - - bpy.ops.object.mode_set(mode='OBJECT') - bone.name = re.compile(re.escape(""), re.IGNORECASE).sub("",bone.name) #remove "NOIK" from bones before translating again, in case an update was done that fixes a translation. - for bone in armature.data.bones: - if simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[simplify_bonename(bone.name)] in resonite_translations: - bone.name = resonite_translations[reverse_bone_lookup[simplify_bonename(bone.name)]] - else: - untranslated_bones.add(bone.name) - - bone.name = bone.name+"" - translate_bone_fails += 1 - - bpy.ops.object.mode_set(mode='OBJECT') - - if translate_bone_fails > 0: - self.report({'INFO'}, t("Tools.bones_translated_with_fails").format(translate_bone_fails=translate_bone_fails)) - else: - self.report({'INFO'}, t("Tools.bones_translated_success")) - - return {'FINISHED'} diff --git a/functions/uv_tools.py b/functions/uv_tools.py index 504fc84..66e6aae 100644 --- a/functions/uv_tools.py +++ b/functions/uv_tools.py @@ -287,6 +287,9 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator): uv_lay.uv[corner].vector = lerped_point #put the vertcies at the point we calculated. except: print("This is probably fine? - @989onan") #TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan + #Note: The above print statement may require reading what this entire operator does before understanding if the above print statment means anything + #so if you value your time, just do a bunch of tests until the above print stament is ran or something + #if in doubt and you cannot get it to run, just spend some time trying to understand the operator - @989onan print("Finished mesh \""+source+"\" for UV's") From ff4ae71832a6a19361860ed1b2c306373ec63a17 Mon Sep 17 00:00:00 2001 From: 989onan Date: Sat, 14 Dec 2024 22:23:37 -0500 Subject: [PATCH 3/4] almost working, getting some off by one byte errors --- core/resonite_loader/common.py | 5 +- core/resonite_loader/resonite_animx.py | 126 +++++++++++++++++-------- core/resonite_loader/resonite_types.py | 19 ++-- 3 files changed, 101 insertions(+), 49 deletions(-) diff --git a/core/resonite_loader/common.py b/core/resonite_loader/common.py index 5c5a943..f42e2e8 100644 --- a/core/resonite_loader/common.py +++ b/core/resonite_loader/common.py @@ -5,8 +5,9 @@ from io import BytesIO def ReadCSharp_str(data: BytesIO) -> str: charamount = read7bitEncoded_int(data) - print(charamount) - return data.read(charamount).decode('utf-8', errors="replace") + string: str = data.read(charamount).decode('utf-8', errors="replace") + print("read string: "+string) + return string def WriteCSharp_str(data: BytesIO, string: str) -> str: write7bitEncoded_int(len(string)) diff --git a/core/resonite_loader/resonite_animx.py b/core/resonite_loader/resonite_animx.py index 4c84e41..3bb8bb1 100644 --- a/core/resonite_loader/resonite_animx.py +++ b/core/resonite_loader/resonite_animx.py @@ -1,10 +1,11 @@ from __future__ import annotations +from os import replace +from types import FrameType import lz4.block from . import resonite_types from . import common -import ctypes import typing import struct from io import BytesIO @@ -17,8 +18,8 @@ KeyframeInterpolation: dict[str, int] = { } class KeyFrame(): - time: resonite_types.float = 0 - interpolation: resonite_types.int = 0 + time: resonite_types.float = resonite_types.float(0) + interpolation: resonite_types.int = resonite_types.int(0) value: resonite_types.ResoType left_tan: resonite_types.ResoType right_tan: resonite_types.ResoType @@ -27,14 +28,12 @@ class KeyFrame(): def __getattr__(self, name: str): if name == "interpolation": interp: int = 0 - if (self.__dict__["left_tan"] != None and self.__dict__["right_tan"] != None): + if (self["left_tan"] != None and self["right_tan"] != None): interp = 3 return resonite_types.int(interp) - return self.__dict__[name] - def __setattr__(self, name, value): - self.__dict__[name] = value + return super().__getattribute__(name) @@ -49,8 +48,8 @@ class KeyFrame(): return False class ResoTrack(resonite_types.ResoType): - _node: resonite_types.string - _property: resonite_types.string + node: resonite_types.string = resonite_types.string("") + property: resonite_types.string = resonite_types.string("") Owner: AnimX FrameType: type[resonite_types.ResoType] keyframes: list[KeyFrame] = [] @@ -59,16 +58,18 @@ class ResoTrack(resonite_types.ResoType): self.FrameType = FrameType def write(self, data: BytesIO): - self._node.write(data) - self._property.write(data) + self.node.write(data) + self.property.write(data) common.write7bitEncoded_ulong(data, len(self.keyframes)) def read(self, data:BytesIO): - self._node.read(data) - self._property.read(data) + self.node.read(data) + self.property.read(data) track_amount: int = int(common.read7bitEncoded_ulong(data)) for i in range(0, track_amount): - self.keyframes.append(KeyFrame()) + key: KeyFrame = KeyFrame() + key.value = self.FrameType() + self.keyframes.append(key) def removeKeyframe(self, time: float | int) -> bool: """Takes a time and removes one with the same time""" @@ -142,12 +143,12 @@ class ResoTrack(resonite_types.ResoType): class RawTrack(ResoTrack): - interval: resonite_types.float = 0 + interval: resonite_types.float = resonite_types.float(0) def __getattr__(self, name: str): if name == "interval": return self.Owner.interval.x - return self.__dict__[name] + return super().__getattribute__(name) def __init__(self, FrameType): super().__init__(FrameType) @@ -204,7 +205,7 @@ class DiscreteTrack(ResoTrack): class CurveTrack(ResoTrack): interpolations: bool = False tangents: bool = False - sharedinterpolation: resonite_types.int = -1 + sharedinterpolation: resonite_types.int = resonite_types.int(-1) def __getattr__(self, name: str): if name == "interpolations": @@ -215,7 +216,7 @@ class CurveTrack(ResoTrack): for key in self.keyframes: if key.interpolation.x == 3 or key.interpolation.x == 4: return True - return self.__dict__[name] + return super().__getattribute__(name) def __init__(self, FrameType): super().__init__(FrameType) @@ -232,6 +233,8 @@ class CurveTrack(ResoTrack): self.sharedinterpolation.write(data) for key in self.keyframes: + if key.value == None: + key.value = self.FrameType() key.value.write(data) key.time.write(data) @@ -242,9 +245,12 @@ class CurveTrack(ResoTrack): def read(self, data:BytesIO): super().read(data) - flags: int = struct.unpack(" 0 + self.tangents = (flags & 2) > 0 + + print(str(self.interpolations)) + print(str(self.tangents)) if(self.interpolations): for key in self.keyframes: @@ -253,11 +259,18 @@ class CurveTrack(ResoTrack): self.sharedinterpolation.read(data) for key in self.keyframes: + print("key read!") + if key.value == None: + key.value = self.FrameType() key.value.read(data) key.time.read(data) if(self.tangents): for key in self.keyframes: + if key.left_tan == None: + key.left_tan = self.FrameType() + if key.right_tan == None: + key.right_tan = self.FrameType() key.left_tan.read(data) key.right_tan.read(data) @@ -300,7 +313,7 @@ class BezierTrack(ResoTrack): """PLACE HOLDER METHOD, DO NOT USE""" raise Exception("BezierTrack track type is unsupported in resonite's code") #This is weird, but thank you python - @989onan -TrackTypes: list[type[ResoTrack]] = [ +TrackTypes: list[type] = [ RawTrack, DiscreteTrack, CurveTrack, @@ -354,7 +367,7 @@ elementTypes: list[type[resonite_types.ResoType]] = [ most_recent_AnimX_vers: int = 1 - +import lzma#HALLLOOOYAH HALLOYAH!! - @989onan class AnimX(): @@ -375,7 +388,25 @@ class AnimX(): def __init__(self): pass - + @classmethod + def decompress_lzma(cls,data, format, filters) -> list: + results = [] + while True: + decomp = lzma.LZMADecompressor(format, None, filters) + try: + res = decomp.decompress(data) + except lzma.LZMAError: + if results: + break # Leftover data is not a valid LZMA/XZ stream; ignore it. + else: + raise # Error on the first iteration; bail out. + results.append(res) + data = decomp.unused_data + if not data: + break + if not decomp.eof: + raise lzma.LZMAError("Compressed data ended before the end-of-stream marker was reached") + return b"".join(results) def read(self, file: str) -> bool: """ @@ -393,10 +424,11 @@ class AnimX(): self.track_amount.x = common.read7bitEncoded_ulong(data) self.global_duration.read(data) - print(self.track_amount.x) + print("track amont: "+str(self.track_amount.x)) + print("file vers: "+str(self.file_version.x)) self.name.read(data) - + print("name: "+self.name.x) match (struct.unpack(' ResoTrack: - TrackType: type[ResoTrack] = TrackTypes[trackType2] - Track: ResoTrack = TrackType[elementTypes[value_type]](elementTypes[value_type]) + def GetTrackType(cls, trackType2: int, value_type: type[resonite_types.ResoType], data: BytesIO) -> ResoTrack: + Track = TrackTypes[trackType2](value_type) + print(type(Track)) + print(value_type) Track.read(data) return Track diff --git a/core/resonite_loader/resonite_types.py b/core/resonite_loader/resonite_types.py index a5c3abe..e1052a7 100644 --- a/core/resonite_loader/resonite_types.py +++ b/core/resonite_loader/resonite_types.py @@ -1,4 +1,3 @@ -import ctypes import typing from io import BytesIO import struct @@ -152,12 +151,12 @@ class bool2(bool): pass def read(self,data: BytesIO): - byte: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(" 0 self.y = (byte & 2) > 0 - def createflags(self) -> ctypes.c_byte: - flags: ctypes.c_ubyte = ctypes.c_ubyte(0) + def createflags(self) -> int: + flags: int = int(0) flags |= (1 if self.x else 0) flags |= (2 if self.y else 0) return flags @@ -173,13 +172,13 @@ class bool3(bool2): pass def read(self,data): - byte: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(" 0 self.y = (byte & 2) > 0 self.z = (byte & 4) > 0 - def createflags(self) -> ctypes.c_byte: - flags: ctypes.c_ubyte = ctypes.c_ubyte(0) + def createflags(self) -> int: + flags: int = int(0) flags |= (1 if self.x else 0) flags |= (2 if self.y else 0) flags |= (3 if self.z else 0) @@ -196,14 +195,14 @@ class bool4(bool3): pass def read(self,data: BytesIO): - byte: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(" 0 self.y = (byte & 2) > 0 self.z = (byte & 4) > 0 self.w = (byte & 8) > 0 - def createflags(self) -> ctypes.c_ubyte: - flags: ctypes.c_ubyte = ctypes.c_ubyte(0) + def createflags(self) -> int: + flags: int = int(0) flags |= (1 if self.x else 0) flags |= (2 if self.y else 0) flags |= (4 if self.z else 0) From 2337449a77f66b26ac09e769fac6a25ced1f33b3 Mon Sep 17 00:00:00 2001 From: 989onan Date: Sun, 15 Dec 2024 16:09:18 -0500 Subject: [PATCH 4/4] AnimX importer done Finished the importer for now, it may contain errors but those would be part of the library --- core/common.py | 49 ----- core/preferences.json | 2 +- core/resonite_loader/common.py | 19 +- core/resonite_loader/resonite_animx.py | 264 ++++++++++++++----------- core/resonite_loader/resonite_types.py | 37 ++-- core/resonite_utils.py | 128 ++++++++---- 6 files changed, 287 insertions(+), 212 deletions(-) diff --git a/core/common.py b/core/common.py index bc06b41..a1f9e7b 100644 --- a/core/common.py +++ b/core/common.py @@ -502,56 +502,7 @@ def transfer_vertex_weights(context: Context, obj: bpy.types.Object, source_grou #Binary tools -import ctypes -def ReadCSharp_str(data: BytesIO) -> str: - return data.read(read7bitEncoded_int(data)).decode('utf-16-le') -def WriteCSharp_str(data: BytesIO, string: str) -> str: - write7bitEncoded_int(len(string)*2) - return data.write(string.encode("utf-16-le")) - -def read7bitEncoded_ulong(data: BytesIO) -> np.int64: - num: ctypes.c_uint = ctypes.c_uint(0) - num2: int = 0 - flag: bool = True - - while (flag): - b: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(' 0) - num |= ((b & 127) << num2) - num2 += 7 - if not flag: - break - - return num - -def read7bitEncoded_int(data: BytesIO) -> ctypes.c_int: - num: ctypes.c_int = ctypes.c_int(0) - num2:ctypes.c_int = ctypes.c_int(0) - while (num2 != 35): - b: ctypes.c_ubyte = ctypes.c_ubyte(struct.unpack(' None: - while integer > ctypes.c_ulong(0): - b: ctypes.c_ubyte = ctypes.c_ubyte(integer & ctypes.c_ulong(127)) - integer >>= 7 - if integer > ctypes.c_ulong(0): - b |= 128 - data.write(b) - if integer <= ctypes.c_ulong(0): - return - -def write7bitEncoded_int(data: BytesIO, value: ctypes.c_int) -> None: - num: ctypes.c_uint = ctypes.c_uint(value) - while(num >= ctypes.c_ubyte(128)): - data.write(ctypes.c_ubyte(num | ctypes.c_ubyte(128))) - num >>= 7 - data.Write(ctypes.c_ubyte(num)) #encoding FrooxEngine/C# types in binary: diff --git a/core/preferences.json b/core/preferences.json index 4fd9b5f..754f04a 100644 --- a/core/preferences.json +++ b/core/preferences.json @@ -1,4 +1,4 @@ { "language": 0, - "last_update_check": 1734208835.8049936 + "last_update_check": 1734295375.2681296 } \ No newline at end of file diff --git a/core/resonite_loader/common.py b/core/resonite_loader/common.py index f42e2e8..49ad0d9 100644 --- a/core/resonite_loader/common.py +++ b/core/resonite_loader/common.py @@ -1,12 +1,21 @@ import ctypes import typing -import struct +import struct from io import BytesIO +def writeNullable(data: BytesIO, value: = None): + + data.write(struct.pack("?", value == None)) + if(value == None): + return + data.write() + + + def ReadCSharp_str(data: BytesIO) -> str: charamount = read7bitEncoded_int(data) string: str = data.read(charamount).decode('utf-8', errors="replace") - print("read string: "+string) + #print("read string: "+string) return string def WriteCSharp_str(data: BytesIO, string: str) -> str: @@ -29,7 +38,7 @@ def read7bitEncoded_ulong(data: BytesIO) -> int: return num def read7bitEncoded_int(data: BytesIO) -> int: - num: int= int(0) + num: int = int(0) num2:int = int(0) while (num2 != 35): b: int = int(struct.unpack(' int: def write7bitEncoded_ulong(data: BytesIO, integer: int) -> None: while integer > int(0): - b: int = ctypes.c_ubyte(integer & int(127)) + b: int = int(integer & int(127)) integer >>= 7 if integer > int(0): b |= 128 @@ -54,4 +63,4 @@ def write7bitEncoded_int(data: BytesIO, value: int) -> None: while(num >= int(128)): data.write(int(num | int(128))) num >>= 7 - data.Write(int(num)) \ No newline at end of file + data.Write(int(num)) diff --git a/core/resonite_loader/resonite_animx.py b/core/resonite_loader/resonite_animx.py index 3bb8bb1..ad6c4cf 100644 --- a/core/resonite_loader/resonite_animx.py +++ b/core/resonite_loader/resonite_animx.py @@ -1,5 +1,6 @@ from __future__ import annotations from os import replace +from re import S from types import FrameType import lz4.block @@ -10,6 +11,7 @@ import typing import struct from io import BytesIO + KeyframeInterpolation: dict[str, int] = { "Hold": 1, "Linear": 2, @@ -18,28 +20,17 @@ KeyframeInterpolation: dict[str, int] = { } class KeyFrame(): - time: resonite_types.float = resonite_types.float(0) - interpolation: resonite_types.int = resonite_types.int(0) + time: resonite_types.float + interpolation: resonite_types.byte value: resonite_types.ResoType left_tan: resonite_types.ResoType right_tan: resonite_types.ResoType - - - def __getattr__(self, name: str): - if name == "interpolation": - interp: int = 0 - if (self["left_tan"] != None and self["right_tan"] != None): - interp = 3 - - - return resonite_types.int(interp) - return super().__getattribute__(name) - def __init__(self): - pass + self.time = resonite_types.float(0) + self.interpolation = resonite_types.byte(0) def RequiresTangents(self) -> bool: @@ -48,14 +39,17 @@ class KeyFrame(): return False class ResoTrack(resonite_types.ResoType): - node: resonite_types.string = resonite_types.string("") - property: resonite_types.string = resonite_types.string("") + node: resonite_types.string + property: resonite_types.string Owner: AnimX - FrameType: type[resonite_types.ResoType] - keyframes: list[KeyFrame] = [] + FrameType: str + keyframes: list[KeyFrame] def __init__(self,FrameType): self.FrameType = FrameType + self.keyframes = [] + self.node = resonite_types.string("") + self.property = resonite_types.string("") def write(self, data: BytesIO): self.node.write(data) @@ -65,10 +59,12 @@ class ResoTrack(resonite_types.ResoType): def read(self, data:BytesIO): self.node.read(data) self.property.read(data) + track_amount: int = int(common.read7bitEncoded_ulong(data)) + #print(track_amount) for i in range(0, track_amount): key: KeyFrame = KeyFrame() - key.value = self.FrameType() + key.value = eval(self.FrameType+"()") self.keyframes.append(key) def removeKeyframe(self, time: float | int) -> bool: @@ -143,28 +139,35 @@ class ResoTrack(resonite_types.ResoType): class RawTrack(ResoTrack): - interval: resonite_types.float = resonite_types.float(0) + interval: resonite_types.float def __getattr__(self, name: str): if name == "interval": - return self.Owner.interval.x + return self.Owner.interval return super().__getattribute__(name) def __init__(self, FrameType): super().__init__(FrameType) + self.interval = resonite_types.float(0) def write(self, data: BytesIO): super().write(data) self.interval.write(data) for key in self.keyframes: - key.value.write(data) + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.value) + else: + key.value.write(data) def read(self, data:BytesIO): super().read(data) self.interval.read(data) for key in self.keyframes: - key.value.read(data) + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.value) + else: + key.value.read(data) def addKeyframe(self, keyframe: KeyFrame) -> int: num: int = super().addKeyframe(keyframe) @@ -187,10 +190,28 @@ class DiscreteTrack(ResoTrack): def write(self, data: BytesIO): super().write(data) + self.interval.write(data) + for key in self.keyframes: + if key.value == None: + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.value) + else: + key.value.write(data) + key.time.write(data) def read(self, data:BytesIO): super().read(data) + self.interval.read(data) + for key in self.keyframes: + if key.value == None: + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.value) + else: + key.value.read(data) + key.time.read(data) def addKeyframe(self, keyframe: KeyFrame) -> int: num: int = super().addKeyframe(keyframe) @@ -203,23 +224,27 @@ class DiscreteTrack(ResoTrack): class CurveTrack(ResoTrack): - interpolations: bool = False - tangents: bool = False - sharedinterpolation: resonite_types.int = resonite_types.int(-1) + interpolations: bool + tangents: bool + sharedinterpolation: resonite_types.byte def __getattr__(self, name: str): if name == "interpolations": + integerframe: int = self.keyframes[0].interpolation.x for key in self.keyframes: - if key.interpolation.x != self.sharedinterpolation.x: + if key.interpolation.x != integerframe: return True elif name == "tangents": for key in self.keyframes: - if key.interpolation.x == 3 or key.interpolation.x == 4: + if key.RequiresTangents(): return True return super().__getattribute__(name) def __init__(self, FrameType): super().__init__(FrameType) + self.sharedinterpolation = resonite_types.byte(-1) + self.interpolations = False + self.tangents = False def write(self, data: BytesIO): super().write(data) @@ -234,45 +259,57 @@ class CurveTrack(ResoTrack): for key in self.keyframes: if key.value == None: - key.value = self.FrameType() - key.value.write(data) + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.value) + else: + key.value.write(data) key.time.write(data) if(self.tangents): for key in self.keyframes: - key.left_tan.write(data) - key.right_tan.write(data) + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.left_tan) + resonite_types.writeNullable(data, key.right_tan) + else: + key.left_tan.write(data) + key.right_tan.write(data) def read(self, data:BytesIO): super().read(data) flags: int = struct.unpack(" 0 - self.tangents = (flags & 2) > 0 + interp: bool = (flags & 1) > 0 + tan: bool = (flags & 2) > 0 - print(str(self.interpolations)) - print(str(self.tangents)) + #print(str(interp)) + #print(str(tan)) + #print(flags) + #print(len(self.keyframes)) - if(self.interpolations): + if(interp): for key in self.keyframes: + #print("reading interp") key.interpolation.read(data) else: self.sharedinterpolation.read(data) - + for key in self.keyframes: - print("key read!") if key.value == None: - key.value = self.FrameType() - key.value.read(data) + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.value) + else: + key.value.read(data) key.time.read(data) - if(self.tangents): + if(tan): for key in self.keyframes: - if key.left_tan == None: - key.left_tan = self.FrameType() - if key.right_tan == None: - key.right_tan = self.FrameType() - key.left_tan.read(data) - key.right_tan.read(data) + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.left_tan) + resonite_types.readNullable(data, key.right_tan) + else: + key.left_tan.read(data) + key.right_tan.read(data) @@ -313,56 +350,56 @@ class BezierTrack(ResoTrack): """PLACE HOLDER METHOD, DO NOT USE""" raise Exception("BezierTrack track type is unsupported in resonite's code") #This is weird, but thank you python - @989onan -TrackTypes: list[type] = [ - RawTrack, - DiscreteTrack, - CurveTrack, - BezierTrack +TrackTypes: list[str] = [ + "RawTrack", + "DiscreteTrack", + "CurveTrack", + "BezierTrack" ] #TODO: add all types here #wooooo - @989onan -elementTypes: list[type[resonite_types.ResoType]] = [ - resonite_types.bool, - resonite_types.bool2, - resonite_types.bool3, - resonite_types.bool4, - resonite_types.byte, - resonite_types.ushort, - resonite_types.uint, - resonite_types.ulong, - resonite_types.sbyte, - resonite_types.short, - resonite_types.int, - resonite_types.long, - resonite_types.int2, - resonite_types.int3, - resonite_types.int4, - resonite_types.uint2, - resonite_types.uint3, - resonite_types.uint4, - resonite_types.long2, - resonite_types.long3, - resonite_types.long4, - resonite_types.float, - resonite_types.float2, - resonite_types.float3, - resonite_types.float4, - resonite_types.floatQ, - resonite_types.float2x2, - resonite_types.float3x3, - resonite_types.float4x4, - resonite_types.double, - resonite_types.double2, - resonite_types.double3, - resonite_types.double4, - resonite_types.doubleQ, - resonite_types.double2x2, - resonite_types.double3x3, - resonite_types.double4x4, - resonite_types.color, - resonite_types.color32, - resonite_types.string +elementTypes: list[str] = [ + "resonite_types.bool", + "resonite_types.bool2", + "resonite_types.bool3", + "resonite_types.bool4", + "resonite_types.byte", + "resonite_types.ushort", + "resonite_types.uint", + "resonite_types.ulong", + "resonite_types.sbyte", + "resonite_types.short", + "resonite_types.int", + "resonite_types.long", + "resonite_types.int2", + "resonite_types.int3", + "resonite_types.int4", + "resonite_types.uint2", + "resonite_types.uint3", + "resonite_types.uint4", + "resonite_types.long2", + "resonite_types.long3", + "resonite_types.long4", + "resonite_types.float", + "resonite_types.float2", + "resonite_types.float3", + "resonite_types.float4", + "resonite_types.floatQ", + "resonite_types.float2x2", + "resonite_types.float3x3", + "resonite_types.float4x4", + "resonite_types.double", + "resonite_types.double2", + "resonite_types.double3", + "resonite_types.double4", + "resonite_types.doubleQ", + "resonite_types.double2x2", + "resonite_types.double3x3", + "resonite_types.double4x4", + "resonite_types.color", + "resonite_types.color32", + "resonite_types.string" ] @@ -372,20 +409,27 @@ class AnimX(): """ - To use Raw Track properly, please set interval (seconds between frames) after reading/creating.\n - Represents data to be written to or read from an AnimX file. + To use Raw Track properly, please set interval (seconds between frames) after creating.\n + Represents data to be written to or read from an AnimX file.\n + default interval to use would be 30. """ - file_version: resonite_types.int = resonite_types.int() - track_amount: resonite_types.int = resonite_types.int() - global_duration: resonite_types.float = resonite_types.float() - name: resonite_types.string = resonite_types.string() + file_version: resonite_types.int + track_amount: resonite_types.int + global_duration: resonite_types.float + name: resonite_types.string - tracks: list[ResoTrack] = [] + tracks: list[ResoTrack] - interval: resonite_types.float = resonite_types.float(1/25) #default value + interval: resonite_types.float def __init__(self): + self.tracks = [] + self.file_version = resonite_types.int() + self.track_amount = resonite_types.int() + self.global_duration = resonite_types.float() + self.name = resonite_types.string() + self.interval = resonite_types.float(1/25) #default value pass @classmethod @@ -424,7 +468,7 @@ class AnimX(): self.track_amount.x = common.read7bitEncoded_ulong(data) self.global_duration.read(data) - print("track amont: "+str(self.track_amount.x)) + print("track amount: "+str(self.track_amount.x)) print("file vers: "+str(self.file_version.x)) self.name.read(data) @@ -453,8 +497,8 @@ class AnimX(): data.read(8) #fuck off stream headers - @989onan data.read(8) #fuck off stream headers - @989onan filelmza: bytes = bytes(AnimX.decompress_lzma(data.read(), lzma.FORMAT_RAW, filters)) - - print(f"decompressed bytes: {filelmza[:100]}") + #print("binary below:") + #print("b'{}'".format(''.join('\\x{:02x}'.format(b) for b in filelmza[:100]))) data = BytesIO(filelmza) case _: raise Exception("Invalid encoding") @@ -515,10 +559,10 @@ class AnimX(): return True @classmethod - def GetTrackType(cls, trackType2: int, value_type: type[resonite_types.ResoType], data: BytesIO) -> ResoTrack: - Track = TrackTypes[trackType2](value_type) - print(type(Track)) - print(value_type) + def GetTrackType(cls, trackType2: int, value_type: str, data: BytesIO) -> ResoTrack: + Track: ResoTrack = eval(TrackTypes[trackType2]+"(value_type)") + #print(value_type) + #print(type(Track)) Track.read(data) return Track diff --git a/core/resonite_loader/resonite_types.py b/core/resonite_loader/resonite_types.py index e1052a7..dbdaf7f 100644 --- a/core/resonite_loader/resonite_types.py +++ b/core/resonite_loader/resonite_types.py @@ -17,6 +17,19 @@ class ResoType(): def read(cls, data: BytesIO): pass +def writeNullable(data: BytesIO, value: ResoType = None): + + data.write(struct.pack("?", value == None)) + if(value == None): + return + value.write(data) + +def readNullable(data: BytesIO, value: ResoType = None): + + hasval: bool = struct.unpack("?", data.read(1)) + if not hasval: + return + value.read(data) #These below are collection of the basic resonite typing made from C#. This is in order to store data in a sane way and decode/encode it. class color(ResoType): @@ -75,8 +88,8 @@ class byte(ResoType): def __int__(self): return self.x - def __init__(self): - pass + def __init__(self,value=0): + self.x = value def write(self, data: BytesIO): data.write(struct.pack(" bpy.types.FCurve: + fcurve = action.fcurves.find(data_path=data_path,index=index) + if fcurve == None: + return action.fcurves.new(data_path,action_group=action_group,index=index) + else: + print("fcurve with data \""+data_path+"\" already exists") + return fcurve + @register_wrap class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper): bl_idname = 'avatar_toolkit.animx_importer' @@ -169,7 +179,7 @@ class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper) @classmethod def poll(cls, context: Context) -> bool: - return True + return context.active_object != None def execute(self, context: Context) -> set: @@ -177,62 +187,112 @@ class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper) #decoding using self contained library: files = [file.name for file in self.files] - files.append(self.filepath) + #files.append(self.filepath) for file in files: froox_animation: resonite_animx.AnimX = resonite_animx.AnimX() - froox_animation.interval.x = 1/25 + froox_animation.interval.x = 30 #should be default fps froox_animation.read(file = os.path.join(self.directory,file)) Froox_animations.append(froox_animation) + #TODO: Allow multiple targets and setting animations to each one somehow with an interface. + target: bpy.types.Object = context.active_object + if target.animation_data == None: + target.animation_data_create() + #Load data into Blender Animations. for froox_animation in Froox_animations: action: bpy.types.Action = bpy.data.actions.new(froox_animation.name.x) + target.animation_data.action = action action.use_fake_user = True for track in froox_animation.tracks: - print("hit here1") - print(track.FrameType) - if(track.FrameType != type(resonite_types.float) and track.FrameType != type(resonite_types.double)): + data_path: str + actualproperty: str = track.property.x + + match(actualproperty): + case("Position"): + actualproperty = "location" + case("Rotation"): + actualproperty = "rotation_quaternion" + case("Scale"): + actualproperty = "scale" + data_path = actualproperty + + if target.type == "ARMATURE": + data_path = "pose.bones[\""+track.node.x+"\"]."+data_path + + for posebone in target.pose.bones: + posebone.rotation_mode = "QUATERNION" + + print("reading frames for "+data_path) + if(track.FrameType == "resonite_types.double" or track.FrameType == "resonite_types.double"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + elif (track.FrameType == "resonite_types.float3" or track.FrameType == "resonite_types.double3"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=2),".y") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=1),".z") + elif (track.FrameType == "resonite_types.float4" or track.FrameType == "resonite_types.double4"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=1),".y") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=2),".z") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=3),".w") + elif (track.FrameType == "resonite_types.doubleQ" or track.FrameType == "resonite_types.floatQ"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=3),".w") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=2),".y") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=1),".z") + else: continue + return {'FINISHED'} + + def readTrackData(self,track: resonite_animx.ResoTrack, fcurve_reso: bpy.types.FCurve, valuetype: str = ""): + tracktype = type(track) + match(tracktype): + case (resonite_animx.RawTrack): + rawtrack: resonite_animx.RawTrack = track - data_path: str = track._node.x+"."+track._property.x - fcurve_reso = action.fcurves.new(data_path,None,track._node.x) - print("hit here2") - match(type(track)): - case (type(resonite_animx.RawTrack)): - rawtrack: resonite_animx.RawTrack = track + + fcurve_reso.keyframe_points.add(count=len(rawtrack.keyframes)) + # populate points + fcurve_reso.keyframe_points.foreach_set("co", [x for co in zip([frame.time.x*track.Owner.interval.x for frame in rawtrack.keyframes], [eval("frame.value"+valuetype) for frame in rawtrack.keyframes]) for x in co]) + fcurve_reso.update() - - - for frame in rawtrack.keyframes: - key: bpy.types.Keyframe = fcurve_reso.keyframe_points.insert(frame.time, float(frame.value)) - + case (resonite_animx.DiscreteTrack): + discretetrack: resonite_animx.DiscreteTrack = track - case (type(resonite_animx.DiscreteTrack)): - discretetrack: resonite_animx.RawTrack = track - for frame in rawtrack.keyframes: - key: bpy.types.Keyframe = fcurve_reso.keyframe_points.insert(frame.time, float(frame.value)) + fcurve_reso.keyframe_points.add(count=len(discretetrack.keyframes)) + # populate points + fcurve_reso.keyframe_points.foreach_set("co", [x for co in zip([frame.time.x*track.Owner.interval.x for frame in discretetrack.keyframes], [eval("frame.value"+valuetype) for frame in discretetrack.keyframes]) for x in co]) + fcurve_reso.update() + case(resonite_animx.CurveTrack): + curvetrack: resonite_animx.CurveTrack = track - case(type(resonite_animx.CurveTrack)): - curvetrack: resonite_animx.RawTrack = track - for frame in curvetrack.keyframes: - key: bpy.types.Keyframe = fcurve_reso.keyframe_points.insert(frame.time, float(frame.value)) - key.handle_left = float(frame.left_tan) - key.handle_left = float(frame.right_tan) + fcurve_reso.keyframe_points.add(count=len(curvetrack.keyframes)) + # populate points + fcurve_reso.keyframe_points.foreach_set("co", [x for co in zip([frame.time.x*track.Owner.interval.x for frame in curvetrack.keyframes], [eval("frame.value"+valuetype) for frame in curvetrack.keyframes]) for x in co]) + interp: bool = curvetrack.tangents + #print("has tangents? "+str(interp)) + for idx,frame in enumerate(curvetrack.keyframes): + + if interp: + fcurve_reso.keyframe_points[idx].handle_left = float(eval("frame.left_tan"+valuetype)) + fcurve_reso.keyframe_points[idx].handle_right = float(eval("frame.right_tan"+valuetype)) + fcurve_reso.keyframe_points[idx].interpolation = "BEZIER" + fcurve_reso.keyframe_points[idx].easing = "EASE_IN" + fcurve_reso.update() - case(type(resonite_animx.BezierTrack)): - beziertrack: resonite_animx.RawTrack = track - # Bezier is not supported rn, ignore. + case(resonite_animx.BezierTrack): + beziertrack: resonite_animx.BezierTrack = track + # Bezier is not supported rn, ignore. + case _: + print("invalid track type, ignoring") + print(track) - - - - return {'FINISHED'}