From e16b219854a1df977b2582d10c4ba2f79496f2cb Mon Sep 17 00:00:00 2001 From: bird_egop Date: Tue, 26 Nov 2024 04:05:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20tm?= =?UTF-8?q?a=20=D0=B2=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80?= =?UTF-8?q?=20=D0=B8=20=D0=BE=D1=82=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BB=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MissionDataUnpacker/Program.cs | 508 ------------------ MissionTmaLib/ArealInfo.cs | 3 + MissionTmaLib/ArealsFileData.cs | 4 + MissionTmaLib/ClanInfo.cs | 30 ++ MissionTmaLib/ClanType.cs | 24 + MissionTmaLib/ClansFileData.cs | 3 + MissionTmaLib/GameObjectInfo.cs | 38 ++ MissionTmaLib/GameObjectSetting.cs | 3 + MissionTmaLib/GameObjectSettings.cs | 3 + MissionTmaLib/GameObjectType.cs | 9 + MissionTmaLib/GameObjectsFileData.cs | 3 + MissionTmaLib/IntFloatValue.cs | 11 + MissionTmaLib/LodeData.cs | 3 + MissionTmaLib/LodeInfo.cs | 3 + MissionTmaLib/MissionTmaLib.csproj | 9 + MissionTmaLib/Parsing/Extensions.cs | 39 ++ .../Parsing/MissionTmaParseResult.cs | 3 + MissionTmaLib/Parsing/MissionTmaParser.cs | 383 +++++++++++++ MissionTmaLib/UnknownClanTreeInfoPart.cs | 3 + MissionTmaLib/Vector3.cs | 3 + NResUI/App.cs | 1 + NResUI/ImGuiUI/MainMenuBar.cs | 45 +- NResUI/ImGuiUI/MissionTmaExplorer.cs | 218 ++++++++ NResUI/ImGuiUI/TexmExplorer.cs | 2 + NResUI/Models/MissionTmaViewModel.cs | 26 + NResUI/NResUI.csproj | 1 + NResUI/Utils.cs | 31 +- ParkanPlayground.sln | 6 + 28 files changed, 878 insertions(+), 537 deletions(-) create mode 100644 MissionTmaLib/ArealInfo.cs create mode 100644 MissionTmaLib/ArealsFileData.cs create mode 100644 MissionTmaLib/ClanInfo.cs create mode 100644 MissionTmaLib/ClanType.cs create mode 100644 MissionTmaLib/ClansFileData.cs create mode 100644 MissionTmaLib/GameObjectInfo.cs create mode 100644 MissionTmaLib/GameObjectSetting.cs create mode 100644 MissionTmaLib/GameObjectSettings.cs create mode 100644 MissionTmaLib/GameObjectType.cs create mode 100644 MissionTmaLib/GameObjectsFileData.cs create mode 100644 MissionTmaLib/IntFloatValue.cs create mode 100644 MissionTmaLib/LodeData.cs create mode 100644 MissionTmaLib/LodeInfo.cs create mode 100644 MissionTmaLib/MissionTmaLib.csproj create mode 100644 MissionTmaLib/Parsing/Extensions.cs create mode 100644 MissionTmaLib/Parsing/MissionTmaParseResult.cs create mode 100644 MissionTmaLib/Parsing/MissionTmaParser.cs create mode 100644 MissionTmaLib/UnknownClanTreeInfoPart.cs create mode 100644 MissionTmaLib/Vector3.cs create mode 100644 NResUI/ImGuiUI/MissionTmaExplorer.cs create mode 100644 NResUI/Models/MissionTmaViewModel.cs diff --git a/MissionDataUnpacker/Program.cs b/MissionDataUnpacker/Program.cs index cdb5521..39ea3bc 100644 --- a/MissionDataUnpacker/Program.cs +++ b/MissionDataUnpacker/Program.cs @@ -7,511 +7,3 @@ using System.Text; // 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.02\\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 clansData = LoadClans(fs); - -var gameObjectsData = LoadGameObjects(fs); - -_ = 5; - -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 ClanInfo(); - - clanTreeInfo.ClanName = fileStream.ReadLengthPrefixedString(); - clanTreeInfo.UnkInt1 = fileStream.ReadInt32LittleEndian(); - clanTreeInfo.X = fileStream.ReadFloatLittleEndian(); - clanTreeInfo.Y = fileStream.ReadFloatLittleEndian(); - clanTreeInfo.ClanType = (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; -} - -GameObjectsFileData LoadGameObjects(FileStream fileStream) -{ - var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian(); - - var gameObjectsCount = fileStream.ReadInt32LittleEndian(); - - Span settingVal1 = stackalloc byte[4]; - Span settingVal2 = stackalloc byte[4]; - Span settingVal3 = stackalloc byte[4]; - - List gameObjectInfos = []; - - for (var i = 0; i < gameObjectsCount; i++) - { - var gameObjectInfo = new GameObjectInfo(); - // ReadGameObjectData - gameObjectInfo.Type = (GameObjectType)fileStream.ReadInt32LittleEndian(); - gameObjectInfo.UnknownFlags = fileStream.ReadInt32LittleEndian(); - - // UNITS\UNITS\HERO\hero_t.dat - gameObjectInfo.DatString = fileStream.ReadLengthPrefixedString(); - - if (2 < gameObjectsFeatureSet) - { - gameObjectInfo.OwningClanIndex = fileStream.ReadInt32LittleEndian(); - } - - if (3 < gameObjectsFeatureSet) - { - gameObjectInfo.UnknownInt3 = fileStream.ReadInt32LittleEndian(); - } - - // читает 12 байт - gameObjectInfo.Position = new Vector3( - fileStream.ReadFloatLittleEndian(), - fileStream.ReadFloatLittleEndian(), - fileStream.ReadFloatLittleEndian() - ); - - // ещё раз читает 12 байт - gameObjectInfo.Rotation = new Vector3( - fileStream.ReadFloatLittleEndian(), - fileStream.ReadFloatLittleEndian(), - fileStream.ReadFloatLittleEndian() - ); - - if (gameObjectsFeatureSet < 10) - { - // если фичесет меньше 10, то игра забивает вектор единицами - gameObjectInfo.Scale = new Vector3(1, 1, 1); - } - else - { - // в противном случае читает ещё вектор из файла - gameObjectInfo.Scale = new Vector3( - fileStream.ReadFloatLittleEndian(), - fileStream.ReadFloatLittleEndian(), - fileStream.ReadFloatLittleEndian() - ); - } - - if (6 < gameObjectsFeatureSet) - { - // у HERO пустая строка - gameObjectInfo.UnknownString2 = fileStream.ReadLengthPrefixedString(); - } - - if (7 < gameObjectsFeatureSet) - { - gameObjectInfo.UnknownInt4 = fileStream.ReadInt32LittleEndian(); - } - - if (8 < gameObjectsFeatureSet) - { - gameObjectInfo.UnknownInt5 = fileStream.ReadInt32LittleEndian(); - gameObjectInfo.UnknownInt6 = fileStream.ReadInt32LittleEndian(); - } - - if (5 < gameObjectsFeatureSet) - { - // тут игра вызывает ещё одну функцию чтения файла - видимо это настройки объекта - - var unused = fileStream.ReadInt32LittleEndian(); - - var innerCount = fileStream.ReadInt32LittleEndian(); - - List 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)); - } - - gameObjectInfo.Settings = new GameObjectSettings(unused, innerCount, settings); - } - - gameObjectInfos.Add(gameObjectInfo); - - // 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 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 - ); -} - -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(); - - if (len == 0) - { - return ""; - } - - 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 ClanCount, List ClanInfos); - -public class ClanInfo -{ - 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 ClanType ClanType { get; set; } - - public string UnkString2 { get; set; } - public int UnknownClanPartCount { get; set; } - public List UnknownParts { get; set; } - - /// - /// Игра называет этот путь TreeName - /// - 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); - -[DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")] -public class IntFloatValue(Span span) -{ - public int AsInt { get; set; } = MemoryMarshal.Read(span); - public float AsFloat { get; set; } = MemoryMarshal.Read(span); -} - -public record GameObjectsFileData(int GameObjectsFeatureSet, int GameObjectsCount, List GameObjectInfos, string LandString, int UnknownInt, string? MissionTechDescription, LodeData? LodeData); - -public class GameObjectInfo -{ - // 0 - здание, 1 - бот, 2 - окружение - public GameObjectType Type { get; set; } - - public int UnknownFlags { get; set; } - - public string DatString { get; set; } - - /// - /// Индекс клана, которому принадлежит объект - /// - /// - /// - /// Некоторые объекты окружения иногда почему-то принадлежат клану отличному от -1 - /// - /// - /// Может быть -1, если объект никому не принадлежит, я такое встречал только у объектов окружения - /// - /// - public int OwningClanIndex { get; set; } - - public int UnknownInt3 { get; set; } - - public Vector3 Position { get; set; } - public Vector3 Rotation { get; set; } - public Vector3 Scale { get; set; } - - public string UnknownString2 { get; set; } - - public int UnknownInt4 { get; set; } - public int UnknownInt5 { get; set; } - public int UnknownInt6 { get; set; } - - public GameObjectSettings Settings { get; set; } -} - -public record GameObjectSettings(int Unused, int SettingsCount, List Settings); - -public record GameObjectSetting(int SettingType, IntFloatValue Unk1, IntFloatValue Unk2, IntFloatValue Unk3, string Name); - -public record LodeData(int Unused, int LodeCount, List Lodes); - -public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownInt2, float UnknownFloat, int UnknownInt3); - -public enum GameObjectType -{ - Building = 0, - Warbot = 1, - Tree = 2, - Stone = 3 -} - -public enum ClanType -{ - Environment = 0, - Player = 1, - AI = 2, - Neutral = 3 -} \ No newline at end of file diff --git a/MissionTmaLib/ArealInfo.cs b/MissionTmaLib/ArealInfo.cs new file mode 100644 index 0000000..31a751c --- /dev/null +++ b/MissionTmaLib/ArealInfo.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record ArealInfo(int Index, int CoordsCount, List Coords); \ No newline at end of file diff --git a/MissionTmaLib/ArealsFileData.cs b/MissionTmaLib/ArealsFileData.cs new file mode 100644 index 0000000..538f7a4 --- /dev/null +++ b/MissionTmaLib/ArealsFileData.cs @@ -0,0 +1,4 @@ +namespace MissionTmaLib; + + +public record ArealsFileData(int UnusedHeader, int ArealCount, List ArealInfos); diff --git a/MissionTmaLib/ClanInfo.cs b/MissionTmaLib/ClanInfo.cs new file mode 100644 index 0000000..fe1ec8a --- /dev/null +++ b/MissionTmaLib/ClanInfo.cs @@ -0,0 +1,30 @@ +namespace MissionTmaLib; + +public class ClanInfo +{ + 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 ClanType ClanType { get; set; } + + public string UnkString2 { get; set; } + public int UnknownClanPartCount { get; set; } + public List UnknownParts { get; set; } + + /// + /// Игра называет этот путь TreeName + /// + public string ResearchNResPath { get; set; } + public int UnkInt3 { get; set; } + public int AlliesMapCount { get; set; } + + /// + /// мапа союзников (ключ - имя клана, значение - число, всегда либо 0 либо 1) + /// + public Dictionary AlliesMap { get; set; } +} \ No newline at end of file diff --git a/MissionTmaLib/ClanType.cs b/MissionTmaLib/ClanType.cs new file mode 100644 index 0000000..d694c69 --- /dev/null +++ b/MissionTmaLib/ClanType.cs @@ -0,0 +1,24 @@ +namespace MissionTmaLib; + +public enum ClanType +{ + Environment = 0, + Player = 1, + AI = 2, + Neutral = 3 +} + +public static class Extensions +{ + public static string ToReadableString(this ClanType clanType) + { + return clanType switch + { + ClanType.Environment => $"Окружение ({clanType:D})", + ClanType.Player => $"Игрок ({clanType:D})", + ClanType.AI => $"AI ({clanType:D})", + ClanType.Neutral => $"Нейтральный ({clanType:D})", + _ => $"Неизвестный ({clanType:D})" + }; + } +} \ No newline at end of file diff --git a/MissionTmaLib/ClansFileData.cs b/MissionTmaLib/ClansFileData.cs new file mode 100644 index 0000000..cf907da --- /dev/null +++ b/MissionTmaLib/ClansFileData.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record ClansFileData(int ClanFeatureSet, int ClanCount, List ClanInfos); \ No newline at end of file diff --git a/MissionTmaLib/GameObjectInfo.cs b/MissionTmaLib/GameObjectInfo.cs new file mode 100644 index 0000000..100146f --- /dev/null +++ b/MissionTmaLib/GameObjectInfo.cs @@ -0,0 +1,38 @@ +namespace MissionTmaLib; + +public class GameObjectInfo +{ + // 0 - здание, 1 - бот, 2 - окружение + public GameObjectType Type { get; set; } + + public int UnknownFlags { get; set; } + + public string DatString { get; set; } + + /// + /// Индекс клана, которому принадлежит объект + /// + /// + /// + /// Некоторые объекты окружения иногда почему-то принадлежат клану отличному от -1 + /// + /// + /// Может быть -1, если объект никому не принадлежит, я такое встречал только у объектов окружения + /// + /// + public int OwningClanIndex { get; set; } + + public int UnknownInt3 { get; set; } + + public Vector3 Position { get; set; } + public Vector3 Rotation { get; set; } + public Vector3 Scale { get; set; } + + public string UnknownString2 { get; set; } + + public int UnknownInt4 { get; set; } + public int UnknownInt5 { get; set; } + public int UnknownInt6 { get; set; } + + public GameObjectSettings Settings { get; set; } +} \ No newline at end of file diff --git a/MissionTmaLib/GameObjectSetting.cs b/MissionTmaLib/GameObjectSetting.cs new file mode 100644 index 0000000..090b0a2 --- /dev/null +++ b/MissionTmaLib/GameObjectSetting.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record GameObjectSetting(int SettingType, IntFloatValue Unk1, IntFloatValue Unk2, IntFloatValue Unk3, string Name); \ No newline at end of file diff --git a/MissionTmaLib/GameObjectSettings.cs b/MissionTmaLib/GameObjectSettings.cs new file mode 100644 index 0000000..7cde1c9 --- /dev/null +++ b/MissionTmaLib/GameObjectSettings.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record GameObjectSettings(int Unused, int SettingsCount, List Settings); \ No newline at end of file diff --git a/MissionTmaLib/GameObjectType.cs b/MissionTmaLib/GameObjectType.cs new file mode 100644 index 0000000..a18ab08 --- /dev/null +++ b/MissionTmaLib/GameObjectType.cs @@ -0,0 +1,9 @@ +namespace MissionTmaLib; + +public enum GameObjectType +{ + Building = 0, + Warbot = 1, + Tree = 2, + Stone = 3 +} \ No newline at end of file diff --git a/MissionTmaLib/GameObjectsFileData.cs b/MissionTmaLib/GameObjectsFileData.cs new file mode 100644 index 0000000..d2ae56c --- /dev/null +++ b/MissionTmaLib/GameObjectsFileData.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record GameObjectsFileData(int GameObjectsFeatureSet, int GameObjectsCount, List GameObjectInfos, string LandString, int UnknownInt, string? MissionTechDescription, LodeData? LodeData); \ No newline at end of file diff --git a/MissionTmaLib/IntFloatValue.cs b/MissionTmaLib/IntFloatValue.cs new file mode 100644 index 0000000..b07eb25 --- /dev/null +++ b/MissionTmaLib/IntFloatValue.cs @@ -0,0 +1,11 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace MissionTmaLib; + +[DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")] +public class IntFloatValue(Span span) +{ + public int AsInt { get; set; } = MemoryMarshal.Read(span); + public float AsFloat { get; set; } = MemoryMarshal.Read(span); +} \ No newline at end of file diff --git a/MissionTmaLib/LodeData.cs b/MissionTmaLib/LodeData.cs new file mode 100644 index 0000000..b6665f5 --- /dev/null +++ b/MissionTmaLib/LodeData.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record LodeData(int Unused, int LodeCount, List Lodes); \ No newline at end of file diff --git a/MissionTmaLib/LodeInfo.cs b/MissionTmaLib/LodeInfo.cs new file mode 100644 index 0000000..aa98163 --- /dev/null +++ b/MissionTmaLib/LodeInfo.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownInt2, float UnknownFloat, int UnknownInt3); \ No newline at end of file diff --git a/MissionTmaLib/MissionTmaLib.csproj b/MissionTmaLib/MissionTmaLib.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/MissionTmaLib/MissionTmaLib.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/MissionTmaLib/Parsing/Extensions.cs b/MissionTmaLib/Parsing/Extensions.cs new file mode 100644 index 0000000..ea009b9 --- /dev/null +++ b/MissionTmaLib/Parsing/Extensions.cs @@ -0,0 +1,39 @@ +using System.Buffers.Binary; +using System.Text; + +namespace MissionTmaLib.Parsing; + +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(); + + if (len == 0) + { + return ""; + } + + var buffer = new byte[len]; + + fs.ReadExactly(buffer, 0, len); + + return Encoding.ASCII.GetString(buffer, 0, len); + } +} \ No newline at end of file diff --git a/MissionTmaLib/Parsing/MissionTmaParseResult.cs b/MissionTmaLib/Parsing/MissionTmaParseResult.cs new file mode 100644 index 0000000..10517f1 --- /dev/null +++ b/MissionTmaLib/Parsing/MissionTmaParseResult.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib.Parsing; + +public record MissionTmaParseResult(MissionTma? Mission, string? Error); \ No newline at end of file diff --git a/MissionTmaLib/Parsing/MissionTmaParser.cs b/MissionTmaLib/Parsing/MissionTmaParser.cs new file mode 100644 index 0000000..300d1a6 --- /dev/null +++ b/MissionTmaLib/Parsing/MissionTmaParser.cs @@ -0,0 +1,383 @@ +namespace MissionTmaLib.Parsing; + +public class MissionTmaParser +{ + public static MissionTmaParseResult ReadFile(string filePath) + { + var fs = new FileStream(filePath, FileMode.Open); + + 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); + } + + private static 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); + } + + private static ClansFileData? LoadClans(FileStream fileStream) + { + var clanFeatureSet = fileStream.ReadInt32LittleEndian(); + + if (clanFeatureSet is <= 0 or >= 7) return null; + + var clanCount = fileStream.ReadInt32LittleEndian(); + + List infos = []; + for (var i = 0; i < clanCount; i++) + { + var clanTreeInfo = new ClanInfo(); + + clanTreeInfo.ClanName = fileStream.ReadLengthPrefixedString(); + clanTreeInfo.UnkInt1 = fileStream.ReadInt32LittleEndian(); + clanTreeInfo.X = fileStream.ReadFloatLittleEndian(); + clanTreeInfo.Y = fileStream.ReadFloatLittleEndian(); + clanTreeInfo.ClanType = (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, clanCount, infos); + + return clanInfo; + } + + private static GameObjectsFileData LoadGameObjects(FileStream fileStream) + { + var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian(); + + var gameObjectsCount = fileStream.ReadInt32LittleEndian(); + + Span settingVal1 = stackalloc byte[4]; + Span settingVal2 = stackalloc byte[4]; + Span settingVal3 = stackalloc byte[4]; + + List gameObjectInfos = []; + + for (var i = 0; i < gameObjectsCount; i++) + { + var gameObjectInfo = new GameObjectInfo(); + // ReadGameObjectData + gameObjectInfo.Type = (GameObjectType) fileStream.ReadInt32LittleEndian(); + gameObjectInfo.UnknownFlags = fileStream.ReadInt32LittleEndian(); + + // UNITS\UNITS\HERO\hero_t.dat + gameObjectInfo.DatString = fileStream.ReadLengthPrefixedString(); + + if (2 < gameObjectsFeatureSet) + { + gameObjectInfo.OwningClanIndex = fileStream.ReadInt32LittleEndian(); + } + + if (3 < gameObjectsFeatureSet) + { + gameObjectInfo.UnknownInt3 = fileStream.ReadInt32LittleEndian(); + } + + // читает 12 байт + gameObjectInfo.Position = new Vector3( + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian() + ); + + // ещё раз читает 12 байт + gameObjectInfo.Rotation = new Vector3( + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian() + ); + + if (gameObjectsFeatureSet < 10) + { + // если фичесет меньше 10, то игра забивает вектор единицами + gameObjectInfo.Scale = new Vector3(1, 1, 1); + } + else + { + // в противном случае читает ещё вектор из файла + gameObjectInfo.Scale = new Vector3( + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian(), + fileStream.ReadFloatLittleEndian() + ); + } + + if (6 < gameObjectsFeatureSet) + { + // у HERO пустая строка + gameObjectInfo.UnknownString2 = fileStream.ReadLengthPrefixedString(); + } + + if (7 < gameObjectsFeatureSet) + { + gameObjectInfo.UnknownInt4 = fileStream.ReadInt32LittleEndian(); + } + + if (8 < gameObjectsFeatureSet) + { + gameObjectInfo.UnknownInt5 = fileStream.ReadInt32LittleEndian(); + gameObjectInfo.UnknownInt6 = fileStream.ReadInt32LittleEndian(); + } + + if (5 < gameObjectsFeatureSet) + { + // тут игра вызывает ещё одну функцию чтения файла - видимо это настройки объекта + + var unused = fileStream.ReadInt32LittleEndian(); + + var innerCount = fileStream.ReadInt32LittleEndian(); + + List 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 + ) + ); + } + + gameObjectInfo.Settings = new GameObjectSettings(unused, innerCount, settings); + } + + gameObjectInfos.Add(gameObjectInfo); + + // 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 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 + ); + } +} + +public record MissionTma(ArealsFileData ArealData, ClansFileData ClansData, GameObjectsFileData GameObjectsData); \ No newline at end of file diff --git a/MissionTmaLib/UnknownClanTreeInfoPart.cs b/MissionTmaLib/UnknownClanTreeInfoPart.cs new file mode 100644 index 0000000..8267951 --- /dev/null +++ b/MissionTmaLib/UnknownClanTreeInfoPart.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record UnknownClanTreeInfoPart(int UnkInt1, Vector3 UnkVector, float UnkInt2, float UnkInt3); \ No newline at end of file diff --git a/MissionTmaLib/Vector3.cs b/MissionTmaLib/Vector3.cs new file mode 100644 index 0000000..9e3c654 --- /dev/null +++ b/MissionTmaLib/Vector3.cs @@ -0,0 +1,3 @@ +namespace MissionTmaLib; + +public record Vector3(float X, float Y, float Z); \ No newline at end of file diff --git a/NResUI/App.cs b/NResUI/App.cs index 34b4689..8cdc248 100644 --- a/NResUI/App.cs +++ b/NResUI/App.cs @@ -52,6 +52,7 @@ public class App serviceCollection.AddSingleton(new NResExplorerViewModel()); serviceCollection.AddSingleton(new TexmExplorerViewModel()); + serviceCollection.AddSingleton(new MissionTmaViewModel()); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/NResUI/ImGuiUI/MainMenuBar.cs b/NResUI/ImGuiUI/MainMenuBar.cs index 47742ff..af05b1e 100644 --- a/NResUI/ImGuiUI/MainMenuBar.cs +++ b/NResUI/ImGuiUI/MainMenuBar.cs @@ -1,5 +1,6 @@ using System.Numerics; using ImGuiNET; +using MissionTmaLib.Parsing; using NativeFileDialogSharp; using NResLib; using NResUI.Abstractions; @@ -12,13 +13,15 @@ namespace NResUI.ImGuiUI { private readonly NResExplorerViewModel _nResExplorerViewModel; private readonly TexmExplorerViewModel _texmExplorerViewModel; + private readonly MissionTmaViewModel _missionTmaViewModel; private readonly MessageBoxModalPanel _messageBox; - public MainMenuBar(NResExplorerViewModel nResExplorerViewModel, TexmExplorerViewModel texmExplorerViewModel, MessageBoxModalPanel messageBox) + public MainMenuBar(NResExplorerViewModel nResExplorerViewModel, TexmExplorerViewModel texmExplorerViewModel, MessageBoxModalPanel messageBox, MissionTmaViewModel missionTmaViewModel) { _nResExplorerViewModel = nResExplorerViewModel; _texmExplorerViewModel = texmExplorerViewModel; _messageBox = messageBox; + _missionTmaViewModel = missionTmaViewModel; } public void OnImGuiRender() @@ -78,6 +81,19 @@ namespace NResUI.ImGuiUI } } + if (ImGui.MenuItem("Open Mission TMA")) + { + var result = Dialog.FileOpen("tma"); + + if (result.IsOk) + { + var path = result.Path; + var parseResult = MissionTmaParser.ReadFile(path); + + _missionTmaViewModel.SetParseResult(parseResult, path); + } + } + if (_nResExplorerViewModel.HasFile) { if (ImGui.MenuItem("Экспортировать NRes")) @@ -102,32 +118,5 @@ namespace NResUI.ImGuiUI } } - // This is a direct port of imgui_demo.cpp HelpMarker function - - // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L190 - - private void ShowHint(string message) - { - // ImGui.TextDisabled("(?)"); - if (ImGui.IsItemHovered()) - { - // Change background transparency - ImGui.PushStyleColor( - ImGuiCol.PopupBg, - new Vector4( - 1, - 1, - 1, - 0.8f - ) - ); - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); - ImGui.TextUnformatted(message); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - ImGui.PopStyleColor(); - } - } } } \ No newline at end of file diff --git a/NResUI/ImGuiUI/MissionTmaExplorer.cs b/NResUI/ImGuiUI/MissionTmaExplorer.cs new file mode 100644 index 0000000..1d730b5 --- /dev/null +++ b/NResUI/ImGuiUI/MissionTmaExplorer.cs @@ -0,0 +1,218 @@ +using ImGuiNET; +using MissionTmaLib; +using NResUI.Abstractions; +using NResUI.Models; + +namespace NResUI.ImGuiUI; + +public class MissionTmaExplorer : IImGuiPanel +{ + private readonly MissionTmaViewModel _viewModel; + + public MissionTmaExplorer(MissionTmaViewModel viewModel) + { + _viewModel = viewModel; + } + + public void OnImGuiRender() + { + if (ImGui.Begin("Mission TMA Explorer")) + { + var mission = _viewModel.Mission; + if (_viewModel.HasFile && mission is not null) + { + ImGui.Text("Путь к файлу: "); + ImGui.SameLine(); + ImGui.Text(_viewModel.Path); + + if (ImGui.TreeNodeEx("Ареалы")) + { + var (unusedHeader, arealCount, arealInfos) = mission.ArealData; + + ImGui.Text("Неиспользуемый заголовок: "); + ImGui.SameLine(); + ImGui.Text(unusedHeader.ToString()); + + ImGui.Text("Количество ареалов: "); + ImGui.SameLine(); + ImGui.Text(arealCount.ToString()); + + if (ImGui.TreeNodeEx("Информация об ареалах")) + { + for (var i = 0; i < arealInfos.Count; i++) + { + var arealInfo = arealInfos[i]; + if (ImGui.TreeNodeEx($"Ареал {i}")) + { + Utils.ShowHint("Кажется, что ареал это просто некая зона на карте"); + ImGui.Text("Индекс: "); + ImGui.SameLine(); + ImGui.Text(arealInfo.Index.ToString()); + + ImGui.Text("Количество координат: "); + ImGui.SameLine(); + ImGui.Text(arealInfo.CoordsCount.ToString()); + + if (ImGui.BeginTable("Координаты", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableSetupColumn("X"); + ImGui.TableSetupColumn("Y"); + ImGui.TableSetupColumn("Z"); + + ImGui.TableHeadersRow(); + + for (int k = 0; k < arealInfo.Coords.Count; k++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text(arealInfo.Coords[k].X.ToString("F2")); + ImGui.TableNextColumn(); + ImGui.Text(arealInfo.Coords[k].Y.ToString("F2")); + ImGui.TableNextColumn(); + ImGui.Text(arealInfo.Coords[k].Z.ToString("F2")); + } + + ImGui.EndTable(); + } + ImGui.TreePop(); + } + } + ImGui.TreePop(); + } + ImGui.TreePop(); + } + + if (ImGui.TreeNodeEx("Кланы")) + { + var (clanFeatureSet, clanCount, clanInfos) = mission.ClansData; + + ImGui.Text("Фиче-сет: "); + Utils.ShowHint("Магическое число из файла, на основе которого игра читает разные секции о клане"); + ImGui.SameLine(); + ImGui.Text(clanFeatureSet.ToString()); + + ImGui.Text("Количество кланов: "); + ImGui.SameLine(); + ImGui.Text(clanCount.ToString()); + + if (ImGui.TreeNodeEx("Информация о кланах")) + { + for (var i = 0; i < clanInfos.Count; i++) + { + var clanInfo = clanInfos[i]; + if (ImGui.TreeNodeEx($"Клан {i} - \"{clanInfo.ClanName}\"")) + { + ImGui.Text("Неизвестное число 1: "); + ImGui.SameLine(); + ImGui.Text(clanInfo.UnkInt1.ToString()); + + ImGui.Text("X: "); + ImGui.SameLine(); + ImGui.Text(clanInfo.X.ToString()); + ImGui.SameLine(); + ImGui.Text(" Y: "); + ImGui.SameLine(); + ImGui.Text(clanInfo.Y.ToString()); + + ImGui.Text("Тип клана: "); + ImGui.SameLine(); + ImGui.Text(clanInfo.ClanType.ToReadableString()); + + ImGui.Text("Неизвестная строка 1: "); + Utils.ShowHint("Кажется это путь к файлу поведения (Behavior), но пока не понятно. Обычно пути соответствуют 2 файла."); + ImGui.SameLine(); + ImGui.Text(clanInfo.UnkString2); + + if (clanInfo.UnknownParts.Count > 0) + { + ImGui.Text("Неизвестная часть"); + if (ImGui.BeginTable("Неизвестная часть##unk1", 6, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableSetupColumn("Число 1"); + ImGui.TableSetupColumn("X"); + ImGui.TableSetupColumn("Y"); + ImGui.TableSetupColumn("Z"); + ImGui.TableSetupColumn("Число 2"); + ImGui.TableSetupColumn("Число 3"); + ImGui.TableHeadersRow(); + + for (var i1 = 0; i1 < clanInfo.UnknownParts.Count; i1++) + { + var unkPart = clanInfo.UnknownParts[i1]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text(unkPart.UnkInt1.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(unkPart.UnkVector.X.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(unkPart.UnkVector.Y.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(unkPart.UnkVector.Z.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(unkPart.UnkInt2.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(unkPart.UnkInt3.ToString()); + } + + ImGui.EndTable(); + } + } + else + { + ImGui.Text("Отсутствует неизвестная часть"); + } + + ImGui.Text("Путь к файлу .trf: "); + Utils.ShowHint("Не до конца понятно, что означает, вероятно это NRes с деревом исследований"); + ImGui.SameLine(); + ImGui.Text(clanInfo.ResearchNResPath); + + ImGui.Text("Неизвестное число 3: "); + ImGui.SameLine(); + ImGui.Text(clanInfo.UnkInt3.ToString()); + + ImGui.Text("Матрица союзников"); + Utils.ShowHint("Если 1, то кланы - союзники, и не нападают друг на друга"); + if (ImGui.BeginTable("Матрица союзников", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableSetupColumn("Клан"); + ImGui.TableSetupColumn("Союзник?"); + ImGui.TableHeadersRow(); + + foreach (var alliesMapKey in clanInfo.AlliesMap.Keys) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text(alliesMapKey); + ImGui.TableNextColumn(); + ImGui.Text(clanInfo.AlliesMap[alliesMapKey].ToString()); + } + + ImGui.EndTable(); + } + + + ImGui.TreePop(); + } + } + + ImGui.TreePop(); + } + + ImGui.TreePop(); + } + + if (ImGui.TreeNodeEx("Объекты")) + { + ImGui.TreePop(); + } + } + else + { + ImGui.Text("Миссия не открыта"); + } + + ImGui.End(); + } + } +} \ No newline at end of file diff --git a/NResUI/ImGuiUI/TexmExplorer.cs b/NResUI/ImGuiUI/TexmExplorer.cs index e6d86b9..4901887 100644 --- a/NResUI/ImGuiUI/TexmExplorer.cs +++ b/NResUI/ImGuiUI/TexmExplorer.cs @@ -116,6 +116,8 @@ public class TexmExplorer : IImGuiPanel ImGui.EndTable(); } + + ImGui.TreePop(); } } diff --git a/NResUI/Models/MissionTmaViewModel.cs b/NResUI/Models/MissionTmaViewModel.cs new file mode 100644 index 0000000..54138c7 --- /dev/null +++ b/NResUI/Models/MissionTmaViewModel.cs @@ -0,0 +1,26 @@ +using MissionTmaLib.Parsing; + +namespace NResUI.Models; + +public class MissionTmaViewModel +{ + public bool HasFile { get; set; } + public string? Error { get; set; } + + public MissionTma? Mission { get; set; } + + public string? Path { get; set; } + + public void SetParseResult(MissionTmaParseResult result, string path) + { + Error = result.Error; + + if (result.Mission != null) + { + HasFile = true; + } + + Mission = result.Mission; + Path = path; + } +} \ No newline at end of file diff --git a/NResUI/NResUI.csproj b/NResUI/NResUI.csproj index ce453ea..f441061 100644 --- a/NResUI/NResUI.csproj +++ b/NResUI/NResUI.csproj @@ -19,6 +19,7 @@ + diff --git a/NResUI/Utils.cs b/NResUI/Utils.cs index aff87cb..d7ed300 100644 --- a/NResUI/Utils.cs +++ b/NResUI/Utils.cs @@ -1,4 +1,6 @@ -using System.Reflection; +using System.Numerics; +using System.Reflection; +using ImGuiNET; namespace NResUI { @@ -60,5 +62,32 @@ namespace NResUI { return string.IsNullOrEmpty(str); } + // This is a direct port of imgui_demo.cpp HelpMarker function + + // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L190 + + public static void ShowHint(string message) + { + // ImGui.TextDisabled("(?)"); + if (ImGui.IsItemHovered()) + { + // Change background transparency + ImGui.PushStyleColor( + ImGuiCol.PopupBg, + new Vector4( + 1, + 1, + 1, + 0.8f + ) + ); + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); + ImGui.TextUnformatted(message); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + ImGui.PopStyleColor(); + } + } } } \ No newline at end of file diff --git a/ParkanPlayground.sln b/ParkanPlayground.sln index d2741c9..1116823 100644 --- a/ParkanPlayground.sln +++ b/ParkanPlayground.sln @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{BAF212FE-A README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MissionTmaLib", "MissionTmaLib\MissionTmaLib.csproj", "{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,5 +61,9 @@ Global {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 + {773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal