From 5c52ab2b2b066262040ea2bf942c747ca120d46a Mon Sep 17 00:00:00 2001 From: bird_egop Date: Tue, 26 Aug 2025 04:29:30 +0300 Subject: [PATCH] msh and cp converters. Mesh broken. --- {MissionTmaLib => Common}/IntFloatValue.cs | 2 +- MissionTmaLib/GameObjectSetting.cs | 4 +- ParkanPlayground/CpDatEntryConverter.cs | 2 +- ParkanPlayground/Msh01.cs | 90 +++++++ ParkanPlayground/Msh02.cs | 186 ++++++++++++++ ParkanPlayground/Msh06.cs | 32 +++ ParkanPlayground/Msh07.cs | 51 ++++ ParkanPlayground/Msh0A.cs | 49 ++++ ParkanPlayground/Msh0D.cs | 48 ++++ ParkanPlayground/MshConverter.cs | 280 +++++++++++++-------- ParkanPlayground/Program.cs | 5 +- README.md | 13 +- 12 files changed, 651 insertions(+), 111 deletions(-) rename {MissionTmaLib => Common}/IntFloatValue.cs (92%) create mode 100644 ParkanPlayground/Msh01.cs create mode 100644 ParkanPlayground/Msh02.cs create mode 100644 ParkanPlayground/Msh06.cs create mode 100644 ParkanPlayground/Msh07.cs create mode 100644 ParkanPlayground/Msh0A.cs create mode 100644 ParkanPlayground/Msh0D.cs diff --git a/MissionTmaLib/IntFloatValue.cs b/Common/IntFloatValue.cs similarity index 92% rename from MissionTmaLib/IntFloatValue.cs rename to Common/IntFloatValue.cs index b07eb25..e22e146 100644 --- a/MissionTmaLib/IntFloatValue.cs +++ b/Common/IntFloatValue.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -namespace MissionTmaLib; +namespace Common; [DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")] public class IntFloatValue(Span span) diff --git a/MissionTmaLib/GameObjectSetting.cs b/MissionTmaLib/GameObjectSetting.cs index 090b0a2..c1d18e7 100644 --- a/MissionTmaLib/GameObjectSetting.cs +++ b/MissionTmaLib/GameObjectSetting.cs @@ -1,3 +1,5 @@ -namespace MissionTmaLib; +using Common; + +namespace MissionTmaLib; public record GameObjectSetting(int SettingType, IntFloatValue Unk1, IntFloatValue Unk2, IntFloatValue Unk3, string Name); \ No newline at end of file diff --git a/ParkanPlayground/CpDatEntryConverter.cs b/ParkanPlayground/CpDatEntryConverter.cs index 5981805..b6e4f64 100644 --- a/ParkanPlayground/CpDatEntryConverter.cs +++ b/ParkanPlayground/CpDatEntryConverter.cs @@ -26,7 +26,7 @@ namespace ParkanPlayground; public class CpDatEntryConverter { const string gameRoot = "C:\\Program Files (x86)\\Nikita\\Iron Strategy"; - const string missionTmaPath = $"{gameRoot}\\MISSIONS\\Single.01\\data.tma"; + const string missionTmaPath = $"{gameRoot}\\MISSIONS\\Campaign\\Campaign.01\\Mission.01\\data.tma"; const string staticRlbPath = $"{gameRoot}\\static.rlb"; const string objectsRlbPath = $"{gameRoot}\\objects.rlb"; diff --git a/ParkanPlayground/Msh01.cs b/ParkanPlayground/Msh01.cs new file mode 100644 index 0000000..672c552 --- /dev/null +++ b/ParkanPlayground/Msh01.cs @@ -0,0 +1,90 @@ +using System.Buffers.Binary; +using NResLib; + +namespace ParkanPlayground; + +public static class Msh01 +{ + public static Msh01Component ReadComponent(FileStream mshFs, NResArchive archive) + { + var headerFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00"); + + if (headerFileEntry is null) + { + throw new Exception("Archive doesn't contain header file (01)"); + } + + var data = new byte[headerFileEntry.ElementCount * headerFileEntry.ElementSize]; + mshFs.Seek(headerFileEntry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + var dataSpan = data.AsSpan(); + + var elements = new List((int)headerFileEntry.ElementCount); + for (var i = 0; i < headerFileEntry.ElementCount; i++) + { + var element = new SubMesh() + { + Type1 = dataSpan[i * headerFileEntry.ElementSize + 0], + Type2 = dataSpan[i * headerFileEntry.ElementSize + 1], + ParentIndex = + BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 2)), + OffsetIntoFile13 = + BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 4)), + IndexInFile08 = + BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 6)), + State00 = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 8)), + State01 = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 10)), + State10 = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 12)), + State11 = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 14)), + State20 = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 16)), + State21 = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 18)), + S20 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 20)), + S22 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 22)), + S24 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 24)), + S26 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 26)), + S28 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 28)), + S30 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 30)), + S32 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 32)), + S34 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 34)), + S36 = BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 36)) + }; + elements.Add(element); + } + + return new Msh01Component() + { + Elements = elements + }; + } + + + public class Msh01Component + { + public List Elements { get; set; } + } + + public class SubMesh + { + public byte Type1 { get; set; } + public byte Type2 { get; set; } + public short ParentIndex { get; set; } + public short OffsetIntoFile13 { get; set; } + public short IndexInFile08 { get; set; } + public ushort State00 { get; set; } + public ushort State01 { get; set; } + public ushort State10 { get; set; } + public ushort State11 { get; set; } + public ushort State20 { get; set; } + public ushort State21 { get; set; } + public short S20 { get; set; } + public short S22 { get; set; } + public short S24 { get; set; } + public short S26 { get; set; } + public short S28 { get; set; } + public short S30 { get; set; } + public short S32 { get; set; } + public short S34 { get; set; } + public short S36 { get; set; } + } +} \ No newline at end of file diff --git a/ParkanPlayground/Msh02.cs b/ParkanPlayground/Msh02.cs new file mode 100644 index 0000000..9d7eecd --- /dev/null +++ b/ParkanPlayground/Msh02.cs @@ -0,0 +1,186 @@ +using System.Buffers.Binary; +using Common; +using NResLib; + +namespace ParkanPlayground; + +public static class Msh02 +{ + public static Msh02Component ReadComponent(FileStream mshFs, NResArchive archive) + { + var fileEntry = archive.Files.FirstOrDefault(x => x.FileType == "02 00 00 00"); + + if (fileEntry is null) + { + throw new Exception("Archive doesn't contain 02 component"); + } + + var data = new byte[fileEntry.FileLength]; + mshFs.Seek(fileEntry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + var header = data.AsSpan(0, 0x8c); // 140 bytes header + + var center = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x60)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x64)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x68)) + ); + var centerW = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x6c)); + + var bb = new BoundingBox(); + bb.Vec1 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8)) + ); + bb.Vec2 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20)) + ); + bb.Vec3 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32)) + ); + bb.Vec4 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44)) + ); + bb.Vec5 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56)) + ); + bb.Vec6 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68)) + ); + bb.Vec7 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80)) + ); + bb.Vec8 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(84)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(88)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(92)) + ); + + var bottom = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(112)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(116)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(120)) + ); + + var top = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(124)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(128)), + BinaryPrimitives.ReadSingleLittleEndian(header.Slice(132)) + ); + + var xyRadius = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(136)); + + + List elements = new List(); + var skippedHeader = data.AsSpan(0x8c); // skip header + for (var i = 0; i < fileEntry.ElementCount; i++) + { + var element = new Msh02Element(); + element.StartIndexIn07 = + BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 0)); + element.CountIn07 = + BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 2)); + element.StartOffsetIn0d = + BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 4)); + element.ByteLengthIn0D = + BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 6)); + element.LocalMinimum = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 8)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 12)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 16)) + ); + element.LocalMaximum = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 20)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 24)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 28)) + ); + element.Center = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 32)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 36)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 40)) + ); + element.Vector4 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 44)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 48)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 52)) + ); + element.Vector5 = new Vector3( + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 56)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 60)), + BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 64)) + ); + elements.Add(element); + + _ = 5; + } + + return new Msh02Component() + { + Header = new Msh02Header() + { + BoundingBox = bb, + Center = center, + CenterW = centerW, + Bottom = bottom, + Top = top, + XYRadius = xyRadius + }, + Elements = elements + }; + } + + public class Msh02Component + { + public Msh02Header Header { get; set; } + public List Elements { get; set; } + } + + public class Msh02Header + { + public BoundingBox BoundingBox { get; set; } + public Vector3 Center { get; set; } + public float CenterW { get; set; } + public Vector3 Bottom { get; set; } + public Vector3 Top { get; set; } + public float XYRadius { get; set; } + } + + public class BoundingBox + { + public Vector3 Vec1 { get; set; } + public Vector3 Vec2 { get; set; } + public Vector3 Vec3 { get; set; } + public Vector3 Vec4 { get; set; } + public Vector3 Vec5 { get; set; } + public Vector3 Vec6 { get; set; } + public Vector3 Vec7 { get; set; } + public Vector3 Vec8 { get; set; } + } + + public class Msh02Element + { + public ushort StartIndexIn07 { get; set; } + public ushort CountIn07 { get; set; } + public ushort StartOffsetIn0d { get; set; } + public ushort ByteLengthIn0D { get; set; } + public Vector3 LocalMinimum { get; set; } + public Vector3 LocalMaximum { get; set; } + public Vector3 Center { get; set; } + public Vector3 Vector4 { get; set; } + public Vector3 Vector5 { get; set; } + } +} \ No newline at end of file diff --git a/ParkanPlayground/Msh06.cs b/ParkanPlayground/Msh06.cs new file mode 100644 index 0000000..8d2622c --- /dev/null +++ b/ParkanPlayground/Msh06.cs @@ -0,0 +1,32 @@ +using System.Buffers.Binary; +using NResLib; + +namespace ParkanPlayground; + +public static class Msh06 +{ + public static List ReadComponent( + FileStream mshFs, NResArchive archive) + { + var entry = archive.Files.FirstOrDefault(x => x.FileType == "06 00 00 00"); + + if (entry is null) + { + throw new Exception("Archive doesn't contain file (06)"); + } + + var data = new byte[entry.ElementCount * entry.ElementSize]; + mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + var elements = new List((int)entry.ElementCount); + for (var i = 0; i < entry.ElementCount; i++) + { + elements.Add( + BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(i * 2)) + ); + } + + return elements; + } +} \ No newline at end of file diff --git a/ParkanPlayground/Msh07.cs b/ParkanPlayground/Msh07.cs new file mode 100644 index 0000000..78c7bdd --- /dev/null +++ b/ParkanPlayground/Msh07.cs @@ -0,0 +1,51 @@ +using System.Buffers.Binary; +using NResLib; + +namespace ParkanPlayground; + +public static class Msh07 +{ + public static List ReadComponent( + FileStream mshFs, NResArchive archive) + { + var entry = archive.Files.FirstOrDefault(x => x.FileType == "07 00 00 00"); + + if (entry is null) + { + throw new Exception("Archive doesn't contain file (07)"); + } + + var data = new byte[entry.ElementCount * entry.ElementSize]; + mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + var elementBytes = data.Chunk(16); + + var elements = elementBytes.Select(x => new Msh07Element() + { + Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)), + Magic02 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)), + Magic04 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)), + OffsetX = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(8)), + OffsetY = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(10)), + OffsetZ = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(12)), + Magic14 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(14)), + }).ToList(); + + return elements; + } + + public class Msh07Element + { + public ushort Flags { get; set; } + public ushort Magic02 { get; set; } + public uint Magic04 { get; set; } + // normalized vector X, need to divide by 32767 to get float in range -1..1 + public short OffsetX { get; set; } + // normalized vector Y, need to divide by 32767 to get float in range -1..1 + public short OffsetY { get; set; } + // normalized vector Z, need to divide by 32767 to get float in range -1..1 + public short OffsetZ { get; set; } + public ushort Magic14 { get; set; } + } +} \ No newline at end of file diff --git a/ParkanPlayground/Msh0A.cs b/ParkanPlayground/Msh0A.cs new file mode 100644 index 0000000..a5a0979 --- /dev/null +++ b/ParkanPlayground/Msh0A.cs @@ -0,0 +1,49 @@ +using System.Buffers.Binary; +using System.Text; +using NResLib; + +namespace ParkanPlayground; + +public class Msh0A +{ + public static List ReadComponent(FileStream mshFs, NResArchive archive) + { + var aFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "0A 00 00 00"); + + if (aFileEntry is null) + { + throw new Exception("Archive doesn't contain 0A component"); + } + + var data = new byte[aFileEntry.FileLength]; + mshFs.Seek(aFileEntry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + int pos = 0; + var strings = new List(); + while (pos < data.Length) + { + var len = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(pos)); + if (len == 0) + { + pos += 4; // empty entry, no string attached + strings.Add(""); // add empty string + } + else + { + // len is not 0, we need to read it + var strBytes = data.AsSpan(pos + 4, len); + var str = Encoding.UTF8.GetString(strBytes); + strings.Add(str); + pos += len + 4 + 1; // skip length prefix and string itself, +1, because it's null-terminated + } + } + + if (strings.Count != aFileEntry.ElementCount) + { + throw new Exception("String count mismatch in 0A component"); + } + + return strings; + } +} \ No newline at end of file diff --git a/ParkanPlayground/Msh0D.cs b/ParkanPlayground/Msh0D.cs new file mode 100644 index 0000000..4e45689 --- /dev/null +++ b/ParkanPlayground/Msh0D.cs @@ -0,0 +1,48 @@ +using System.Buffers.Binary; +using NResLib; + +namespace ParkanPlayground; + +public static class Msh0D +{ + public const int ElementSize = 20; + + public static List ReadComponent( + FileStream mshFs, NResArchive archive) + { + var entry = archive.Files.FirstOrDefault(x => x.FileType == "0D 00 00 00"); + + if (entry is null) + { + throw new Exception("Archive doesn't contain file (0D)"); + } + + var data = new byte[entry.ElementCount * entry.ElementSize]; + mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + var elementBytes = data.Chunk(ElementSize); + + var elements = elementBytes.Select(x => new Msh0DElement() + { + Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)), + Magic04 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)), + number_of_triangles = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)), + IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(10)), + Magic0C = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(14)), + IndexInto03 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(16)), + }).ToList(); + + return elements; + } + + public class Msh0DElement + { + public uint Flags { get; set; } + public uint Magic04 { get; set; } + public ushort number_of_triangles { get; set; } + public int IndexInto06 { get; set; } + public ushort Magic0C { get; set; } + public int IndexInto03 { get; set; } + } +} \ No newline at end of file diff --git a/ParkanPlayground/MshConverter.cs b/ParkanPlayground/MshConverter.cs index 3b6730f..92ac0b1 100644 --- a/ParkanPlayground/MshConverter.cs +++ b/ParkanPlayground/MshConverter.cs @@ -10,9 +10,188 @@ public class MshConverter public void Convert(string mshPath) { var mshNresResult = NResParser.ReadFile(mshPath); - var mshNres = mshNresResult.Archive!; + using var mshFs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + var component01 = Msh01.ReadComponent(mshFs, mshNres); + var component02 = Msh02.ReadComponent(mshFs, mshNres); + var component0A = Msh0A.ReadComponent(mshFs, mshNres); + var component07 = Msh07.ReadComponent(mshFs, mshNres); + var component0D = Msh0D.ReadComponent(mshFs, mshNres); + var component06 = Msh06.ReadComponent(mshFs, mshNres); + var component03 = Read03Component(mshFs, mshNres); + + // --- Write OBJ --- + using var sw = new StreamWriter("test.obj", false, Encoding.UTF8); + + foreach (var v in component03) + sw.WriteLine($"v {v.X:F8} {v.Y:F8} {v.Z:F8}"); + + var vertices = new List(); + var faces = new List<(int, int, int)>(); // store indices into vertices list + + for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++) + { + var piece = component01.Elements[pieceIndex]; + var state = (piece.State00 == 0xffff) ? 0 : piece.State00; + + var mesh02Element = component02.Elements[state]; + + int indexInto07 = mesh02Element.StartIndexIn07; + + var element0Dstart = mesh02Element.StartOffsetIn0d; + var element0Dcount = mesh02Element.ByteLengthIn0D; + + Console.WriteLine($"Started piece {pieceIndex}. State={state}. 0D start={element0Dstart}, count={element0Dcount}"); + + for (var comp0Dindex = 0; comp0Dindex < element0Dcount; comp0Dindex++) + { + var element0D = component0D[element0Dstart + comp0Dindex]; + + var indexInto03 = element0D.IndexInto03; + var indexInto06 = element0D.IndexInto06; + + var count = (uint)element0D.number_of_triangles; + + // Convert IndexInto06 to ushort array index (3 ushorts per triangle) + Console.WriteLine( + $"Processing 0D element[{element0Dstart + comp0Dindex}]. IndexInto03={indexInto03}, IndexInto06={indexInto06}. Number of triangles={count}"); + + if (count != 0) + { + sw.WriteLine($"o piece_{pieceIndex++}_of_mesh_{comp0Dindex}"); + if (count % 3 != 0) + { + Console.WriteLine("Number of triangles is not multiple of 3"); + _ = 5; + } + + count = (count + 2) / 3; // number of triangles + + for (int tri = 0; tri < count; tri++) + { + // Each triangle uses 3 consecutive ushorts in component06 + + var comp07 = component07[indexInto07]; + + var i1 = indexInto03 + component06[indexInto06]; + var i2 = indexInto03 + component06[indexInto06 + 1]; + var i3 = indexInto03 + component06[indexInto06 + 2]; + + var v1 = component03[i1]; + var v2 = component03[i2]; + var v3 = component03[i3]; + + sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}"); + + // push vertices to global list + vertices.Add(v1); + vertices.Add(v2); + vertices.Add(v3); + + int baseIndex = vertices.Count; + // record face (OBJ is 1-based indexing!) + faces.Add((baseIndex - 2, baseIndex - 1, baseIndex)); + + indexInto07++; + indexInto06 += 3; // step by 3 since each triangle uses 3 ushorts + } + + _ = 5; + } + } + } + } + + public record Face(Vector3 P1, Vector3 P2, Vector3 P3); + + public static void ExportCube(string filePath, Vector3[] points) + { + if (points.Length != 8) + throw new ArgumentException("Cube must have exactly 8 points."); + + using (StreamWriter writer = new StreamWriter(filePath)) + { + // Write vertices + foreach (var p in points) + { + writer.WriteLine($"v {p.X} {p.Y} {p.Z}"); + } + + // Write faces (each face defined by 4 vertices, using 1-based indices) + int[][] faces = new int[][] + { + new int[] { 1, 2, 3, 4 }, // bottom + new int[] { 5, 6, 7, 8 }, // top + new int[] { 1, 2, 6, 5 }, // front + new int[] { 2, 3, 7, 6 }, // right + new int[] { 3, 4, 8, 7 }, // back + new int[] { 4, 1, 5, 8 } // left + }; + + foreach (var f in faces) + { + writer.WriteLine($"f {f[0]} {f[1]} {f[2]} {f[3]}"); + } + } + } + + public static void ExportCubesAtPositions(string filePath, List centers, float size = 2f) + { + float half = size / 2f; + using (StreamWriter writer = new StreamWriter(filePath)) + { + int vertexOffset = 0; + + foreach (var c in centers) + { + // Generate 8 vertices for this cube + Vector3[] vertices = new Vector3[] + { + new Vector3(c.X - half, c.Y - half, c.Z - half), + new Vector3(c.X + half, c.Y - half, c.Z - half), + new Vector3(c.X + half, c.Y - half, c.Z + half), + new Vector3(c.X - half, c.Y - half, c.Z + half), + + new Vector3(c.X - half, c.Y + half, c.Z - half), + new Vector3(c.X + half, c.Y + half, c.Z - half), + new Vector3(c.X + half, c.Y + half, c.Z + half), + new Vector3(c.X - half, c.Y + half, c.Z + half) + }; + + // Write vertices + foreach (var v in vertices) + { + writer.WriteLine($"v {v.X} {v.Y} {v.Z}"); + } + + // Define faces (1-based indices, counter-clockwise) + int[][] faces = new int[][] + { + new int[] { 1, 2, 3, 4 }, // bottom + new int[] { 5, 6, 7, 8 }, // top + new int[] { 1, 2, 6, 5 }, // front + new int[] { 2, 3, 7, 6 }, // right + new int[] { 3, 4, 8, 7 }, // back + new int[] { 4, 1, 5, 8 } // left + }; + + // Write faces with offset + foreach (var f in faces) + { + writer.WriteLine( + $"f {f[0] + vertexOffset} {f[1] + vertexOffset} {f[2] + vertexOffset} {f[3] + vertexOffset}"); + } + + vertexOffset += 8; + } + } + } + + + private static List Read03Component(FileStream mshFs, NResArchive mshNres) + { var verticesFileEntry = mshNres.Files.FirstOrDefault(x => x.FileType == "03 00 00 00"); if (verticesFileEntry is null) @@ -24,104 +203,7 @@ public class MshConverter { throw new Exception("Vertices file (03) element size is not 12"); } - - using var mshFs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read); - - ReadComponent01(mshFs, mshNres); - var vertices = ReadVertices(verticesFileEntry, mshFs); - - var edgesFileEntry = mshNres.Files.FirstOrDefault(x => x.FileType == "06 00 00 00"); - - if (edgesFileEntry is null) - { - throw new Exception("Archive doesn't contain edges file (06)"); - } - - var edgesFile = new byte[edgesFileEntry.ElementCount * edgesFileEntry.ElementSize]; - mshFs.Seek(edgesFileEntry.OffsetInFile, SeekOrigin.Begin); - mshFs.ReadExactly(edgesFile, 0, edgesFile.Length); - - var edges = new List((int)edgesFileEntry.ElementCount / 2); - - for (int i = 0; i < edgesFileEntry.ElementCount / 2; i++) - { - var index1 = BinaryPrimitives.ReadUInt16LittleEndian(edgesFile.AsSpan().Slice(i * 2)); - var index2 = BinaryPrimitives.ReadUInt16LittleEndian(edgesFile.AsSpan().Slice(i * 2 + 2)); - edges.Add(new IndexedEdge(index1, index2)); - } - - Export($"{Path.GetFileNameWithoutExtension(mshPath)}.obj", vertices, edges); - - } - - private static List TryRead0AComponent(FileStream mshFs, NResArchive archive) - { - var aFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "0A 00 00 00"); - - if (aFileEntry is null) - { - return []; - } - var data = new byte[aFileEntry.FileLength]; - mshFs.Seek(aFileEntry.OffsetInFile, SeekOrigin.Begin); - mshFs.ReadExactly(data, 0, data.Length); - - int pos = 0; - var strings = new List(); - while (pos < data.Length) - { - var len = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(pos)); - if (len == 0) - { - pos += 4; // empty entry, no string attached - strings.Add(""); // add empty string - } - else - { - // len is not 0, we need to read it - var strBytes = data.AsSpan(pos + 4, len); - var str = Encoding.UTF8.GetString(strBytes); - strings.Add(str); - pos += len + 4 + 1; // skip length prefix and string itself, +1, because it's null-terminated - } - } - if (strings.Count != aFileEntry.ElementCount) - { - throw new Exception("String count mismatch in 0A component"); - } - - return strings; - } - - private static void ReadComponent01(FileStream mshFs, NResArchive archive) - { - var headerFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00"); - - if (headerFileEntry is null) - { - throw new Exception("Archive doesn't contain header file (01)"); - } - var headerData = new byte[headerFileEntry.ElementCount * headerFileEntry.ElementSize]; - mshFs.Seek(headerFileEntry.OffsetInFile, SeekOrigin.Begin); - mshFs.ReadExactly(headerData, 0, headerData.Length); - - var descriptions = TryRead0AComponent(mshFs, archive); - - var chunks = headerData.Chunk(headerFileEntry.ElementSize).ToList(); - - var converted = chunks.Select(x => new - { - Byte00 = x[0], - Byte01 = x[1], - Bytes0204 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)), - }).ToList(); - - _ = 5; - } - - private static List ReadVertices(ListMetadataItem verticesFileEntry, FileStream mshFs) - { var verticesFile = new byte[verticesFileEntry.ElementCount * verticesFileEntry.ElementSize]; mshFs.Seek(verticesFileEntry.OffsetInFile, SeekOrigin.Begin); mshFs.ReadExactly(verticesFile, 0, verticesFile.Length); @@ -135,7 +217,7 @@ public class MshConverter return vertices; } - void Export(string filePath, List vertices, List edges) + void Export(string filePath, IEnumerable vertices, List edges) { using (var writer = new StreamWriter(filePath)) { diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs index 7181415..940afdf 100644 --- a/ParkanPlayground/Program.cs +++ b/ParkanPlayground/Program.cs @@ -9,4 +9,7 @@ using ParkanPlayground; var converter = new MshConverter(); -converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\133_fr_m_bunker.msh"); \ No newline at end of file +converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\133_fr_m_bunker.msh"); +// converter.Convert("E:\\ParkanUnpacked\\intsys.rlb\\277_MESH_o_pws_l_01.msh"); +// converter.Convert("E:\\ParkanUnpacked\\static.rlb\\2_MESH_s_stn_0_01.msh"); +// converter.Convert("E:\\ParkanUnpacked\\bases.rlb\\25_MESH_R_H_02.msh"); \ No newline at end of file diff --git a/README.md b/README.md index f08add8..ee4cba4 100644 --- a/README.md +++ b/README.md @@ -214,14 +214,14 @@ grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' . - Тип 01 - заголовок ``` нулевому элементу добавляется флаг 0x1000000 - Хранит куски меша. + Хранит стейты меша (в один стейт может входить несколько submesh) Содержит 2 ссылки на файлы анимаций (короткие - файл 13, длинные - файл 08) Если интерполируется анимация -0.5s короче чем magic1 у файла 13 И у файла есть OffsetIntoFile13 И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда) Тогда вместо IndexInFile08 используется значение из файла 13 по этому оффсету (второй байт) ``` -- Тип 02 +- Тип 02 - описание submesh ``` Вначале идёт заголовок 0x8C (140) байт В заголовке: @@ -230,9 +230,10 @@ grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' . 1 Vector3 - bottom 1 Vector3 - top 1 float - xy_radius + Далее инфа про куски меша ``` - Тип 03 - это вершины (vertex) -- Тип 06 - это то ли рёбра, то ли треугольники - не понятно +- Тип 06 - - Тип 04 - скорее всего какие-то цвета RGBA или типа того - Тип 08 - меш-анимации (см файл 01) ``` @@ -251,7 +252,7 @@ grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' . Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time) ``` - Тип 12 - microtexture mapping -- Тип 13 - короткие меш-анимации +- Тип 13 - короткие меш-анимации (почему я это не дописал?) ``` Буквально (hex) 00 01 01 02 ... @@ -275,10 +276,6 @@ grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' . Загружается в `World3D.dll/LoadMatManager` -## `.wea` - -Загружается в `World3D.dll/LoadMatManager` - # Внутренняя система ID - `1` - IMesh2 ???