mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
restree leftovers
This commit is contained in:
@@ -4,13 +4,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Package versions used across the solution -->
|
<!-- Package versions used across the solution -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||||
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
<PackageVersion Include="NativeFileDialogSharp" Version="0.5.0" />
|
<PackageVersion Include="NativeFileDialogSharp" Version="0.5.0" />
|
||||||
<PackageVersion Include="Silk.NET" Version="2.22.0" />
|
<PackageVersion Include="Silk.NET" Version="2.22.0" />
|
||||||
<PackageVersion Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
|
<PackageVersion Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
|
||||||
|
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@@ -15,4 +15,8 @@
|
|||||||
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
|
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+57
-10
@@ -1,16 +1,63 @@
|
|||||||
using NResLib;
|
|
||||||
using ResTreeLib;
|
using ResTreeLib;
|
||||||
|
|
||||||
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||||
|
var trfPath = Directory.EnumerateFiles(@"C:\Program Files (x86)\Nikita\Iron Strategy\MISSIONS\SCRIPTS", "*.trf").First();
|
||||||
|
var nodes = ResTreeParser.Parse(trfPath);
|
||||||
|
|
||||||
foreach (var trfFile in Directory.EnumerateFiles("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS", "*.trf"))
|
var active = nodes.Where(n => !string.IsNullOrWhiteSpace(n.ShortName) && (n.Node.ResearchCost > 0 || n.Node.ResearchTime > 0)).ToList();
|
||||||
|
Console.WriteLine($"active={active.Count}");
|
||||||
|
|
||||||
|
Analyze("X from Cost", active.Select(n => (A:n.Node.ResearchCost, B:n.Node.ViewPosX, Name:n.ShortName, Idx:n.Index)).ToList());
|
||||||
|
Analyze("Y from Time", active.Select(n => (A:n.Node.ResearchTime, B:n.Node.ViewPosY, Name:n.ShortName, Idx:n.Index)).ToList());
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Largest |Y-Time| (active):");
|
||||||
|
foreach (var n in active.OrderByDescending(n => Math.Abs(n.Node.ViewPosY - n.Node.ResearchTime)).Take(20))
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"idx={n.Index,3} {n.ShortName,-12} T={n.Node.ResearchTime,6:0.##} Y={n.Node.ViewPosY,6:0.##} d={n.Node.ViewPosY-n.Node.ResearchTime,6:0.##}");
|
||||||
using var fs = new FileStream(trfFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
}
|
||||||
|
|
||||||
var nres = NResParser.ReadFile(trfFile);
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Largest |X-Cost| (active):");
|
||||||
var resTree = ResTreeParser.Parse(nres.Archive!, fs);
|
foreach (var n in active.OrderByDescending(n => Math.Abs(n.Node.ViewPosX - n.Node.ResearchCost)).Take(20))
|
||||||
Console.WriteLine(trfFile);
|
{
|
||||||
_ = 5;
|
Console.WriteLine($"idx={n.Index,3} {n.ShortName,-12} C={n.Node.ResearchCost,6:0.##} X={n.Node.ViewPosX,6:0.##} d={n.Node.ViewPosX-n.Node.ResearchCost,6:0.##}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Analyze(string title, List<(float A, float B, string Name, int Idx)> data)
|
||||||
|
{
|
||||||
|
float meanA = data.Average(x => x.A);
|
||||||
|
float meanB = data.Average(x => x.B);
|
||||||
|
float cov=0, varA=0;
|
||||||
|
foreach (var d in data)
|
||||||
|
{
|
||||||
|
var da = d.A - meanA;
|
||||||
|
var db = d.B - meanB;
|
||||||
|
cov += da*db;
|
||||||
|
varA += da*da;
|
||||||
|
}
|
||||||
|
float slope = varA == 0 ? 0 : cov / varA;
|
||||||
|
float intercept = meanB - slope*meanA;
|
||||||
|
|
||||||
|
float mae = data.Average(d => Math.Abs((slope*d.A + intercept) - d.B));
|
||||||
|
float rmse = MathF.Sqrt(data.Average(d => MathF.Pow((slope*d.A + intercept) - d.B,2)));
|
||||||
|
|
||||||
|
float corr = Corr(data.Select(x=>x.A).ToArray(), data.Select(x=>x.B).ToArray());
|
||||||
|
|
||||||
|
Console.WriteLine($"{title}: corr={corr:0.###}, slope={slope:0.###}, intercept={intercept:0.###}, MAE={mae:0.###}, RMSE={rmse:0.###}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static float Corr(float[] a, float[] b)
|
||||||
|
{
|
||||||
|
if (a.Length != b.Length || a.Length == 0) return 0;
|
||||||
|
var ma = a.Average();
|
||||||
|
var mb = b.Average();
|
||||||
|
float top=0, aa=0, bb=0;
|
||||||
|
for(int i=0;i<a.Length;i++)
|
||||||
|
{
|
||||||
|
var da=a[i]-ma; var db=b[i]-mb;
|
||||||
|
top += da*db; aa += da*da; bb += db*db;
|
||||||
|
}
|
||||||
|
if (aa<=0||bb<=0) return 0;
|
||||||
|
return top / MathF.Sqrt(aa*bb);
|
||||||
}
|
}
|
||||||
+76
-35
@@ -1,4 +1,4 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Common;
|
using Common;
|
||||||
using NResLib;
|
using NResLib;
|
||||||
|
|
||||||
@@ -22,19 +22,17 @@ public class ResTreeParser
|
|||||||
|
|
||||||
public static List<ResearchNodeData> Parse(NResArchive archive, Stream stream)
|
public static List<ResearchNodeData> Parse(NResArchive archive, Stream stream)
|
||||||
{
|
{
|
||||||
// Register provider for Windows-1251 if not already done globally
|
// Register provider for Windows-1251 if not already done globally.
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
if (archive.Files.Any(x => x.FileType == "TRFB"))
|
// 1. Locate the master nodes (TRF0).
|
||||||
|
var trf0 = archive.Files.FirstOrDefault(f => f.FileType == "TRF0");
|
||||||
|
if (trf0 == null)
|
||||||
{
|
{
|
||||||
_ = 5;
|
return new List<ResearchNodeData>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Locate the Master Nodes (TRF0)
|
// 2. Load node states (TRF1): one byte per node.
|
||||||
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");
|
byte[] initialStates = LoadRawBuffer(stream, archive, "TRF1");
|
||||||
|
|
||||||
var nodes = new List<Trf0Element>();
|
var nodes = new List<Trf0Element>();
|
||||||
@@ -44,21 +42,21 @@ public class ResTreeParser
|
|||||||
nodes.Add(ReadTrf0Element(stream));
|
nodes.Add(ReadTrf0Element(stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Load String Pools
|
// 3. Load string pools.
|
||||||
byte[] shortNamesPool_trf7 = LoadRawBuffer(stream, archive, "TRF7");
|
byte[] shortNamesPoolTrf7 = LoadRawBuffer(stream, archive, "TRF7");
|
||||||
byte[] longNamesPool_trf8 = LoadRawBuffer(stream, archive, "TRF8");
|
byte[] longNamesPoolTrf8 = LoadRawBuffer(stream, archive, "TRF8");
|
||||||
byte[] helpTextPool_trf9 = LoadRawBuffer(stream, archive, "TRF9");
|
byte[] helpTextPoolTrf9 = LoadRawBuffer(stream, archive, "TRF9");
|
||||||
byte[] descriptionsPool_trfa = LoadRawBuffer(stream, archive, "TRFA");
|
byte[] descriptionsPoolTrfa = LoadRawBuffer(stream, archive, "TRFA");
|
||||||
|
|
||||||
// 4. Load Relationship Maps
|
// 4. Load relationship maps.
|
||||||
// TRF2/3 = Prerequisites
|
var prereqMap = LoadRelationMap(stream, archive, "TRF2", "TRF3", strict: true);
|
||||||
var prereqMap = LoadRelationMap(stream, archive, "TRF2", "TRF3");
|
var unlockMap = LoadRelationMap(stream, archive, "TRF4", "TRF5", strict: true);
|
||||||
// TRF4/5 = Unlocks/Effects
|
|
||||||
var unlockMap = LoadRelationMap(stream, archive, "TRF4", "TRF5");
|
|
||||||
// TRFB/6 = unknown
|
|
||||||
var aux_trf6 = LoadRelationMap(stream, archive, "TRFB", "TRF6");
|
|
||||||
|
|
||||||
// 5. Assemble
|
// 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.
|
||||||
var result = new List<ResearchNodeData>();
|
var result = new List<ResearchNodeData>();
|
||||||
for (int i = 0; i < nodes.Count; i++)
|
for (int i = 0; i < nodes.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -67,12 +65,11 @@ public class ResTreeParser
|
|||||||
{
|
{
|
||||||
Index = i,
|
Index = i,
|
||||||
Node = element,
|
Node = element,
|
||||||
// Cast the TRF1 byte to our NodeState Flags
|
State = initialStates.Length > i ? (NodeState)initialStates[i] : NodeState.Hidden,
|
||||||
State = (initialStates.Length > i) ? (NodeState)initialStates[i] : NodeState.Hidden,
|
ShortName = GetStringFromPool(shortNamesPoolTrf7, element.OffsetShortName_TRF7),
|
||||||
ShortName = GetStringFromPool(shortNamesPool_trf7, element.OffsetShortName_TRF7),
|
LongName = GetStringFromPool(longNamesPoolTrf8, element.OffsetLongName_TRF8),
|
||||||
LongName = GetStringFromPool(longNamesPool_trf8, element.OffsetLongName_TRF8),
|
HelpText = GetStringFromPool(helpTextPoolTrf9, element.OffsetHelpText_TRF9),
|
||||||
HelpText = GetStringFromPool(helpTextPool_trf9, element.OffsetHelpText_TRF9),
|
Description = GetStringFromPool(descriptionsPoolTrfa, element.OffsetDescription_TRFA),
|
||||||
Description = GetStringFromPool(descriptionsPool_trfa, element.OffsetDescription_TRFA),
|
|
||||||
PrerequisiteIds = prereqMap.TryGetValue(i, out var pValue) ? pValue : [],
|
PrerequisiteIds = prereqMap.TryGetValue(i, out var pValue) ? pValue : [],
|
||||||
UnlockIds = unlockMap.TryGetValue(i, out var uValue) ? uValue : []
|
UnlockIds = unlockMap.TryGetValue(i, out var uValue) ? uValue : []
|
||||||
});
|
});
|
||||||
@@ -83,7 +80,7 @@ public class ResTreeParser
|
|||||||
|
|
||||||
private static Trf0Element ReadTrf0Element(Stream s)
|
private static Trf0Element ReadTrf0Element(Stream s)
|
||||||
{
|
{
|
||||||
// Total size must be 40 bytes (0x28)
|
// TRF0 element size is 40 bytes (0x28).
|
||||||
return new Trf0Element
|
return new Trf0Element
|
||||||
{
|
{
|
||||||
ResearchCost = s.ReadFloatLittleEndian(),
|
ResearchCost = s.ReadFloatLittleEndian(),
|
||||||
@@ -104,14 +101,21 @@ public class ResTreeParser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadRelationMap and LoadRawBuffer remain the same as your provided code
|
private static Dictionary<int, uint[]> LoadRelationMap(
|
||||||
private static Dictionary<int, uint[]> LoadRelationMap(Stream s, NResArchive archive, string headerType, string dataType)
|
Stream s,
|
||||||
|
NResArchive archive,
|
||||||
|
string headerType,
|
||||||
|
string dataType,
|
||||||
|
bool strict)
|
||||||
{
|
{
|
||||||
var header = archive.Files.FirstOrDefault(f => f.FileType == headerType);
|
var header = archive.Files.FirstOrDefault(f => f.FileType == headerType);
|
||||||
var data = archive.Files.FirstOrDefault(f => f.FileType == dataType);
|
var data = archive.Files.FirstOrDefault(f => f.FileType == dataType);
|
||||||
var map = new Dictionary<int, uint[]>();
|
var map = new Dictionary<int, uint[]>();
|
||||||
|
|
||||||
if (header == null || data == null) return map;
|
if (header == null || data == null)
|
||||||
|
{
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
uint[] counts = new uint[header.ElementCount];
|
uint[] counts = new uint[header.ElementCount];
|
||||||
s.Position = header.OffsetInFile;
|
s.Position = header.OffsetInFile;
|
||||||
@@ -120,15 +124,46 @@ public class ResTreeParser
|
|||||||
counts[i] = s.ReadUInt32LittleEndian();
|
counts[i] = s.ReadUInt32LittleEndian();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
s.Position = data.OffsetInFile;
|
s.Position = data.OffsetInFile;
|
||||||
|
|
||||||
for (int i = 0; i < counts.Length; i++)
|
for (int i = 0; i < counts.Length; i++)
|
||||||
{
|
{
|
||||||
uint count = counts[i];
|
uint count = counts[i];
|
||||||
|
if (s.Position + count * sizeof(uint) > dataEnd)
|
||||||
|
{
|
||||||
|
if (strict)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException(
|
||||||
|
$"Relation map {headerType}/{dataType} overreads section at node {i} (count={count}).");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
uint[] ids = new uint[count];
|
uint[] ids = new uint[count];
|
||||||
for (int j = 0; j < count; j++)
|
for (int j = 0; j < count; j++)
|
||||||
{
|
{
|
||||||
ids[j] = s.ReadUInt32LittleEndian();
|
ids[j] = s.ReadUInt32LittleEndian();
|
||||||
}
|
}
|
||||||
|
|
||||||
map[i] = ids;
|
map[i] = ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +173,10 @@ public class ResTreeParser
|
|||||||
private static byte[] LoadRawBuffer(Stream s, NResArchive archive, string type)
|
private static byte[] LoadRawBuffer(Stream s, NResArchive archive, string type)
|
||||||
{
|
{
|
||||||
var item = archive.Files.FirstOrDefault(f => f.FileType == type);
|
var item = archive.Files.FirstOrDefault(f => f.FileType == type);
|
||||||
if (item == null) return [];
|
if (item == null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
byte[] buffer = new byte[item.FileLength];
|
byte[] buffer = new byte[item.FileLength];
|
||||||
s.Position = item.OffsetInFile;
|
s.Position = item.OffsetInFile;
|
||||||
@@ -148,10 +186,13 @@ public class ResTreeParser
|
|||||||
|
|
||||||
private static string GetStringFromPool(byte[] pool, uint offset)
|
private static string GetStringFromPool(byte[] pool, uint offset)
|
||||||
{
|
{
|
||||||
if (pool.Length == 0 || offset >= pool.Length) return string.Empty;
|
if (pool.Length == 0 || offset >= pool.Length)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
int end = Array.IndexOf(pool, (byte)0, (int)offset);
|
int end = Array.IndexOf(pool, (byte)0, (int)offset);
|
||||||
int length = (end == -1) ? pool.Length - (int)offset : end - (int)offset;
|
int length = end == -1 ? pool.Length - (int)offset : end - (int)offset;
|
||||||
|
|
||||||
return CyrillicEncoding.GetString(pool, (int)offset, length);
|
return CyrillicEncoding.GetString(pool, (int)offset, length);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-13
@@ -1,18 +1,22 @@
|
|||||||
namespace ResTreeLib;
|
namespace ResTreeLib;
|
||||||
|
|
||||||
// Result DTO for easier use
|
|
||||||
|
|
||||||
|
// TRF0 node payload (40 bytes).
|
||||||
|
// Field naming for the first 16 bytes is based on empirical checks over the mission .trf set.
|
||||||
|
// ResearchCost/ResearchTime look gameplay-related (strong mutual correlation and upgrade-level scaling).
|
||||||
|
// ViewPosX/ViewPosY behave like 2D layout coordinates; very large Y outliers are mostly unnamed/off-screen nodes.
|
||||||
public record Trf0Element
|
public record Trf0Element
|
||||||
{
|
{
|
||||||
public float ResearchCost; // field_0
|
public float ResearchCost; // 0..45 in sample files
|
||||||
public float ResearchTime; // field_4
|
public float ResearchTime; // 0..150 in sample files
|
||||||
public float ViewPosX; // field_8
|
public float ViewPosX; // 0..80 in sample files
|
||||||
public float ViewPosY; // field_12
|
public float ViewPosY; // 0..1500 in sample files
|
||||||
public uint OffsetShortName_TRF7; // offset_into_TRF7
|
|
||||||
public uint OffsetLongName_TRF8; // offset_into_TRF8
|
public uint OffsetShortName_TRF7;
|
||||||
public uint OffsetHelpText_TRF9; // offset_into_TRF9
|
public uint OffsetLongName_TRF8;
|
||||||
public uint OffsetDescription_TRFA; // offset_into_TRFA
|
public uint OffsetHelpText_TRF9;
|
||||||
public ushort OffsetAux_TRFB; // offset_into_TRFB
|
public uint OffsetDescription_TRFA;
|
||||||
|
|
||||||
|
public ushort OffsetAux_TRFB;
|
||||||
public byte TurretType;
|
public byte TurretType;
|
||||||
public byte MainType;
|
public byte MainType;
|
||||||
public byte SubType;
|
public byte SubType;
|
||||||
@@ -27,5 +31,5 @@ public enum NodeState : byte
|
|||||||
Hidden = 0,
|
Hidden = 0,
|
||||||
Available = 1 << 0, // 0x01
|
Available = 1 << 0, // 0x01
|
||||||
Researched = 1 << 1, // 0x02
|
Researched = 1 << 1, // 0x02
|
||||||
Active = 1 << 2 // 0x04 - If not set, node is disabled
|
Active = 1 << 2 // 0x04: if not set, node is disabled
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user