From 35af4da326f58c29a6e7a4c2415f0617935385a7 Mon Sep 17 00:00:00 2001 From: bird_egop Date: Sat, 23 Aug 2025 19:03:03 +0300 Subject: [PATCH] read msh 0A file --- ParkanPlayground/MshConverter.cs | 68 ++++++++++++++++++++++++++++++++ ParkanPlayground/Program.cs | 7 ++-- README.md | 37 +++++++++++++++-- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/ParkanPlayground/MshConverter.cs b/ParkanPlayground/MshConverter.cs index 150112c..3b6730f 100644 --- a/ParkanPlayground/MshConverter.cs +++ b/ParkanPlayground/MshConverter.cs @@ -1,4 +1,5 @@ using System.Buffers.Binary; +using System.Text; using Common; using NResLib; @@ -25,6 +26,8 @@ public class MshConverter } using var mshFs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + ReadComponent01(mshFs, mshNres); var vertices = ReadVertices(verticesFileEntry, mshFs); @@ -52,6 +55,71 @@ public class MshConverter } + 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]; diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs index d8999eb..7181415 100644 --- a/ParkanPlayground/Program.cs +++ b/ParkanPlayground/Program.cs @@ -4,10 +4,9 @@ using MissionTmaLib.Parsing; using NResLib; using ParkanPlayground; -var cpDatEntryConverter = new CpDatEntryConverter(); - -cpDatEntryConverter.Convert(); +// var cpDatEntryConverter = new CpDatEntryConverter(); +// cpDatEntryConverter.Convert(); var converter = new MshConverter(); -converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\161_fr_b_tower.msh"); \ No newline at end of file +converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\133_fr_m_bunker.msh"); \ No newline at end of file diff --git a/README.md b/README.md index 9771cd2..4ab83b4 100644 --- a/README.md +++ b/README.md @@ -196,20 +196,51 @@ grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' . | 11 | Research | `misload.dll LoadResearch` | | 12 | Agent | `animesh.dll LoadAgent` | -Будет дополняться по мере реверса/ +Будет дополняться по мере реверса. + +Всем этим функциям передаётся `nres_file_name, nres_entry_name, 0, player_id` + +## `fr FORT` файл + +Всегда 0x80 байт +Содержит 2 ссылки на файлы: +- `.bas` +- `.ctl` - вызывается `LoadAgent` ## `.msh` +Загружается в `AniMesh.dll/LoadAniMesh` + - Тип 03 - это вершины (vertex) - Тип 06 - это рёбра (edge) - Тип 04 - скорее всего какие-то цвета RGBA или типа того - Тип 12 - microtexture mapping +- Тип 0A + ``` + Не имеет фиксированной длины. Хранит какие-то строки в следующем формате. + Игра обращается по индексу, пропуская суммарную длину и пропуская 4 байта на каждую строку (длина). + т.е. буквально файл выглядит так + 00 00 00 00 - пустая строка + 03 00 00 00 - длина строки 1 + 73 74 72 00 - строка "str" + null terminator + .. и повторяется до конца файла + Кол-во элементов из NRes должно быть равно кол-ву строк в этом файле, хотя игра это не проверяет. + ``` -Тип 02 имеет какой-то заголовок. +Тип 02 имеет заголовок 140 байт. Игра сначала читает из него первые 96 байт. А затем пропускает с начала 148 байт +## `.wea` + +Загружается в `World3D.dll/LoadMatManager` + +## `.wea` + +Загружается в `World3D.dll/LoadMatManager` + # Внутренняя система ID +- `1` - IMesh2 ??? - `4` - IShader - `5` - ITerrain - `6` - IGameObject (0x138) @@ -226,7 +257,7 @@ grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' . - `0x16` - ILifeSystem - `0x17` - IBuilding - `0x18` - IMesh2 -- `0x19` - unknown (implemented by Wizard in Wizard.dll) +- `0x19` - unknown (implemented by Wizard in Wizard.dll, also by Agent in AniMesh.dll) - `0x20` - IJointMesh - `0x21` - IShade - `0x24` - IGameObject2