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
+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
}