mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
Parse cp .dat files. Object schemes.
Test parsing of .msh
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
Reference in New Issue
Block a user