2025-08-23 03:00:14 +03:00
|
|
|
using Common;
|
|
|
|
|
|
|
|
|
|
namespace MissionTmaLib.Parsing;
|
2024-11-26 04:05:25 +03:00
|
|
|
|
|
|
|
|
public class MissionTmaParser
|
|
|
|
|
{
|
|
|
|
|
public static MissionTmaParseResult ReadFile(string filePath)
|
|
|
|
|
{
|
2025-08-23 03:00:14 +03:00
|
|
|
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
2024-11-26 04:05:25 +03:00
|
|
|
|
2025-10-05 18:17:18 +03:00
|
|
|
return ReadFile(fs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MissionTmaParseResult ReadFile(Stream fs)
|
|
|
|
|
{
|
2024-11-26 04:05:25 +03:00
|
|
|
var arealData = LoadAreals(fs);
|
|
|
|
|
|
|
|
|
|
var clansData = LoadClans(fs);
|
|
|
|
|
|
|
|
|
|
if (clansData is null) return new MissionTmaParseResult(null, "Не обнаружена информация о кланах");
|
|
|
|
|
|
|
|
|
|
var gameObjectsData = LoadGameObjects(fs);
|
|
|
|
|
|
|
|
|
|
var missionDat = new MissionTma(arealData, clansData, gameObjectsData);
|
|
|
|
|
return new MissionTmaParseResult(missionDat, null);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 18:17:18 +03:00
|
|
|
private static ArealsFileData LoadAreals(Stream fileStream)
|
2024-11-26 04:05:25 +03:00
|
|
|
{
|
|
|
|
|
var unusedHeader = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
var arealCount = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
// В демо миссии нет ареалов, ровно как и в первой миссии кампании
|
|
|
|
|
// Span<byte> arealBuffer = stackalloc byte[12];
|
|
|
|
|
|
|
|
|
|
List<ArealInfo> infos = [];
|
|
|
|
|
for (var i = 0; i < arealCount; i++)
|
|
|
|
|
{
|
|
|
|
|
// игра читает 4 байта - видимо количество
|
|
|
|
|
var unknown4Bytes = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
var count = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
List<Vector3> 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 18:17:18 +03:00
|
|
|
private static ClansFileData? LoadClans(Stream fileStream)
|
2024-11-26 04:05:25 +03:00
|
|
|
{
|
|
|
|
|
var clanFeatureSet = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
if (clanFeatureSet is <= 0 or >= 7) return null;
|
|
|
|
|
|
|
|
|
|
var clanCount = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
List<ClanInfo> infos = [];
|
|
|
|
|
for (var i = 0; i < clanCount; i++)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
var clanName = fileStream.ReadLengthPrefixedString();
|
|
|
|
|
var unkInt1 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
var x = fileStream.ReadFloatLittleEndian();
|
|
|
|
|
var y = fileStream.ReadFloatLittleEndian();
|
|
|
|
|
var clanType = (ClanType) fileStream.ReadInt32LittleEndian();
|
|
|
|
|
var scriptsString = string.Empty;
|
|
|
|
|
var unknownClanPartCount = 0;
|
|
|
|
|
List<UnknownClanTreeInfoPart> unknownParts = [];
|
|
|
|
|
var researchNResPath = string.Empty;
|
|
|
|
|
var brains = 0;
|
|
|
|
|
var alliesMapCount = 0;
|
|
|
|
|
Dictionary<string, int> alliesMap = [];
|
2024-11-26 04:05:25 +03:00
|
|
|
|
|
|
|
|
if (1 < clanFeatureSet)
|
|
|
|
|
{
|
|
|
|
|
// MISSIONS\SCRIPTS\default
|
|
|
|
|
// MISSIONS\SCRIPTS\tut1_pl
|
|
|
|
|
// MISSIONS\SCRIPTS\tut1_en
|
2026-05-09 20:36:43 +03:00
|
|
|
scriptsString = fileStream.ReadLengthPrefixedString();
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (2 < clanFeatureSet)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
unknownClanPartCount = fileStream.ReadInt32LittleEndian();
|
2024-11-26 04:05:25 +03:00
|
|
|
|
|
|
|
|
// тут игра читает число, затем 12 байт и ещё 2 числа
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
for (var i1 = 0; i1 < unknownClanPartCount; i1++)
|
2024-11-26 04:05:25 +03:00
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
unknownParts.Add(
|
2024-11-26 04:05:25 +03:00
|
|
|
new UnknownClanTreeInfoPart(
|
|
|
|
|
fileStream.ReadInt32LittleEndian(),
|
|
|
|
|
new Vector3(
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian()
|
|
|
|
|
),
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian()
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (3 < clanFeatureSet)
|
|
|
|
|
{
|
|
|
|
|
// MISSIONS\SCRIPTS\auto.trf
|
|
|
|
|
// MISSIONS\SCRIPTS\data.trf
|
|
|
|
|
// указатель на NRes файл с данными
|
|
|
|
|
// может быть пустым, например у Ntrl в туториале
|
2026-05-09 20:36:43 +03:00
|
|
|
researchNResPath = fileStream.ReadLengthPrefixedString();
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (4 < clanFeatureSet)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
brains = fileStream.ReadInt32LittleEndian();
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (5 < clanFeatureSet)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
alliesMapCount = fileStream.ReadInt32LittleEndian();
|
2024-11-26 04:05:25 +03:00
|
|
|
|
|
|
|
|
// тут какая-то мапа
|
|
|
|
|
// в демо миссии тут
|
|
|
|
|
// player -> 1
|
|
|
|
|
// player2 -> 0
|
|
|
|
|
|
|
|
|
|
// в туториале
|
|
|
|
|
// Plr -> 1
|
|
|
|
|
// Trgt -> 1
|
|
|
|
|
// Enm -> 0
|
|
|
|
|
// Ntrl -> 1
|
2026-05-09 20:36:43 +03:00
|
|
|
for (var i1 = 0; i1 < alliesMapCount; i1++)
|
2024-11-26 04:05:25 +03:00
|
|
|
{
|
|
|
|
|
var keyIdString = fileStream.ReadLengthPrefixedString();
|
|
|
|
|
// это число всегда либо 0 либо 1
|
|
|
|
|
var unkNumber = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
alliesMap[keyIdString] = unkNumber;
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
infos.Add(new ClanInfo(
|
|
|
|
|
clanName,
|
|
|
|
|
unkInt1,
|
|
|
|
|
x,
|
|
|
|
|
y,
|
|
|
|
|
clanType,
|
|
|
|
|
scriptsString,
|
|
|
|
|
unknownClanPartCount,
|
|
|
|
|
unknownParts,
|
|
|
|
|
researchNResPath,
|
|
|
|
|
brains,
|
|
|
|
|
alliesMapCount,
|
|
|
|
|
alliesMap));
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var clanInfo = new ClansFileData(clanFeatureSet, clanCount, infos);
|
|
|
|
|
|
|
|
|
|
return clanInfo;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 18:17:18 +03:00
|
|
|
private static GameObjectsFileData LoadGameObjects(Stream fileStream)
|
2024-11-26 04:05:25 +03:00
|
|
|
{
|
|
|
|
|
var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
var gameObjectsCount = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
Span<byte> settingVal1 = stackalloc byte[4];
|
|
|
|
|
Span<byte> settingVal2 = stackalloc byte[4];
|
|
|
|
|
Span<byte> settingVal3 = stackalloc byte[4];
|
|
|
|
|
|
|
|
|
|
List<GameObjectInfo> gameObjectInfos = [];
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < gameObjectsCount; i++)
|
|
|
|
|
{
|
|
|
|
|
// ReadGameObjectData
|
2026-05-09 20:36:43 +03:00
|
|
|
var type = (GameObjectType) fileStream.ReadInt32LittleEndian();
|
|
|
|
|
var unknownFlags = fileStream.ReadInt32LittleEndian();
|
2024-11-26 04:05:25 +03:00
|
|
|
|
|
|
|
|
// UNITS\UNITS\HERO\hero_t.dat
|
2026-05-09 20:36:43 +03:00
|
|
|
var datString = fileStream.ReadLengthPrefixedString();
|
|
|
|
|
var owningClanIndex = 0;
|
|
|
|
|
var order = 0;
|
|
|
|
|
var unknownString2 = string.Empty;
|
|
|
|
|
var unknownInt4 = 0;
|
|
|
|
|
var unknownInt5 = 0;
|
|
|
|
|
var unknownInt6 = 0;
|
|
|
|
|
var settingsData = new GameObjectSettings(0, 0, []);
|
2024-11-26 04:05:25 +03:00
|
|
|
|
|
|
|
|
if (2 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
owningClanIndex = fileStream.ReadInt32LittleEndian();
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (3 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
order = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
if (type == GameObjectType.Building)
|
2025-02-24 23:35:55 +03:00
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
order += int.MaxValue;
|
2025-02-24 23:35:55 +03:00
|
|
|
}
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// читает 12 байт
|
2026-05-09 20:36:43 +03:00
|
|
|
var position = new Vector3(
|
2024-11-26 04:05:25 +03:00
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ещё раз читает 12 байт
|
2026-05-09 20:36:43 +03:00
|
|
|
var rotation = new Vector3(
|
2024-11-26 04:05:25 +03:00
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian()
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
Vector3 scale;
|
2024-11-26 04:05:25 +03:00
|
|
|
if (gameObjectsFeatureSet < 10)
|
|
|
|
|
{
|
|
|
|
|
// если фичесет меньше 10, то игра забивает вектор единицами
|
2026-05-09 20:36:43 +03:00
|
|
|
scale = new Vector3(1, 1, 1);
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// в противном случае читает ещё вектор из файла
|
2026-05-09 20:36:43 +03:00
|
|
|
scale = new Vector3(
|
2024-11-26 04:05:25 +03:00
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (6 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
|
|
|
|
// у HERO пустая строка
|
2026-05-09 20:36:43 +03:00
|
|
|
unknownString2 = fileStream.ReadLengthPrefixedString();
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (7 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
unknownInt4 = fileStream.ReadInt32LittleEndian();
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (8 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
unknownInt5 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
unknownInt6 = fileStream.ReadInt32LittleEndian();
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (5 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
|
|
|
|
// тут игра вызывает ещё одну функцию чтения файла - видимо это настройки объекта
|
|
|
|
|
|
|
|
|
|
var unused = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
var innerCount = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
List<GameObjectSetting> settings = [];
|
|
|
|
|
for (var i1 = 0; i1 < innerCount; i1++)
|
|
|
|
|
{
|
|
|
|
|
// судя по всему это тип настройки
|
|
|
|
|
// 0 - float, 1 - int, 2?
|
|
|
|
|
var settingType = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
settingVal1.Clear();
|
|
|
|
|
settingVal2.Clear();
|
|
|
|
|
settingVal3.Clear();
|
|
|
|
|
fileStream.ReadExactly(settingVal1);
|
|
|
|
|
fileStream.ReadExactly(settingVal2);
|
|
|
|
|
fileStream.ReadExactly(settingVal3);
|
|
|
|
|
|
|
|
|
|
IntFloatValue val1;
|
|
|
|
|
IntFloatValue val2;
|
|
|
|
|
IntFloatValue val3;
|
|
|
|
|
|
|
|
|
|
if (settingType == 0)
|
|
|
|
|
{
|
|
|
|
|
// float
|
|
|
|
|
val1 = new IntFloatValue(settingVal1);
|
|
|
|
|
val2 = new IntFloatValue(settingVal2);
|
|
|
|
|
val3 = new IntFloatValue(settingVal3);
|
|
|
|
|
// var innerFloat1 = fileStream.ReadFloatLittleEndian();
|
|
|
|
|
// var innerFloat2 = fileStream.ReadFloatLittleEndian();
|
|
|
|
|
// судя по всему это значение настройки
|
|
|
|
|
// var innerFloat3 = fileStream.ReadFloatLittleEndian();
|
|
|
|
|
}
|
|
|
|
|
else if (settingType == 1)
|
|
|
|
|
{
|
|
|
|
|
val1 = new IntFloatValue(settingVal1);
|
|
|
|
|
val2 = new IntFloatValue(settingVal2);
|
|
|
|
|
val3 = new IntFloatValue(settingVal3);
|
|
|
|
|
// var innerInt1 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
// var innerInt2 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
// судя по всему это значение настройки
|
|
|
|
|
// var innerInt3 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Settings value type is not float or int");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Invulnerability
|
|
|
|
|
// Life state
|
|
|
|
|
// LogicalID
|
|
|
|
|
// ClanID
|
|
|
|
|
// Type
|
|
|
|
|
// MaxSpeedPercent
|
|
|
|
|
// MaximumOre
|
|
|
|
|
// CurrentOre
|
|
|
|
|
var name = fileStream.ReadLengthPrefixedString();
|
|
|
|
|
|
|
|
|
|
settings.Add(
|
|
|
|
|
new GameObjectSetting(
|
|
|
|
|
settingType,
|
|
|
|
|
val1,
|
|
|
|
|
val2,
|
|
|
|
|
val3,
|
|
|
|
|
name
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
settingsData = new GameObjectSettings(unused, innerCount, settings);
|
2024-11-26 04:05:25 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
gameObjectInfos.Add(new GameObjectInfo(
|
|
|
|
|
type,
|
|
|
|
|
unknownFlags,
|
|
|
|
|
datString,
|
|
|
|
|
owningClanIndex,
|
|
|
|
|
order,
|
|
|
|
|
position,
|
|
|
|
|
rotation,
|
|
|
|
|
scale,
|
|
|
|
|
unknownString2,
|
|
|
|
|
unknownInt4,
|
|
|
|
|
unknownInt5,
|
|
|
|
|
unknownInt6,
|
|
|
|
|
settingsData));
|
2024-11-26 04:05:25 +03:00
|
|
|
|
|
|
|
|
// end ReadGameObjectData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DATA\MAPS\KM_2\land
|
|
|
|
|
// DATA\MAPS\SC_3\land
|
|
|
|
|
var landString = fileStream.ReadLengthPrefixedString();
|
|
|
|
|
|
|
|
|
|
int unkInt7 = 0;
|
|
|
|
|
string? missionTechDescription = null;
|
|
|
|
|
if (1 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
|
|
|
|
unkInt7 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
// ? - байт cd
|
|
|
|
|
|
|
|
|
|
// Mission??????????trm\Is.\Ir
|
|
|
|
|
// Skirmish 1. Full Base, One opponent?????
|
|
|
|
|
// New mission?????????????????M
|
|
|
|
|
missionTechDescription = fileStream.ReadLengthPrefixedString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LodeData? lodeData = null;
|
|
|
|
|
if (4 < gameObjectsFeatureSet)
|
|
|
|
|
{
|
|
|
|
|
var unused = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
var lodeCount = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
List<LodeInfo> lodeInfos = [];
|
|
|
|
|
for (var i1 = 0; i1 < lodeCount; i1++)
|
|
|
|
|
{
|
|
|
|
|
var unkLodeVector = new Vector3(
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian(),
|
|
|
|
|
fileStream.ReadFloatLittleEndian()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
var unkLodeInt1 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
var unkLodeFlags2 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
var unkLodeFloat3 = fileStream.ReadFloatLittleEndian();
|
|
|
|
|
var unkLodeInt4 = fileStream.ReadInt32LittleEndian();
|
|
|
|
|
|
|
|
|
|
lodeInfos.Add(
|
|
|
|
|
new LodeInfo(
|
|
|
|
|
unkLodeVector,
|
|
|
|
|
unkLodeInt1,
|
|
|
|
|
unkLodeFlags2,
|
|
|
|
|
unkLodeFloat3,
|
|
|
|
|
unkLodeInt4
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lodeData = new LodeData(unused, lodeCount, lodeInfos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new GameObjectsFileData(
|
|
|
|
|
gameObjectsFeatureSet,
|
|
|
|
|
gameObjectsCount,
|
|
|
|
|
gameObjectInfos,
|
|
|
|
|
landString,
|
|
|
|
|
unkInt7,
|
|
|
|
|
missionTechDescription,
|
|
|
|
|
lodeData
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-09 20:36:43 +03:00
|
|
|
}
|