diff --git a/MissionDataUnpacker/MissionDataUnpacker.csproj b/MissionDataUnpacker/MissionDataUnpacker.csproj new file mode 100644 index 0000000..2f4fc77 --- /dev/null +++ b/MissionDataUnpacker/MissionDataUnpacker.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/MissionDataUnpacker/Program.cs b/MissionDataUnpacker/Program.cs new file mode 100644 index 0000000..4c09068 --- /dev/null +++ b/MissionDataUnpacker/Program.cs @@ -0,0 +1,219 @@ +using System.Buffers.Binary; +using System.Text; + +// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Autodemo.00\\data.tma"; +// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Tutorial.01\\data.tma"; +var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\CAMPAIGN\\CAMPAIGN.01\\Mission.03\\data.tma"; +// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Single.01\\data.tma"; +var fs = new FileStream(missionFilePath, FileMode.Open); + +var arealData = LoadAreals(fs); + +var clans = LoadClans(fs); + +_ = 5; +// LoadGameObjects(fs); + +ArealsFileData LoadAreals(FileStream fileStream) +{ + var unusedHeader = fileStream.ReadInt32LittleEndian(); + var arealCount = fileStream.ReadInt32LittleEndian(); + + // В демо миссии нет ареалов, ровно как и в первой миссии кампании + // Span arealBuffer = stackalloc byte[12]; + + List infos = []; + for (var i = 0; i < arealCount; i++) + { + // игра читает 4 байта - видимо количество + var unknown4Bytes = fileStream.ReadInt32LittleEndian(); + + var count = fileStream.ReadInt32LittleEndian(); + + List vectors = []; + if (0 < count) + { + for (var i1 = 0; i1 < count; i1++) + { + // потом читает 12 байт разом (тут видимо какой-то вектор) + var unknownFloat1 = fileStream.ReadFloatLittleEndian(); + var unknownFloat2 = fileStream.ReadFloatLittleEndian(); + var unknownFloat3 = fileStream.ReadFloatLittleEndian(); + + vectors.Add(new Vector3(unknownFloat1, unknownFloat2, unknownFloat3)); + } + } + + infos.Add(new ArealInfo(unknown4Bytes, count, vectors)); + } + + return new ArealsFileData(unusedHeader, arealCount, infos); +} + +ClansFileData? LoadClans(FileStream fileStream) +{ + var clanFeatureSet = fileStream.ReadInt32LittleEndian(); + + if (clanFeatureSet is <= 0 or >= 7) return null; + + var treeInfoCount = fileStream.ReadInt32LittleEndian(); + + List infos = []; + for (var i = 0; i < treeInfoCount; i++) + { + var clanTreeInfo = new ClanTreeInfo(); + + clanTreeInfo.ClanName = fileStream.ReadLengthPrefixedString(); + clanTreeInfo.UnkInt1 = fileStream.ReadInt32LittleEndian(); + clanTreeInfo.X = fileStream.ReadFloatLittleEndian(); + clanTreeInfo.Y = fileStream.ReadFloatLittleEndian(); + clanTreeInfo.ClanType = fileStream.ReadInt32LittleEndian(); + + if (1 < clanFeatureSet) + { + // MISSIONS\SCRIPTS\default + // MISSIONS\SCRIPTS\tut1_pl + // MISSIONS\SCRIPTS\tut1_en + clanTreeInfo.UnkString2 = fileStream.ReadLengthPrefixedString(); + } + + if (2 < clanFeatureSet) + { + clanTreeInfo.UnknownClanPartCount = fileStream.ReadInt32LittleEndian(); + + // тут игра читает число, затем 12 байт и ещё 2 числа + + List unknownClanTreeInfoParts = []; + for (var i1 = 0; i1 < clanTreeInfo.UnknownClanPartCount; i1++) + { + unknownClanTreeInfoParts.Add( + new UnknownClanTreeInfoPart( + fileStream.ReadInt32LittleEndian(), + new Vector3( + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian() + ), + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian() + ) + ); + } + + clanTreeInfo.UnknownParts = unknownClanTreeInfoParts; + } + + if (3 < clanFeatureSet) + { + // MISSIONS\SCRIPTS\auto.trf + // MISSIONS\SCRIPTS\data.trf + // указатель на NRes файл с данными + // может быть пустым, например у Ntrl в туториале + clanTreeInfo.ResearchNResPath = fileStream.ReadLengthPrefixedString(); + } + + if (4 < clanFeatureSet) + { + clanTreeInfo.UnkInt3 = fileStream.ReadInt32LittleEndian(); + } + + if (5 < clanFeatureSet) + { + clanTreeInfo.AlliesMapCount = fileStream.ReadInt32LittleEndian(); + + // тут какая-то мапа + // в демо миссии тут + // player -> 1 + // player2 -> 0 + + // в туториале + // Plr -> 1 + // Trgt -> 1 + // Enm -> 0 + // Ntrl -> 1 + Dictionary map = []; + for (var i1 = 0; i1 < clanTreeInfo.AlliesMapCount; i1++) + { + var keyIdString = fileStream.ReadLengthPrefixedString(); + // это число всегда либо 0 либо 1 + var unkNumber = fileStream.ReadInt32LittleEndian(); + + map[keyIdString] = unkNumber; + } + + clanTreeInfo.AlliesMap = map; + } + + infos.Add(clanTreeInfo); + } + + var clanInfo = new ClansFileData(clanFeatureSet, treeInfoCount, infos); + + return clanInfo; +} + +public static class Extensions +{ + public static int ReadInt32LittleEndian(this FileStream fs) + { + Span buf = stackalloc byte[4]; + fs.ReadExactly(buf); + + return BinaryPrimitives.ReadInt32LittleEndian(buf); + } + + public static float ReadFloatLittleEndian(this FileStream fs) + { + Span buf = stackalloc byte[4]; + fs.ReadExactly(buf); + + return BinaryPrimitives.ReadSingleLittleEndian(buf); + } + + public static string ReadLengthPrefixedString(this FileStream fs) + { + var len = fs.ReadInt32LittleEndian(); + + var buffer = new byte[len]; + + fs.ReadExactly(buffer, 0, len); + + return Encoding.ASCII.GetString(buffer, 0, len); + } +} + +public record ArealsFileData(int UnusedHeader, int ArealCount, List ArealInfos); + +public record ArealInfo(int Index, int CoordsCount, List Coords); + +public record Vector3(float X, float Y, float Z); + +// ---- + +public record ClansFileData(int ClanFeatureSet, int TreeCount, List TreeInfos); + +public class ClanTreeInfo +{ + public string ClanName { get; set; } + public int UnkInt1 { get; set; } + public float X { get; set; } + public float Y { get; set; } + + /// + /// 1 - игрок, 2 AI, 3 - нейтральный + /// + public int ClanType { get; set; } + public string UnkString2 { get; set; } + public int UnknownClanPartCount { get; set; } + public List UnknownParts { get; set; } + public string ResearchNResPath { get; set; } + public int UnkInt3 { get; set; } + public int AlliesMapCount { get; set; } + + /// + /// мапа союзников (ключ - имя клана, значение - число, всегда либо 0 либо 1) + /// + public Dictionary AlliesMap { get; set; } +} + +public record UnknownClanTreeInfoPart(int UnkInt1, Vector3 UnkVector, float UnkInt2, float UnkInt3); \ No newline at end of file diff --git a/ParkanPlayground.sln b/ParkanPlayground.sln index 4f762b2..a551a8a 100644 --- a/ParkanPlayground.sln +++ b/ParkanPlayground.sln @@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeshUnpacker", "MeshUnpacke EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TexmLib", "TexmLib\TexmLib.csproj", "{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MissionDataUnpacker", "MissionDataUnpacker\MissionDataUnpacker.csproj", "{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,5 +50,9 @@ Global {40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Release|Any CPU.Build.0 = Release|Any CPU + {7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal