mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2025-08-23 17:40:26 +03:00
Parse cp .dat files. Object schemes.
Test parsing of .msh
This commit is contained in:
3
Common/Common.csproj
Normal file
3
Common/Common.csproj
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
</Project>
|
@@ -1,7 +1,7 @@
|
|||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace MissionTmaLib.Parsing;
|
namespace Common;
|
||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
@@ -13,6 +13,14 @@ public static class Extensions
|
|||||||
return BinaryPrimitives.ReadInt32LittleEndian(buf);
|
return BinaryPrimitives.ReadInt32LittleEndian(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static uint ReadUInt32LittleEndian(this FileStream fs)
|
||||||
|
{
|
||||||
|
Span<byte> buf = stackalloc byte[4];
|
||||||
|
fs.ReadExactly(buf);
|
||||||
|
|
||||||
|
return BinaryPrimitives.ReadUInt32LittleEndian(buf);
|
||||||
|
}
|
||||||
|
|
||||||
public static float ReadFloatLittleEndian(this FileStream fs)
|
public static float ReadFloatLittleEndian(this FileStream fs)
|
||||||
{
|
{
|
||||||
Span<byte> buf = stackalloc byte[4];
|
Span<byte> buf = stackalloc byte[4];
|
||||||
@@ -21,6 +29,24 @@ public static class Extensions
|
|||||||
return BinaryPrimitives.ReadSingleLittleEndian(buf);
|
return BinaryPrimitives.ReadSingleLittleEndian(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ReadNullTerminatedString(this FileStream fs)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var b = fs.ReadByte();
|
||||||
|
if (b == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append((char)b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
public static string ReadLengthPrefixedString(this FileStream fs)
|
public static string ReadLengthPrefixedString(this FileStream fs)
|
||||||
{
|
{
|
||||||
var len = fs.ReadInt32LittleEndian();
|
var len = fs.ReadInt32LittleEndian();
|
3
Common/IndexedEdge.cs
Normal file
3
Common/IndexedEdge.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Common;
|
||||||
|
|
||||||
|
public record IndexedEdge(ushort Index1, ushort Index2);
|
3
Common/Vector3.cs
Normal file
3
Common/Vector3.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Common;
|
||||||
|
|
||||||
|
public record Vector3(float X, float Y, float Z);
|
@@ -14,6 +14,6 @@
|
|||||||
<EnablePackageVersionOverride>false</EnablePackageVersionOverride>
|
<EnablePackageVersionOverride>false</EnablePackageVersionOverride>
|
||||||
|
|
||||||
<!-- Suppress package version warnings -->
|
<!-- Suppress package version warnings -->
|
||||||
<NoWarn>$(NoWarn);NU1507</NoWarn>
|
<NoWarn>$(NoWarn);NU1507;CS1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
namespace MissionTmaLib;
|
using Common;
|
||||||
|
|
||||||
|
namespace MissionTmaLib;
|
||||||
|
|
||||||
public record ArealInfo(int Index, int CoordsCount, List<Vector3> Coords);
|
public record ArealInfo(int Index, int CoordsCount, List<Vector3> Coords);
|
@@ -1,4 +1,6 @@
|
|||||||
namespace MissionTmaLib;
|
using Common;
|
||||||
|
|
||||||
|
namespace MissionTmaLib;
|
||||||
|
|
||||||
public class GameObjectInfo
|
public class GameObjectInfo
|
||||||
{
|
{
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
namespace MissionTmaLib;
|
using Common;
|
||||||
|
|
||||||
|
namespace MissionTmaLib;
|
||||||
|
|
||||||
public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownFlags2, float UnknownFloat, int UnknownInt3);
|
public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownFlags2, float UnknownFloat, int UnknownInt3);
|
@@ -1,3 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
namespace MissionTmaLib.Parsing;
|
using Common;
|
||||||
|
|
||||||
|
namespace MissionTmaLib.Parsing;
|
||||||
|
|
||||||
public class MissionTmaParser
|
public class MissionTmaParser
|
||||||
{
|
{
|
||||||
public static MissionTmaParseResult ReadFile(string filePath)
|
public static MissionTmaParseResult ReadFile(string filePath)
|
||||||
{
|
{
|
||||||
var fs = new FileStream(filePath, FileMode.Open);
|
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
|
||||||
var arealData = LoadAreals(fs);
|
var arealData = LoadAreals(fs);
|
||||||
|
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
namespace MissionTmaLib;
|
using Common;
|
||||||
|
|
||||||
|
namespace MissionTmaLib;
|
||||||
|
|
||||||
public record UnknownClanTreeInfoPart(int UnkInt1, Vector3 UnkVector, float UnkInt2, float UnkInt3);
|
public record UnknownClanTreeInfoPart(int UnkInt1, Vector3 UnkVector, float UnkInt2, float UnkInt3);
|
@@ -1,3 +0,0 @@
|
|||||||
namespace MissionTmaLib;
|
|
||||||
|
|
||||||
public record Vector3(float X, float Y, float Z);
|
|
@@ -7,7 +7,7 @@ public static class NResParser
|
|||||||
{
|
{
|
||||||
public static NResParseResult ReadFile(string path)
|
public static NResParseResult ReadFile(string path)
|
||||||
{
|
{
|
||||||
using FileStream nResFs = new FileStream(path, FileMode.Open);
|
using FileStream nResFs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
|
||||||
if (nResFs.Length < 16)
|
if (nResFs.Length < 16)
|
||||||
{
|
{
|
||||||
|
@@ -137,9 +137,9 @@ namespace NResUI
|
|||||||
|
|
||||||
public void SetLod(int @base, int min, int max)
|
public void SetLod(int @base, int min, int max)
|
||||||
{
|
{
|
||||||
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, @base);
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, in @base);
|
||||||
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, min);
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, in min);
|
||||||
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, max);
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, in max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetWrap(TextureCoordinate coord, TextureWrapMode mode)
|
public void SetWrap(TextureCoordinate coord, TextureWrapMode mode)
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
<File Path="Directory.Packages.props" />
|
<File Path="Directory.Packages.props" />
|
||||||
<File Path="README.md" />
|
<File Path="README.md" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Project Path="Common\Common.csproj" Type="Classic C#" />
|
||||||
<Project Path="MeshUnpacker/MeshUnpacker.csproj" />
|
<Project Path="MeshUnpacker/MeshUnpacker.csproj" />
|
||||||
<Project Path="MissionDataUnpacker/MissionDataUnpacker.csproj" />
|
<Project Path="MissionDataUnpacker/MissionDataUnpacker.csproj" />
|
||||||
<Project Path="MissionTmaLib/MissionTmaLib.csproj" />
|
<Project Path="MissionTmaLib/MissionTmaLib.csproj" />
|
||||||
@@ -16,4 +17,4 @@
|
|||||||
<Project Path="TextureDecoder/TextureDecoder.csproj" />
|
<Project Path="TextureDecoder/TextureDecoder.csproj" />
|
||||||
<Project Path="VarsetLib/VarsetLib.csproj" />
|
<Project Path="VarsetLib/VarsetLib.csproj" />
|
||||||
<Project Path="Visualisator/Visualisator.csproj" />
|
<Project Path="Visualisator/Visualisator.csproj" />
|
||||||
</Solution>
|
</Solution>
|
161
ParkanPlayground/CpDatEntryConverter.cs
Normal file
161
ParkanPlayground/CpDatEntryConverter.cs
Normal 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
ParkanPlayground/MshConverter.cs
Normal file
90
ParkanPlayground/MshConverter.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
|
||||||
<ProjectReference Include="..\NResLib\NResLib.csproj" />
|
<ProjectReference Include="..\NResLib\NResLib.csproj" />
|
||||||
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
|
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
|
||||||
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
|
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
|
||||||
|
@@ -1,115 +1,13 @@
|
|||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Numerics;
|
using Common;
|
||||||
using System.Text.Json;
|
using MissionTmaLib.Parsing;
|
||||||
using ScrLib;
|
using NResLib;
|
||||||
using VarsetLib;
|
using ParkanPlayground;
|
||||||
|
|
||||||
|
var cpDatEntryConverter = new CpDatEntryConverter();
|
||||||
|
|
||||||
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\default.scr";
|
cpDatEntryConverter.Convert();
|
||||||
// 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
|
|
||||||
// );
|
|
||||||
|
|
||||||
// var items = VarsetParser.Parse(path);
|
var converter = new MshConverter();
|
||||||
|
|
||||||
// Console.WriteLine(items.Count);
|
converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\161_fr_b_tower.msh");
|
||||||
|
|
||||||
// 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; } = [];
|
|
||||||
}
|
|
189
README.md
189
README.md
@@ -46,6 +46,195 @@
|
|||||||
- Игра активно и обильно течёт по памяти, оставляя после чтения файлов их `MapViewOfFile` и подобные штуки.
|
- Игра активно и обильно течёт по памяти, оставляя после чтения файлов их `MapViewOfFile` и подобные штуки.
|
||||||
- Игра нормально не работает на Win10. Мне помог dgVoodoo. Хотя с ним не работает `MisEditor`.
|
- Игра нормально не работает на Win10. Мне помог dgVoodoo. Хотя с ним не работает `MisEditor`.
|
||||||
|
|
||||||
|
## Как быстро найти текст среди всех файлов игры
|
||||||
|
|
||||||
|
```shell
|
||||||
|
grep -rl --include="*" "s_tree_05" .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Как быстро найти байты среди всех файлов игры
|
||||||
|
|
||||||
|
```shell
|
||||||
|
grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Как работает игра
|
||||||
|
|
||||||
|
Главное меню:
|
||||||
|
|
||||||
|
Игра сканирует хардкод папку `missions` на наличие файлов миссий. (буквально 01, 02, 03 и т.д.)
|
||||||
|
|
||||||
|
Сначала игра читает название миссии из файла `descr` - тут название для меню.
|
||||||
|
|
||||||
|
- Одиночные игры - `missions/single.{index}/descr`
|
||||||
|
- Тренировочные миссии - `missions/tutorial.{index}/descr`
|
||||||
|
- Кампания - `missions/campaign/campaign.{index1}/descr`
|
||||||
|
* Далее используются подпапки - `missions/campaign/campaign.{index1}/mission.{index2}/descr`
|
||||||
|
|
||||||
|
Как только игра не находит файл `descr`, заканчивается итерация по папкам (понял, т.к. пробуется файл 05 - он не существует).
|
||||||
|
|
||||||
|
Загрузка миссии:
|
||||||
|
|
||||||
|
Читается файл `ui/game_resources.cfg`
|
||||||
|
Из этого файла загружаются ресурсы
|
||||||
|
- `library = "ui\\ui.lib"` - загружается файл `ui.lib`
|
||||||
|
- `library = "ui\\font.lib"` - загружается файл `font.lib`
|
||||||
|
- `library = "sounds.lib"` - загружается файл `sounds.lib`
|
||||||
|
- `library = "voices.lib"` - загружается файл `voices.lib`
|
||||||
|
|
||||||
|
Затем игра читает `save/saveslots.cfg` - тут слоты сохранения
|
||||||
|
|
||||||
|
Затем `Comp.ini` - тут системные функции, которые используются для загрузки объектов.
|
||||||
|
|
||||||
|
- `Host.url` - этого файла нет
|
||||||
|
- `palettes.lib` - тут палитры, но этот NRes пустой
|
||||||
|
- `system.rlb` - не понятно что
|
||||||
|
- `Textures.lib` - тут текстуры
|
||||||
|
- `Material.lib` - тут какие-то материалы - не понятно
|
||||||
|
- `LightMap.lib` - видимо это карты освещения - не понятно
|
||||||
|
- `sys.lib` - не понятно
|
||||||
|
|
||||||
|
|
||||||
|
- `ScanCode.dsc` - текстовый файл с мапом клавиш
|
||||||
|
- `command.dsc` - текстовый файл с мапом клавиш
|
||||||
|
|
||||||
|
Тут видимо идёт конфигурация ввода
|
||||||
|
|
||||||
|
- `table_1.man` - текстовый файл
|
||||||
|
- `table_2.man` - текстовый файл
|
||||||
|
- `hero.man` - текстовый файл
|
||||||
|
- `addition.man` - текстовый файл
|
||||||
|
- Снова `table_1.man`
|
||||||
|
- Снова `table_1.man`
|
||||||
|
- `M1.tbl` - текстовый файл
|
||||||
|
- Снова `table_2.man`
|
||||||
|
- Снова `table_2.man`
|
||||||
|
- `M2.tbl` - текстовый файл
|
||||||
|
- Снова `hero.man`
|
||||||
|
- Снова `hero.man`
|
||||||
|
- `HERO.TBL`
|
||||||
|
- Снова `addition.man`
|
||||||
|
|
||||||
|
|
||||||
|
- `ui/hq.cfg`
|
||||||
|
- Снова `ui/hq.cfg`
|
||||||
|
|
||||||
|
|
||||||
|
Дальше непосредственно читается миссия
|
||||||
|
|
||||||
|
- `mission.cfg` - метадата миссии
|
||||||
|
- `units\\units\\prebld\\scr_pre1.dat` из метаданных `object prebuild` - `cp` файл (грузятся подряд все)
|
||||||
|
- Опять `ui/hq.cfg`
|
||||||
|
- `mistips.mis` - описание для игрока (экран F1)
|
||||||
|
- `scancode.dsc` - хз
|
||||||
|
- `command.dsc` - хз
|
||||||
|
- `ui_hero.man` - хз
|
||||||
|
- `ui_bots.man` - хз
|
||||||
|
- `ui_hq.man` - хз
|
||||||
|
- `ui_other.man` - хз
|
||||||
|
- Цикл чтения курсоров
|
||||||
|
* `ui/cursor.cfg` - тут настройки курсора.
|
||||||
|
* `ui/{name}` - курсор
|
||||||
|
- Снова `mission.cfg` - метадата миссии
|
||||||
|
- `descr` - название
|
||||||
|
- `data/textres.cfg` - конфиг текстов
|
||||||
|
- Снова `mission.cfg` - метадата миссии
|
||||||
|
- Ещё раз `mission.cfg` - метадата миссии
|
||||||
|
- `ui/minimap.lib` - NRes с текстурами миникарты.
|
||||||
|
- `messages.cfg` - Tutorial messages
|
||||||
|
|
||||||
|
УРА НАКОНЕЦ-ТО `data.tma`
|
||||||
|
|
||||||
|
- Из `.tma` берётся LAND строка (я её так назвал)
|
||||||
|
- `DATA\\MAPS\\SC_3\\land1.wea`
|
||||||
|
- `DATA\\MAPS\\SC_3\\land2.wea`
|
||||||
|
- `BuildDat.lst` - Behaviour will use these schemes to Build Fortification
|
||||||
|
- `DATA\\MAPS\\SC_3\\land.map`
|
||||||
|
- `DATA\\MAPS\\SC_3\\land.msh`
|
||||||
|
|
||||||
|
|
||||||
|
- `effects.rlb`
|
||||||
|
|
||||||
|
Цикл по кланам из `.tma`
|
||||||
|
- `MISSIONS\\SCRIPTS\\screampl.scr`
|
||||||
|
- `varset.var`
|
||||||
|
- `MISSIONS\\SCRIPTS\\varset.var`
|
||||||
|
- `MISSIONS\\SCRIPTS\\screampl.fml`
|
||||||
|
|
||||||
|
|
||||||
|
- `missions/single.01/sky.ske`
|
||||||
|
- `missions/single.01/sky.wea`
|
||||||
|
|
||||||
|
Дальше начинаются объекты игры
|
||||||
|
- `"UNITS\\BUILDS\\BUNKER\\mbunk01.dat"` - cp файл
|
||||||
|
|
||||||
|
## Загрузка `cp` файлов
|
||||||
|
|
||||||
|
`cp` файл - схема. Он содержит дерево частей объекта.
|
||||||
|
|
||||||
|
`cp` файл читается в `ArealMap.dll/CreateObjectFromScheme`
|
||||||
|
|
||||||
|
В зависимости от типа объекта внутри схемы (байты 4..8) выбирается функция, с помощью которой загружается схема.
|
||||||
|
|
||||||
|
Функция выбирается на основе файла `Comp.ini`.
|
||||||
|
- Для ClassBuilding (0x80000000) - вызывается функция по индексу 3.
|
||||||
|
- Для всех остальных - функция по индексу 4.
|
||||||
|
|
||||||
|
Однако контент файла `Comp.ini` буквально выглядит так:
|
||||||
|
```
|
||||||
|
3 animesh.dll LoadAgent // comments...
|
||||||
|
4 animesh.dll LoadAgent //
|
||||||
|
```
|
||||||
|
А значит независимо от класса объекта вызывается `animesh.dll/LoadAgent`.
|
||||||
|
|
||||||
|
## `.msh`
|
||||||
|
|
||||||
|
- Тип 03 - это вершины (vertex)
|
||||||
|
- Тип 06 - это рёбра (edge)
|
||||||
|
- Тип 04 - скорее всего какие-то цвета RGBA или типа того
|
||||||
|
- Тип 12 - microtexture mapping
|
||||||
|
|
||||||
|
Тип 02 имеет какой-то заголовок.
|
||||||
|
Игра сначала читает из него первые 96 байт. А затем пропускает с начала 148 байт
|
||||||
|
|
||||||
|
# Внутренняя система ID
|
||||||
|
|
||||||
|
- `4` - IShader
|
||||||
|
- `5` - ITerrain
|
||||||
|
- `6` - IGameObject (0x138)
|
||||||
|
- `8` - ICamera
|
||||||
|
- `9` - Queue
|
||||||
|
- `10` - IControl
|
||||||
|
- `0xb` - IAnimation
|
||||||
|
- `0xd` - IMatManager
|
||||||
|
- `0x10` - unknown (implemented by Wizard in Wizard.dll, also by Hallway in ArealMap.dll)
|
||||||
|
- `0x11` - IBasement
|
||||||
|
- `0x12` - ICamera2 - BufferingCamera
|
||||||
|
- `0x13` - IEffectManager
|
||||||
|
- `0x14` - IPosition
|
||||||
|
- `0x16` - ILifeSystem
|
||||||
|
- `0x17` - IBuilding
|
||||||
|
- `0x18` - IMesh2
|
||||||
|
- `0x19` - unknown (implemented by Wizard in Wizard.dll)
|
||||||
|
- `0x20` - IJointMesh
|
||||||
|
- `0x21` - IShade
|
||||||
|
- `0x24` - IGameObject2
|
||||||
|
- `0x101` - 3DRender
|
||||||
|
- `0x201` - IWizard
|
||||||
|
- `0x202` - IItemManager
|
||||||
|
- `0x203` - ICollManager
|
||||||
|
- `0x301` - IArealMap
|
||||||
|
- `0x302` - ISystemArealMap
|
||||||
|
- `0x303` - IHallway
|
||||||
|
- `0x401` - ISuperAI
|
||||||
|
- `0x105` - NResFile
|
||||||
|
- `0x106` - NResFileMetadata
|
||||||
|
- `0x501` - MissionData
|
||||||
|
- `0x502` - ResTree
|
||||||
|
- `0x700` - NetWatcher
|
||||||
|
- `0x701` - INetworkInterface
|
||||||
|
- `0x10d` - CreateVertexBufferData
|
||||||
|
|
||||||
## Контакты
|
## Контакты
|
||||||
|
|
||||||
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).
|
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).
|
||||||
|
@@ -1,39 +0,0 @@
|
|||||||
using System.Buffers.Binary;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace ScrLib;
|
|
||||||
|
|
||||||
public static class Extensions
|
|
||||||
{
|
|
||||||
public static int ReadInt32LittleEndian(this FileStream fs)
|
|
||||||
{
|
|
||||||
Span<byte> buf = stackalloc byte[4];
|
|
||||||
fs.ReadExactly(buf);
|
|
||||||
|
|
||||||
return BinaryPrimitives.ReadInt32LittleEndian(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float ReadFloatLittleEndian(this FileStream fs)
|
|
||||||
{
|
|
||||||
Span<byte> buf = stackalloc byte[4];
|
|
||||||
fs.ReadExactly(buf);
|
|
||||||
|
|
||||||
return BinaryPrimitives.ReadSingleLittleEndian(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ReadLengthPrefixedString(this FileStream fs)
|
|
||||||
{
|
|
||||||
var len = fs.ReadInt32LittleEndian();
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = new byte[len];
|
|
||||||
|
|
||||||
fs.ReadExactly(buffer, 0, len);
|
|
||||||
|
|
||||||
return Encoding.ASCII.GetString(buffer, 0, len);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
namespace ScrLib;
|
using Common;
|
||||||
|
|
||||||
|
namespace ScrLib;
|
||||||
|
|
||||||
public class ScrParser
|
public class ScrParser
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user