From f2bed4b14191a4e6a2dc4a2e0059ab790e2953fe Mon Sep 17 00:00:00 2001 From: bird_egop Date: Thu, 4 Sep 2025 02:45:26 +0300 Subject: [PATCH] Allow to view cp .dat in UI --- CpDatLib/CpDatEntry.cs | 12 ++ CpDatLib/CpDatLib.csproj | 5 + CpDatLib/CpDatParseResult.cs | 3 + CpDatLib/CpDatParser.cs | 68 ++++++++++ CpDatLib/CpDatScheme.cs | 3 + CpDatLib/CpEntryType.cs | 29 +++++ NResUI/App.cs | 1 + NResUI/ImGuiUI/CpDatSchemeExplorer.cs | 120 ++++++++++++++++++ NResUI/ImGuiUI/MainMenuBar.cs | 17 +++ NResUI/Models/CpDatSchemeViewModel.cs | 40 ++++++ NResUI/NResUI.csproj | 1 + ParkanPlayground.slnx | 1 + ParkanPlayground/CpDatEntryConverter.cs | 161 ------------------------ README.md | 2 +- 14 files changed, 301 insertions(+), 162 deletions(-) create mode 100644 CpDatLib/CpDatEntry.cs create mode 100644 CpDatLib/CpDatLib.csproj create mode 100644 CpDatLib/CpDatParseResult.cs create mode 100644 CpDatLib/CpDatParser.cs create mode 100644 CpDatLib/CpDatScheme.cs create mode 100644 CpDatLib/CpEntryType.cs create mode 100644 NResUI/ImGuiUI/CpDatSchemeExplorer.cs create mode 100644 NResUI/Models/CpDatSchemeViewModel.cs delete mode 100644 ParkanPlayground/CpDatEntryConverter.cs diff --git a/CpDatLib/CpDatEntry.cs b/CpDatLib/CpDatEntry.cs new file mode 100644 index 0000000..435f881 --- /dev/null +++ b/CpDatLib/CpDatEntry.cs @@ -0,0 +1,12 @@ +namespace CpDatLib; + +public record CpDatEntry( + string ArchiveFile, + string ArchiveEntryName, + int Magic1, + int Magic2, + string Description, + int Magic3, + int ChildCount, // игра не хранит это число в объекте, но оно есть в файле + List Children +); \ No newline at end of file diff --git a/CpDatLib/CpDatLib.csproj b/CpDatLib/CpDatLib.csproj new file mode 100644 index 0000000..38858c5 --- /dev/null +++ b/CpDatLib/CpDatLib.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/CpDatLib/CpDatParseResult.cs b/CpDatLib/CpDatParseResult.cs new file mode 100644 index 0000000..92d4cfc --- /dev/null +++ b/CpDatLib/CpDatParseResult.cs @@ -0,0 +1,3 @@ +namespace CpDatLib; + +public record CpDatParseResult(CpDatScheme? Scheme, string? Error); \ No newline at end of file diff --git a/CpDatLib/CpDatParser.cs b/CpDatLib/CpDatParser.cs new file mode 100644 index 0000000..547beda --- /dev/null +++ b/CpDatLib/CpDatParser.cs @@ -0,0 +1,68 @@ +using Common; + +namespace CpDatLib; + +public class CpDatParser +{ + public static CpDatParseResult Parse(string filePath) + { + Span f0f1 = stackalloc byte[4]; + + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + + if (fs.Length < 8) + return new CpDatParseResult(null, "File too small to be a valid \"cp\" .dat file."); + + fs.ReadExactly(f0f1); + + if (f0f1[0] != 0xf1 || f0f1[1] != 0xf0) + { + return new CpDatParseResult(null, "File does not start with expected header bytes f1_f0"); + } + + var schemeType = (SchemeType)fs.ReadInt32LittleEndian(); + + var entryLength = 0x6c + 4; // нам нужно прочитать 0x6c (108) байт - это root, и ещё 4 байта - кол-во вложенных объектов + if ((fs.Length - 8) % entryLength != 0) + { + return new CpDatParseResult(null, "File size is not valid according to expected entry length."); + } + + CpDatEntry root = ReadEntryRecursive(fs); + + var scheme = new CpDatScheme(schemeType, root); + + return new CpDatParseResult(scheme, null); + } + + private static CpDatEntry ReadEntryRecursive(FileStream fs) + { + var str1 = fs.ReadNullTerminatedString(); + + fs.Seek(32 - str1.Length - 1, SeekOrigin.Current); // -1 ignore null terminator + + var str2 = fs.ReadNullTerminatedString(); + + fs.Seek(32 - str2.Length - 1, SeekOrigin.Current); // -1 ignore null terminator + var magic1 = fs.ReadInt32LittleEndian(); + var magic2 = fs.ReadInt32LittleEndian(); + + var descriptionString = fs.ReadNullTerminatedString(); + + fs.Seek(32 - descriptionString.Length - 1, SeekOrigin.Current); // -1 ignore null terminator + var magic3 = fs.ReadInt32LittleEndian(); + + // игра не читает количество внутрь схемы, вместо этого она сразу рекурсией читает нужно количество вложенных объектов + var childCount = fs.ReadInt32LittleEndian(); + + List children = new List(childCount); + + for (var i = 0; i < childCount; i++) + { + var child = ReadEntryRecursive(fs); + children.Add(child); + } + + return new CpDatEntry(str1, str2, magic1, magic2, descriptionString, magic3, childCount, Children: children); + } +} \ No newline at end of file diff --git a/CpDatLib/CpDatScheme.cs b/CpDatLib/CpDatScheme.cs new file mode 100644 index 0000000..6894db5 --- /dev/null +++ b/CpDatLib/CpDatScheme.cs @@ -0,0 +1,3 @@ +namespace CpDatLib; + +public record CpDatScheme(SchemeType Type, CpDatEntry Root); \ No newline at end of file diff --git a/CpDatLib/CpEntryType.cs b/CpDatLib/CpEntryType.cs new file mode 100644 index 0000000..12e3ab5 --- /dev/null +++ b/CpDatLib/CpEntryType.cs @@ -0,0 +1,29 @@ +namespace CpDatLib; + +public enum SchemeType : 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, +} \ No newline at end of file diff --git a/NResUI/App.cs b/NResUI/App.cs index fa01212..dd327ea 100644 --- a/NResUI/App.cs +++ b/NResUI/App.cs @@ -56,6 +56,7 @@ public class App serviceCollection.AddSingleton(new BinaryExplorerViewModel()); serviceCollection.AddSingleton(new ScrViewModel()); serviceCollection.AddSingleton(new VarsetViewModel()); + serviceCollection.AddSingleton(new CpDatSchemeViewModel()); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/NResUI/ImGuiUI/CpDatSchemeExplorer.cs b/NResUI/ImGuiUI/CpDatSchemeExplorer.cs new file mode 100644 index 0000000..7e3a90f --- /dev/null +++ b/NResUI/ImGuiUI/CpDatSchemeExplorer.cs @@ -0,0 +1,120 @@ +using CpDatLib; +using ImGuiNET; +using NResUI.Abstractions; +using NResUI.Models; +using ScrLib; + +namespace NResUI.ImGuiUI; + +public class CpDatSchemeExplorer : IImGuiPanel +{ + private readonly CpDatSchemeViewModel _viewModel; + + public CpDatSchemeExplorer(CpDatSchemeViewModel viewModel) + { + _viewModel = viewModel; + } + + public void OnImGuiRender() + { + if (ImGui.Begin("cp .dat Scheme Explorer")) + { + var cpDat = _viewModel.CpDatScheme; + if (_viewModel.HasFile && cpDat is not null) + { + ImGui.Text("Тип объекта в схеме: "); + ImGui.SameLine(); + ImGui.Text(cpDat.Type.ToString("G")); + + var root = cpDat.Root; + + DrawEntry(root, 0); + + ImGui.Separator(); + + if (ImGui.BeginTable("content", 7, + ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableSetupColumn("Уровень вложенности"); + ImGui.TableSetupColumn("Архив"); + ImGui.TableSetupColumn("Элемент"); + ImGui.TableSetupColumn("Magic1"); + ImGui.TableSetupColumn("Magic2"); + ImGui.TableSetupColumn("Описание"); + ImGui.TableSetupColumn("Magic3"); + + ImGui.TableHeadersRow(); + + for (int i = 0; i < _viewModel.FlatList.Count; i++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.FlatList[i].Level.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveFile); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveEntryName); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.FlatList[i].Entry.Magic1.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.FlatList[i].Entry.Magic2.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.FlatList[i].Entry.Description); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.FlatList[i].Entry.Magic3.ToString()); + } + + ImGui.EndTable(); + } + + void DrawEntry(CpDatEntry entry, int index) + { + if (ImGui.TreeNodeEx( + $"Элемент: \"{entry.ArchiveFile}/{entry.ArchiveEntryName}\" - {entry.Description}##entry_{index}")) + { + ImGui.Text("Magic1: "); + ImGui.SameLine(); + ImGui.Text(entry.Magic1.ToString()); + + ImGui.Text("Magic2: "); + ImGui.SameLine(); + ImGui.Text(entry.Magic2.ToString()); + + ImGui.Text("Magic3: "); + ImGui.SameLine(); + ImGui.Text(entry.Magic3.ToString()); + + ImGui.Text("Кол-во дочерних элементов: "); + ImGui.SameLine(); + ImGui.Text(entry.ChildCount.ToString()); + + if (entry.Children.Count > 0) + { + if (ImGui.TreeNodeEx("Дочерние элементы")) + { + foreach (var child in entry.Children) + { + DrawEntry(child, ++index); + } + + ImGui.TreePop(); + } + } + + ImGui.TreePop(); + } + } + } + else if (_viewModel.Error is not null) + { + ImGui.Text(_viewModel.Error); + } + else + { + ImGui.Text("cp .dat не открыт"); + } + + ImGui.End(); + } + } +} \ No newline at end of file diff --git a/NResUI/ImGuiUI/MainMenuBar.cs b/NResUI/ImGuiUI/MainMenuBar.cs index c005e54..704eb17 100644 --- a/NResUI/ImGuiUI/MainMenuBar.cs +++ b/NResUI/ImGuiUI/MainMenuBar.cs @@ -1,4 +1,5 @@ using System.Numerics; +using CpDatLib; using ImGuiNET; using MissionTmaLib; using MissionTmaLib.Parsing; @@ -18,6 +19,7 @@ namespace NResUI.ImGuiUI ScrViewModel scrViewModel, MissionTmaViewModel missionTmaViewModel, VarsetViewModel varsetViewModel, + CpDatSchemeViewModel cpDatSchemeViewModel, MessageBoxModalPanel messageBox) : IImGuiPanel { @@ -121,6 +123,21 @@ namespace NResUI.ImGuiUI } } + if (ImGui.MenuItem("Open cp .dat Scheme File")) + { + var result = Dialog.FileOpen("dat"); + + if (result.IsOk) + { + var path = result.Path; + var parseResult = CpDatParser.Parse(path); + + cpDatSchemeViewModel.SetParseResult(parseResult, path); + + Console.WriteLine("Read cp .dat"); + } + } + if (nResExplorerViewModel.HasFile) { if (ImGui.MenuItem("Экспортировать NRes")) diff --git a/NResUI/Models/CpDatSchemeViewModel.cs b/NResUI/Models/CpDatSchemeViewModel.cs new file mode 100644 index 0000000..7018c1c --- /dev/null +++ b/NResUI/Models/CpDatSchemeViewModel.cs @@ -0,0 +1,40 @@ +using CpDatLib; +using ScrLib; + +namespace NResUI.Models; + +public class CpDatSchemeViewModel +{ + public bool HasFile { get; set; } + public string? Error { get; set; } + + public CpDatScheme? CpDatScheme { get; set; } + + public List<(int Level, CpDatEntry Entry)> FlatList { get; set; } + + public string? Path { get; set; } + + public void SetParseResult(CpDatParseResult parseResult, string path) + { + CpDatScheme = parseResult.Scheme; + Error = parseResult.Error; + HasFile = true; + Path = path; + + if (CpDatScheme is not null) + { + FlatList = []; + + CollectEntries(CpDatScheme.Root, 0); + + void CollectEntries(CpDatEntry entry, int level) + { + FlatList.Add((level, entry)); + foreach (var child in entry.Children) + { + CollectEntries(child, level + 1); + } + } + } + } +} \ No newline at end of file diff --git a/NResUI/NResUI.csproj b/NResUI/NResUI.csproj index e3f4a27..b9bf4ee 100644 --- a/NResUI/NResUI.csproj +++ b/NResUI/NResUI.csproj @@ -16,6 +16,7 @@ + diff --git a/ParkanPlayground.slnx b/ParkanPlayground.slnx index 272885f..5383a33 100644 --- a/ParkanPlayground.slnx +++ b/ParkanPlayground.slnx @@ -5,6 +5,7 @@ + diff --git a/ParkanPlayground/CpDatEntryConverter.cs b/ParkanPlayground/CpDatEntryConverter.cs deleted file mode 100644 index b6e4f64..0000000 --- a/ParkanPlayground/CpDatEntryConverter.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Common; -using MissionTmaLib.Parsing; -using NResLib; - -namespace ParkanPlayground; - -/// -/// Игра называет этот объект "схемой" -/// -/// -/// В игре файл .dat читается в ArealMap.dll/CreateObjectFromScheme -/// -/// -/// -/// struct Scheme -/// { -/// char[32] str1; // имя архива -/// char[32] str2; // имя объекта в архиве -/// undefined4 magic1; -/// undefined4 magic2; -/// char[32] str3; // описание объекта -/// undefined4 magic3; -/// } -/// -/// -public class CpDatEntryConverter -{ - const string gameRoot = "C:\\Program Files (x86)\\Nikita\\Iron Strategy"; - const string missionTmaPath = $"{gameRoot}\\MISSIONS\\Campaign\\Campaign.01\\Mission.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 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 children = new List(); - - 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 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, - } -} \ No newline at end of file diff --git a/README.md b/README.md index bef959e..471ef5b 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4) - `6` - IGameObject (0x138) - `7` - IShadeConfig (у меня в папке с игрой его не оказалось) - `8` - ICamera -- `9` - Queue +- `9` - IQueue - `10` - IControl - `0xb` - IAnimation - `0xd` - IMatManager