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 CpDatSchemeViewModel());
|
||||
serviceCollection.AddSingleton(new MaterialViewModel());
|
||||
serviceCollection.AddSingleton(new ResearchTreeViewModel());
|
||||
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using NativeFileDialogSharp;
|
||||
using NResLib;
|
||||
using NResUI.Abstractions;
|
||||
using NResUI.Models;
|
||||
using ResTreeLib;
|
||||
using ScrLib;
|
||||
using TexmLib;
|
||||
using VarsetLib;
|
||||
@@ -20,6 +21,7 @@ namespace NResUI.ImGuiUI
|
||||
MissionTmaViewModel missionTmaViewModel,
|
||||
VarsetViewModel varsetViewModel,
|
||||
CpDatSchemeViewModel cpDatSchemeViewModel,
|
||||
ResearchTreeViewModel researchTreeViewModel,
|
||||
MessageBoxModalPanel messageBox)
|
||||
: 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 (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="..\MissionTmaLib\MissionTmaLib.csproj" />
|
||||
<ProjectReference Include="..\NResLib\NResLib.csproj" />
|
||||
<ProjectReference Include="..\ResTreeLib\ResTreeLib.csproj" />
|
||||
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
|
||||
<ProjectReference Include="..\TexmLib\TexmLib.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.Packages.props" />
|
||||
<File Path="README.md" />
|
||||
<File Path="AI.md" />
|
||||
</Folder>
|
||||
<Project Path="Common\Common.csproj" Type="Classic C#" />
|
||||
<Project Path="CpDatLib\CpDatLib.csproj" Type="Classic C#" />
|
||||
@@ -13,6 +14,7 @@
|
||||
<Project Path="NResUI/NResUI.csproj" />
|
||||
<Project Path="PalLib\PalLib.csproj" Type="Classic C#" />
|
||||
<Project Path="ParkanPlayground/ParkanPlayground.csproj" />
|
||||
<Project Path="ResTreeLib\ResTreeLib.csproj" Type="Classic C#" />
|
||||
<Project Path="ScrLib/ScrLib.csproj" />
|
||||
<Project Path="TexmLib/TexmLib.csproj" />
|
||||
<Project Path="VarsetLib/VarsetLib.csproj" />
|
||||
|
||||
@@ -25,9 +25,8 @@ public static class Msh0D
|
||||
|
||||
var elements = elementBytes.Select(x => new Msh0DElement()
|
||||
{
|
||||
Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
|
||||
Magic04 = x.AsSpan(4)[0],
|
||||
Magic05 = x.AsSpan(5)[0],
|
||||
Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
|
||||
TriangleCount = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
|
||||
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
|
||||
CountOf06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
|
||||
IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0xA)),
|
||||
@@ -44,8 +43,7 @@ public static class Msh0D
|
||||
|
||||
// Magic04 и Magic06 обрабатываются вместе
|
||||
|
||||
public byte Magic04 { get; set; }
|
||||
public byte Magic05 { get; set; }
|
||||
public ushort TriangleCount { get; set; }
|
||||
public ushort Magic06 { get; set; }
|
||||
public ushort CountOf06 { get; set; }
|
||||
public int IndexInto06 { get; set; }
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
|
||||
<ProjectReference Include="..\NResLib\NResLib.csproj" />
|
||||
<ProjectReference Include="..\PalLib\PalLib.csproj" />
|
||||
<ProjectReference Include="..\ResTreeLib\ResTreeLib.csproj" />
|
||||
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
|
||||
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
|
||||
<ProjectReference Include="..\MaterialLib\MaterialLib.csproj" />
|
||||
|
||||
+12
-328
@@ -1,332 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using ParkanPlayground.Effects;
|
||||
using static ParkanPlayground.Effects.FxidReader;
|
||||
using NResLib;
|
||||
using ResTreeLib;
|
||||
|
||||
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];
|
||||
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
|
||||
bool dumpHeaders = args.Length > 1 && args[1] == "--dump-headers";
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
var files = Directory.EnumerateFiles(path, "*.bin").ToList();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
using var fs = new FileStream(trfFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
|
||||
var nres = NResParser.ReadFile(trfFile);
|
||||
|
||||
var resTree = ResTreeParser.Parse(nres.Archive!, fs);
|
||||
Console.WriteLine(trfFile);
|
||||
_ = 5;
|
||||
}
|
||||
+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