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:
12
CpDatLib/CpDatEntry.cs
Normal file
12
CpDatLib/CpDatEntry.cs
Normal 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
5
CpDatLib/CpDatLib.csproj
Normal file
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
3
CpDatLib/CpDatParseResult.cs
Normal file
3
CpDatLib/CpDatParseResult.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace CpDatLib;
|
||||
|
||||
public record CpDatParseResult(CpDatScheme? Scheme, string? Error);
|
68
CpDatLib/CpDatParser.cs
Normal file
68
CpDatLib/CpDatParser.cs
Normal 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
3
CpDatLib/CpDatScheme.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace CpDatLib;
|
||||
|
||||
public record CpDatScheme(SchemeType Type, CpDatEntry Root);
|
29
CpDatLib/CpEntryType.cs
Normal file
29
CpDatLib/CpEntryType.cs
Normal 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,
|
||||
}
|
@@ -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();
|
||||
|
||||
|
120
NResUI/ImGuiUI/CpDatSchemeExplorer.cs
Normal file
120
NResUI/ImGuiUI/CpDatSchemeExplorer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"))
|
||||
|
40
NResUI/Models/CpDatSchemeViewModel.cs
Normal file
40
NResUI/Models/CpDatSchemeViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" />
|
||||
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
|
||||
<ProjectReference Include="..\NResLib\NResLib.csproj" />
|
||||
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
|
||||
|
@@ -5,6 +5,7 @@
|
||||
<File Path="README.md" />
|
||||
</Folder>
|
||||
<Project Path="Common\Common.csproj" Type="Classic C#" />
|
||||
<Project Path="CpDatLib\CpDatLib.csproj" Type="Classic C#" />
|
||||
<Project Path="MeshUnpacker/MeshUnpacker.csproj" />
|
||||
<Project Path="MissionDataUnpacker/MissionDataUnpacker.csproj" />
|
||||
<Project Path="MissionTmaLib/MissionTmaLib.csproj" />
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user