0
mirror of https://github.com/sampletext32/ParkanPlayground.git synced 2025-09-13 10:20:28 +03:00

Allow to view cp .dat in UI

This commit is contained in:
bird_egop
2025-09-04 02:45:26 +03:00
parent 7f0246f996
commit f2bed4b141
14 changed files with 301 additions and 162 deletions

12
CpDatLib/CpDatEntry.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CpDatLib;
public record CpDatEntry(
string ArchiveFile,
string ArchiveEntryName,
int Magic1,
int Magic2,
string Description,
int Magic3,
int ChildCount, // игра не хранит это число в объекте, но оно есть в файле
List<CpDatEntry> Children
);

5
CpDatLib/CpDatLib.csproj Normal file
View File

@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
namespace CpDatLib;
public record CpDatParseResult(CpDatScheme? Scheme, string? Error);

68
CpDatLib/CpDatParser.cs Normal file
View File

@@ -0,0 +1,68 @@
using Common;
namespace CpDatLib;
public class CpDatParser
{
public static CpDatParseResult Parse(string filePath)
{
Span<byte> 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<CpDatEntry> children = new List<CpDatEntry>(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);
}
}

3
CpDatLib/CpDatScheme.cs Normal file
View File

@@ -0,0 +1,3 @@
namespace CpDatLib;
public record CpDatScheme(SchemeType Type, CpDatEntry Root);

29
CpDatLib/CpEntryType.cs Normal file
View File

@@ -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,
}

View File

@@ -56,6 +56,7 @@ public class App
serviceCollection.AddSingleton(new BinaryExplorerViewModel()); serviceCollection.AddSingleton(new BinaryExplorerViewModel());
serviceCollection.AddSingleton(new ScrViewModel()); serviceCollection.AddSingleton(new ScrViewModel());
serviceCollection.AddSingleton(new VarsetViewModel()); serviceCollection.AddSingleton(new VarsetViewModel());
serviceCollection.AddSingleton(new CpDatSchemeViewModel());
var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProvider = serviceCollection.BuildServiceProvider();

View File

@@ -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();
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using CpDatLib;
using ImGuiNET; using ImGuiNET;
using MissionTmaLib; using MissionTmaLib;
using MissionTmaLib.Parsing; using MissionTmaLib.Parsing;
@@ -18,6 +19,7 @@ namespace NResUI.ImGuiUI
ScrViewModel scrViewModel, ScrViewModel scrViewModel,
MissionTmaViewModel missionTmaViewModel, MissionTmaViewModel missionTmaViewModel,
VarsetViewModel varsetViewModel, VarsetViewModel varsetViewModel,
CpDatSchemeViewModel cpDatSchemeViewModel,
MessageBoxModalPanel messageBox) MessageBoxModalPanel messageBox)
: IImGuiPanel : 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 (nResExplorerViewModel.HasFile)
{ {
if (ImGui.MenuItem("Экспортировать NRes")) if (ImGui.MenuItem("Экспортировать NRes"))

View File

@@ -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);
}
}
}
}
}

View File

@@ -16,6 +16,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" />
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" /> <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" />

View File

@@ -5,6 +5,7 @@
<File Path="README.md" /> <File Path="README.md" />
</Folder> </Folder>
<Project Path="Common\Common.csproj" Type="Classic C#" /> <Project Path="Common\Common.csproj" Type="Classic C#" />
<Project Path="CpDatLib\CpDatLib.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" />

View File

@@ -1,161 +0,0 @@
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\\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<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,
}
}

View File

@@ -304,7 +304,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
- `6` - IGameObject (0x138) - `6` - IGameObject (0x138)
- `7` - IShadeConfig (у меня в папке с игрой его не оказалось) - `7` - IShadeConfig (у меня в папке с игрой его не оказалось)
- `8` - ICamera - `8` - ICamera
- `9` - Queue - `9` - IQueue
- `10` - IControl - `10` - IControl
- `0xb` - IAnimation - `0xb` - IAnimation
- `0xd` - IMatManager - `0xd` - IMatManager