Files
parkan-playground/ResTreeLib/ResTreeParser.cs
T

200 lines
6.7 KiB
C#
Raw Permalink Normal View History

2026-05-09 18:39:26 +03:00
using System.Text;
2026-02-15 23:44:20 +03:00
using Common;
using NResLib;
namespace ResTreeLib;
public class ResTreeParser
{
2026-05-09 18:39:26 +03:00
private static readonly Encoding CyrillicEncoding = CodePagesEncodingProvider.Instance.GetEncoding(1251)
2026-02-15 23:44:20 +03:00
?? 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);
2026-05-09 18:39:26 +03:00
2026-02-15 23:44:20 +03:00
return Parse(nres.Archive!, fs);
}
2026-05-09 18:39:26 +03:00
2026-02-15 23:44:20 +03:00
public static List<ResearchNodeData> Parse(NResArchive archive, Stream stream)
{
2026-05-09 18:39:26 +03:00
// Register provider for Windows-1251 if not already done globally.
2026-02-15 23:44:20 +03:00
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
2026-05-09 18:39:26 +03:00
// 1. Locate the master nodes (TRF0).
2026-02-15 23:44:20 +03:00
var trf0 = archive.Files.FirstOrDefault(f => f.FileType == "TRF0");
2026-05-09 18:39:26 +03:00
if (trf0 == null)
{
return new List<ResearchNodeData>();
}
2026-02-15 23:44:20 +03:00
2026-05-09 18:39:26 +03:00
// 2. Load node states (TRF1): one byte per node.
2026-02-15 23:44:20 +03:00
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));
}
2026-05-09 18:39:26 +03:00
// 3. Load string pools.
byte[] shortNamesPoolTrf7 = LoadRawBuffer(stream, archive, "TRF7");
byte[] longNamesPoolTrf8 = LoadRawBuffer(stream, archive, "TRF8");
byte[] helpTextPoolTrf9 = LoadRawBuffer(stream, archive, "TRF9");
byte[] descriptionsPoolTrfa = LoadRawBuffer(stream, archive, "TRFA");
// 4. Load relationship maps.
var prereqMap = LoadRelationMap(stream, archive, "TRF2", "TRF3", strict: true);
var unlockMap = LoadRelationMap(stream, archive, "TRF4", "TRF5", strict: true);
// TRFB/TRF6 is auxiliary and currently unknown.
// Real game files may have inconsistent counts here, so keep it non-strict.
_ = LoadRelationMap(stream, archive, "TRFB", "TRF6", strict: false);
// 5. Assemble DTOs.
2026-02-15 23:44:20 +03:00
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,
2026-05-09 18:39:26 +03:00
State = initialStates.Length > i ? (NodeState)initialStates[i] : NodeState.Hidden,
ShortName = GetStringFromPool(shortNamesPoolTrf7, element.OffsetShortName_TRF7),
LongName = GetStringFromPool(longNamesPoolTrf8, element.OffsetLongName_TRF8),
HelpText = GetStringFromPool(helpTextPoolTrf9, element.OffsetHelpText_TRF9),
Description = GetStringFromPool(descriptionsPoolTrfa, element.OffsetDescription_TRFA),
2026-02-15 23:44:20 +03:00
PrerequisiteIds = prereqMap.TryGetValue(i, out var pValue) ? pValue : [],
UnlockIds = unlockMap.TryGetValue(i, out var uValue) ? uValue : []
});
}
return result;
}
private static Trf0Element ReadTrf0Element(Stream s)
{
2026-05-09 18:39:26 +03:00
// TRF0 element size is 40 bytes (0x28).
2026-02-15 23:44:20 +03:00
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()
};
}
2026-05-09 18:39:26 +03:00
private static Dictionary<int, uint[]> LoadRelationMap(
Stream s,
NResArchive archive,
string headerType,
string dataType,
bool strict)
2026-02-15 23:44:20 +03:00
{
var header = archive.Files.FirstOrDefault(f => f.FileType == headerType);
var data = archive.Files.FirstOrDefault(f => f.FileType == dataType);
var map = new Dictionary<int, uint[]>();
2026-05-09 18:39:26 +03:00
if (header == null || data == null)
{
return map;
}
2026-02-15 23:44:20 +03:00
uint[] counts = new uint[header.ElementCount];
s.Position = header.OffsetInFile;
for (int i = 0; i < header.ElementCount; i++)
{
counts[i] = s.ReadUInt32LittleEndian();
}
2026-05-09 18:39:26 +03:00
long dataWordsRequired = 0;
for (int i = 0; i < counts.Length; i++)
{
dataWordsRequired += counts[i];
}
if (dataWordsRequired * sizeof(uint) > data.FileLength)
{
if (strict)
{
throw new InvalidDataException(
$"Relation map {headerType}/{dataType} expects {dataWordsRequired * sizeof(uint)} bytes but section has {data.FileLength} bytes.");
}
return map;
}
long dataEnd = data.OffsetInFile + data.FileLength;
2026-02-15 23:44:20 +03:00
s.Position = data.OffsetInFile;
2026-05-09 18:39:26 +03:00
2026-02-15 23:44:20 +03:00
for (int i = 0; i < counts.Length; i++)
{
uint count = counts[i];
2026-05-09 18:39:26 +03:00
if (s.Position + count * sizeof(uint) > dataEnd)
{
if (strict)
{
throw new EndOfStreamException(
$"Relation map {headerType}/{dataType} overreads section at node {i} (count={count}).");
}
break;
}
2026-02-15 23:44:20 +03:00
uint[] ids = new uint[count];
for (int j = 0; j < count; j++)
{
ids[j] = s.ReadUInt32LittleEndian();
}
2026-05-09 18:39:26 +03:00
2026-02-15 23:44:20 +03:00
map[i] = ids;
}
return map;
}
private static byte[] LoadRawBuffer(Stream s, NResArchive archive, string type)
{
var item = archive.Files.FirstOrDefault(f => f.FileType == type);
2026-05-09 18:39:26 +03:00
if (item == null)
{
return [];
}
2026-02-15 23:44:20 +03:00
byte[] buffer = new byte[item.FileLength];
s.Position = item.OffsetInFile;
s.ReadExactly(buffer);
return buffer;
}
private static string GetStringFromPool(byte[] pool, uint offset)
{
2026-05-09 18:39:26 +03:00
if (pool.Length == 0 || offset >= pool.Length)
{
return string.Empty;
}
2026-02-15 23:44:20 +03:00
int end = Array.IndexOf(pool, (byte)0, (int)offset);
2026-05-09 18:39:26 +03:00
int length = end == -1 ? pool.Length - (int)offset : end - (int)offset;
2026-02-15 23:44:20 +03:00
return CyrillicEncoding.GetString(pool, (int)offset, length);
}
2026-05-09 18:39:26 +03:00
}