using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace OpenVIII { public static class VertexPositionTexturePointersGRP_Ext { public static bool IsNotSet(this Debug_battleDat.VertexPositionTexturePointersGRP vertexPositionTexturePointersGRP) { if ((vertexPositionTexturePointersGRP.VPT?.Length ?? 0) > 0 && (vertexPositionTexturePointersGRP.TexturePointers?.Length ?? 0) > 0) { //3 vertices per every texture pointer. Debug.Assert(vertexPositionTexturePointersGRP.VPT.Length / 3 == vertexPositionTexturePointersGRP.TexturePointers.Length); return false; } return true; } public static bool IsSet(this Debug_battleDat.VertexPositionTexturePointersGRP vertexPositionTexturePointersGRP) => !vertexPositionTexturePointersGRP.IsNotSet(); } public partial class Debug_battleDat { private byte[] buffer; private const float BaseLineMaxYFilter = 10f; public const float SCALEHELPER = 2048.0f; private const float DEGREES = 360f; public struct DatFile { public uint cSections; public uint[] pSections; public uint eof; } public DatFile datFile; #region section 1 Skeleton [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 16)] public struct Skeleton { public ushort cBones; public ushort scale; public ushort unk2; public ushort unk3; public ushort unk4; public ushort unk5; public ushort unk6; public ushort unk7; public Bone[] bones; public Vector3 GetScale => new Vector3(scale / SCALEHELPER * 12, scale / SCALEHELPER * 12, scale / SCALEHELPER * 12); } [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 44)] public struct Bone { public ushort parentId; public short boneSize; private short rotx; //360 private short roty; //360 private short rotz; //360 private short unk4; private short unk5; private short unk6; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] public byte[] Unk; public float Size => boneSize / SCALEHELPER; public float RotX => rotx / 4096.0f * 360.0f; //rotX public float RotY => roty / 4096.0f * 360.0f; //rotY public float RotZ => rotz / 4096.0f * 360.0f; //rotZ public float Unk4 => unk4 / 4096.0f; //unk1v public float Unk5 => unk5 / 4096.0f; //unk2v public float Unk6 => unk6 / 4096.0f; //unk3v } /// /// Skeleton data section /// /// private void ReadSection1(uint start) { br.BaseStream.Seek(start, SeekOrigin.Begin); #if _WINDOWS //looks like Linux Mono doesn't like marshalling structure with LPArray to Bone[] skeleton = Extended.ByteArrayToStructure(br.ReadBytes(16)); #else skeleton = new Skeleton() { cBones = br.ReadUInt16(), scale = br.ReadUInt16(), unk2 = br.ReadUInt16(), unk3 = br.ReadUInt16(), unk4 = br.ReadUInt16(), unk5 = br.ReadUInt16(), unk6 = br.ReadUInt16(), unk7 = br.ReadUInt16() }; #endif skeleton.bones = new Bone[skeleton.cBones]; for (int i = 0; i < skeleton.cBones; i++) { skeleton.bones[i] = Extended.ByteArrayToStructure(br.ReadBytes(44)); br.BaseStream.Seek(4, SeekOrigin.Current); } //string debugBuffer = string.Empty; //for (int i = 0; i< skeleton.cBones; i++) // debugBuffer += $"{i}|{skeleton.bones[i].parentId}|{skeleton.bones[i].boneSize}|{skeleton.bones[i].Size}\n"; //Console.WriteLine(debugBuffer); return; } public Skeleton skeleton; #endregion section 1 Skeleton #region section 2 Geometry public struct Geometry { public uint cObjects; public uint[] pObjects; public Object[] objects; public uint cTotalVert; } public struct Object { public ushort cVertices; public VerticeData[] vertexData; public ushort cTriangles; public ushort cQuads; public ulong padding; public Triangle[] triangles; public Quad[] quads; } public struct VerticeData { public ushort boneId; public ushort cVertices; public Vector3[] vertices; } [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 8)] public struct Vertex { public short x { get; private set; } public short y { get; private set; } public short z { get; private set; } public Vertex(short x, short y, short z) { this.x = x; this.y = y; this.z = z; } public static implicit operator Vector3(Vertex v) => new Vector3(-v.x / SCALEHELPER, -v.z / SCALEHELPER, -v.y / SCALEHELPER); public override string ToString() => $"x={x}, y={y}, z={z}, Vector3={(Vector3)this}"; } [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 16)] public struct Triangle { private ushort A; private ushort B; private ushort C; public UV vta; public UV vtb; public ushort texUnk; public UV vtc; public ushort u; private VertexPositionTexture[] TempVPT; public ushort A1 { get => (ushort)(A & 0xFFF); set => A = value; } public ushort B1 { get => (ushort)(B & 0xFFF); set => B = value; } public ushort C1 { get => (ushort)(C & 0xFFF); set => C = value; } public byte textureIndex => (byte)((texUnk >> 6) & 0b111); public static Triangle Create(BinaryReader br) { Triangle r = new Triangle() { A = br.ReadUInt16(), B = br.ReadUInt16(), C = br.ReadUInt16(), vta = UV.Create(br), vtb = UV.Create(br), texUnk = br.ReadUInt16(), vtc = UV.Create(br), u = br.ReadUInt16(), TempVPT = new VertexPositionTexture[Count] }; return r; } public ushort GetIndex(int i) { switch (i) { case 0: return C1; //for some reason C is first in triangle case 1: return A1; case 2: return B1; } throw new IndexOutOfRangeException($"{this} :: 0-2 are only valid values"); } public UV GetUV(int i) { switch (i) { case 0: return vta; case 1: return vtb; case 2: return vtc; } throw new IndexOutOfRangeException($"{this} :: 0-2 are only valid values"); } public byte this[int i] { get { switch (i) { case 0: return 0; case 1: return 1; case 2: return 2; } throw new IndexOutOfRangeException($"{this} :: 0-2 are only valid values"); } } public byte[] Indices => new byte[] { this[0], this[1], this[2] }; public static byte Count => 3; public VertexPositionTexture[] GenerateVPT(List verts, Quaternion rotation, Vector3 translationPosition, Texture2D preVarTex) { if (TempVPT == null) TempVPT = new VertexPositionTexture[Count]; VertexPositionTexture GetVPT(ref Triangle triangle, byte i) { Vector3 GetVertex(ref Triangle _triangle, byte _i) { return TransformVertex(verts[_triangle.GetIndex(_i)], translationPosition, rotation); } return new VertexPositionTexture(GetVertex(ref triangle, i), triangle.GetUV(i).ToVector2(preVarTex.Width, preVarTex.Height)); } TempVPT[0] = GetVPT(ref this, this[0]); TempVPT[1] = GetVPT(ref this, this[1]); TempVPT[2] = GetVPT(ref this, this[2]); return TempVPT; } } [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 20)] public struct Quad { private ushort A; private ushort B; private ushort C; private ushort D; public UV vta; public ushort texUnk; public UV vtb; public ushort u; public UV vtc; public UV vtd; private VertexPositionTexture[] TempVPT; public ushort A1 { get => (ushort)(A & 0xFFF); set => A = value; } public ushort B1 { get => (ushort)(B & 0xFFF); set => B = value; } public ushort C1 { get => (ushort)(C & 0xFFF); set => C = value; } public ushort D1 { get => (ushort)(D & 0xFFF); set => D = value; } public byte textureIndex => (byte)((texUnk >> 6) & 0b111); public static Quad Create(BinaryReader br) => new Quad() { A = br.ReadUInt16(), B = br.ReadUInt16(), C = br.ReadUInt16(), D = br.ReadUInt16(), vta = UV.Create(br), texUnk = br.ReadUInt16(), vtb = UV.Create(br), u = br.ReadUInt16(), vtc = UV.Create(br), vtd = UV.Create(br), TempVPT = new VertexPositionTexture[Count] }; public ushort GetIndex(int i) { switch (i) { case 0: return A1; case 1: return B1; case 2: return C1; case 3: return D1; } throw new IndexOutOfRangeException($"{this} :: 0-3 are only valid values"); } public UV GetUV(int i) { switch (i) { case 0: return vta; case 1: return vtb; case 2: return vtc; case 3: return vtd; } throw new IndexOutOfRangeException($"{this} :: 0-3 are only valid values"); } public byte this[int i] { get { switch (i) { case 0: case 3: return 0; case 1: return 1; case 2: case 5: return 3; case 4: return 2; } throw new IndexOutOfRangeException($"{this} :: 0-5 are only valid values"); } } public byte[] Indices => new byte[] { this[0], this[1], this[2], this[3], this[4], this[5] }; public static byte Count => 6; public VertexPositionTexture[] GenerateVPT(List verts, Quaternion rotation, Vector3 translationPosition, Texture2D preVarTex) { if (TempVPT == null) TempVPT = new VertexPositionTexture[Count]; VertexPositionTexture GetVPT(ref Quad quad, byte i) { Vector3 GetVertex(ref Quad _quad, byte _i) { return TransformVertex(verts[_quad.GetIndex(_i)], translationPosition, rotation); } return new VertexPositionTexture(GetVertex(ref quad, i), quad.GetUV(i).ToVector2(preVarTex.Width, preVarTex.Height)); } TempVPT[0] = TempVPT[3] = GetVPT(ref this, this[0]); TempVPT[1] = GetVPT(ref this, this[1]); TempVPT[4] = GetVPT(ref this, this[4]); TempVPT[2] = TempVPT[5] = GetVPT(ref this, this[2]); return TempVPT; } } [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 2)] public struct UV { public byte U; public byte V; public float U1(float h = 128f) => U / h; public float V1(float w = 128f) => V > 128 ? //if bigger than 128, then multitexture index to odd (V - 128f) / w : w == 32 ? //if equals 32, then it's weapon texture and should be in range of 96-128 (V - 96) / w : V / w; //if none of these cases, then divide by resolution; public static UV Create(BinaryReader br) => new UV() { U = br.ReadByte(), V = br.ReadByte() }; public Vector2 ToVector2(float w, float h) => new Vector2(U1(w), V1(h)); public override string ToString() => $"{U};{U1()};{V};{V1()}"; } public Geometry geometry; /// /// Model Geometry section /// /// /// /// private void ReadSection2(uint start) { br.BaseStream.Seek(start, SeekOrigin.Begin); geometry = new Geometry { cObjects = br.ReadUInt32() }; geometry.pObjects = new uint[geometry.cObjects]; for (int i = 0; i < geometry.cObjects; i++) geometry.pObjects[i] = br.ReadUInt32(); geometry.objects = new Object[geometry.cObjects]; for (int i = 0; i < geometry.cObjects; i++) geometry.objects[i] = ReadGeometryObject(start + geometry.pObjects[i]); geometry.cTotalVert = br.ReadUInt32(); } private Object ReadGeometryObject(uint start) { br.BaseStream.Seek(start, SeekOrigin.Begin); Object @object = new Object { cVertices = br.ReadUInt16() }; @object.vertexData = new VerticeData[@object.cVertices]; if (br.BaseStream.Position + @object.cVertices * 6 >= br.BaseStream.Length) return @object; for (int n = 0; n < @object.cVertices; n++) { @object.vertexData[n].boneId = br.ReadUInt16(); @object.vertexData[n].cVertices = br.ReadUInt16(); @object.vertexData[n].vertices = new Vector3[@object.vertexData[n].cVertices]; for (int i = 0; i < @object.vertexData[n].cVertices; i++) @object.vertexData[n].vertices[i] = Extended.ByteArrayToStructure(br.ReadBytes(6)); } br.BaseStream.Seek(4 - (br.BaseStream.Position % 4 == 0 ? 4 : br.BaseStream.Position % 4), SeekOrigin.Current); @object.cTriangles = br.ReadUInt16(); @object.cQuads = br.ReadUInt16(); @object.padding = br.ReadUInt64(); @object.triangles = new Triangle[@object.cTriangles]; if (@object.cTriangles == 0 && @object.cQuads == 0) return @object; @object.quads = new Quad[@object.cQuads]; for (int i = 0; i < @object.cTriangles; i++) @object.triangles[i] = Triangle.Create(br);//Extended.ByteArrayToStructure(br.ReadBytes(16)); for (int i = 0; i < @object.cQuads; i++) @object.quads[i] = Quad.Create(br);// Extended.ByteArrayToStructure(br.ReadBytes(20)); return @object; } public struct VectorBoneGRP { private Vector3 Vector { get; set; } public int BoneID { get; private set; } public float X => Vector.X; public float Y => Vector.Y; public float Z => Vector.Z; public static implicit operator Vector3(VectorBoneGRP vbg) => vbg.Vector; public VectorBoneGRP(Vector3 vector, int boneID) { Vector = vector; BoneID = boneID; } public override string ToString() => $"Vector: {Vector}, Bone ID: {BoneID}"; } public struct VertexPositionTexturePointersGRP { public VertexPositionTexturePointersGRP(VertexPositionTexture[] vpt, byte[] texturepointers) : this() { this.VPT = vpt; this.TexturePointers = texturepointers; } public VertexPositionTexture[] VPT { get; private set; } public byte[] TexturePointers { get; private set; } } public Vector3 IndicatorPoint => _indicatorPoint; //public ConcurrentDictionary, float> lowpoints = new ConcurrentDictionary, float>(); /// /// This method returns geometry data AFTER animation matrix translations, local /// position/rotation translations. This is the final step of calculation. This data should /// be used only by Renderer. Any translations/vertices manipulation should happen inside /// this method or earlier /// /// /// Monsters can have more than one object. Treat it like multi-model geometry. They are all /// needed to build whole model /// /// a Vector3 to set global position /// a Quaternion to set the correct rotation. 1=90, 2=180 ... /// an animation pointer. Animation 0 is always idle /// /// an animation frame from animation id. You should pass incrementing frame and reset to 0 /// when frameCount max is hit /// /// /// FEATURE: This float (0.0 - 1.0) is used in Linear interpolation in animation frames /// blending. 0.0 means frameN, 1.0 means FrameN+1. Usually this should be a result of /// deltaTime to see if computer is capable of rendering smooth animations rather than /// constant 15 FPS /// /// public VertexPositionTexturePointersGRP GetVertexPositions(int objectId, ref Vector3 translationPosition, Quaternion rotation, ref Module_battle_debug.AnimationSystem animationSystem, double step) { if (animationSystem.AnimationFrame >= animHeader.animations[animationSystem.AnimationId].animationFrames.Length || animationSystem.AnimationFrame < 0) animationSystem.AnimationFrame = 0; AnimationFrame nextFrame = animHeader.animations[animationSystem.AnimationId].animationFrames[animationSystem.AnimationFrame]; int lastAnimationFrame = animationSystem.LastAnimationFrame; AnimationFrame[] lastAnimationFrames = animHeader.animations[animationSystem.LastAnimationId].animationFrames; lastAnimationFrame = lastAnimationFrames.Length > lastAnimationFrame ? lastAnimationFrame : lastAnimationFrames.Length - 1; AnimationFrame frame = lastAnimationFrames[lastAnimationFrame]; Object obj = geometry.objects[objectId]; int i = 0; List verts = GetVertices(obj, frame, nextFrame, step); //float minY = verts.Min(x => x.Y); //Vector2 HLPTS = FindLowHighPoints(translationPosition, rotation, frame, nextFrame, step); byte[] texturePointers = new byte[obj.cTriangles + obj.cQuads * 2]; List vpt = new List(texturePointers.Length * 3); if (objectId == 0) { Module_battle_debug.AnimationSystem _animationSystem = animationSystem; AnimationYOffset lastoffsets = AnimationYOffsets?.First(x => x.ID == _animationSystem.LastAnimationId && x.Frame == lastAnimationFrame) ?? default; AnimationYOffset nextoffsets = AnimationYOffsets?.First(x => x.ID == _animationSystem.AnimationId && x.Frame == _animationSystem.AnimationFrame) ?? default; float offsetylow = MathHelper.Lerp(lastoffsets.LowY, nextoffsets.LowY, (float)step); _indicatorPoint.X = MathHelper.Lerp(lastoffsets.MidX, nextoffsets.MidX, (float)step); _indicatorPoint.Y = MathHelper.Lerp(lastoffsets.HighY, nextoffsets.HighY, (float)step); _indicatorPoint.Z = MathHelper.Lerp(lastoffsets.MidZ, nextoffsets.MidZ, (float)step); // Move All Y axis down to 0 based on Lowest Y axis in Animation ID 0. if (OffsetY < 0) { translationPosition.Y += OffsetY; } // If any Y axis readings are lower than 0 in Animation ID >0. Bring it up to zero. if (offsetylow < 0) translationPosition.Y -= offsetylow; _indicatorPoint += translationPosition; } //Triangle parsing for (; i < obj.cTriangles; i++) { Texture2D preVarTex = (Texture2D)textures.textures[obj.triangles[i].textureIndex]; vpt.AddRange(obj.triangles[i].GenerateVPT(verts, rotation, translationPosition, preVarTex)); texturePointers[i] = obj.triangles[i].textureIndex; } //Quad parsing for (i = 0; i < obj.cQuads; i++) { Texture2D preVarTex = (Texture2D)textures.textures[obj.quads[i].textureIndex]; vpt.AddRange(obj.quads[i].GenerateVPT(verts, rotation, translationPosition, preVarTex)); texturePointers[obj.cTriangles + i * 2] = obj.quads[i].textureIndex; texturePointers[obj.cTriangles + i * 2 + 1] = obj.quads[i].textureIndex; } return new VertexPositionTexturePointersGRP(vpt.ToArray(), texturePointers); } private List GetVertices(Object @object, AnimationFrame frame, AnimationFrame nextFrame, double step) => @object.vertexData.SelectMany(vertexdata => vertexdata.vertices.Select(vertex => CalculateFrame(new VectorBoneGRP(vertex, vertexdata.boneId), frame, nextFrame, step))).ToList(); private Vector4 FindLowHighPoints(Vector3 translationPosition, Quaternion rotation, AnimationFrame frame, AnimationFrame nextFrame, double step) { List vertices = geometry.objects.SelectMany(@object => GetVertices(@object, frame, nextFrame, step)).ToList(); if (translationPosition != Vector3.Zero || rotation != Quaternion.Identity) { IEnumerable _vertices = vertices.Select(vertex => TransformVertex(vertex, translationPosition, rotation)); // midpoints return new Vector4(_vertices.Min(x => x.Y), _vertices.Max(x => x.Y), (_vertices.Min(x => x.X) + _vertices.Max(x => x.X)) / 2f, (_vertices.Min(x => x.Z) + _vertices.Max(x => x.Z)) / 2f); } else // alt midpoints return new Vector4(vertices.Min(x => x.Y), vertices.Max(x => x.Y), (vertices.Min(x => x.X) + vertices.Max(x => x.X)) / 2f, (vertices.Min(x => x.Z) + vertices.Max(x => x.Z)) / 2f); } public static Vector3 TransformVertex(Vector3 vertex, Vector3 localTranslate, Quaternion rotation) => Vector3.Transform(Vector3.Transform(vertex, rotation), Matrix.CreateTranslation(localTranslate)); /// /// Complex function that provides linear interpolation between two matrices of actual /// to-render animation frame and next frame data for blending /// /// the tuple that contains vertex and bone ident /// current animation frame to render /// /// animation frame to render that is AFTER the actual one. If last frame, then usually 0 is /// the 'next' frame /// /// /// step is variable used to determinate the progress for linear interpolation. I.e. 0 for /// current frame and 1 for next frame; 0.5 for blend of two frames /// /// private VectorBoneGRP CalculateFrame(VectorBoneGRP VBG, AnimationFrame frame, AnimationFrame nextFrame, double step) { Vector3 getvector(Matrix matrix) { Vector3 r = new Vector3( matrix.M11 * VBG.X + matrix.M41 + matrix.M12 * VBG.Z + matrix.M13 * -VBG.Y, matrix.M21 * VBG.X + matrix.M42 + matrix.M22 * VBG.Z + matrix.M23 * -VBG.Y, matrix.M31 * VBG.X + matrix.M43 + matrix.M32 * VBG.Z + matrix.M33 * -VBG.Y); r = Vector3.Transform(r, Matrix.CreateScale(skeleton.GetScale)); return r; } Vector3 rootFramePos = getvector(frame.boneMatrix[VBG.BoneID]); //get's bone matrix if (step > 0f) { Vector3 nextFramePos = getvector(nextFrame.boneMatrix[VBG.BoneID]); rootFramePos = Vector3.Lerp(rootFramePos, nextFramePos, (float)step); } return new VectorBoneGRP(rootFramePos, VBG.BoneID); } #endregion section 2 Geometry #region section 3 Animation public struct AnimationData { public uint cAnimations; public uint[] pAnimations; public Animation[] animations; } public struct Animation { public byte cFrames; public AnimationFrame[] animationFrames; } public struct AnimationFrame { private Vector3 position; public Vector3[] bonesVectorRotations; public Matrix[] boneMatrix; public Vector3 Position { get => position; set => position = value; } } /// /// Model Animation section /// /// /// /// private void ReadSection3(uint start) { br.BaseStream.Seek(start, SeekOrigin.Begin); animHeader = new AnimationData() { cAnimations = br.ReadUInt32() }; animHeader.pAnimations = new uint[animHeader.cAnimations]; for (int i = 0; i < animHeader.cAnimations; i++) { animHeader.pAnimations[i] = br.ReadUInt32(); //Console.WriteLine($"{i}|{animHeader.pAnimations[i]}"); } animHeader.animations = new Animation[animHeader.cAnimations]; for (int i = 0; i < animHeader.cAnimations; i++) //animation { br.BaseStream.Seek(start + animHeader.pAnimations[i], SeekOrigin.Begin); //Get to pointer of animation Id animHeader.animations[i] = new Animation() { cFrames = br.ReadByte() }; //Create new animation with cFrames frames animHeader.animations[i].animationFrames = new AnimationFrame[animHeader.animations[i].cFrames]; ExtapathyExtended.BitReader bitReader = new ExtapathyExtended.BitReader(br.BaseStream); for (int n = 0; n < animHeader.animations[i].cFrames; n++) //frames { //Step 1. It starts with bone0.position. Let's read that into AnimationFrames[animId]- it's only one position per frame float x = bitReader.ReadPositionType() * .01f; float y = bitReader.ReadPositionType() * .01f; float z = bitReader.ReadPositionType() * .01f; animHeader.animations[i].animationFrames[n] = n == 0 ? new AnimationFrame() { Position = new Vector3(x, y, z) } : new AnimationFrame() { Position = new Vector3( animHeader.animations[i].animationFrames[n - 1].Position.X + x, animHeader.animations[i].animationFrames[n - 1].Position.Y + y, animHeader.animations[i].animationFrames[n - 1].Position.Z + z) }; byte ModeTest = (byte)bitReader.ReadBits(1); //used to determine if additional info is required if (i == 0 && n == 0) Console.WriteLine($"{i} {n}: {ModeTest}"); animHeader.animations[i].animationFrames[n].boneMatrix = new Matrix[skeleton.cBones]; animHeader.animations[i].animationFrames[n].bonesVectorRotations = new Vector3[skeleton.cBones]; //Step 2. We read the position and we need to store the bones rotations or save base rotation if frame==0 for (int k = 0; k < skeleton.cBones; k++) //bones iterator { if (n != 0) //just like position the data for next frames are added to previous { animHeader.animations[i].animationFrames[n].bonesVectorRotations[k] = new Vector3() { X = bitReader.ReadRotationType(), Y = bitReader.ReadRotationType(), Z = bitReader.ReadRotationType() }; if (ModeTest > 0) _ = GetAdditionalRotationInformation(bitReader); Vector3 previousFrame = animHeader.animations[i].animationFrames[n - 1].bonesVectorRotations[k]; Vector3 currentFrame = animHeader.animations[i].animationFrames[n].bonesVectorRotations[k]; animHeader.animations[i].animationFrames[n].bonesVectorRotations[k] = previousFrame + currentFrame; } else //if this is zero frame, then we need to set the base rotations for bones { animHeader.animations[i].animationFrames[n].bonesVectorRotations[k] = new Vector3() { X = bitReader.ReadRotationType(), Y = bitReader.ReadRotationType(), Z = bitReader.ReadRotationType() }; if (ModeTest > 0) _ = GetAdditionalRotationInformation(bitReader); } } //Step 3. We now have all bone rotations stored into short. We need to convert that into Matrix and 360/4096 for (int k = 0; k < skeleton.cBones; k++) { Vector3 boneRotation = animHeader.animations[i].animationFrames[n].bonesVectorRotations[k]; boneRotation = Extended.S16VectorToFloat(boneRotation); //we had vector3 containing direct copy of short to float, now we need them in real floating point values boneRotation *= DEGREES; //bone rotations are in 360 scope //maki way Matrix xRot = Extended.GetRotationMatrixX(-boneRotation.X); Matrix yRot = Extended.GetRotationMatrixY(-boneRotation.Y); Matrix zRot = Extended.GetRotationMatrixZ(-boneRotation.Z); //this is the monogame way and gives same results as above. //Matrix xRot = Matrix.CreateRotationX(MathHelper.ToRadians(boneRotation.X)); //Matrix yRot = Matrix.CreateRotationY(MathHelper.ToRadians(boneRotation.Y)); //Matrix zRot = Matrix.CreateRotationZ(MathHelper.ToRadians(boneRotation.Z)); Matrix MatrixZ = Extended.MatrixMultiply_transpose(yRot, xRot); MatrixZ = Extended.MatrixMultiply_transpose(zRot, MatrixZ); if (skeleton.bones[k].parentId == 0xFFFF) //if parentId is 0xFFFF then the current bone is core aka bone0 { MatrixZ.M41 = -animHeader.animations[i].animationFrames[n].Position.X; MatrixZ.M42 = -animHeader.animations[i].animationFrames[n].Position.Y; //up/down MatrixZ.M43 = animHeader.animations[i].animationFrames[n].Position.Z; MatrixZ.M44 = 1; } else { Matrix parentBone = animHeader.animations[i].animationFrames[n].boneMatrix[skeleton.bones[k].parentId]; //gets the parent bone MatrixZ.M43 = skeleton.bones[skeleton.bones[k].parentId].Size; Matrix rMatrix = Matrix.Multiply(parentBone, MatrixZ); rMatrix.M41 = parentBone.M11 * MatrixZ.M41 + parentBone.M12 * MatrixZ.M42 + parentBone.M13 * MatrixZ.M43 + parentBone.M41; rMatrix.M42 = parentBone.M21 * MatrixZ.M41 + parentBone.M22 * MatrixZ.M42 + parentBone.M23 * MatrixZ.M43 + parentBone.M42; rMatrix.M43 = parentBone.M31 * MatrixZ.M41 + parentBone.M32 * MatrixZ.M42 + parentBone.M33 * MatrixZ.M43 + parentBone.M43; rMatrix.M44 = 1; MatrixZ = rMatrix; } animHeader.animations[i].animationFrames[n].boneMatrix[k] = MatrixZ; } } } } /// /// Some enemies use additional information that is saved for bone AFTER rotation types. We /// are still not sure what it does as enemy works without it /// /// /// private Tuple GetAdditionalRotationInformation(ExtapathyExtended.BitReader bitReader) { short unk1v = 0, unk2v = 0, unk3v = 0; byte unk1 = (byte)bitReader.ReadBits(1); if (unk1 > 0) unk1v = (short)(bitReader.ReadBits(16) + 1024); else unk1v = 1024; byte unk2 = (byte)bitReader.ReadBits(1); if (unk2 > 0) unk2v = (short)(bitReader.ReadBits(16) + 1024); else unk2v = 1024; byte unk3 = (byte)bitReader.ReadBits(1); if (unk3 > 0) unk3v = (short)(bitReader.ReadBits(16) + 1024); else unk3v = 1024; return new Tuple(unk1v, unk2v, unk3v); } public AnimationData animHeader; public int frame; public float frameperFPS = 0.0f; #endregion section 3 Animation #region section 11 Textures public struct Textures { /// /// TIM count /// public uint cTims; /// /// File pointers /// public uint[] pTims; /// /// EOF /// public uint Eof; /// /// Texture 2D wrapped in TextureHandler for mod support /// public TextureHandler[] textures; } /// /// TIMS - Textures /// /// /// /// private void ReadSection11(uint start) { #if DEBUG //Dump for debug br.BaseStream.Seek(start, SeekOrigin.Begin); using (BinaryWriter fs = new BinaryWriter(File.Create(Path.Combine(Path.GetTempPath(), $"{start}.dump"), (int)(br.BaseStream.Length - br.BaseStream.Position), FileOptions.None))) fs.Write(br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position))); #endif br.BaseStream.Seek(start, SeekOrigin.Begin); //Begin create Textures struct //populate the tim count; textures = new Textures() { cTims = br.ReadUInt32() }; //create arrays per count. textures.pTims = new uint[textures.cTims]; textures.textures = new TextureHandler[textures.cTims]; //Read pointers into array for (int i = 0; i < textures.cTims; i++) textures.pTims[i] = br.ReadUInt32(); //Read EOF textures.Eof = br.ReadUInt32(); //Read TIM -> TextureHandler into array for (int i = 0; i < textures.cTims; i++) if (buffer[start + textures.pTims[i]] == 0x10) { TIM2 tm = new TIM2(buffer, start + textures.pTims[i]); //broken textures.textures[i] = TextureHandler.Create($"{fileName}_{i/*.ToString("D2")*/}", tm, 0);// tm.GetTexture(0); } else Debug.WriteLine($"DEBUG: {this}.{this.id}.{start + textures.pTims[i]} :: Not a tim file!"); } public Textures textures; private BinaryReader br; //private float lowpoint; #endregion section 11 Textures public enum EntityType { Monster, Character, Weapon }; public Debug_battleDat() { } /// /// Creates new instance of DAT class that provides every sections parsed into structs and /// helper functions for renderer /// /// This number is used in c0m(fileId) or d(fileId)cXYZ /// Supply Monster, character or weapon (0,1,2) /// Used only in character or weapon to supply for d(fileId)[c/w](additionalFileId) public static Debug_battleDat Load(int fileId, EntityType entityType, int additionalFileId = -1, Debug_battleDat skeletonReference = null) { Debug_battleDat r = new Debug_battleDat() { id = fileId, altid = additionalFileId }; Console.WriteLine($"DEBUG: Creating new BattleDat with {fileId},{entityType},{additionalFileId}"); ArchiveWorker aw = new ArchiveWorker(Memory.Archives.A_BATTLE); char et = entityType == EntityType.Weapon ? 'w' : entityType == EntityType.Character ? 'c' : default; string fileName = entityType == EntityType.Monster ? $"c0m{r.id.ToString("D03")}" : entityType == EntityType.Character || entityType == EntityType.Weapon ? $"d{fileId.ToString("x")}{et}{additionalFileId.ToString("D03")}" : string.Empty; r.entityType = entityType; if (string.IsNullOrEmpty(fileName)) return null; string path = null; string searchstring = ""; if (et != default) { searchstring = $"d{fileId.ToString("x")}{et}"; IEnumerable test = aw.GetListOfFiles().Where(x => x.IndexOf(searchstring, StringComparison.OrdinalIgnoreCase) >= 0); path = test.FirstOrDefault(x => x.ToLower().Contains(fileName)); if (string.IsNullOrWhiteSpace(path) && test.Count() > 0 && entityType == EntityType.Character) path = test.First(); } else path = aw.GetListOfFiles().FirstOrDefault(x => x.ToLower().Contains(fileName)); r.fileName = fileName; if (!string.IsNullOrWhiteSpace(path)) r.buffer = ArchiveWorker.GetBinaryFile(Memory.Archives.A_BATTLE, path); if (r.buffer == null || r.buffer.Length < 0) { Debug.WriteLine($"Search String: {searchstring} Not Found skipping {entityType}; So resulting file buffer is null."); return null; } r.ExportFile(); r.LoadFile(skeletonReference); r.FindAllLowHighPoints(); return r; } private void FindAllLowHighPoints() { if (entityType == EntityType.Character || entityType == EntityType.Monster) { // Get baseline from running function on only Animation 0; if (animHeader.animations == null) return; List baseline = animHeader.animations[0].animationFrames.Select(x => FindLowHighPoints(Vector3.Zero, Quaternion.Identity, x, x, 0f)).ToList(); //X is lowY, Y is high Y, Z is mid x, W is mid z float baselinelowY = baseline.Min(x => x.X); float baselinehighY = baseline.Max(x => x.Y); float offsetY = 0f; if (Math.Abs(baselinelowY) < BaseLineMaxYFilter) { offsetY -= baselinelowY; baselinehighY += offsetY; } // Default indicator point _indicatorPoint = new Vector3(0, baselinehighY, 0); // Need to add this later to bring baselinelow to 0. OffsetY = offsetY; // Brings all Y values less than baselinelow to baselinelow AnimationYOffsets = animHeader.animations.SelectMany((animation, animationid) => animation.animationFrames.Select((animationframe, animationframenumber) => new AnimationYOffset(animationid, animationframenumber, FindLowHighPoints(OffsetYVector, Quaternion.Identity, animationframe, animationframe, 0f)))).ToList(); } } private List AnimationYOffsets; private Vector3 _indicatorPoint; private float OffsetY { get; set; } private Vector3 OffsetYVector => new Vector3(0f, OffsetY, 0f); public struct AnimationYOffset { public int ID { get; private set; } public int Frame { get; private set; } public float LowY { get; private set; } public float HighY { get; private set; } public float MidX { get; private set; } public float MidZ { get; private set; } public AnimationYOffset(int iD, int frame, Vector4 lowhigh) : this(iD, frame, lowhigh.X, lowhigh.Y, lowhigh.Z, lowhigh.W) { } public AnimationYOffset(int iD, int frame, float low, float high, float midx, float midz) { ID = iD; Frame = frame; LowY = low; HighY = high; MidX = midx; MidZ = midz; } public override string ToString() => $"[{ID}, {Frame}, {LowY}, {HighY}, {MidX}, {MidZ}]"; } private void ExportFile() { #if _WINDOWS && DEBUG try { string targetdir = @"d:\"; if (Directory.Exists(targetdir)) { DriveInfo[] drivei = DriveInfo.GetDrives().Where(x => x.Name.IndexOf(Path.GetPathRoot(targetdir), StringComparison.OrdinalIgnoreCase) >= 0).ToArray(); DirectoryInfo di = new DirectoryInfo(targetdir); if (!di.Attributes.HasFlag(FileAttributes.ReadOnly) && drivei.Count() == 1 && drivei[0].DriveType == DriveType.Fixed) Extended.DumpBuffer(buffer, Path.Combine(targetdir, "out.dat")); } } catch (IOException) { } #endif } private Debug_battleDat LoadFile(Debug_battleDat skeletonReference) { using (br = new BinaryReader(new MemoryStream(buffer))) { if (br.BaseStream.Length - br.BaseStream.Position < 4) return null; Init(); switch (entityType) { case EntityType.Monster: return LoadMonster(); case EntityType.Character: return LoadCharacter(); case EntityType.Weapon: return LoadWeapon(skeletonReference); } return null; } } private void Init() { datFile = new DatFile { cSections = br.ReadUInt32() }; datFile.pSections = new uint[datFile.cSections]; for (int i = 0; i < datFile.cSections; i++) datFile.pSections[i] = br.ReadUInt32(); datFile.eof = br.ReadUInt32(); } private Debug_battleDat LoadMonster() { if (id == 127) { // per wiki 127 only have 7 & 8 //ReadSection7(datFile.pSections[6], br, fileName); //ReadSection8(datFile.pSections[7]); return null; } ReadSection1(datFile.pSections[0]); ReadSection3(datFile.pSections[2]); // animation data ReadSection2(datFile.pSections[1]); //ReadSection4(datFile.pSections[3]); ReadSection5(datFile.pSections[4], datFile.pSections[5]); //ReadSection6(datFile.pSections[5]); ReadSection7(datFile.pSections[6]); //ReadSection8(datFile.pSections[7]); // battle scripts/ai ReadSection9(datFile.pSections[8], datFile.pSections[9]); //AKAO sounds //ReadSection10(datFile.pSections[9], datFile.pSections[10], br, fileName); ReadSection11(datFile.pSections[10]); return this; } private Debug_battleDat LoadCharacter() { ReadSection1(datFile.pSections[0]); ReadSection3(datFile.pSections[2]); ReadSection2(datFile.pSections[1]); if (id == 7 && entityType == EntityType.Character) { ReadSection11(datFile.pSections[8]); ReadSection5(datFile.pSections[5], datFile.pSections[6]); } else ReadSection11(datFile.pSections[5]); return this; } private Debug_battleDat LoadWeapon(Debug_battleDat skeletonReference) { if (id != 1 && id != 9) { ReadSection1(datFile.pSections[0]); ReadSection3(datFile.pSections[2]); ReadSection2(datFile.pSections[1]); ReadSection5(datFile.pSections[3], datFile.pSections[4]); ReadSection11(datFile.pSections[6]); } else if (skeletonReference != null) { skeleton = skeletonReference.skeleton; animHeader = skeletonReference.animHeader; ReadSection2(datFile.pSections[0]); ReadSection5(datFile.pSections[1], datFile.pSections[2]); ReadSection11(datFile.pSections[4]); } return this; } /// /// Sounds /// /// /// /// /// private void ReadSection9(uint start, uint end) { //Contains AKAO sequences(can be empty). //Offset Length Description //0 2 bytes Number of AKAOs //2 nbAKAOs * 2 bytes AKAOs Positions //2 + nbAKAOs * 2 2 bytes End of section 9 //4 + nbAKAOs * 2 Varies* nbAKAOs AKAOs // nothing final in here just was trying to dump data to see what was there. // http://wiki.ffrtt.ru/index.php?title=FF7/Field/Script/Opcodes/F2_AKAO related? br.BaseStream.Seek(start, SeekOrigin.Begin); uint[] offsets = new uint[br.ReadUInt16()]; for (ushort i = 0; i < offsets.Length; i++) { ushort offset = br.ReadUInt16(); if (offset == 0) continue; offsets[i] = offset + start; } uint newend = br.ReadUInt16() + start; if (newend < end) end = newend; List sortedoffsets = offsets.Where(x => x > 0).Distinct().OrderBy(x => x).ToList(); Dictionary dataatoffsets = new Dictionary(sortedoffsets.Count); for (ushort i = 0; i < sortedoffsets.Count; i++) { uint offset = sortedoffsets[i]; uint localend = end; if (i + 1 < sortedoffsets.Count) localend = sortedoffsets[i + 1]; br.BaseStream.Seek(offset, SeekOrigin.Begin); if (offset < localend) dataatoffsets.Add(offset, br.ReadBytes(checked((int)(localend - offset)))); } } public List Sequences { get; private set; } public struct AnimationSequence { public int id; public uint offset; public byte[] data; /// /// Test-Reason for list is so i can go read the data with out removing it. /// public List AnimationQueue { get; set; } //public static Dictionary> ParseData = new Dictionary>{ // { 0xA3, (byte[] data, int i) => { } } }; public void GenerateQueue(Debug_battleDat dat) { AnimationQueue = new List(); for (int i = 0; data != null && i < data.Length; i++) { byte b; byte[] data = this.data; byte get(int _i = -1) { return b = data[_i < 0 ? i : _i]; } if (get() < (dat.animHeader.animations?.Length ?? 0)) { AnimationQueue.Add(b); } //else switch(b) //{ // case 0xA3: // // following value is animation. // break; // case 0xE6: // switch (get(++i)) // { // case 0x03: // i += 1; // break; // } // break; // case 0xEA: // switch (get(++i)) // { // case 0x05: // i += 1; // break; // case 0x06: // i += 2; // break; // } // break; // default: // i++;//skip next byte //as might not be a animation. // break; //} } } } /// /// Animation Sequences /// /// /// /// /// /// private void ReadSection5(uint start, uint end) { // nothing final in here just was trying to dump data to see what was there. br.BaseStream.Seek(start, SeekOrigin.Begin); uint[] offsets = new uint[br.ReadUInt16()]; for (ushort i = 0; i < offsets.Length; i++) { ushort offset = br.ReadUInt16(); if (offset == 0) continue; offsets[i] = offset + start; } List sortedoffsets = offsets.Where(x => x > 0).Distinct().OrderBy(x => x).ToList(); Sequences = new List(sortedoffsets.Count); //Dictionary dataatoffsets = new Dictionary(sortedoffsets.Count); for (ushort i = 0; i < sortedoffsets.Count; i++) { uint offset = sortedoffsets[i]; //uint tie = endpoint; //if (i + 1 < t.Count) // tie = t[i + 1]; uint localend = offset; br.BaseStream.Seek(offset, SeekOrigin.Begin); do localend++; while (br.ReadByte() != 0xa2 && br.BaseStream.Position < end && (i + 1 < sortedoffsets.Count ? br.BaseStream.Position < sortedoffsets[i + 1] : true)); br.BaseStream.Seek(offset, SeekOrigin.Begin); foreach (var offsetindexed in offsets.Select((value, index) => new { value, index }).Where(x => x.value == offset)) { AnimationSequence sequence = new AnimationSequence { id = offsetindexed.index, offset = offsetindexed.value, data = br.ReadBytes(checked((int)(localend - offset))) }; sequence.GenerateQueue(this); Sequences.Add(sequence); } } //foreach (KeyValuePair ob in dataatoffsets) //{ // Debug.Write($"{ob.Key}({string.Format("{0:x}", offsets[ob.Key])}) - "); // for (int i = 0; i< ob.Value.Length; i++) // { // byte b; // byte Get(int pos = -1) // { return b = ob.Value[pos<0?i:pos]; } // switch (Get()) // { // case 0xa5: // Debug.Write("{Aura-A5}"); // switch (Get(++i)) // { // case 0x00: // Debug.Write("{Magic-00}"); // break; // case 0x01: // Debug.Write("{GF-01}"); // break; // case 0x02: // Debug.Write("{Limit-02}"); // break; // case 0x03: // Debug.Write("{Finisher-03}"); // break; // case 0x04: // Debug.Write("{Enemy Magic-04}"); // break; // default: // Debug.Write(string.Format("{0:x2}", b)); // break; // } // break; // case 0xb5: // Debug.Write("Sound-B5"); // break; // case 0xbb: // Debug.Write("{Effect-BB}"); // break; // case 0xa2: // Debug.Write("{Return-A2}"); // break; // case 0xa0: // Debug.Write("{Loop-A0}"); // Debug.Write("{Anim}"); // Debug.Write(string.Format("{0:x2} ", Get(++i))); // break; // case 0xc1: // if (Get(i + 2) == 0xe5 && Get(i + 3) == 0x7f) // { // Debug.Write("{Repeat-C1}"); // //loop 0x1E times Animation 28 // //C1 1E E5 7F 28 // Debug.Write("{Count}"); // Debug.Write($"{Get(++i)} "); // Debug.Write(string.Format("{0:x2} ", Get(++i))); // Debug.Write(string.Format("{0:x2} ", Get(++i))); // Debug.Write("{Anim}"); // Debug.Write(string.Format("{0:x2} ", Get(++i))); // } // else if(Get(i + 1) == 0x00 && Get(i + 2) == 0xe5 && Get(i + 3) == 0x0f) // { // //Return to home location. // //C1 00 E5 0F // Debug.Write("{Place model at home location}"); // } // else // Debug.Write(string.Format(" {0:x2}", Get())); // break; // case 0xc3: // Debug.Write("{Special-C3}"); // //C3 7F C5 FF E5 7F E7 F9 //wait till previous sequence is complete. // //C3 0c e1 23 e5 7f ba // //C3 08 d8 00 01 e5 08 {04,05} // break; // case 0x91: // Debug.Write("{Text-91}"); // break; // case 0x1e: // Debug.Write("{TextREF-1E}"); // break; // case 0xa3: // Debug.Write("{End-A3}"); // Debug.Write("{Anim}"); // Debug.Write(string.Format("{0:x2} ", Get(++i))); // i += 2; // break; // case 0xa8: // Debug.Write("{Visibility-A8}"); // switch (Get(++i)) // { // case 0x02: // Debug.Write("{Hide-02}"); // break; // case 0x03: // Debug.Write("{Show-03}"); // break; // default: // Debug.Write(string.Format("{0:x2}", b)); // break; // } // Debug.Write("{Anim}"); // Debug.Write(string.Format("{0:x2} ", Get(++i))); // break; // default: // Debug.Write(string.Format(" {0:x2}", b)); // break; // } // } // Debug.Write($" ({ob.Value.Length} length)\n"); //} } public int GetId => id; public int altid { get; private set; } public int id { get; private set; } public EntityType entityType { get; private set; } public string fileName { get; private set; } } }