using Lumina.Data; using Lumina.Extensions; using System.Runtime.InteropServices; using System.Text; using static Lumina.Data.Parsing.MdlStructs; namespace MareSynchronos.Interop.GameModel; #pragma warning disable S1104 // Fields should not have public accessibility // This code is completely and shamelessly borrowed from Penumbra to load V5 and V6 model files. // Original Source: https://github.com/Ottermandias/Penumbra.GameData/blob/main/Files/MdlFile.cs public class MdlFile { public const int V5 = 0x01000005; public const int V6 = 0x01000006; public const uint NumVertices = 17; public const uint FileHeaderSize = 0x44; // Raw data to write back. public uint Version = 0x01000005; public float Radius; public float ModelClipOutDistance; public float ShadowClipOutDistance; public byte BgChangeMaterialIndex; public byte BgCrestChangeMaterialIndex; public ushort CullingGridCount; public byte Flags3; public byte Unknown6; public ushort Unknown8; public ushort Unknown9; // Offsets are stored relative to RuntimeSize instead of file start. public uint[] VertexOffset = [0, 0, 0]; public uint[] IndexOffset = [0, 0, 0]; public uint[] VertexBufferSize = [0, 0, 0]; public uint[] IndexBufferSize = [0, 0, 0]; public byte LodCount; public bool EnableIndexBufferStreaming; public bool EnableEdgeGeometry; public ModelFlags1 Flags1; public ModelFlags2 Flags2; public VertexDeclarationStruct[] VertexDeclarations = []; public ElementIdStruct[] ElementIds = []; public MeshStruct[] Meshes = []; public BoundingBoxStruct[] BoneBoundingBoxes = []; public LodStruct[] Lods = []; public ExtraLodStruct[] ExtraLods = []; public MdlFile(string filePath) { using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); using var r = new LuminaBinaryReader(stream); var header = LoadModelFileHeader(r); LodCount = header.LodCount; VertexBufferSize = header.VertexBufferSize; IndexBufferSize = header.IndexBufferSize; VertexOffset = header.VertexOffset; IndexOffset = header.IndexOffset; var dataOffset = FileHeaderSize + header.RuntimeSize + header.StackSize; for (var i = 0; i < LodCount; ++i) { VertexOffset[i] -= dataOffset; IndexOffset[i] -= dataOffset; } VertexDeclarations = new VertexDeclarationStruct[header.VertexDeclarationCount]; for (var i = 0; i < header.VertexDeclarationCount; ++i) VertexDeclarations[i] = VertexDeclarationStruct.Read(r); _ = LoadStrings(r); var modelHeader = LoadModelHeader(r); ElementIds = new ElementIdStruct[modelHeader.ElementIdCount]; for (var i = 0; i < modelHeader.ElementIdCount; i++) ElementIds[i] = ElementIdStruct.Read(r); Lods = new LodStruct[3]; for (var i = 0; i < 3; i++) { var lod = r.ReadStructure(); if (i < LodCount) { lod.VertexDataOffset -= dataOffset; lod.IndexDataOffset -= dataOffset; } Lods[i] = lod; } ExtraLods = modelHeader.Flags2.HasFlag(ModelFlags2.ExtraLodEnabled) ? r.ReadStructuresAsArray(3) : []; Meshes = new MeshStruct[modelHeader.MeshCount]; for (var i = 0; i < modelHeader.MeshCount; i++) Meshes[i] = MeshStruct.Read(r); } private ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r) { var header = ModelFileHeader.Read(r); Version = header.Version; EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; EnableEdgeGeometry = header.EnableEdgeGeometry; return header; } private ModelHeader LoadModelHeader(BinaryReader r) { var modelHeader = r.ReadStructure(); Radius = modelHeader.Radius; Flags1 = modelHeader.Flags1; Flags2 = modelHeader.Flags2; ModelClipOutDistance = modelHeader.ModelClipOutDistance; ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; CullingGridCount = modelHeader.CullingGridCount; Flags3 = modelHeader.Flags3; Unknown6 = modelHeader.Unknown6; Unknown8 = modelHeader.Unknown8; Unknown9 = modelHeader.Unknown9; BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex; BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex; return modelHeader; } private static (uint[], string[]) LoadStrings(BinaryReader r) { var stringCount = r.ReadUInt16(); r.ReadUInt16(); var stringSize = (int)r.ReadUInt32(); var stringData = r.ReadBytes(stringSize); var start = 0; var strings = new string[stringCount]; var offsets = new uint[stringCount]; for (var i = 0; i < stringCount; ++i) { var span = stringData.AsSpan(start); var idx = span.IndexOf((byte)'\0'); strings[i] = Encoding.UTF8.GetString(span[..idx]); offsets[i] = (uint)start; start = start + idx + 1; } return (offsets, strings); } [StructLayout(LayoutKind.Sequential)] public unsafe struct ModelHeader { // MeshHeader public float Radius; public ushort MeshCount; public ushort AttributeCount; public ushort SubmeshCount; public ushort MaterialCount; public ushort BoneCount; public ushort BoneTableCount; public ushort ShapeCount; public ushort ShapeMeshCount; public ushort ShapeValueCount; public byte LodCount; public ModelFlags1 Flags1; public ushort ElementIdCount; public byte TerrainShadowMeshCount; public ModelFlags2 Flags2; public float ModelClipOutDistance; public float ShadowClipOutDistance; public ushort CullingGridCount; public ushort TerrainShadowSubmeshCount; public byte Flags3; public byte BGChangeMaterialIndex; public byte BGCrestChangeMaterialIndex; public byte Unknown6; public ushort BoneTableArrayCountTotal; public ushort Unknown8; public ushort Unknown9; private fixed byte _padding[6]; } public struct ShapeStruct { public uint StringOffset; public ushort[] ShapeMeshStartIndex; public ushort[] ShapeMeshCount; public static ShapeStruct Read(LuminaBinaryReader br) { ShapeStruct ret = new ShapeStruct(); ret.StringOffset = br.ReadUInt32(); ret.ShapeMeshStartIndex = br.ReadUInt16Array(3); ret.ShapeMeshCount = br.ReadUInt16Array(3); return ret; } } [Flags] public enum ModelFlags1 : byte { DustOcclusionEnabled = 0x80, SnowOcclusionEnabled = 0x40, RainOcclusionEnabled = 0x20, Unknown1 = 0x10, LightingReflectionEnabled = 0x08, WavingAnimationDisabled = 0x04, LightShadowDisabled = 0x02, ShadowDisabled = 0x01, } [Flags] public enum ModelFlags2 : byte { Unknown2 = 0x80, BgUvScrollEnabled = 0x40, EnableForceNonResident = 0x20, ExtraLodEnabled = 0x10, ShadowMaskEnabled = 0x08, ForceLodRangeEnabled = 0x04, EdgeGeometryEnabled = 0x02, Unknown3 = 0x01 } public struct VertexDeclarationStruct { // There are always 17, but stop when stream = -1 public VertexElement[] VertexElements; public static VertexDeclarationStruct Read(LuminaBinaryReader br) { VertexDeclarationStruct ret = new VertexDeclarationStruct(); var elems = new List(); // Read the vertex elements that we need var thisElem = br.ReadStructure(); do { elems.Add(thisElem); thisElem = br.ReadStructure(); } while (thisElem.Stream != 255); // Skip the number of bytes that we don't need to read // We skip elems.Count * 9 because we had to read the invalid element int toSeek = 17 * 8 - (elems.Count + 1) * 8; br.Seek(br.BaseStream.Position + toSeek); ret.VertexElements = elems.ToArray(); return ret; } } } #pragma warning restore S1104 // Fields should not have public accessibility