mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
Research tree parsing and view
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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!");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user