Parse cp .dat files. Object schemes.

Test parsing of .msh
This commit is contained in:
bird_egop
2025-08-23 03:00:30 +03:00
parent 67c9020b96
commit b9e15541c5
22 changed files with 515 additions and 166 deletions
+161
View File
@@ -0,0 +1,161 @@
using Common;
using MissionTmaLib.Parsing;
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// Игра называет этот объект "схемой"
/// </summary>
/// <remarks>
/// В игре файл .dat читается в ArealMap.dll/CreateObjectFromScheme
/// </remarks>
/// <code>
///
/// struct Scheme
/// {
/// char[32] str1; // имя архива
/// char[32] str2; // имя объекта в архиве
/// undefined4 magic1;
/// undefined4 magic2;
/// char[32] str3; // описание объекта
/// undefined4 magic3;
/// }
///
/// </code>
public class CpDatEntryConverter
{
const string gameRoot = "C:\\Program Files (x86)\\Nikita\\Iron Strategy";
const string missionTmaPath = $"{gameRoot}\\MISSIONS\\Single.01\\data.tma";
const string staticRlbPath = $"{gameRoot}\\static.rlb";
const string objectsRlbPath = $"{gameRoot}\\objects.rlb";
// Схема такая:
// Файл обязан начинаться с 0xf1 0xf0 ("cp\0\0") - типа заголовок
// Далее 4 байта - тип объекта, который содержится в схеме (их я выдернул из .var файла)
// Далее 0x6c (108) байт - root объект
public void Convert()
{
var tma = MissionTmaParser.ReadFile(missionTmaPath);
var staticRlbResult = NResParser.ReadFile(staticRlbPath);
var objectsRlbResult = NResParser.ReadFile(objectsRlbPath);
var mission = tma.Mission!;
var sRlb = staticRlbResult.Archive!;
var oRlb = objectsRlbResult.Archive!;
Span<byte> f0f1 = stackalloc byte[4];
foreach (var gameObject in mission.GameObjectsData.GameObjectInfos)
{
var gameObjectDatPath = gameObject.DatString;
if (gameObjectDatPath.Contains('\\'))
{
// если это путь, то надо искать его в папке
string datFullPath = $"{gameRoot}\\{gameObjectDatPath}";
using FileStream fs = new FileStream(datFullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
fs.ReadExactly(f0f1);
if (f0f1[0] != 0xf1 || f0f1[1] != 0xf0)
{
_ = 5;
}
var fileFlags = (CpEntryType)fs.ReadInt32LittleEndian();
var entryLength = 0x6c + 4; // нам нужно прочитать 0x6c (108) байт - это root, и ещё 4 байта - кол-во вложенных объектов
if ((fs.Length - 8) % entryLength != 0)
{
_ = 5;
}
DatEntry entry = ReadEntryRecursive(fs);
// var objects = entries.Select(x => oRlb.Files.FirstOrDefault(y => y.FileName == x.ArchiveEntryName))
// .ToList();
_ = 5;
}
else
{
// это статический объект, который будет в objects.rlb
var sEntry = oRlb.Files.FirstOrDefault(x => x.FileName == gameObjectDatPath);
_ = 5;
}
}
}
private DatEntry ReadEntryRecursive(FileStream fs)
{
var str1 = fs.ReadNullTerminatedString();
fs.Seek(32 - str1.Length - 1, SeekOrigin.Current);
var str2 = fs.ReadNullTerminatedString();
fs.Seek(32 - str2.Length - 1, SeekOrigin.Current);
var magic1 = fs.ReadInt32LittleEndian();
var magic2 = fs.ReadInt32LittleEndian();
var descriptionString = fs.ReadNullTerminatedString();
fs.Seek(32 - descriptionString.Length - 1, SeekOrigin.Current);
var magic3 = fs.ReadInt32LittleEndian();
// игра не читает количество внутрь схемы, вместо этого она сразу рекурсией читает нужно количество вложенных объектов
var childCount = fs.ReadInt32LittleEndian();
List<DatEntry> children = new List<DatEntry>();
for (var i = 0; i < childCount; i++)
{
var child = ReadEntryRecursive(fs);
children.Add(child);
}
return new DatEntry(str1, str2, magic1, magic2, descriptionString, magic3, childCount, Children: children);
}
public record DatEntry(
string ArchiveFile,
string ArchiveEntryName,
int Magic1,
int Magic2,
string Description,
int Magic3,
int ChildCount, // игра не хранит это число в объекте, но оно есть в файле
List<DatEntry> Children
);
enum CpEntryType : uint
{
ClassBuilding = 0x80000000,
ClassRobot = 0x01000000,
ClassAnimal = 0x20000000,
BunkerSmall = 0x80010000,
BunkerMedium = 0x80020000,
BunkerLarge = 0x80040000,
Generator = 0x80000002,
Mine = 0x80000004,
Storage = 0x80000008,
Plant = 0x80000010,
Hangar = 0x80000040,
TowerMedium = 0x80100000,
TowerLarge = 0x80200000,
MainTeleport = 0x80000200,
Institute = 0x80000400,
Bridge = 0x80001000,
Ruine = 0x80002000,
RobotTransport = 0x01002000,
RobotBuilder = 0x01004000,
RobotBattleunit = 0x01008000,
RobotHq = 0x01010000,
RobotHero = 0x01020000,
}
}
+90
View File
@@ -0,0 +1,90 @@
using System.Buffers.Binary;
using Common;
using NResLib;
namespace ParkanPlayground;
public class MshConverter
{
public void Convert(string mshPath)
{
var mshNresResult = NResParser.ReadFile(mshPath);
var mshNres = mshNresResult.Archive!;
var verticesFileEntry = mshNres.Files.FirstOrDefault(x => x.FileType == "03 00 00 00");
if (verticesFileEntry is null)
{
throw new Exception("Archive doesn't contain vertices file (03)");
}
if (verticesFileEntry.ElementSize != 12)
{
throw new Exception("Vertices file (03) element size is not 12");
}
using var mshFs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
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<IndexedEdge>((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<Vector3> 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);
var vertices = verticesFile.Chunk(12).Select(x => new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(0)),
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(4)),
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(8))
)
).ToList();
return vertices;
}
void Export(string filePath, List<Vector3> vertices, List<IndexedEdge> edges)
{
using (var writer = new StreamWriter(filePath))
{
writer.WriteLine("# Exported OBJ file");
// Write vertices
foreach (var v in vertices)
{
writer.WriteLine($"v {v.X:F2} {v.Y:F2} {v.Z:F2}");
}
// Write edges as lines ("l" elements in .obj format)
foreach (var e in edges)
{
// OBJ uses 1-based indexing
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 1}");
}
}
}
}
+1
View File
@@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
+8 -110
View File
@@ -1,115 +1,13 @@
using System.Buffers.Binary;
using System.Numerics;
using System.Text.Json;
using ScrLib;
using VarsetLib;
using Common;
using MissionTmaLib.Parsing;
using NResLib;
using ParkanPlayground;
var cpDatEntryConverter = new CpDatEntryConverter();
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\default.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scr_pl_1.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream1.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\varset.var";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\preload.lda";
//
// var fs = new FileStream(path, FileMode.Open);
//
// var count = fs.ReadInt32LittleEndian();
//
// Span<byte> data = stackalloc byte[0x124];
//
// for (var i = 0; i < count; i++)
// {
// fs.ReadExactly(data);
// }
//
// Console.WriteLine(
// fs.Position == fs.Length
// );
cpDatEntryConverter.Convert();
// var items = VarsetParser.Parse(path);
var converter = new MshConverter();
// Console.WriteLine(items.Count);
// Span<byte> flt = stackalloc byte[4];
// flt[0] = 0x7f;
// flt[1] = 0x7f;
// flt[2] = 0xff;
// flt[3] = 0xff;
// var f = BinaryPrimitives.ReadSingleBigEndian(flt);
//
// Console.WriteLine(f);
// return;
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MisLoad.dll";
var path = "C:\\ParkanUnpacked\\Land.msh\\2_03 00 00 00_Land.bin";
var fs = new FileStream(path, FileMode.Open);
var outputFs = new FileStream("Land.obj", FileMode.Create);
var sw = new StreamWriter(outputFs);
List<Vector3D> points = [];
var count = 0;
while (fs.Position < fs.Length)
{
var x = fs.ReadFloatLittleEndian();
var y = fs.ReadFloatLittleEndian();
var z = fs.ReadFloatLittleEndian();
var vertex = new Vector3D(x, y, z);
sw.WriteLine($"v {x} {y} {z}");
var seenIndex = points.FindIndex(vec => vec == vertex);
if (seenIndex != -1)
{
vertex.Duplicates = seenIndex;
}
points.Add(vertex);
count++;
}
File.WriteAllText("human-readable.json", JsonSerializer.Serialize(points, new JsonSerializerOptions()
{
WriteIndented = true
}));
Console.WriteLine($"Total vertices: {count}");
// for (int i = 0; i < count / 4; i++)
public record Vector3D(float X, float Y, float Z)
{
public int Duplicates { get; set; }
}
// var indices = string.Join(" ", Enumerable.Range(1, count));
//
// sw.WriteLine($"l {indices}");
//
// fs.Seek(0x1000, SeekOrigin.Begin);
//
// byte[] buf = new byte[34];
// fs.ReadExactly(buf);
//
// var disassembler = new SharpDisasm.Disassembler(buf, ArchitectureMode.x86_32);
// foreach (var instruction in disassembler.Disassemble())
// {
// Console.WriteLine($"{instruction.PC - instruction.Offset}: {instruction}");
//
// new Instruction()
// {
// Action = instruction.Mnemonic.ToString(),
// Arguments = {instruction.Operands[0].ToString()}
// };
// }
public class Instruction
{
public string Action { get; set; } = "";
public List<string> Arguments { get; set; } = [];
}
converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\161_fr_b_tower.msh");