f16105517e
add to menu fix resonite animx importer bug add flip animations add flip animation keyframes to help users rekey and remake animations as if they were mirrored.
568 lines
19 KiB
Python
568 lines
19 KiB
Python
from __future__ import annotations
|
|
from os import replace
|
|
from re import S
|
|
from types import FrameType
|
|
|
|
from . import resonite_types
|
|
from . import common
|
|
|
|
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
|
|
interpolation: resonite_types.byte
|
|
value: resonite_types.ResoType
|
|
left_tan: resonite_types.ResoType
|
|
right_tan: resonite_types.ResoType
|
|
|
|
|
|
|
|
def __init__(self):
|
|
self.time = resonite_types.float(0)
|
|
self.interpolation = resonite_types.byte(0)
|
|
|
|
|
|
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: 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)
|
|
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))
|
|
#print(track_amount)
|
|
for i in range(0, track_amount):
|
|
key: KeyFrame = KeyFrame()
|
|
key.value = eval(self.FrameType+"()")
|
|
self.keyframes.append(key)
|
|
|
|
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
|
|
|
|
def __getattr__(self, name: str):
|
|
if name == "interval":
|
|
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:
|
|
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:
|
|
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)
|
|
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)
|
|
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)
|
|
return num
|
|
def removeKeyframe(self, time: float | int) -> bool:
|
|
success: bool = super().removeKeyframe(time)
|
|
return success
|
|
|
|
|
|
|
|
|
|
class CurveTrack(ResoTrack):
|
|
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 != integerframe:
|
|
return True
|
|
elif name == "tangents":
|
|
for key in self.keyframes:
|
|
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)
|
|
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:
|
|
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)
|
|
|
|
if(self.tangents):
|
|
for key in self.keyframes:
|
|
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("<B",data.read(1))[0]
|
|
interp: bool = (flags & 1) > 0
|
|
tan: bool = (flags & 2) > 0
|
|
|
|
#print(str(interp))
|
|
#print(str(tan))
|
|
#print(flags)
|
|
#print(len(self.keyframes))
|
|
|
|
if(interp):
|
|
for key in self.keyframes:
|
|
#print("reading interp")
|
|
key.interpolation.read(data)
|
|
else:
|
|
self.sharedinterpolation.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)
|
|
|
|
if(tan):
|
|
for key in self.keyframes:
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BezierTrack(ResoTrack):
|
|
"""PLACE HOLDER CLASS, DO NOT USE"""
|
|
|
|
|
|
def __init__(self, FrameType):
|
|
super().__init__(FrameType)
|
|
"""PLACE HOLDER METHOD, DO NOT USE"""
|
|
#raise Exception("BezierTrack track type is unsupported in resonite's code")
|
|
|
|
def write(self, data: BytesIO):
|
|
"""PLACE HOLDER METHOD, DO NOT USE"""
|
|
raise Exception("BezierTrack track type is unsupported in resonite's code")
|
|
def read(self, data:BytesIO):
|
|
"""PLACE HOLDER METHOD, DO NOT USE"""
|
|
raise Exception("BezierTrack track type is unsupported in resonite's code")
|
|
|
|
def removeKeyframe(self, keyframe: KeyFrame) -> 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[str] = [
|
|
"RawTrack",
|
|
"DiscreteTrack",
|
|
"CurveTrack",
|
|
"BezierTrack"
|
|
]
|
|
|
|
#TODO: add all types here
|
|
#wooooo - @989onan
|
|
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"
|
|
]
|
|
|
|
|
|
most_recent_AnimX_vers: int = 1
|
|
import lzma#HALLLOOOYAH HALLOYAH!! - @989onan
|
|
class AnimX():
|
|
|
|
|
|
"""
|
|
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
|
|
track_amount: resonite_types.int
|
|
global_duration: resonite_types.float
|
|
name: resonite_types.string
|
|
|
|
tracks: list[ResoTrack]
|
|
|
|
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
|
|
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:
|
|
"""
|
|
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("track amount: "+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('<B', data.read(1))[0]):
|
|
case 0:
|
|
pass
|
|
case 1:
|
|
from lz4.frame import decompress #why do you have to be a wheel? - @989onan
|
|
data = BytesIO(decompress(data.read()))
|
|
case 2:
|
|
|
|
filters = [
|
|
{"id" : lzma.FILTER_LZMA1, #idfk man - @989onan
|
|
"dict_size" : 2097152,
|
|
"lc" : 3,
|
|
"lp" : 0,
|
|
"pb" : 2, # private static int posStateBits = 2; //<-froox engine derived.
|
|
"mode" : lzma.MODE_NORMAL,
|
|
"nice_len" : 32, # private static int numFastBytes = 32; //<-froox engine derived.
|
|
"mf" : lzma.MF_BT4,
|
|
},
|
|
]
|
|
data.read(5) #fuck off headers - @989onan
|
|
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("binary below:")
|
|
#print("b'{}'".format(''.join('\\x{:02x}'.format(b) for b in filelmza[:100])))
|
|
data = BytesIO(filelmza)
|
|
case _:
|
|
raise Exception("Invalid encoding")
|
|
|
|
for i in range(0,self.track_amount.x):
|
|
trackType2: int = 0
|
|
num4: int = 0
|
|
if (self.file_version == 0):
|
|
b: int = int(struct.unpack('<B', data.read(1))[0])
|
|
num3: int = int(b & 1)
|
|
trackType: int = 0
|
|
if (num3 != 0):
|
|
if (num3 != 1):
|
|
raise Exception("[InvalidDataException]: Invalid track type data: " + str(b))
|
|
trackType = 2
|
|
else:
|
|
trackType = 0
|
|
trackType2 = trackType
|
|
num4 = b >> 1
|
|
else:
|
|
trackType2 = int(struct.unpack('<B', data.read(1))[0])
|
|
num4 = int(struct.unpack('<B', data.read(1))[0])
|
|
try:
|
|
animationTrack = AnimX.GetTrackType(trackType2, elementTypes[num4], data)
|
|
animationTrack.Owner = self
|
|
self.tracks.append(animationTrack)
|
|
except:
|
|
raise Exception("[InvalidDataException]: element type exception, beyond range: "+str(num4))
|
|
|
|
return True
|
|
|
|
def write(self, file: str) -> 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('<B', 0)) #default encoding, so we don't have to use lzma.
|
|
|
|
|
|
for i in range(0,self.track_amount.x):
|
|
|
|
data.write(struct.pack('<B', TrackTypes.index(type(self.tracks[i]))))
|
|
data.write(struct.pack('<B', elementTypes.index(self.tracks[i].FrameType)))
|
|
self.tracks[i].write(data)
|
|
|
|
return True
|
|
|
|
@classmethod
|
|
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
|
|
|