Research tree parsing and view

This commit is contained in:
bird_egop
2026-02-15 23:44:20 +03:00
parent a8536f938d
commit 8091a727ba
17 changed files with 537 additions and 333 deletions
View File
+1
View File
@@ -61,6 +61,7 @@ public class App
serviceCollection.AddSingleton(new VarsetViewModel()); serviceCollection.AddSingleton(new VarsetViewModel());
serviceCollection.AddSingleton(new CpDatSchemeViewModel()); serviceCollection.AddSingleton(new CpDatSchemeViewModel());
serviceCollection.AddSingleton(new MaterialViewModel()); serviceCollection.AddSingleton(new MaterialViewModel());
serviceCollection.AddSingleton(new ResearchTreeViewModel());
var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProvider = serviceCollection.BuildServiceProvider();
+17
View File
@@ -7,6 +7,7 @@ using NativeFileDialogSharp;
using NResLib; using NResLib;
using NResUI.Abstractions; using NResUI.Abstractions;
using NResUI.Models; using NResUI.Models;
using ResTreeLib;
using ScrLib; using ScrLib;
using TexmLib; using TexmLib;
using VarsetLib; using VarsetLib;
@@ -20,6 +21,7 @@ namespace NResUI.ImGuiUI
MissionTmaViewModel missionTmaViewModel, MissionTmaViewModel missionTmaViewModel,
VarsetViewModel varsetViewModel, VarsetViewModel varsetViewModel,
CpDatSchemeViewModel cpDatSchemeViewModel, CpDatSchemeViewModel cpDatSchemeViewModel,
ResearchTreeViewModel researchTreeViewModel,
MessageBoxModalPanel messageBox) MessageBoxModalPanel messageBox)
: IImGuiPanel : IImGuiPanel
{ {
@@ -138,6 +140,21 @@ namespace NResUI.ImGuiUI
} }
} }
if (ImGui.MenuItem("Open research .trf File"))
{
var result = Dialog.FileOpen("trf");
if (result.IsOk)
{
var path = result.Path;
var parseResult = ResTreeParser.Parse(path);
researchTreeViewModel.SetParseResult(parseResult, path);
Console.WriteLine("Read .trf");
}
}
if (nResExplorerViewModel.HasFile) if (nResExplorerViewModel.HasFile)
{ {
if (ImGui.MenuItem("Экспортировать NRes")) if (ImGui.MenuItem("Экспортировать NRes"))
+101
View File
@@ -0,0 +1,101 @@
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
namespace NResUI.ImGuiUI;
public class ResearchTreeExplorer : IImGuiPanel
{
private readonly ResearchTreeViewModel _viewModel;
public ResearchTreeExplorer(ResearchTreeViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnImGuiRender()
{
if (ImGui.Begin("Research Tree Explorer (trf)"))
{
ImGui.Text("trf - это файл дерева исследований. Их можно найти в папке MISSIONS/SCRIPTS");
ImGui.Separator();
var nodes = _viewModel.ResearchNodeDatas;
if (_viewModel.HasFile && nodes is not null)
{
if (ImGui.TreeNodeEx("Узлы"))
{
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (ImGui.TreeNodeEx($"{i} - \"{node.LongName}\" (\"{node.ShortName}\")"))
{
ImGui.Text("Состояние: ");
ImGui.SameLine();
ImGui.Text(node.State.ToString("G"));
ImGui.Text("ShortName: ");
ImGui.SameLine();
ImGui.Text(node.ShortName);
ImGui.Text("LongName: ");
ImGui.SameLine();
ImGui.Text(node.LongName);
ImGui.Text("HelpText: ");
ImGui.SameLine();
ImGui.Text(node.HelpText);
ImGui.Text("Description: ");
ImGui.SameLine();
ImGui.Text(node.Description);
ImGui.Text("Тип турели: ");
ImGui.SameLine();
ImGui.Text(node.Node.TurretType.ToString());
ImGui.Text("Основной тип: ");
ImGui.SameLine();
ImGui.Text(node.Node.MainType.ToString());
ImGui.Text("Подтип: ");
ImGui.SameLine();
ImGui.Text(node.Node.SubType.ToString());
ImGui.Text("Подтип строения: ");
ImGui.SameLine();
ImGui.Text(node.Node.BuildSubSystem.ToString());
ImGui.Text("Размер типа: ");
ImGui.SameLine();
ImGui.Text(node.Node.SizeOfType.ToString());
ImGui.Text("Уровень апгрейда: ");
ImGui.SameLine();
ImGui.Text(node.Node.UpgradeLevel.ToString());
ImGui.Text("Условия: ");
ImGui.SameLine();
ImGui.Text(string.Join(", ", node.PrerequisiteIds));
ImGui.Text("Открывает: ");
ImGui.SameLine();
ImGui.Text(string.Join(", ", node.UnlockIds));
ImGui.TreePop();
}
}
ImGui.TreePop();
}
}
else
{
ImGui.Text("trf не открыт");
}
ImGui.End();
}
}
}
+20
View File
@@ -0,0 +1,20 @@
using ResTreeLib;
namespace NResUI.Models;
public class ResearchTreeViewModel
{
public bool HasFile { get; set; }
public string? Error { get; set; }
public List<ResearchNodeData>? ResearchNodeDatas { get; set; }
public string? Path { get; set; }
public void SetParseResult(List<ResearchNodeData> researchNodeDatas, string path)
{
ResearchNodeDatas = researchNodeDatas;
HasFile = true;
Path = path;
}
}
+1
View File
@@ -20,6 +20,7 @@
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" /> <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="..\ResTreeLib\ResTreeLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" /> <ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\TexmLib\TexmLib.csproj" /> <ProjectReference Include="..\TexmLib\TexmLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" /> <ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
+3
View File
@@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASafeFileHandle_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6ef8e7d549f6bfb5b9eafaaa0ff48d8cbfc564f32be26323b5f60e63aef4dd1_003FSafeFileHandle_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc7102cd0ffb8973777e61b1942c3fffac7e14016a511d055c3adf73ff91748_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
+2
View File
@@ -3,6 +3,7 @@
<File Path="Directory.Build.props" /> <File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" /> <File Path="Directory.Packages.props" />
<File Path="README.md" /> <File Path="README.md" />
<File Path="AI.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="CpDatLib\CpDatLib.csproj" Type="Classic C#" />
@@ -13,6 +14,7 @@
<Project Path="NResUI/NResUI.csproj" /> <Project Path="NResUI/NResUI.csproj" />
<Project Path="PalLib\PalLib.csproj" Type="Classic C#" /> <Project Path="PalLib\PalLib.csproj" Type="Classic C#" />
<Project Path="ParkanPlayground/ParkanPlayground.csproj" /> <Project Path="ParkanPlayground/ParkanPlayground.csproj" />
<Project Path="ResTreeLib\ResTreeLib.csproj" Type="Classic C#" />
<Project Path="ScrLib/ScrLib.csproj" /> <Project Path="ScrLib/ScrLib.csproj" />
<Project Path="TexmLib/TexmLib.csproj" /> <Project Path="TexmLib/TexmLib.csproj" />
<Project Path="VarsetLib/VarsetLib.csproj" /> <Project Path="VarsetLib/VarsetLib.csproj" />
+3 -5
View File
@@ -25,9 +25,8 @@ public static class Msh0D
var elements = elementBytes.Select(x => new Msh0DElement() var elements = elementBytes.Select(x => new Msh0DElement()
{ {
Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)), Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
Magic04 = x.AsSpan(4)[0], TriangleCount = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
Magic05 = x.AsSpan(5)[0],
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)), Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
CountOf06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)), CountOf06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0xA)), IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0xA)),
@@ -44,8 +43,7 @@ public static class Msh0D
// Magic04 и Magic06 обрабатываются вместе // Magic04 и Magic06 обрабатываются вместе
public byte Magic04 { get; set; } public ushort TriangleCount { get; set; }
public byte Magic05 { get; set; }
public ushort Magic06 { get; set; } public ushort Magic06 { get; set; }
public ushort CountOf06 { get; set; } public ushort CountOf06 { get; set; }
public int IndexInto06 { get; set; } public int IndexInto06 { get; set; }
+1
View File
@@ -8,6 +8,7 @@
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" /> <ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" /> <ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\PalLib\PalLib.csproj" /> <ProjectReference Include="..\PalLib\PalLib.csproj" />
<ProjectReference Include="..\ResTreeLib\ResTreeLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" /> <ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" /> <ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
<ProjectReference Include="..\MaterialLib\MaterialLib.csproj" /> <ProjectReference Include="..\MaterialLib\MaterialLib.csproj" />
+9 -325
View File
@@ -1,332 +1,16 @@
using System; using NResLib;
using System.Collections.Generic; using ResTreeLib;
using System.IO;
using System.Text;
using ParkanPlayground.Effects;
using static ParkanPlayground.Effects.FxidReader;
Console.OutputEncoding = Encoding.UTF8; System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
if (args.Length == 0) foreach (var trfFile in Directory.EnumerateFiles("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS", "*.trf"))
{ {
Console.WriteLine("Usage: ParkanPlayground <effects-directory-or-fxid-file>");
return;
}
var path = args[0]; using var fs = new FileStream(trfFile, FileMode.Open, FileAccess.Read, FileShare.Read);
bool anyError = false;
var sizeByType = new Dictionary<byte, int>
{
[1] = 0xE0, // 1: Billboard
[2] = 0x94, // 2: Sound
[3] = 0xC8, // 3: AnimParticle
[4] = 0xCC, // 4: AnimBillboard
[5] = 0x70, // 5: Trail
[6] = 0x04, // 6: Point
[7] = 0xD0, // 7: Plane
[8] = 0xF8, // 8: Model
[9] = 0xD0, // 9: AnimModel
[10] = 0xD0, // 10: Cube
};
// Check for --dump-headers flag var nres = NResParser.ReadFile(trfFile);
bool dumpHeaders = args.Length > 1 && args[1] == "--dump-headers";
if (Directory.Exists(path)) var resTree = ResTreeParser.Parse(nres.Archive!, fs);
{ Console.WriteLine(trfFile);
var files = Directory.EnumerateFiles(path, "*.bin").ToList(); _ = 5;
if (dumpHeaders)
{
// Collect all headers for analysis
var headers = new List<(string name, EffectHeader h)>();
foreach (var file in files)
{
try
{
using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
if (fs.Length >= 60)
{
headers.Add((Path.GetFileName(file), ReadEffectHeader(br)));
}
}
catch { }
}
// Analyze unique values
Console.WriteLine("=== UNIQUE VALUES ANALYSIS ===\n");
var uniqueUnk1 = headers.Select(x => x.h.Unknown1).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown1 unique values ({uniqueUnk1.Count}): {string.Join(", ", uniqueUnk1)}");
var uniqueUnk2 = headers.Select(x => x.h.Unknown2).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown2 unique values ({uniqueUnk2.Count}): {string.Join(", ", uniqueUnk2.Select(x => x.ToString("F2")))}");
var uniqueFlags = headers.Select(x => x.h.Flags).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Flags unique values ({uniqueFlags.Count}): {string.Join(", ", uniqueFlags.Select(x => $"0x{x:X4}"))}");
var uniqueUnk3 = headers.Select(x => x.h.Unknown3).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown3 unique values ({uniqueUnk3.Count}): {string.Join(", ", uniqueUnk3)}");
Console.WriteLine($"Unknown3 as hex: {string.Join(", ", uniqueUnk3.Select(x => $"0x{x:X3}"))}");
Console.WriteLine($"Unknown3 decoded (hi.lo): {string.Join(", ", uniqueUnk3.Select(x => $"{x >> 8}.{x & 0xFF}"))}");
// Check reserved bytes
var nonZeroReserved = headers.Where(x => x.h.Reserved.Any(b => b != 0)).ToList();
Console.WriteLine($"\nFiles with non-zero Reserved bytes: {nonZeroReserved.Count} / {headers.Count}");
// Check scales
var uniqueScales = headers.Select(x => (x.h.ScaleX, x.h.ScaleY, x.h.ScaleZ)).Distinct().ToList();
Console.WriteLine($"Unique scale combinations: {string.Join(", ", uniqueScales.Select(s => $"({s.ScaleX:F2},{s.ScaleY:F2},{s.ScaleZ:F2})"))}");
Console.WriteLine("\n=== SAMPLE HEADERS (first 30) ===");
Console.WriteLine($"{"File",-40} | {"Cnt",3} | {"U1",2} | {"Duration",8} | {"U2",6} | {"Flags",6} | {"U3",4} | Scale");
Console.WriteLine(new string('-', 100));
foreach (var (name, h) in headers.Take(30))
{
Console.WriteLine($"{name,-40} | {h.ComponentCount,3} | {h.Unknown1,2} | {h.Duration,8:F2} | {h.Unknown2,6:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | ({h.ScaleX:F1},{h.ScaleY:F1},{h.ScaleZ:F1})");
}
}
else
{
foreach (var file in files)
{
if (!ValidateFxidFile(file))
{
anyError = true;
}
}
Console.WriteLine(anyError
? "Validation finished with errors."
: "All FXID files parsed successfully.");
}
}
else if (File.Exists(path))
{
anyError = !ValidateFxidFile(path);
Console.WriteLine(anyError ? "Validation failed." : "Validation OK.");
}
else
{
Console.WriteLine($"Path not found: {path}");
}
void DumpEffectHeader(string path)
{
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
if (fs.Length < 60)
{
Console.WriteLine($"{Path.GetFileName(path)}: file too small");
return;
}
var h = ReadEffectHeader(br);
// Format reserved bytes as hex (show first 8 bytes for brevity)
var reservedHex = BitConverter.ToString(h.Reserved, 0, Math.Min(8, h.Reserved.Length)).Replace("-", " ");
if (h.Reserved.Length > 8) reservedHex += "...";
// Check if reserved has any non-zero bytes
bool reservedAllZero = h.Reserved.All(b => b == 0);
Console.WriteLine($"{Path.GetFileName(path),-40} | {h.ComponentCount,7} | {h.Unknown1,4} | {h.Duration,8:F2} | {h.Unknown2,8:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | {(reservedAllZero ? "(all zero)" : reservedHex),-20} | ({h.ScaleX:F2}, {h.ScaleY:F2}, {h.ScaleZ:F2})");
}
catch (Exception ex)
{
Console.WriteLine($"{Path.GetFileName(path)}: {ex.Message}");
}
}
bool ValidateFxidFile(string path)
{
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
const int headerSize = 60; // sizeof(EffectHeader) on disk
if (fs.Length < headerSize)
{
Console.WriteLine($"{path}: file too small ({fs.Length} bytes).");
return false;
}
var header = ReadEffectHeader(br);
var typeCounts = new Dictionary<byte, int>();
for (int i = 0; i < header.ComponentCount; i++)
{
long blockStart = fs.Position;
if (fs.Position + 4 > fs.Length)
{
Console.WriteLine($"{path}: component {i}: unexpected EOF before type (offset 0x{fs.Position:X}, size 0x{fs.Length:X}).");
return false;
}
uint typeAndFlags = br.ReadUInt32();
byte type = (byte)(typeAndFlags & 0xFF);
if (!typeCounts.TryGetValue(type, out var count))
{
count = 0;
}
typeCounts[type] = count + 1;
if (!sizeByType.TryGetValue(type, out int blockSize))
{
Console.WriteLine($"{path}: component {i}: unknown type {type} (typeAndFlags=0x{typeAndFlags:X8}).");
return false;
}
int remaining = blockSize - 4;
if (fs.Position + remaining > fs.Length)
{
Console.WriteLine($"{path}: component {i}: block size 0x{blockSize:X} runs past EOF (blockStart=0x{blockStart:X}, fileSize=0x{fs.Length:X}).");
return false;
}
if (type == 1)
{
var def = ReadBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x50)
{
Console.WriteLine($"{path}: component {i}: type 1 reserved length {def.Reserved.Length}, expected 0x50.");
return false;
}
}
else if (type == 2)
{
var def = ReadSoundComponent(br, typeAndFlags);
if (def.SoundNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 2 reserved length {def.SoundNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 3)
{
var def = ReadAnimParticleComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 3 reserved length {def.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 4)
{
var def = ReadAnimBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 4 reserved length {def.Reserved.Length}, expected 0x3C.");
return false;
}
}
else if (type == 5)
{
var def = ReadTrailComponent(br, typeAndFlags);
if (def.Unknown04To10.Length != 0x10)
{
Console.WriteLine($"{path}: component {i}: type 5 prefix length {def.Unknown04To10.Length}, expected 0x10.");
return false;
}
if (def.TextureNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 5 tail length {def.TextureNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 6)
{
// Point components have no extra bytes beyond the 4-byte typeAndFlags header.
var def = ReadPointComponent(typeAndFlags);
}
else if (type == 7)
{
var def = ReadPlaneComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 7 base reserved length {def.Base.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 8)
{
var def = ReadModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 8 tail length {def.TextureNameAndFlags.Length}, expected 0x40.");
return false;
}
}
else if (type == 9)
{
var def = ReadAnimModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x48)
{
Console.WriteLine($"{path}: component {i}: type 9 tail length {def.TextureNameAndFlags.Length}, expected 0x48.");
return false;
}
}
else if (type == 10)
{
var def = ReadCubeComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 10 base reserved length {def.Base.Reserved.Length}, expected 0x3C.");
return false;
}
}
else
{
// Skip the remaining bytes for other component types.
fs.Position += remaining;
}
}
// Dump a compact per-file summary of component types and counts.
var sb = new StringBuilder();
bool first = true;
foreach (var kv in typeCounts)
{
if (!first)
{
sb.Append(", ");
}
sb.Append(kv.Key);
sb.Append('x');
sb.Append(kv.Value);
first = false;
}
Console.WriteLine($"{path}: components={header.ComponentCount}, types=[{sb}]");
if (fs.Position != fs.Length)
{
Console.WriteLine($"{path}: parsed to 0x{fs.Position:X}, but file size is 0x{fs.Length:X} (leftover {fs.Length - fs.Position} bytes).");
return false;
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{path}: exception while parsing: {ex.Message}");
return false;
}
} }
+146
View File
@@ -0,0 +1,146 @@
using System.Buffers.Binary;
using MaterialLib;
using NResLib;
using ParkanPlayground;
// ========== ANALYZE MATERIALS 72 AND 88 FROM LANDSCAPE 0B ==========
// 1. Load Material.lib
var materialLibPath = @"E:\ParkanUnpacked\Material.lib.nres";
if (!File.Exists(materialLibPath))
{
// Try alternative path
materialLibPath = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\Material.lib";
}
Console.WriteLine($"Loading Material.lib from: {materialLibPath}");
var matLibResult = NResParser.ReadFile(materialLibPath);
if (matLibResult.Archive is null)
{
Console.WriteLine($"ERROR loading Material.lib: {matLibResult.Error}");
Console.WriteLine("Trying to list available .lib files...");
// List what's available
var dataPath = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA";
if (Directory.Exists(dataPath))
{
foreach (var f in Directory.GetFiles(dataPath, "*.lib"))
{
Console.WriteLine($" Found: {f}");
}
}
return;
}
Console.WriteLine($"Material.lib loaded: {matLibResult.Archive.Files.Count} files\n");
// 2. Find materials by index (72 and 88)
using var matFs = new FileStream(materialLibPath, FileMode.Open, FileAccess.Read, FileShare.Read);
var targetIds = new[] { 72, 88 };
foreach (var targetId in targetIds)
{
// Material files are stored with their ID in the archive
var matEntry = matLibResult.Archive.Files.FirstOrDefault(f => f.Index == targetId);
if (matEntry == null)
{
Console.WriteLine($"=== Material {targetId}: NOT FOUND ===\n");
continue;
}
Console.WriteLine($"=== Material {targetId} ===");
Console.WriteLine($" Index: {matEntry.Index}");
Console.WriteLine($" Name: {matEntry.FileName}");
Console.WriteLine($" ElementCount (Version): {matEntry.ElementCount}");
Console.WriteLine($" ElementSize (Magic1): {matEntry.ElementSize}");
Console.WriteLine($" Offset: {matEntry.OffsetInFile}");
Console.WriteLine($" Data size: {matEntry.ElementCount * matEntry.ElementSize} bytes");
// Parse the material
matFs.Seek(matEntry.OffsetInFile, SeekOrigin.Begin);
try
{
var material = MaterialParser.ReadFromStream(
matFs,
matEntry.FileName ?? $"MAT_{targetId}",
matEntry.ElementCount,
matEntry.ElementSize);
Console.WriteLine($"\n Parsed Material:");
Console.WriteLine($" Rendering Type: {material.MaterialRenderingType}");
Console.WriteLine($" Supports Bump: {material.SupportsBumpMapping}");
Console.WriteLine($" Source Blend: {material.SourceBlendMode}");
Console.WriteLine($" Dest Blend: {material.DestBlendMode}");
Console.WriteLine($" Stages: {material.Stages.Count}");
Console.WriteLine($" Animations: {material.Animations.Count}");
for (int i = 0; i < material.Stages.Count; i++)
{
var stage = material.Stages[i];
Console.WriteLine($"\n Stage {i}:");
Console.WriteLine($" Texture: \"{stage.TextureName}\"");
Console.WriteLine($" TextureStageIndex: {stage.TextureStageIndex}");
Console.WriteLine($" Diffuse: ({stage.DiffuseR:F2}, {stage.DiffuseG:F2}, {stage.DiffuseB:F2}, {stage.DiffuseA:F2})");
Console.WriteLine($" Ambient: ({stage.AmbientR:F2}, {stage.AmbientG:F2}, {stage.AmbientB:F2}, {stage.AmbientA:F2})");
}
}
catch (Exception ex)
{
Console.WriteLine($" ERROR parsing: {ex.Message}");
}
Console.WriteLine();
}
// 3. Also show .wea file contents for context
Console.WriteLine("=== WEA FILES (landscape materials) ===");
var weaPath1 = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land1.wea";
var weaPath2 = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land2.wea";
if (File.Exists(weaPath1))
{
Console.WriteLine($"\nLand1.wea:");
foreach (var line in File.ReadAllLines(weaPath1))
Console.WriteLine($" {line}");
}
if (File.Exists(weaPath2))
{
Console.WriteLine($"\nLand2.wea:");
foreach (var line in File.ReadAllLines(weaPath2))
Console.WriteLine($" {line}");
}
// 4. Find materials referenced in .wea by name
Console.WriteLine("\n=== MATERIALS REFERENCED IN WEA ===");
var weaMatNames = new[] { "B_S0", "L04", "L02", "L00", "DEFAULT", "L05", "L03", "L01" };
foreach (var name in weaMatNames)
{
var searchName = $"MAT0_{name}";
var found = matLibResult.Archive.Files
.FirstOrDefault(f => f.FileName?.Contains(searchName, StringComparison.OrdinalIgnoreCase) == true);
if (found != null)
{
Console.WriteLine($" {name,-10} -> Index {found.Index,3}: {found.FileName}");
}
else
{
Console.WriteLine($" {name,-10} -> NOT FOUND");
}
}
Console.WriteLine("\nDone!");
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" />
</ItemGroup>
</Project>
+158
View File
@@ -0,0 +1,158 @@
using System.Text;
using Common;
using NResLib;
namespace ResTreeLib;
public class ResTreeParser
{
private static readonly Encoding CyrillicEncoding = CodePagesEncodingProvider.Instance.GetEncoding(1251)
?? Encoding.GetEncoding("windows-1251");
public static List<ResearchNodeData> Parse(string filePath)
{
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var nres = NResParser.ReadFile(fs);
fs.Seek(0, SeekOrigin.Begin);
return Parse(nres.Archive!, fs);
}
public static List<ResearchNodeData> Parse(NResArchive archive, Stream stream)
{
// Register provider for Windows-1251 if not already done globally
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
if (archive.Files.Any(x => x.FileType == "TRFB"))
{
_ = 5;
}
// 1. Locate the Master Nodes (TRF0)
var trf0 = archive.Files.FirstOrDefault(f => f.FileType == "TRF0");
if (trf0 == null) return new List<ResearchNodeData>();
// 2. Load the Node States (TRF1) - 1 byte per node
byte[] initialStates = LoadRawBuffer(stream, archive, "TRF1");
var nodes = new List<Trf0Element>();
stream.Position = trf0.OffsetInFile;
for (int i = 0; i < trf0.ElementCount; i++)
{
nodes.Add(ReadTrf0Element(stream));
}
// 3. Load String Pools
byte[] shortNamesPool_trf7 = LoadRawBuffer(stream, archive, "TRF7");
byte[] longNamesPool_trf8 = LoadRawBuffer(stream, archive, "TRF8");
byte[] helpTextPool_trf9 = LoadRawBuffer(stream, archive, "TRF9");
byte[] descriptionsPool_trfa = LoadRawBuffer(stream, archive, "TRFA");
// 4. Load Relationship Maps
// TRF2/3 = Prerequisites
var prereqMap = LoadRelationMap(stream, archive, "TRF2", "TRF3");
// TRF4/5 = Unlocks/Effects
var unlockMap = LoadRelationMap(stream, archive, "TRF4", "TRF5");
// TRFB/6 = unknown
var aux_trf6 = LoadRelationMap(stream, archive, "TRFB", "TRF6");
// 5. Assemble
var result = new List<ResearchNodeData>();
for (int i = 0; i < nodes.Count; i++)
{
var element = nodes[i];
result.Add(new ResearchNodeData
{
Index = i,
Node = element,
// Cast the TRF1 byte to our NodeState Flags
State = (initialStates.Length > i) ? (NodeState)initialStates[i] : NodeState.Hidden,
ShortName = GetStringFromPool(shortNamesPool_trf7, element.OffsetShortName_TRF7),
LongName = GetStringFromPool(longNamesPool_trf8, element.OffsetLongName_TRF8),
HelpText = GetStringFromPool(helpTextPool_trf9, element.OffsetHelpText_TRF9),
Description = GetStringFromPool(descriptionsPool_trfa, element.OffsetDescription_TRFA),
PrerequisiteIds = prereqMap.TryGetValue(i, out var pValue) ? pValue : [],
UnlockIds = unlockMap.TryGetValue(i, out var uValue) ? uValue : []
});
}
return result;
}
private static Trf0Element ReadTrf0Element(Stream s)
{
// Total size must be 40 bytes (0x28)
return new Trf0Element
{
ResearchCost = s.ReadFloatLittleEndian(),
ResearchTime = s.ReadFloatLittleEndian(),
ViewPosX = s.ReadFloatLittleEndian(),
ViewPosY = s.ReadFloatLittleEndian(),
OffsetShortName_TRF7 = s.ReadUInt32LittleEndian(),
OffsetLongName_TRF8 = s.ReadUInt32LittleEndian(),
OffsetHelpText_TRF9 = s.ReadUInt32LittleEndian(),
OffsetDescription_TRFA = s.ReadUInt32LittleEndian(),
OffsetAux_TRFB = s.ReadUInt16LittleEndian(),
TurretType = (byte)s.ReadByte(),
MainType = (byte)s.ReadByte(),
SubType = (byte)s.ReadByte(),
BuildSubSystem = (byte)s.ReadByte(),
SizeOfType = (byte)s.ReadByte(),
UpgradeLevel = (byte)s.ReadByte()
};
}
// LoadRelationMap and LoadRawBuffer remain the same as your provided code
private static Dictionary<int, uint[]> LoadRelationMap(Stream s, NResArchive archive, string headerType, string dataType)
{
var header = archive.Files.FirstOrDefault(f => f.FileType == headerType);
var data = archive.Files.FirstOrDefault(f => f.FileType == dataType);
var map = new Dictionary<int, uint[]>();
if (header == null || data == null) return map;
uint[] counts = new uint[header.ElementCount];
s.Position = header.OffsetInFile;
for (int i = 0; i < header.ElementCount; i++)
{
counts[i] = s.ReadUInt32LittleEndian();
}
s.Position = data.OffsetInFile;
for (int i = 0; i < counts.Length; i++)
{
uint count = counts[i];
uint[] ids = new uint[count];
for (int j = 0; j < count; j++)
{
ids[j] = s.ReadUInt32LittleEndian();
}
map[i] = ids;
}
return map;
}
private static byte[] LoadRawBuffer(Stream s, NResArchive archive, string type)
{
var item = archive.Files.FirstOrDefault(f => f.FileType == type);
if (item == null) return [];
byte[] buffer = new byte[item.FileLength];
s.Position = item.OffsetInFile;
s.ReadExactly(buffer);
return buffer;
}
private static string GetStringFromPool(byte[] pool, uint offset)
{
if (pool.Length == 0 || offset >= pool.Length) return string.Empty;
int end = Array.IndexOf(pool, (byte)0, (int)offset);
int length = (end == -1) ? pool.Length - (int)offset : end - (int)offset;
return CyrillicEncoding.GetString(pool, (int)offset, length);
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace ResTreeLib;
public record ResearchNodeData
{
public int Index { get; init; }
public Trf0Element Node { get; init; } = null!;
public NodeState State { get; init; } // From TRF1
public string ShortName { get; set; } = "";
public string LongName { get; init; } = "";
public string HelpText { get; set; } = "";
public string Description { get; init; } = "";
/// <summary>
/// These IDs define what technologies a player must complete before the current node becomes available for research.
/// These are derived from TRF2 (which stores the count of parents) and TRF3 (which contains the flat list of parent IDs).
/// </summary>
/// <example>
/// If Flamethrower Mk II has PrerequisiteIds = [18], then the node with index 18 (Flamethrower Mk I) must be finished first.
/// </example>
public uint[] PrerequisiteIds { get; init; } = []; // From TRF2/3
/// <summary>
/// These IDs define the immediate "downstream" effects or technologies that are triggered when the current research is finished.
/// These are derived from TRF4 (the count of unlocks) and TRF5 (the flat list of target IDs).
/// </summary>
public uint[] UnlockIds { get; init; } = []; // From TRF4/5
}
+31
View File
@@ -0,0 +1,31 @@
namespace ResTreeLib;
// Result DTO for easier use
public record Trf0Element
{
public float ResearchCost; // field_0
public float ResearchTime; // field_4
public float ViewPosX; // field_8
public float ViewPosY; // field_12
public uint OffsetShortName_TRF7; // offset_into_TRF7
public uint OffsetLongName_TRF8; // offset_into_TRF8
public uint OffsetHelpText_TRF9; // offset_into_TRF9
public uint OffsetDescription_TRFA; // offset_into_TRFA
public ushort OffsetAux_TRFB; // offset_into_TRFB
public byte TurretType;
public byte MainType;
public byte SubType;
public byte BuildSubSystem;
public byte SizeOfType;
public byte UpgradeLevel;
}
[Flags]
public enum NodeState : byte
{
Hidden = 0,
Available = 1 << 0, // 0x01
Researched = 1 << 1, // 0x02
Active = 1 << 2 // 0x04 - If not set, node is disabled
}